目录
[五、解决方案2:函数级 try-catch](#五、解决方案2:函数级 try-catch)
一、构造函数可以抛异常吗?
可以,而且有时候必须抛异常。
构造函数没有返回值,无法像普通函数那样返回错误码。如果构造过程中发生错误(比如分配内存失败、参数无效),唯一的选择就是抛出异常。
cpp
class Buffer {
private:
int* data;
size_t size;
public:
Buffer(size_t n) : size(n) {
if (n == 0) {
throw std::invalid_argument("大小不能为0");
}
data = new int[n]; // 可能抛出 bad_alloc
}
~Buffer() { delete[] data; }
};
二、构造函数抛异常时,哪些资源会自动释放?
C++ 保证:已完成构造的成员对象会自动析构。
cpp
#include <iostream>
#include <stdexcept>
using namespace std;
class Member {
string name;
public:
Member(const string& n) : name(n) {
cout << "Member 构造: " << name << endl;
}
~Member() {
cout << "Member 析构: " << name << endl;
}
};
class Container {
Member m1;
Member m2;
Member m3;
public:
Container() : m1("m1"), m2("m2"), m3("m3") {
cout << "Container 构造函数体开始" << endl;
throw runtime_error("构造失败");
cout << "Container 构造函数体结束" << endl;
}
~Container() {
cout << "Container 析构" << endl;
}
};
int main() {
try {
Container c;
}
catch (const exception& e) {
cout << "捕获异常: " << e.what() << endl;
}
return 0;
}
输出:
text
Member 构造: m1
Member 构造: m2
Member 构造: m3
Container 构造函数体开始
Member 析构: m3
Member 析构: m2
Member 析构: m1
捕获异常: 构造失败
关键观察:
-
先构造成员对象(
m1→m2→m3) -
构造函数体抛异常
-
成员对象按构造逆序自动析构 (
m3→m2→m1) -
Container的析构函数不会被调用(因为对象本身未完成构造)
三、裸指针的陷阱:资源泄露
上面的规则有一个重要的例外:裸指针指向的动态内存不会自动释放。
cpp
class Leaky {
private:
int* p1;
int* p2;
public:
Leaky() {
p1 = new int[1000]; // 第一步
p2 = new int[1000]; // 第二步:如果这里抛出异常
}
~Leaky() {
delete[] p1;
delete[] p2;
}
};
如果 new int[1000] 抛出 bad_alloc:
-
p1已经指向一块内存 -
异常被抛出,
Leaky对象构造失败 -
~Leaky()不会被调用(因为对象未完成构造) -
p1指向的内存永远不会被释放 → 内存泄露
这就是构造函数异常中最隐蔽的内存泄露来源。
四、解决方案1:用智能指针管理动态资源
cpp
#include <memory>
using namespace std;
class Safe {
private:
unique_ptr<int[]> p1;
unique_ptr<int[]> p2;
public:
Safe(size_t n1, size_t n2)
: p1(make_unique<int[]>(n1)),
p2(make_unique<int[]>(n2)) {
// 即使 p2 的构造抛异常,p1 也会被 unique_ptr 自动释放
// 因为 p1 作为成员对象,已经完成构造,会被自动析构
}
// 不需要手动写析构函数!
};
原理 :unique_ptr 是成员对象,C++ 保证已构造的成员对象会被自动析构。即使构造函数体还没执行,初始化列表中的成员构造失败也会正确清理。
五、解决方案2:函数级 try-catch
如果必须使用裸指针,可以在构造函数中使用 function-try-block:
cpp
class Safe {
private:
int* p1;
int* p2;
public:
Safe(size_t n1, size_t n2)
try : p1(new int[n1]), p2(new int[n2]) {
// 构造函数体
}
catch (...) {
// 注意:这里必须清理已分配的资源
delete[] p1; // p2 分配失败时,p1 已被分配,需要手动清理
delete[] p2;
throw; // 重新抛出异常
}
~Safe() {
delete[] p1;
delete[] p2;
}
};
但这种写法比智能指针繁琐且易错,不推荐。
六、完整例子:对比三种方案
cpp
#include <iostream>
#include <memory>
#include <stdexcept>
using namespace std;
// ========== 方案1:裸指针(危险)==========
class LeakyPointer {
int* big1;
int* big2;
public:
LeakyPointer() {
big1 = new int[1000000];
cout << "已分配 big1" << endl;
// 模拟第二个分配失败
throw bad_alloc();
big2 = new int[1000000];
}
~LeakyPointer() {
delete[] big1;
delete[] big2;
cout << "LeakyPointer 析构" << endl;
}
};
// ========== 方案2:智能指针(安全)==========
class SafePointer {
unique_ptr<int[]> big1;
unique_ptr<int[]> big2;
public:
SafePointer()
: big1(make_unique<int[]>(1000000)),
big2(make_unique<int[]>(1000000)) { // 如果这里抛异常
cout << "SafePointer 构造完成" << endl;
}
// 不需要析构函数
};
// ========== 方案3:成员对象管理(安全)==========
class BigArray {
unique_ptr<int[]> data;
public:
BigArray(size_t n) : data(make_unique<int[]>(n)) {
cout << "BigArray 构造 " << n << " 个元素" << endl;
}
};
class Composite {
BigArray a1;
BigArray a2;
public:
Composite() : a1(1000000), a2(1000000) {
cout << "Composite 构造完成" << endl;
}
// 不需要析构函数
};
int main() {
cout << "=== 测试智能指针方案(安全)===" << endl;
try {
SafePointer sp;
}
catch (const bad_alloc& e) {
cout << "捕获异常: " << e.what() << endl;
}
cout << "\n=== 测试成员对象方案(安全)===" << endl;
try {
Composite c;
}
catch (const bad_alloc& e) {
cout << "捕获异常: " << e.what() << endl;
}
cout << "\n=== 测试裸指针方案(危险)===" << endl;
try {
LeakyPointer lp; // 内存泄露!
}
catch (const bad_alloc& e) {
cout << "捕获异常: " << e.what() << endl;
cout << "注意:big1 的内存已经泄露" << endl;
}
return 0;
}
输出(示意):
text
=== 测试智能指针方案(安全)===
SafePointer 构造完成
=== 测试成员对象方案(安全)===
BigArray 构造 1000000 个元素
BigArray 构造 1000000 个元素
Composite 构造完成
=== 测试裸指针方案(危险)===
已分配 big1
捕获异常: std::bad_alloc
注意:big1 的内存已经泄露
七、构造函数异常与继承
派生类构造函数抛异常时,基类子对象已经被构造,会被自动析构。
cpp
class Base {
int* data;
public:
Base() : data(new int[1000]) {
cout << "Base 构造" << endl;
}
~Base() {
delete[] data;
cout << "Base 析构" << endl;
}
};
class Derived : public Base {
int* more_data;
public:
Derived() : Base(), more_data(new int[1000]) {
cout << "Derived 构造" << endl;
throw runtime_error("构造失败");
}
~Derived() {
delete[] more_data;
cout << "Derived 析构" << endl;
}
};
int main() {
try {
Derived d;
}
catch (...) {
cout << "捕获异常" << endl;
}
return 0;
}
输出:
text
Base 构造
Base 析构
捕获异常
Derived 的析构函数没有被调用(因为对象未完成构造),但 Base 子对象已经被析构,data 被正确释放。
八、最佳实践总结
| 规则 | 说明 |
|---|---|
| 用智能指针代替裸指针 | unique_ptr、shared_ptr 自动管理资源 |
| 遵循 RAII | 资源应在构造函数中获取,析构函数中释放 |
| 使用成员对象管理资源 | 让 vector、string、智能指针等 RAII 类持有资源 |
| 避免在构造函数体中分配资源 | 优先在初始化列表中分配 |
| 如果必须用裸指针 | 使用 function-try-block 并手动清理 |
| 不要在构造函数中抛异常后留下裸指针资源 | 这是内存泄露的常见来源 |
九、这一篇的收获
你现在应该理解:
-
构造函数可以抛异常,这是报告构造失败的唯一方式
-
已完成构造的成员对象会自动析构(包括智能指针)
-
裸指针指向的动态内存不会自动释放,即使它是成员变量
-
用智能指针 (
unique_ptr、shared_ptr)代替裸指针,自动解决这个问题 -
派生类构造失败时,基类子对象会被正确析构
💡 小作业:写一个
FileHandler类,构造函数接收文件名并打开文件(用fopen)。如果文件打开失败,抛出异常。用unique_ptr<FILE, CustomDeleter>管理文件句柄,确保构造失败时不会泄露。写测试代码验证异常安全。
下一篇预告 :第36篇《析构函数应永远不抛出异常------原因与最佳实践》------为什么析构函数抛异常是"死刑"?栈展开过程中抛异常会导致 terminate。如何写"不抛异常"的析构函数?catch 所有异常并记录日志是最佳实践。