15. 禁止拷贝与移动:=delete的应用、不可拷贝类的设计、单例模式中的拷贝控制

拷贝和移动操作,是C++里很强大的特性。但有时候,我们反而要禁止它们。为什么?

我遇到过不少新手,写了个管理资源的类,结果不小心拷贝了,程序就崩了。嗯,这种问题很常见。今天我们就聊聊怎么用 =delete 来禁止拷贝和移动,以及它在实际项目中的应用。

15.1 =delete 的基本用法

=delete 是C++11引入的。它的作用很简单:告诉编译器,这个函数不要生成,也不准用。

说白了,就是把这个函数给「删掉」了。

class FileHandle {
public:
    FileHandle(const char* filename) {
        // 打开文件
    }
    
    // 禁止拷贝
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    // 禁止移动
    FileHandle(FileHandle&&) = delete;
    FileHandle& operator=(FileHandle&&) = delete;
    
    ~FileHandle() {
        // 关闭文件
    }
};

你看,这样写之后,任何试图拷贝或移动 FileHandle 对象的代码,都会编译报错。

我个人习惯,在写资源管理类时,第一时间就把拷贝和移动给禁掉。省得后面出问题。

小提示: =delete 可以用于任何函数,不限于拷贝/移动。比如你想禁止某些类型转换,也可以用。

15.2 不可拷贝类的设计

什么样的类需要设计成不可拷贝?我总结了几种常见场景:

  • 管理独占资源的类:比如文件句柄、互斥锁、数据库连接
  • 单例类:整个程序只允许一个实例
  • 拥有内部唯一标识的类:比如每个对象需要唯一ID
  • 大对象:拷贝代价太高,不如禁止

我记得有一次,团队里有人写了个网络连接池类。没禁用拷贝,结果某个函数返回时不小心拷贝了一份,导致两个对象管理同一个socket。嗯,后果就是连接被关闭了两次,程序直接崩溃。

从那以后,我写这类代码都会先问自己:这个类允许拷贝吗?如果答案是否,就立刻加上 =delete

15.3 单例模式中的拷贝控制

单例模式,说白了就是保证一个类只有一个实例。那拷贝和移动肯定要禁掉,不然你拷贝一份,不就变成两个了?

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
    
    // 禁止拷贝
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    // 禁止移动
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;
    
    void doSomething() {
        // 业务逻辑
    }
    
private:
    Singleton() = default;
    ~Singleton() = default;
};

这里有几个关键点:

  • 构造函数和析构函数放在 private 里,防止外部创建或删除
  • 拷贝和移动全部 =delete
  • 通过静态方法 getInstance() 获取唯一实例

你想想看,如果不禁掉拷贝,别人写个 auto s2 = Singleton::getInstance();,那不就出问题了?

注意: 单例模式虽然好用,但不要滥用。我见过有些项目把什么类都搞成单例,最后代码耦合得一塌糊涂。单例适合管理全局唯一的资源,比如日志系统、配置管理器。不适合做业务逻辑的载体。

15.4 更优雅的禁止方式:基类 + 继承

如果你有很多类都需要禁止拷贝,每次都写四行 =delete 也挺烦的。我建议定义一个基类:

class NonCopyable {
protected:
    NonCopyable() = default;
    ~NonCopyable() = default;
    
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
    
    NonCopyable(NonCopyable&&) = delete;
    NonCopyable& operator=(NonCopyable&&) = delete;
};

class DatabaseConnection : private NonCopyable {
    // 这个类自动禁止拷贝和移动
};

嗯,这样写起来就清爽多了。C++标准库里的 std::mutex 就是这么干的,它也是不可拷贝的。

15.5 知识结构图

下面这张图,帮你理清本章的核心逻辑:

禁止拷贝与移动:知识体系 =delete 应用 基本用法 拷贝构造/赋值 =delete 不可拷贝类设计 资源管理、唯一标识 单例模式 全局唯一实例 编译期检查 防止误用 独占资源 基类封装 私有构造 静态实例 禁止拷贝/移动 核心原则 不需要拷贝时,果断 =delete,让编译器帮你把关

15.6 避坑指南

最后,分享几个我踩过的坑:

  • 别忘了移动操作:有些人只禁了拷贝,忘了禁移动。结果 std::move() 一用,资源还是被转移了。
  • 析构函数不要 delete:除非你明确知道自己在做什么。否则对象无法正常销毁,内存泄漏等着你。
  • 继承时要小心:如果基类禁了拷贝,派生类默认也是禁的。但如果你在派生类里手动实现了拷贝构造,那基类的限制就失效了。

我曾经在一个项目里,看到有人把 =delete 写在了 private 区域里。嗯,其实没必要,=delete 本身就够强了,放哪都一样。但为了代码可读性,我建议统一放在 public 区域。

好了,关于禁止拷贝和移动,就聊这么多。记住一句话:不需要的,就删掉。让编译器帮你守住这道防线。


公众号:蓝海资料掘金营,微信deep3321