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 知识结构图
下面这张图,帮你理清本章的核心逻辑:
15.6 避坑指南
最后,分享几个我踩过的坑:
- 别忘了移动操作:有些人只禁了拷贝,忘了禁移动。结果
std::move()一用,资源还是被转移了。 - 析构函数不要 delete:除非你明确知道自己在做什么。否则对象无法正常销毁,内存泄漏等着你。
- 继承时要小心:如果基类禁了拷贝,派生类默认也是禁的。但如果你在派生类里手动实现了拷贝构造,那基类的限制就失效了。
我曾经在一个项目里,看到有人把 =delete 写在了 private 区域里。嗯,其实没必要,=delete 本身就够强了,放哪都一样。但为了代码可读性,我建议统一放在 public 区域。
好了,关于禁止拷贝和移动,就聊这么多。记住一句话:不需要的,就删掉。让编译器帮你守住这道防线。
公众号:蓝海资料掘金营,微信deep3321