C/C++ Linux 开发教程(GCC + CMake) / 构造函数、析构函数与 RAII
构造函数、析构函数与 RAII
1. 构造函数类型总览
| 类型 | 语法示例 | 触发场景 |
|---|---|---|
| 默认构造 | T() | 无参创建对象 |
| 参数化构造 | T(int, double) | 带参创建对象 |
| 拷贝构造 | T(const T&) | 拷贝初始化 |
| 移动构造 | T(T&&) | 移动初始化 |
| 委托构造 | T() : T(0, 0.0) | 转发到其他构造函数 |
#include <iostream>
#include <string>
class Resource {
std::string name_;
int* data_;
size_t size_;
public:
// 默认构造
Resource() : name_("empty"), data_(nullptr), size_(0) {
std::cout << "[默认构造] " << name_ << "\n";
}
// 参数化构造
Resource(const std::string& name, size_t size)
: name_(name), size_(size) {
data_ = new int[size]{};
std::cout << "[参数化构造] " << name_ << " (size=" << size_ << ")\n";
}
// 委托构造
Resource(size_t size) : Resource("unnamed", size) {}
// 拷贝构造(深拷贝)
Resource(const Resource& other)
: name_(other.name_ + "_copy"), size_(other.size_) {
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "[拷贝构造] " << name_ << "\n";
}
// 移动构造
Resource(Resource&& other) noexcept
: name_(std::move(other.name_)), data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
std::cout << "[移动构造] " << name_ << "\n";
}
// 析构函数
~Resource() {
std::cout << "[析构] " << name_ << "\n";
delete[] data_;
}
void print() const {
std::cout << name_ << " (size=" << size_
<< ", data=" << static_cast<void*>(data_) << ")\n";
}
};
int main() {
Resource a; // 默认构造
Resource b("buffer", 100); // 参数化构造
Resource c(50); // 委托构造
Resource d = b; // 拷贝构造
Resource e = std::move(b); // 移动构造
std::cout << "--- 对象状态 ---\n";
a.print();
b.print(); // 移动后为空
d.print();
e.print();
return 0; // 析构顺序与构造相反
}
2. 初始化列表
初始化列表在构造函数体执行之前初始化成员变量,效率更高。
#include <iostream>
#include <string>
class Config {
const int id_; // const 成员必须用初始化列表
const std::string& ref_; // 引用成员必须用初始化列表
std::string name_;
int value_;
public:
// ✅ 正确:使用初始化列表
Config(int id, const std::string& ref, const std::string& name, int value)
: id_(id)
, ref_(ref)
, name_(name)
, value_(value)
{
std::cout << "Config created: id=" << id_ << "\n";
}
void print() const {
std::cout << "id=" << id_
<< ", name=" << name_
<< ", value=" << value_ << "\n";
}
};
int main() {
std::string refStr = "reference";
Config cfg(1, refStr, "my_config", 42);
cfg.print();
return 0;
}
⚠️ 注意:成员初始化顺序取决于声明顺序,而非初始化列表中的书写顺序。
class Foo {
int a_;
int b_;
public:
// ⚠️ 危险:b_ 在 a_ 之前声明,所以 b_ 先初始化
// 这里 b_ 使用了尚未初始化的 a_!
Foo(int val) : b_(val), a_(b_) {} // a_ 是未定义值!
// ✅ 正确:按声明顺序初始化
Foo(int val) : a_(val), b_(a_) {}
};
3. 析构函数
析构函数在对象生命周期结束时自动调用,用于释放资源。
#include <iostream>
#include <fstream>
#include <string>
class FileHandler {
std::string filename_;
std::ofstream file_;
bool isOpen_;
public:
FileHandler(const std::string& filename)
: filename_(filename), isOpen_(false) {
file_.open(filename);
if (file_.is_open()) {
isOpen_ = true;
std::cout << "打开文件: " << filename_ << "\n";
}
}
~FileHandler() {
// 析构时自动清理资源
if (isOpen_) {
file_.close();
std::cout << "关闭文件: " << filename_ << "\n";
}
}
void write(const std::string& data) {
if (isOpen_) {
file_ << data;
}
}
// 禁止拷贝(资源语义)
FileHandler(const FileHandler&) = delete;
FileHandler& operator=(const FileHandler&) = delete;
};
int main() {
{
FileHandler fh("/tmp/test.txt");
fh.write("Hello RAII\n");
fh.write("自动关闭文件\n");
} // fh 离开作用域 → ~FileHandler() 自动调用
std::cout << "文件已自动关闭\n";
return 0;
}
4. RAII(资源获取即初始化)
RAII 是 C++ 最重要的编程范式:在构造函数中获取资源,在析构函数中释放资源。
典型 RAII 资源类型
| 资源类型 | RAII 包装 |
|---|---|
| 堆内存 | std::unique_ptr, std::shared_ptr |
| 文件 | std::fstream |
| 互斥锁 | std::lock_guard, std::unique_lock |
| 网络连接 | 自定义类 |
| 数据库连接 | 自定义类 |
自定义 RAII:互斥锁守卫
#include <iostream>
#include <mutex>
// 模拟一个 RAII 锁守卫
template <typename Mutex>
class LockGuard {
Mutex& mtx_;
public:
explicit LockGuard(Mutex& mtx) : mtx_(mtx) {
mtx_.lock();
std::cout << "🔒 已加锁\n";
}
~LockGuard() {
mtx_.unlock();
std::cout << "🔓 已解锁\n";
}
// 禁止拷贝和移动
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
};
class SimpleMutex {
public:
void lock() { /* 实际加锁逻辑 */ }
void unlock() { /* 实际解锁逻辑 */ }
};
SimpleMutex g_mutex;
void criticalSection() {
LockGuard<SimpleMutex> guard(g_mutex); // 构造时加锁
std::cout << "执行临界区操作...\n";
// 如果这里抛出异常,guard 的析构函数仍然会被调用 → 自动解锁
} // guard 离开作用域 → 自动解锁
int main() {
criticalSection();
return 0;
}
5. Rule of Three / Five / Zero
| 规则 | 内容 | 何时适用 |
|---|---|---|
| Rule of Three | 如果需要自定义 析构/拷贝构造/拷贝赋值 中的任意一个,通常三个都需要 | 管理资源的类 |
| Rule of Five | 在 Three 的基础上加上 移动构造/移动赋值 | C++11 移动语义 |
| Rule of Zero | 让编译器生成所有特殊成员函数;资源管理委托给 RAII 类型 | 推荐的现代 C++ 风格 |
Rule of Five 示例
#include <iostream>
#include <cstring>
class Buffer {
char* data_;
size_t size_;
public:
explicit Buffer(size_t size = 0)
: data_(size ? new char[size]{} : nullptr), size_(size) {}
// 1. 析构函数
~Buffer() {
delete[] data_;
std::cout << "~Buffer\n";
}
// 2. 拷贝构造
Buffer(const Buffer& other)
: data_(other.size_ ? new char[other.size_] : nullptr)
, size_(other.size_) {
if (data_) std::memcpy(data_, other.data_, size_);
std::cout << "拷贝构造\n";
}
// 3. 拷贝赋值
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = size_ ? new char[size_] : nullptr;
if (data_) std::memcpy(data_, other.data_, size_);
}
std::cout << "拷贝赋值\n";
return *this;
}
// 4. 移动构造
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
std::cout << "移动构造\n";
}
// 5. 移动赋值
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
std::cout << "移动赋值\n";
return *this;
}
size_t size() const { return size_; }
};
int main() {
Buffer a(100); // 参数化构造
Buffer b = a; // 拷贝构造
Buffer c = std::move(a); // 移动构造
Buffer d;
d = b; // 拷贝赋值
d = std::move(c); // 移动赋值
std::cout << "a.size=" << a.size() << "\n"; // 0(已移动)
return 0;
}
Rule of Zero 示例(推荐)
#include <iostream>
#include <memory>
#include <string>
// 使用标准库 RAII 类型,不写任何特殊成员函数
class Widget {
std::string name_;
std::unique_ptr<int[]> data_;
size_t size_;
public:
Widget(const std::string& name, size_t size)
: name_(name), data_(std::make_unique<int[]>(size)), size_(size) {}
// 编译器自动生成的特殊成员函数是正确的:
// - 析构:自动销毁 name_ 和 data_
// - 拷贝:被 delete(unique_ptr 不可拷贝)
// - 移动:自动生成且正确
void print() const {
std::cout << name_ << " (size=" << size_ << ")\n";
}
};
int main() {
Widget a("widget_a", 10);
// Widget b = a; // 编译错误:不可拷贝
Widget c = std::move(a); // OK:移动语义
c.print(); // widget_a (size=10)
a.print(); // (空名称,size=0)
return 0;
}
6. 拷贝语义 vs 移动语义
| 语义 | 操作 | 源对象状态 | 性能 |
|---|---|---|---|
| 拷贝 | 复制数据到新对象 | 不变 | O(n) |
| 移动 | 窃取资源指针 | 有效但未指定状态 | O(1) |
#include <iostream>
#include <vector>
#include <string>
#include <chrono>
// 模拟大对象拷贝 vs 移动
class LargeBuffer {
std::vector<int> data_;
public:
explicit LargeBuffer(size_t n) : data_(n, 42) {}
// 拷贝构造 — 深拷贝
LargeBuffer(const LargeBuffer& other) : data_(other.data_) {
std::cout << "拷贝: " << data_.size() << " 个元素\n";
}
// 移动构造 — O(1) 窃取内部缓冲区
LargeBuffer(LargeBuffer&& other) noexcept : data_(std::move(other.data_)) {
std::cout << "移动: O(1) 完成\n";
}
};
int main() {
LargeBuffer buf(1'000'000);
auto t1 = std::chrono::steady_clock::now();
LargeBuffer copy = buf; // 拷贝:O(n)
auto t2 = std::chrono::steady_clock::now();
LargeBuffer moved = std::move(buf); // 移动:O(1)
auto t3 = std::chrono::steady_clock::now();
auto copy_us = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
auto move_us = std::chrono::duration_cast<std::chrono::microseconds>(t3 - t2).count();
std::cout << "拷贝耗时: " << copy_us << " μs\n";
std::cout << "移动耗时: " << move_us << " μs\n";
return 0;
}
7. std::move 与 std::forward
#include <iostream>
#include <utility>
#include <string>
// std::move:无条件转换为右值引用
// std::forward:仅在实参为右值时转换(完美转发)
class Widget {
std::string name_;
public:
explicit Widget(const std::string& name) : name_(name) {
std::cout << "构造: " << name_ << "\n";
}
Widget(const Widget& other) : name_(other.name_) {
std::cout << "拷贝: " << name_ << "\n";
}
Widget(Widget&& other) noexcept : name_(std::move(other.name_)) {
std::cout << "移动: " << name_ << "\n";
}
};
// 完美转发工厂函数
template <typename T, typename... Args>
T create(Args&&... args) {
return T(std::forward<Args>(args)...);
}
int main() {
std::string s = "hello";
// std::move:强制移动
std::string s2 = std::move(s); // s 被移动,不再持有数据
std::cout << "s2=" << s2 << ", s=\"" << s << "\"\n";
// 完美转发
Widget w1("normal");
Widget w2 = create<Widget>("forwarded"); // 直接构造
Widget w3 = create<Widget>(std::move(w1)); // 移动构造
return 0;
}
| 工具 | 作用 | 条件 |
|---|---|---|
std::move(x) | 无条件将 x 转为右值 | 总是转换 |
std::forward<T>(x) | 仅当 T 是右值引用时转换 | 条件转换(完美转发) |
⚠️ 注意:
std::move本身不做任何移动操作,它只是将参数转换为右值引用。真正的移动发生在移动构造函数/移动赋值中。
8. 编译器生成的默认函数
使用 = default 显式要求编译器生成默认版本:
#include <iostream>
class Defaultable {
int value_;
public:
Defaultable() = default; // 默认构造
explicit Defaultable(int v) : value_(v) {} // 参数化构造
~Defaultable() = default; // 默认析构
Defaultable(const Defaultable&) = default; // 默认拷贝构造
Defaultable(Defaultable&&) = default; // 默认移动构造
Defaultable& operator=(const Defaultable&) = default; // 默认拷贝赋值
Defaultable& operator=(Defaultable&&) = default; // 默认移动赋值
int value() const { return value_; }
};
// = delete 显式禁止某些操作
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 禁止拷贝
NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值
NonCopyable(NonCopyable&&) = default; // 允许移动
NonCopyable& operator=(NonCopyable&&) = default;
};
int main() {
Defaultable a(42);
Defaultable b = a; // 拷贝
Defaultable c = std::move(a); // 移动
NonCopyable d;
// NonCopyable e = d; // 编译错误:拷贝已删除
NonCopyable f = std::move(d); // OK
return 0;
}
实际场景:数据库连接池中的 RAII
#include <iostream>
#include <string>
#include <stdexcept>
// 模拟数据库连接
class DBConnection {
std::string connStr_;
bool active_;
public:
explicit DBConnection(const std::string& connStr)
: connStr_(connStr), active_(true) {
std::cout << "🔗 建立连接: " << connStr_ << "\n";
}
~DBConnection() {
if (active_) {
std::cout << "🔌 关闭连接: " << connStr_ << "\n";
}
}
void execute(const std::string& sql) {
if (!active_) throw std::runtime_error("连接已关闭");
std::cout << "📤 执行: " << sql << "\n";
}
void close() {
active_ = false;
std::cout << "🔌 手动关闭连接\n";
}
};
// RAII 连接守卫
class ConnectionGuard {
DBConnection* conn_;
public:
explicit ConnectionGuard(DBConnection* conn) : conn_(conn) {}
~ConnectionGuard() {
if (conn_) {
conn_->close();
}
}
DBConnection* operator->() { return conn_; }
DBConnection& operator*() { return *conn_; }
// 禁止拷贝
ConnectionGuard(const ConnectionGuard&) = delete;
ConnectionGuard& operator=(const ConnectionGuard&) = delete;
};
int main() {
try {
DBConnection conn("localhost:5432/mydb");
ConnectionGuard guard(&conn);
guard->execute("SELECT * FROM users");
guard->execute("INSERT INTO logs VALUES ('hello')");
// 如果这里抛异常,guard 仍会关闭连接
throw std::runtime_error("模拟错误");
} catch (const std::exception& e) {
std::cout << "❌ 捕获异常: " << e.what() << "\n";
}
return 0;
}
扩展阅读
- cppreference — Constructors
- cppreference — Destructor
- C++ Core Guidelines — R: Resource management
- Effective Modern C++ 条款 17-22(特殊成员函数与移动语义)
- Meeting C++ — Rule of Zero