条款 05: 了解 C++ 默默编写并调用哪些函数
在 C++ 中,即使定义一个看似空无一物的类,编译器也会在需要时隐式生成一系列特殊成员函数。这些函数如同幕后工作者,支撑着对象的基础操作。
空类的默认函数生成机制
当定义class Empty {};这样的空类时,编译器会隐式声明以下特殊成员函数,且仅在被调用时才生成实际实现:
class Empty {
public:
Empty() {} // 默认构造函数
Empty(const Empty& rhs) {} // 拷贝构造函数
~Empty() {} // 析构函数
Empty& operator=(const Empty& rhs) {} // 拷贝赋值操作符
};
调用示例:
Empty e1; // 触发默认构造函数调用
Empty e2(e1); // 触发拷贝构造函数调用
e2 = e1; // 触发拷贝赋值操作符调用
C++11 后的特殊成员函数扩展
随着 C++ 标准的演进,特殊成员函数的生成规则也更加丰富和细化。
- 移动构造与移动赋值(C++11+):
如果类未定义拷贝 / 移动操作或析构函数,编译器会生成默认的移动操作,用于高效转移资源所有权:
class MoveExample {
int* data;
public:
MoveExample(MoveExample&&) = default; // 移动构造
MoveExample& operator=(MoveExample&&) = default; // 移动赋值
};
- 默认构造函数的限制:
若类包含未初始化的const成员或引用成员,编译器将不会生成默认构造函数,此时必须手动定义构造函数,否则会导致编译错误:
class RefConstClass {
const int c;
int& ref;
}; // 需手动定义构造函数,否则编译错误
- 拷贝操作的限制:
对于内含const成员或引用成员的类,编译器不会自动生成拷贝构造和拷贝赋值函数,开发者必须手动定义,以满足特殊成员的初始化和赋值需求:
class SpecialMem {
const int id;
int& ref;
public:
SpecialMem(int i, int& r) : id(i), ref(r) {}
SpecialMem(const SpecialMem& o) : id(o.id), ref(o.ref) {} // 手动定义拷贝构造
SpecialMem& operator=(const SpecialMem& o) {
if (this != &o) { /* 处理非自我赋值 */ }
return *this;
}
};
条款 06: 若不想使用编译器自动生成的函数,就该明确拒绝
在某些场景下,我们需要禁止编译器自动生成特定的成员函数,以满足程序设计的特殊要求。
传统解决方案(C++98)
在 C++98 时代,通过继承Uncopyable基类来禁止类的拷贝操作是常见做法:
class Uncopyable {
protected:
Uncopyable() {} // 允许派生类构造
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); // 声明为私有且不实现
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale : private Uncopyable {
// 无需声明拷贝相关函数,继承后自动禁用
};
禁止拷贝场景:
HomeForSale h1, h2;
HomeForSale h3(h1); // 编译错误:无法访问私有拷贝构造
h1 = h2; // 编译错误:无法访问私有拷贝赋值
C++11+ 改进方案(=delete 语法)
C++11 引入了更简洁直观的=delete语法,可直接在类内禁用默认函数:
class HomeForSale {
public:
HomeForSale() = default;
HomeForSale(const HomeForSale&) = delete; // 禁用拷贝构造
HomeForSale& operator=(const HomeForSale&) = delete; // 禁用拷贝赋值
~HomeForSale() = default;
};
扩展:禁用移动操作:
若需要同时禁用移动构造和移动赋值,也可使用=delete实现:
class NonMovable {
public:
NonMovable(const NonMovable&) = delete;
NonMovable& operator=(const NonMovable&) = delete;
NonMovable(NonMovable&&) = delete; // 禁用移动构造
NonMovable& operator=(NonMovable&&) = delete; // 禁用移动赋值
};
条款 07: 为多态基类声明 virtual 析构函数
在涉及多态的继承体系中,析构函数的设计关乎资源的正确释放。
析构函数与多态资源释放
错误示例(内存泄漏):
当基类析构函数不是虚函数时,通过基类指针删除派生类对象会导致资源泄漏:
class TimeKeeper {
public:
TimeKeeper() {}
~TimeKeeper() {} // 非虚析构函数
};
class AtomicClock : public TimeKeeper {
int* data = new int(0);
public:
~AtomicClock() { delete data; } // 派生类析构
};
TimeKeeper* ptk = new AtomicClock();
delete ptk; // 仅调用TimeKeeper::~TimeKeeper(),AtomicClock资源泄漏!
正确实现(虚析构函数):
为基类声明虚析构函数,确保通过基类指针删除派生类对象时,能正确调用派生类析构函数:
class TimeKeeper {
public:
virtual ~TimeKeeper() {} // 虚析构函数
};
delete ptk; // 先调用AtomicClock::~AtomicClock(),再调用基类析构,资源正确释放
标准库陷阱与 C++11 规范
继承非虚析构函数的标准库类(如std::string):
由于std::string的析构函数不是虚函数,继承它并添加额外资源管理时会引发未定义行为:
class SpecialString : public std::string {
char* extraData = new char[10];
public:
~SpecialString() { delete[] extraData; } // 派生类析构
};
std::string* ps = new SpecialString("test");
delete ps; // 未定义行为:仅调用std::string::~string(),extraData泄漏!
C++11 纯虚析构函数规范:
在 C++11 中,若基类定义了纯虚析构函数,必须提供实现:
class AbstractShape {
public:
virtual ~AbstractShape() = 0; // 纯虚析构函数需声明
};
AbstractShape::~AbstractShape() {} // 必须提供实现
条款 08: 别让异常逃离析构函数
析构函数中的异常处理不当,会导致程序出现严重问题。
异常逃离的风险与解决方案
风险示例:
当析构函数抛出异常时,可能导致程序调用std::terminate而异常终止:
class DBConnection {
public:
void close() { throw std::exception(); } // 可能抛异常
~DBConnection() { close(); } // 析构函数抛异常,导致程序终止
};
void process() {
DBConnection db; // 析构时close抛异常,程序调用std::terminate()
}
C++11 改进方案(noexcept修饰符):
C++11 引入的noexcept修饰符可明确声明析构函数不会抛出异常,提升程序稳定性:
class DBConn {
private:
DBConnection db;
bool closed = false;
public:
~DBConn() noexcept { // 声明析构不抛异常
if (!closed) {
try {
db.close();
closed = true;
} catch (...) {
std::cerr << "Error closing DB" << std::endl;
std::abort(); // 安全终止程序
}
}
}
void close() { db.close(); closed = true; } // 显式关闭接口
};
条款 09: 绝不在构造和析构过程中调用 virtual 函数
在对象的构造和析构阶段调用虚函数,会导致意想不到的结果。
构造 / 析构期间的多态失效
示例:构造函数中调用虚函数:
在基类构造函数中调用虚函数,实际调用的是基类版本,而非派生类重写后的版本:
class Base {
public:
Base() { virtualFunc(); } // 调用Base::virtualFunc()
virtual void virtualFunc() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
Derived() : Base() {}
void virtualFunc() override { std::cout << "Derived\n"; }
};
Derived d; // 输出"Base",而非"Derived"
替代方案:使用非虚初始化函数:
为避免问题,可通过非虚函数进行初始化和清理操作:
class Base {
public:
Base() { init(); } // 调用非虚函数
void init() { /* 初始化逻辑 */ }
};
条款 10: 令 operator = 返回一个 reference to *this
为operator=返回对象引用,可支持链式赋值操作。
链式赋值的规范实现
class Widget {
public:
Widget& operator=(const Widget& rhs) {
// 赋值逻辑
return *this;
}
};
Widget w1, w2, w3;
w1 = w2 = w3; // 等价于w1.operator=(w2.operator=(w3))
条款 11: 在 operator = 中处理 “自我赋值”
处理operator=中的自我赋值情况,可确保赋值操作的安全性。
证同测试与 Copy-and-Swap 技术
传统证同测试:
通过检查是否为自我赋值,避免不必要的资源释放和复制:
Widget& operator=(const Widget& rhs) {
if (this == &rhs) return *this; // 自我赋值检测
// 安全的资源复制逻辑
return *this;
}
C++11 推荐:Copy-and-Swap:
利用值传递自动生成副本,并通过交换实现安全赋值:
Widget& operator=(Widget rhs) { // 值传递自动生成副本
swap(*this, rhs); // 交换资源
return *this;
}
条款 12: 复制对象时勿忘其每一个成分
在实现拷贝函数时,需确保基类和派生类的所有成员都被正确复制。
派生类拷贝的常见错误与修正
错误示例(遗漏基类拷贝):
派生类拷贝构造函数若未调用基类拷贝构造函数,会导致基类部分未被正确复制:
class Base {
int value;
public:
Base(int v) : value(v) {}
Base(const Base& b) : value(b.value) {}
};
class Derived : public Base {
double data;
public:
Derived(int v, double d) : Base(v), data(d) {}
Derived(const Derived& d) : data(d.data) {} // 错误:未调用Base拷贝构造
};
正确实现:
显式调用基类拷贝构造函数,确保基类成员也被正确复制:
Derived(const Derived& d) : Base(d), data(d.data) {} // 显式调用基类拷贝构造
C++11 统一初始化语法
C++11 引入的统一初始化语法,可使成员初始化更简洁、安全:
Derived(int v, double d) : Base(v), data{d} {} // 使用花括号初始化
C++11+ 关键特性总结
特性 | 相关条款 | 说明 |
---|---|---|
=default | 5,6 | 显式要求编译器生成默认函数,如 MoveExample() = default; |
=delete | 6 | 显式禁用函数,如 HomeForSale(const&) = delete; |
移动语义 | 5,11 | 优化资源转移,如 MoveExample(MoveExample&&) noexcept; |
noexcept | 8 | 声明函数不抛异常,如 ~DBConn() noexcept; |
override | 7,9 | 确保虚函数正确重写,如 void f() override; |