新对象特性

学习定位:现代 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)

cpp对象特性 > 拷贝函数调用时机

禁止拷贝

——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:移动操作不应抛异常,标准容器在重新分配时才会使用移动(否则用拷贝)。
  • 移动后应将原对象资源句柄置为无效(如 -1nullptr),析构时检查 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 所示。