闽公网安备 35020302035485号
用大白话来说,零法则就是:设计一个类的时候,尽量别去手动定义那些特殊的成员函数——比如析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符(统称为“五法则”涉及的那五个)。相反,把资源管理交给现代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(资源获取即初始化),把资源生命周期绑定到对象上,编译器生成的默认特殊成员函数完美配合。