用大白话来说,零法则就是:设计一个类的时候,尽量别去手动定义那些特殊的成员函数——比如析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符(统称为“五法则”涉及的那五个)。相反,把资源管理交给现代C++的工具(比如智能指针、标准库容器),让编译器自动生成这些函数。结果呢?你的代码更简洁、更安全,维护起来也更轻松。
class Resource { public: Resource() : data(new int(42)) { std::cout << "资源分配\n"; } ~Resource() { delete data; std::cout << "资源释放\n"; } // 默认拷贝构造函数会浅拷贝,危险! private: int* data; };这段代码看起来没啥问题吧?但你运行一下试试:
int main() { Resource r1; Resource r2 = r1; // 默认浅拷贝 // r1和r2的data指向同一块内存,析构时双重delete,程序崩溃! }问题出在哪?默认的拷贝构造函数只复制了指针(浅拷贝),没分配新的内存。结果,r1和r2的data指向同一个地址,析构时两次delete,直接未定义行为(UB),程序崩了。要修好这代码,你得手动加拷贝构造函数和赋值运算符:
class Resource { public: Resource() : data(newint(42)) {} ~Resource() { delete data; } Resource(const Resource& other) : data(newint(*other.data)) {} // 深拷贝 Resource& operator=(const Resource& other) { if (this != &other) { delete data; data = newint(*other.data); } return *this; } private: int* data; };这下安全了,但你看看,代码量翻倍了!而且万一哪天加个移动构造函数没写对,又是一堆麻烦。手动管理资源就像走钢丝,稍微不小心就摔得鼻青脸肿。
#include <memory> class Resource { public: Resource() : data(std::make_unique<int>(42)) { std::cout << "资源分配\n"; } // 啥都不用写,编译器自动搞定! private: std::unique_ptr<int> data; };试试同样的测试代码:
int main() { Resource r1; // Resource r2 = r1; // 编译错误!unique_ptr禁止拷贝 Resource r2(std::move(r1)); // 移动没问题 }这回咋样?用了std::unique_ptr,压根不用写析构函数,资源释放自动完成。拷贝?unique_ptr压根不让你拷贝,省得你犯错。想转移资源?用std::move就行,安全又高效。特殊成员函数数量?零!这就是零法则的魅力。
#include <string> #include <iostream> class Book { public: Book(conststd::string& title, conststd::string& author) : title_(title), author_(newstd::string(author)) { std::cout << "构造\n"; } ~Book() { delete author_; std::cout << "析构\n"; } Book(const Book& other) : title_(other.title_), author_(newstd::string(*other.author_)) { std::cout << "拷贝构造\n"; } Book& operator=(const Book& other) { if (this != &other) { delete author_; author_ = newstd::string(*other.author_); title_ = other.title_; } std::cout << "拷贝赋值\n"; return *this; } private: std::string title_; std::string* author_; };测试一下:
int main() { Book b1("C++ Primer", "Stanley"); Book b2 = b1; // 拷贝构造 Book b3("Effective C++", "Scott"); b3 = b1; // 拷贝赋值 }输出没问题,但你看看这代码:析构、拷贝构造、拷贝赋值,全得自己写。万一哪天加个新成员忘了更新这些函数,bug就来了。而且这还没考虑移动语义,要是加了移动构造和赋值,又得再写两坨代码。
#include <memory> #include <string> #include <iostream> class Book { public: Book(conststd::string& title, conststd::string& author) : title_(title), author_(std::make_unique<std::string>(author)) { std::cout << "构造\n"; } // 堆代码 duidaima.com // 啥都不写,编译器全包! private: std::string title_; std::unique_ptr<std::string> author_; };测试:
int main() { Book b1("C++ Primer", "Stanley"); // Book b2 = b1; // 编译错误,unique_ptr不让拷贝 Book b2(std::move(b1)); // 移动构造,自动生成 }这版本多清爽!析构不用写,unique_ptr管着;拷贝被禁用,避免误操作;移动语义自动支持,性能还高。底层知识点也清晰了:unique_ptr利用RAII(资源获取即初始化),把资源生命周期绑定到对象上,编译器生成的默认特殊成员函数完美配合。