一、正常情况下
以下函数会自动生成:
cpp
#include <iostream>
using namespace std;
class Empty {
public:
Empty() {} // 默认构造函数
Empty(const Empty& rhs) {} // 拷贝构造函数 rhs是右操作符缩写
~Empty() {} // 析构函数
// 拷贝赋值运算符
// 第一个&:返回类型是引用,第二个&:参数是一个常量对象的引用
Empty& operator=(const Empty& rhs) {
// this是一个隐含的指针参数,存在于每一个非静态成员函数中
// 当你调用一个对象的成员函数的时,编译器会自动把该对象的地址传入this
if(this!=&rhs){
// 拷贝成员变量
}
return *this;
}
int x;
static int count; // 静态成员变量(属于类,全局唯一)
// 非静态成员函数的本质,其实是一个普通函数,只是多了一个隐式的"this 指针"参数。
// 非静态成员函数,属于对象
void show(){ // 等于 void show(Empty* this)
cout<<"x="<<x<<endl;
}
// 静态成员函数,属于类本身
// 静态函数属于类,不属于对象,调用时没有this,因此不能访问对象成员
// 静态成员函数可以访问静态变量
static void info(){
cout<<"count="<<count<<endl;
}
};
int main(){
Empty a; // 创建一个对象
a.x =10;
a.show(); // 编译器幕后会改写为 Empty::show(&a);而this==&a
return 0;
}
二、若不想使用编译器自动生成的函数,就该明确拒绝(Effective C++ 06)
下面的例子是编译可以通过,但是可能报错的情况:
cpp
#include <iostream>
using namespace std;
class File {
public:
File(const std::string& filename) {
cout << "打开文件: " << filename << endl;
}
~File() { cout << "关闭文件\n"; }
// 没有禁拷贝!
};
int main() {
File f1("data.txt");
File f2 = f1; // ✅ 编译能过,但逻辑错误!
}
// 可以编译通过
// 编译器会自动生成一个"浅拷贝"构造函数;
// f1 和 f2 都指向同一个文件;
// 当 main() 结束时,~File() 会执行两次;
// 💥 程序可能崩溃(重复关闭同一文件描述符)。
可以明确拒绝默认构造:
cpp
#include <iostream>
using namespace std;
class File { // File类用来管理文件的打开与关闭
public:
File(const std::string& filename) { open(filename); } // 构造函数:当创建FILE对象时,会自动打开
~File() { close(); } //析构函数
File(const File&) = delete; // ❌ 禁止拷贝构造
File& operator=(const File&) = delete; // ❌ 禁止赋值
private:
void open(const std::string& filename);
void close();
};
int main(){
File f1("data.txt");
// File f2 = f1; // ❌ 编译器会自动调用拷贝构造函数!
return 0;
}
明确拒绝的含义:
不要让编译器帮你做不安全的事情。
如果你知道这个类不应该被拷贝/赋值。
就要用=delete明确告诉编译器禁止生成。
三、为多态基类声明virtual析构函数(Effective C++ 07)
如果一个类打算被当作基类使用(尤其是通过基类指针删除派生类对象) ,
那它的析构函数就必须是 virtual 的。
问题示例:
cpp
#include <iostream>
using namespace std;
class Base {
public:
~Base() { cout << "Base::~Base()\n"; }
};
class Derived : public Base {
public:
~Derived() { cout << "Derived::~Derived()\n"; }
};
int main() {
Base* p = new Derived();
// p的静态类型是Base*
// 调用delete p;时,编译器只知道它是个Base*
// 没有virtual,就不会做动态绑定
//所以只调用Base::~Base
delete p; // ❌ 问题点!
}
// Derived 的析构函数 没有被调用!
// Derived 中的资源(比如动态内存、文件、锁等)不会被释放!
// 导致资源泄漏甚至未定义行为。
当析构函数不是
virtual时,delete p;只会调用指针静态类型的析构函数。
正确写法:
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() { cout << "Base::~Base()\n"; }
};
class Derived : public Base {
public:
~Derived() { cout << "Derived::~Derived()\n"; }
};
int main() {
Base* p = new Derived();
delete p; // ✅ 正确释放
}

如果你的类打算被继承,会通过基类指针或引用操作对象,尤其通过delete basePtr;删除对象,就必须加virtual.
四、别让异常逃离析构函数(Effective C++ 08)
因为在 C++ 中,如果析构函数在栈展开(stack unwinding)过程中抛出异常,
会导致 程序直接终止(std::terminate())。
栈展开:当异常被抛出后,程序未来找到合适的catch块,会依次退出当前函数调用栈中的函数,并在退出的过程中自动调用已构造对象的析构函数。
简单说,就是从抛出点开始,一层层弹出函数调用栈,清理已创建的局部对象。

错误示例:
cpp
#include <iostream>
using namespace std;
class Test {
public:
~Test() { throw runtime_error("析构函数异常!"); } // 3析构函数又抛出了第二个异常
};
void func() {
Test t; // 局部对象
throw runtime_error("函数内部异常!"); // 2抛出异常,程序开始栈展开,准备析构局部对象t
}
int main() {
try {
func(); // 1
} catch (const exception& e) {
cout << "捕获异常:" << e.what() << endl;
}
}

C++ 语言规定:
如果一个异常在栈展开期间再次抛出,程序必须调用
std::terminate()。
原因:
-
栈展开期间系统已经在处理一个异常;
-
若又有另一个异常冒出,编译器无法同时处理两个异常;
-
为保证系统稳定,只能直接终止程序。
cpp
#include <iostream>
using namespace std;
class Test {
public:
~Test() {
try {
danger(); // 可能抛出异常
} catch (const exception& e) {
cerr << "析构函数捕获异常" << e.what() << endl;
}
}
private:
void danger() { throw runtime_error("文件关闭失败"); }
// 如果上层函数没有 catch,异常会继续向上层传播,这样可以不让它往上跑
};
void func() {
Test t; // 局部对象
throw runtime_error("函数内部异常!"); // 2抛出异常,程序开始栈展开,准备析构局部对象t
}
int main() {
try {
func(); // 1
} catch (const exception& e) {
cout << "捕获异常:" << e.what() << endl;
}
}
五、绝不在构造和析构过程中调用virtual函数(Effective C++ 09)
因为在构造和析构期间,对象的多态性还没"完全建立"或已经"失效"。
调用虚函数不会发生多态,而只会调用当前类版本的函数。
c++的对象在构造时是分阶段构造的:
1️⃣ 构造 Base 部分(调用 Base::Base())
2️⃣ 再构造 Derived 部分(调用 Derived::Derived())
也就是说,当执行 Base() 构造函数时,
整个对象的"有效类型"其实就是 Base。
错误示例:

正确示例:

如果非要在构造过程中自动调用怎么办?
用工厂函数或静态创建函数。
工厂函数:
把对象的创建过程封装起来,而不是直接在外部用
new或构造函数创建对象。
六、令operator=返回一个reference to *this(Effective C++ 10)
"让赋值运算符函数返回当前对象本身的引用。"
因为我们希望赋值操作能连续使用。
这样可以支持连锁赋值,就像内建类型(int,double)那样。
a = b = c;
编译器会当成:
a.operator=( b.operator=(c) );
要想这样写能成立,
b.operator=(c) 必须返回 b 自己(的引用) ,
这样外层的 a.operator=(...) 才能继续执行。
错误写法:
cpp
class A {
public:
A operator=(const A& rhs) { // 返回一个"副本",注意:返回类型是 A(值),不是 A&
// 拷贝数据...
return *this; // 返回一个临时对象
}
};
这样会发生:
-
(b = c)返回了一个临时副本; -
外层
a = (b = c)就变成了a = 临时对象; -
临时对象马上销毁;
-
效率差,行为不符合期望。
解释:多一次拷贝,效率变差
正确写法:
cpp
class A {
public:
A& operator=(const A& rhs) { // 返回引用
if (this != &rhs) {
// 拷贝数据
}
return *this; // 返回自己
}
};
上面这份代码就不会产生一个临时对象。
七、在operator=中处理"自我赋值"(Effective C++ 11)
a=a ;
a = f(); // f() 恰好返回 a 的引用
这个就是自我赋值,如果没有处理好,就可能在赋值过程中把自己给毁掉了。

加一条:检查自我赋值就可以了。

八、复制对象时勿忘其每一个成分(Effective C++ 12)
当你为一个类写拷贝构造函数或者拷贝赋值运算符时,一定要记得复制对象的所有成员和所有基类部分。
错误示范:

正确示范:

一旦定义了自己的拷贝构造函数或operator=,编译器就不会再自动生成默认版本,而默认版本里才会包含"基类+成员"的完整拷贝逻辑。
所以,当你手动定义了,就必须接住这个责任。