第三章 类与对象:封装的艺术
说实话,C++ 的类与对象,是我当年从 C 语言转向 C++ 时最兴奋的部分。你想想看,以前写 C 代码,数据和操作数据的函数是分开的,就像把菜刀和菜板分开放,用起来总得来回跑。而类,就是把它们打包在一起,形成一个完整的“小世界”。
这一章,我们聊聊封装、构造与析构、this 指针,还有拷贝构造和赋值运算符。这些概念,说白了就是 C++ 面向对象的基石。我见过不少新手,甚至一些老手,在这些地方栽跟头。咱们一个一个来拆解。
3.1 封装:把秘密藏起来
封装,是面向对象的第一大特性。它的核心思想很简单:把数据和操作数据的方法捆绑在一起,对外隐藏内部实现细节。
为什么要这么做?举个例子。你开一辆车,你只需要知道方向盘、油门、刹车怎么用,不需要知道发动机内部是怎么燃烧汽油的。封装就是这种“黑盒”思想。它让使用者只关心接口,不关心实现。
在 C++ 中,我们用访问权限来实现封装:
- public:公开的,谁都能访问。
- protected:受保护的,只有自己和子类能访问。
- private:私有的,只有自己(类内部)能访问。
我个人习惯,成员变量一律设为 private,然后通过 public 的 getter/setter 来访问。这不是矫情,而是为了以后好维护。你想想看,如果哪天你想改变内部数据的存储方式,比如把 int 改成 long long,如果变量是 public 的,所有用到它的地方都得改。如果是 private 的,你只需要改 getter/setter 的实现就行。
核心原则:高内聚,低耦合。类内部要高度凝聚,类与类之间要尽量松耦合。
3.2 构造函数与析构函数:对象的生与死
每个对象都有生命周期。出生时,需要初始化;死亡时,需要清理。C++ 用构造函数和析构函数来管理这两个时刻。
3.2.1 构造函数
构造函数的名字和类名相同,没有返回值。它会在对象创建时自动调用。我遇到过不少新手,忘了写构造函数,结果成员变量是随机值,程序跑起来莫名其妙。嗯,这里要注意:如果你没有定义任何构造函数,编译器会生成一个默认的无参构造函数,但它不会初始化内置类型的成员变量。
class Student {
public:
// 构造函数
Student(const std::string& name, int age)
: name_(name), age_(age) { // 初始化列表
// 构造函数体
}
private:
std::string name_;
int age_;
};
看到那个 : name_(name), age_(age) 了吗?这叫初始化列表。我个人强烈建议:能用初始化列表,就别在构造函数体里赋值。为什么?因为初始化列表是真正的初始化,而构造函数体里的赋值是“先默认构造,再赋值”,多了一步操作。对于复杂类型,性能差距很明显。
小技巧:const 成员变量和引用类型成员变量,必须在初始化列表中初始化,不能在构造函数体里赋值。
3.2.2 析构函数
析构函数的名字是 ~类名,也没有返回值。它在对象销毁时自动调用。如果你在类里动态分配了内存(new 出来的),那析构函数就是你释放内存的地方。
class Buffer {
public:
Buffer(size_t size) : data_(new char[size]), size_(size) {}
~Buffer() {
delete[] data_; // 释放内存
}
private:
char* data_;
size_t size_;
};
我曾经在一个项目中,看到有人忘了写析构函数,导致内存泄漏。程序跑了几天后,内存占用飙升到几个 GB。排查起来非常痛苦。所以,只要你用了 new,就一定要在析构函数里 delete。这是铁律。
警告:如果类里有指针成员,并且你手动管理内存,一定要遵循“三/五法则”(Rule of Three/Five)。我们后面会讲到。
3.3 this 指针:我是谁?
每个成员函数都有一个隐藏的参数,就是 this 指针。它指向调用该函数的对象本身。说白了,this 就是对象自己的地址。
什么时候需要显式使用 this?最常见的情况是:成员变量名和函数参数名冲突。
class Person {
public:
Person(const std::string& name) {
this->name = name; // 这里的 this->name 是成员变量,右边的 name 是参数
}
private:
std::string name;
};
另外,this 还有一个经典用法:返回对象自身的引用,用于链式调用。
class Calculator {
public:
Calculator& add(int value) {
result_ += value;
return *this; // 返回自身引用
}
Calculator& multiply(int value) {
result_ *= value;
return *this;
}
int getResult() const { return result_; }
private:
int result_ = 0;
};
// 使用链式调用
Calculator calc;
calc.add(5).multiply(3); // 结果就是 15
你想想看,如果没有 return *this,这种链式写法就实现不了。我个人觉得,这种写法让代码读起来很流畅,像在描述一个流程。
3.4 拷贝构造与赋值运算符:复制与赋值
这是 C++ 里最容易出坑的地方之一。我当年刚学的时候,也在这里摔过跟头。
3.4.1 拷贝构造函数
拷贝构造函数,是用一个已存在的对象来初始化一个新对象。它的形式是 ClassName(const ClassName& other)。
class MyString {
public:
MyString(const char* str) {
size_ = strlen(str);
data_ = new char[size_ + 1];
strcpy(data_, str);
}
// 拷贝构造函数
MyString(const MyString& other) {
size_ = other.size_;
data_ = new char[size_ + 1]; // 深拷贝
strcpy(data_, other.data_);
}
~MyString() {
delete[] data_;
}
private:
char* data_;
size_t size_;
};
注意看,拷贝构造函数里,我重新分配了内存,然后把数据复制过来。这叫深拷贝。如果不这么做,只是简单地 data_ = other.data_,那就是浅拷贝。两个对象会指向同一块内存,析构时就会 double free,程序直接崩溃。
核心要点:如果类里有指针成员,并且你手动管理内存,一定要实现深拷贝的拷贝构造函数和赋值运算符。
3.4.2 赋值运算符
赋值运算符 operator=,是用一个已存在的对象给另一个已存在的对象赋值。它和拷贝构造函数的区别是:拷贝构造是创建新对象,赋值是修改已有对象。
class MyString {
public:
// 赋值运算符
MyString& operator=(const MyString& other) {
if (this == &other) {
return *this; // 自我赋值检查
}
delete[] data_; // 释放旧内存
size_ = other.size_;
data_ = new char[size_ + 1];
strcpy(data_, other.data_);
return *this;
}
// ... 其他成员
};
这里有几个关键点:
- 自我赋值检查:
if (this == &other)。如果不检查,自己给自己赋值时,会先释放自己的内存,然后从自己身上复制数据,结果就是未定义行为。 - 先释放旧内存,再分配新内存:顺序不能乱。
- 返回
*this:支持链式赋值,比如a = b = c。
我曾经在一个项目中,看到有人写的赋值运算符没有做自我赋值检查。结果某个模块在极端情况下触发了自我赋值,程序就随机崩溃了。排查了整整两天才找到原因。从那以后,我写赋值运算符的第一行永远是自我赋值检查。
3.4.3 三/五法则
如果你需要手动实现析构函数、拷贝构造函数、赋值运算符中的任何一个,那么你很可能需要实现全部三个(C++11 之后是五个,加上移动构造和移动赋值)。这就是著名的三/五法则。
| 函数 | 作用 | 是否需要实现 |
|---|---|---|
| 析构函数 | 释放资源 | 如果类管理资源,必须实现 |
| 拷贝构造函数 | 用已有对象创建新对象 | 如果类管理资源,必须实现深拷贝 |
| 拷贝赋值运算符 | 用已有对象给另一个对象赋值 | 如果类管理资源,必须实现深拷贝 |
| 移动构造函数 (C++11) | 转移资源所有权 | 可选,但推荐实现以提升性能 |
| 移动赋值运算符 (C++11) | 转移资源所有权 | 可选,但推荐实现以提升性能 |
你想想看,如果你管理了资源(比如 new 出来的内存),编译器生成的默认拷贝构造和赋值运算符只会做浅拷贝。那结果就是两个对象共享同一块内存,析构时 double free。所以,要么你全部自己实现,要么就遵循三/五法则。
现代 C++ 建议:尽量使用智能指针(std::unique_ptr、std::shared_ptr)来管理资源,这样就不需要手动实现析构、拷贝构造和赋值运算符了。编译器生成的默认版本就够用。这叫“零法则”(Rule of Zero)。
知识体系总览
下面这张图,把本章的核心知识点串了起来。你可以把它当作一个思维导图来看。
这张图把封装、构造与析构、this 指针、拷贝构造与赋值、三/五法则串在了一起。你可以看到,它们都围绕着“类与对象”这个核心。封装是设计思想,构造与析构是生命周期管理,this 指针是对象自指,拷贝与赋值是对象复制,三/五法则是资源管理的铁律。
好了,这一章的内容就到这里。记住,类与对象是 C++ 的基石,把这些概念吃透了,后面的多态、继承、模板才能学得轻松。写代码的时候,多想想封装是否合理,资源管理是否安全,拷贝和赋值有没有坑。养成好习惯,后面会省很多事。
公众号:蓝海资料掘金营,微信deep3321