新对象特性
学习定位:现代 C++ 高阶知识。普通开发者需要先掌握 RAII、智能指针和移动语义;本页用于进一步理解对象构造、拷贝、移动和特殊成员函数如何配合资源管理。
参考 tinywebserver::Socket 的设计,理解现代 C++ 如何管理资源。
1. 委托构造
——(Delegating Constructor, C++11)
作用:一个构造函数可以调用同一个类的另一个构造函数,避免代码重复。
语法:在成员初始化列表里调用目标构造函数。
class Socket {
int fd_;
bool owned_;
public:
// 主构造函数:真正打开 socket
Socket(int domain, int type, int protocol)
: fd_(::socket(domain, type, protocol)), owned_(true) {
if (fd_ == -1) throw std::runtime_error("socket failed");
}
// 委托构造:直接委托给主构造,默认参数
Socket() : Socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0) {}
// 另一个委托:从已有 fd 构造
Socket(int fd, bool owned) : Socket(AF_INET, SOCK_STREAM, 0) {
// 但这里无法直接委托,因为参数不同 => 可以单独实现初始化逻辑
// 更好的方式:单独一个 init 函数,或者用不同的主构造。
// 实际通常不混用,展示概念即可。
}
};要点:
- 委托构造函数不能再有其它成员初始化列表(因为目标构造已经初始化了所有成员)。
- 常用于提供不同的参数默认值或简化重载。
2. 拷贝构造
——(Copy Constructor)
禁止拷贝
——RAII 类常见做法
class Socket {
// ...
public:
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
};原因:Socket 中的 fd_ 是系统资源,不能有两个对象同时拥有并各自调用 close。
3. 移动构造
——(Move Constructor, C++11)
作用:将资源从临时对象(右值)「窃取」过来,避免拷贝的开销。移动后原对象处于有效但未指定的状态(通常将原指针/句柄置空)。
语法:
class Socket {
int fd_;
bool owned_;
public:
// 移动构造
Socket(Socket&& other) noexcept
: fd_(other.fd_), owned_(other.owned_) {
other.fd_ = -1; // 让 other 不再拥有资源
other.owned_ = false;
}
// 移动赋值
Socket& operator=(Socket&& other) noexcept {
if (this != &other) {
close(); // 释放当前资源
fd_ = other.fd_;
owned_ = other.owned_;
other.fd_ = -1;
other.owned_ = false;
}
return *this;
}
void close() { if (owned_ && fd_ != -1) ::close(fd_); }
};关键点:
- 参数是
T&&(右值引用)。 - 标记
noexcept:移动操作不应抛异常,标准容器在重新分配时才会使用移动(否则用拷贝)。 - 移动后应将原对象资源句柄置为无效(如
-1或nullptr),析构时检查owned_避免重复释放。
何时移动
- 如果用户声明了拷贝、析构或移动中的任何一个,编译器不再自动生成移动构造/赋值。
- 推荐
= default或= delete显式控制。
4. 最佳实践
——(结合项目分析)
资源管理类应禁用拷贝,启用移动
class Socket {
int fd_;
bool owned_;
public:
Socket(Socket&&) noexcept;
Socket& operator=(Socket&&) noexcept;
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
};这正是
tinywebserver::Socket的设计:不可拷贝,可移动,析构时自动close。
移动操作应标记 noexcept
标准容器(如 std::vector)在扩容时,若移动构造是 noexcept,会使用移动;否则使用拷贝(以保证强异常安全)。
不标记 noexcept 可能损失性能。
使用 std::exchange 简化移动赋值
Socket& operator=(Socket&& other) noexcept {
if (this != &other) {
close();
fd_ = std::exchange(other.fd_, -1);
owned_ = std::exchange(other.owned_, false);
}
return *this;
}工厂函数返回 Socket 时,移动自动生效
Socket createSocket() {
Socket sock;
return sock; // 优先移动(C++11 起),无需 std::move
}需要拷贝时,实现深拷贝或使用引用计数
例如 std::shared_ptr 可以共享所有权,适合需要多个所有者的情况。
遵循「三五法则」或「零法则」
- 零法则:如果类不需要自定义析构、拷贝、移动,就让编译器生成默认的(使用 RAII 成员,如
std::vector)。 - 三五法则:若需要自定义析构,则几乎必然需要自定义拷贝构造、拷贝赋值,以及(C++11 起)移动构造、移动赋值。或者
= delete/= default明确意图。
Socket 类自定义了析构(关闭 fd_),因此必须禁用拷贝,并实现移动操作(符合三五法则)。
5. 小测试:识别 Socket 中的移动语义
Socket s1; // 构造
Socket s2 = std::move(s1); // 移动构造,s1.fd_ 变为 -1
s1 = Socket(); // 移动赋值(右边是临时对象)此时 s1 原先的资源被释放,然后接管临时对象的资源。
总结表
| 特性 | 作用 | 典型写法 |
|---|---|---|
| 委托构造 | 构造函数复用 | Socket() : Socket(AF_INET, SOCK_STREAM, 0) {} |
| 拷贝构造 | 深拷贝资源 | Socket(const Socket&) = delete;(禁) |
| 移动构造 | 转移资源所有权,高效率 | Socket(Socket&&) noexcept : fd_(other.fd_) { other.fd_ = -1; } |
| 移动赋值 | 释放已有,转移新资源 | 类似移动构造,加自赋值判断 |
noexcept | 保证移动不抛异常,适配容器优化 | 移动操作必须加 |
现代 C++ 中,移动语义是性能优化和资源安全转移的关键。结合 RAII,可以写出既安全又高效的代码,如 Socket 所示。