搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第12章 面向对象编程入门

第12章 面向对象编程入门

入门 👤 已掌握C语言结构体和函数基础的开发者 🔧 构建模块化、可复用、易于维护的中大型软件系统
本章你将学到
  • 类与对象的基本概念和定义方法
  • 成员函数(方法)的声明与实现
  • 构造函数与析构函数的作用与用法
核心概念类对象构造函数

如果你用C语言的结构体写过项目,可能会遇到这样的麻烦:数据和操作数据的函数是分离的。你需要手动把结构体变量传给函数,函数内部再访问其成员。面向对象编程(OOP)将数据和操作数据的方法“打包”在一起,形成一个更强大的单元——类。

12.1 从结构体到类:理解类与对象

生活类比: “类”就像一张汽车设计图纸,定义了汽车应有的部件(轮子、发动机)和功能(启动、刹车);“对象”则是根据这张图纸生产出来的、停在车库里的那辆实实在在的汽车。

技术定义: 类(Class)是一种用户自定义的数据类型,它封装了数据(称为“成员变量”或“属性”)和操作这些数据的函数(称为“成员函数”或“方法”)。对象(Object)是类的一个具体实例,拥有类所定义的独立数据副本。

12.2 定义一个简单的类

下面我们定义一个表示矩形的类 Rectangle。它包含宽和高两个数据,以及计算面积的方法。

class Rectangle {
public:
    // 成员变量(属性)
    double width;
    double height;

    // 成员函数(方法)声明
    double calculateArea();
};

// 成员函数在类外部的实现(定义)
double Rectangle::calculateArea() {
    return width * height;
}

关键点: 类定义以关键字 class 开头,后跟类名和一对花括号。类内部的 public: 是一个访问说明符,表示其后的成员可以被外部代码访问。成员函数在类内部声明,可以在类外部使用 类名::函数名 的语法进行定义。

12.3 创建与使用对象

定义了类之后,就可以像使用基本类型一样创建对象,并通过点运算符 . 访问其成员。

int main() {
    // 创建Rectangle类的对象rect1
    Rectangle rect1;

    // 访问对象的成员变量并赋值
    rect1.width = 5.0;
    rect1.height = 3.0;

    // 调用对象的成员函数
    double area = rect1.calculateArea();
    // area 的值为 15.0

    return 0;
}

这个过程可以清晰地用以下流程图表示:

定义类(设计图纸)
→
创建对象(生产产品)
→
访问成员(使用产品)

⚠️ 常见误区

  • 混淆类与对象: 类是一种类型定义,不占用内存;对象是类的实例,占用实际内存。声明 Rectangle rect1; 时,rect1 这个对象才在栈上分配了存储 width 和 height 的内存。
  • 忘记实现成员函数: 如果在类中声明了成员函数(如 calculateArea),但忘记在类外部给出具体实现(定义),链接时会报错“undefined reference”。
  • 直接对类名调用成员函数: 错误写法:Rectangle::calculateArea(); 成员函数必须通过具体的对象来调用,因为它操作的是该对象独有的数据。

12.4 构造函数:对象的“出生证明”

在刚才的例子中,我们创建 rect1 后,需要手动为它的 width 和 height 赋值。如果忘记赋值,它们将是未定义的垃圾值,计算面积就会出错。构造函数(Constructor)专门用于在对象创建时自动初始化其成员变量。

生活类比: 就像新生儿出生时,医院会开具一份出生证明,自动记录下出生时间、体重、身高等基本信息。构造函数就是对象的“出生证明”,确保对象一诞生就处于一个明确、有效的状态。

技术定义: 构造函数是一种特殊的成员函数,其名称与类名完全相同,没有返回类型(连 void 都没有)。它在创建对象时被编译器自动调用,用于初始化对象的数据成员。

class Rectangle {
public:
    double width;
    double height;

    // 构造函数声明
    Rectangle(double w, double h);
    double calculateArea();
};

// 构造函数的实现
Rectangle::Rectangle(double w, double h) {
    width = w;
    height = h;
    // 实际开发中,这里可以加入参数校验等逻辑
}

int main() {
    // 创建对象时,构造函数被自动调用,传入初始值
    Rectangle rect2(4.0, 6.0); // rect2的width=4.0, height=6.0
    double area2 = rect2.calculateArea(); // 得到24.0

    // 如果没有合适的构造函数,这样写会编译错误
    // Rectangle rect3; // 错误!编译器找不到无参的构造函数

    return 0;
}

编者提示: 构造函数中为成员变量赋值的写法 width = w; 是可行的,但在更复杂的类中,推荐使用第13章会详细介绍的“初始化列表”语法,效率更高且是初始化 const 成员或引用成员的唯一方式。

12.5 默认构造函数与重载

如果你既想支持带参数的初始化,也想支持创建时不指定参数(使用默认值),该怎么办?C++允许函数重载,构造函数也不例外。

class Rectangle {
public:
    double width;
    double height;

    // 构造函数重载
    Rectangle(); // 默认构造函数
    Rectangle(double w, double h); // 带参构造函数
};

// 默认构造函数实现:将成员初始化为固定值
Rectangle::Rectangle() {
    width = 1.0;
    height = 1.0;
    // 我在项目中踩过这个坑:忘记在默认构造函数中初始化指针成员,导致后续访问时程序崩溃。
}

// 带参构造函数实现
Rectangle::Rectangle(double w, double h) {
    width = w;
    height = h;
}

int main() {
    Rectangle rectA; // 调用默认构造函数,width=1.0, height=1.0
    Rectangle rectB(2.5, 3.5); // 调用带参构造函数
    return 0;
}

构造函数的不同使用场景对比如下:

创建对象语句调用的构造函数对象初始状态
Rectangle rect1;默认构造函数 Rectangle()width=1.0, height=1.0
Rectangle rect2(5.0, 10.0);带参构造函数 Rectangle(double, double)width=5.0, height=10.0
Rectangle rect3 = Rectangle(2.0, 2.0);带参构造函数width=2.0, height=2.0

12.6 析构函数:对象的“临终清理”

有始有终。构造函数负责对象诞生时的初始化,析构函数(Destructor)则负责对象生命周期结束时的清理工作。

class SimpleLogger {
public:
    char* logBuffer;

    // 构造函数:动态分配内存
    SimpleLogger(int size) {
        logBuffer = new char[size]; // 动态分配
    }

    // 析构函数:释放动态分配的内存
    ~SimpleLogger() {
        delete[] logBuffer; // 防止内存泄漏
        logBuffer = nullptr;
    }
};

int main() {
    {
        SimpleLogger logger(1024); // 进入作用域,构造函数被调用
        // 使用logger...
    } // 离开作用域,logger对象被销毁,析构函数被自动调用,释放内存
    return 0;
}

关键点: 析构函数名称是在类名前加波浪号 ~,没有参数,也没有返回类型。当对象离开其作用域(例如函数结束、块结束)或被显式 delete 时,编译器会自动调用析构函数。对于管理了动态内存、文件句柄、网络连接等资源的类,析构函数至关重要。

简单数据封装

  • 类:Student (name, id, score)
  • 方法:printInfo(), updateScore()
  • 特点:数据与操作绑定,避免传参错误

资源管理类

  • 类:FileHandler
  • 构造:打开文件
  • 析构:关闭文件
  • 特点:利用RAII(资源获取即初始化)自动管理资源

配置信息类

  • 类:AppConfig
  • 构造:从文件/默认值加载配置
  • 方法:getValue(key), save()
  • 特点:集中管理配置,保证初始化状态

实际开发中,大部分团队会强制要求:如果一个类管理了动态资源,就必须显式定义析构函数来释放它们。 这是避免内存泄漏等资源问题的第一道防线。

动手试一试

1. 创建一个名为 BankAccount 的类,包含以下成员:
- 私有成员变量:accountNumber (字符串), balance (双精度浮点数)。
- 公有构造函数:接收账号和初始余额,用于初始化成员变量。
- 公有成员函数:deposit(double amount) (存款), withdraw(double amount) (取款,需检查余额是否充足), getBalance() (查询余额)。
在 main 函数中创建一个账户对象,并进行几次存取款操作,打印余额变化。

2. 为上述 BankAccount 类添加一个默认构造函数,将余额初始化为0,账号初始化为“未分配”。

检验你的理解

  1. 下面代码中,obj.data 的值最终是多少?
    class MyClass {
    public:
        int data;
        MyClass(int val) { data = val * 2; }
    };
    int main() {
        MyClass obj(5);
    }
  2. 如果一个类没有定义任何构造函数,编译器会为我们生成什么?这个生成的构造函数会做什么?
  3. 析构函数在什么情况下会被自动调用?请列举两种场景。

本章小结

  • 类与对象: 类是蓝图,定义数据和方法;对象是实例,拥有独立的数据存储。
  • 成员函数: 属于类的函数,通过对象调用,直接操作对象内部的成员变量。
  • 构造函数: 与类同名的特殊函数,在创建对象时自动调用,用于初始化对象状态。支持重载。
  • 默认构造函数: 无参数的构造函数。如果用户未定义任何构造函数,编译器会自动生成一个空的默认构造函数。
  • 析构函数: 名称为 ~类名,在对象销毁时自动调用,用于释放对象占用的资源(如动态内存)。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注