第12章 面向对象编程入门
第12章 面向对象编程入门
- 类与对象的基本概念和定义方法
- 成员函数(方法)的声明与实现
- 构造函数与析构函数的作用与用法
如果你用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,账号初始化为“未分配”。
检验你的理解
- 下面代码中,obj.data 的值最终是多少?
class MyClass { public: int data; MyClass(int val) { data = val * 2; } }; int main() { MyClass obj(5); } - 如果一个类没有定义任何构造函数,编译器会为我们生成什么?这个生成的构造函数会做什么?
- 析构函数在什么情况下会被自动调用?请列举两种场景。
本章小结
- 类与对象: 类是蓝图,定义数据和方法;对象是实例,拥有独立的数据存储。
- 成员函数: 属于类的函数,通过对象调用,直接操作对象内部的成员变量。
- 构造函数: 与类同名的特殊函数,在创建对象时自动调用,用于初始化对象状态。支持重载。
- 默认构造函数: 无参数的构造函数。如果用户未定义任何构造函数,编译器会自动生成一个空的默认构造函数。
- 析构函数: 名称为 ~类名,在对象销毁时自动调用,用于释放对象占用的资源(如动态内存)。
微信扫码关注
抖音扫码关注