
Effective C 条款06若不想使用编译器自动生成的函数就应该明确拒绝在 C 中编译器会默默地为你生成一些函数。但有时候这些好心的自动生成反而会成为代码中的隐患。今天我们来聊聊如何对编译器说不。一、编译器会默默生成哪些函数在 C 中如果你声明了一个空类classEmpty{};编译器会自动为它生成以下函数如果它们被需要的话自动生成的函数说明默认构造函数Empty()拷贝构造函数Empty(const Empty rhs)拷贝赋值运算符Empty operator(const Empty rhs)析构函数~Empty()移动构造函数 (C11)Empty(Empty rhs)移动赋值运算符 (C11)Empty operator(Empty rhs)这意味着下面这段代码完全可以编译通过Empty e1;// 调用默认构造函数Emptye2(e1);// 调用拷贝构造函数Empty e3;e3e1;// 调用拷贝赋值运算符编译器的这种行为在大多数情况下是便利的但在某些场景下我们恰恰不希望对象被拷贝或赋值。二、为什么要拒绝编译器自动生成的函数2.1 典型场景想象一下你正在实现一个文件句柄管理类classFileHandle{public:FileHandle(constchar*filename){fd_open(filename,O_RDONLY);}~FileHandle(){if(fd_!-1)close(fd_);}private:intfd_;};如果允许拷贝这个对象会发生什么FileHandlefh1(data.txt);FileHandle fh2fh1;// 危险两个对象持有同一个 fd// fh1 和 fh2 析构时都会 close(fd_)导致双重关闭又或者你设计了一个单例模式的类或者一个表示唯一资源标识的类如数据库连接、锁句柄等拷贝行为在语义上本身就是不合理的。2.2 问题的本质这些类的共同特点是拷贝会导致资源重复释放或状态不一致拷贝在业务逻辑上没有意义需要保证对象的唯一性因此我们必须明确拒绝编译器自动生成拷贝构造函数和拷贝赋值运算符。三、C98 时代的解决方案private 不实现在 C11 之前Scott Meyers 在《Effective C》中推荐的做法是将不想使用的成员函数声明为private并且不予实现。3.1 具体做法classFileHandle{public:FileHandle(constchar*filename){fd_open(filename,O_RDONLY);}~FileHandle(){if(fd_!-1)close(fd_);}private:// 声明为 private阻止外部调用FileHandle(constFileHandle);// 只声明不实现FileHandleoperator(constFileHandle);// 只声明不实现intfd_;};3.2 为什么这样做有效层面防护效果外部代码无法访问 private 成员编译报错成员函数 / 友元如果误调用链接时会报错符号未定义3.3 更优雅的做法Uncopyable 基类为了代码复用可以设计一个专门的基类classUncopyable{protected:Uncopyable(){}// 允许派生类构造~Uncopyable(){}// 允许派生类析构private:Uncopyable(constUncopyable);// 阻止拷贝Uncopyableoperator(constUncopyable);// 阻止赋值};使用时只需继承即可classFileHandle:privateUncopyable{public:FileHandle(constchar*filename){/* ... */}~FileHandle(){/* ... */}private:intfd_;};这样做的好处是语义清晰一眼就能看出该类不可拷贝代码复用不需要在每个类中重复声明 private 函数错误信息友好编译错误会指向 Uncopyable提示尝试拷贝不可拷贝的对象Boost 库中的boost::noncopyable就是这一思想的实现。四、现代 C 的解决方案 delete从 C11 开始我们有了更直接、更优雅的语法classFileHandle{public:FileHandle(constchar*filename){/* ... */}~FileHandle(){/* ... */}FileHandle(constFileHandle)delete;// 明确删除FileHandleoperator(constFileHandle)delete;// 明确删除private:intfd_;};4.1 delete 的优势特性private 不实现 delete语义清晰度间接需要理解设计意图直接表达此函数被删除错误时机链接期调用时编译期任何尝试使用的地方代码简洁度需要额外基类或重复声明一行搞定现代性C98 兼容C11 及以上4.2 delete 的额外能力 delete不仅可以用于自动生成的函数还可以用于禁止特定类型的隐式转换classWidget{public:Widget(intid){/* ... */}Widget(double)delete;// 禁止 double 隐式转换构造};Widgetw1(42);// OKWidgetw2(3.14);// 编译错误double 构造函数被删除五、实际应用场景5.1 资源管理类RAIIclassMutexLock{public:explicitMutexLock(pthread_mutex_t*mutex):mutex_(mutex){pthread_mutex_lock(mutex_);}~MutexLock(){pthread_mutex_unlock(mutex_);}MutexLock(constMutexLock)delete;MutexLockoperator(constMutexLock)delete;private:pthread_mutex_t*mutex_;};5.2 单例模式的基础classSingleton{public:staticSingletongetInstance(){staticSingleton instance;returninstance;}Singleton(constSingleton)delete;Singletonoperator(constSingleton)delete;private:Singleton()default;};5.3 不可复制的策略对象classLoggingStrategy{public:virtualvoidlog(conststd::stringmsg)0;virtual~LoggingStrategy()default;LoggingStrategy(constLoggingStrategy)delete;LoggingStrategyoperator(constLoggingStrategy)delete;};六、常见误区与注意事项6.1 只声明 private 但不实现真的安全吗在 C98 中如果类的成员函数或友元函数内部调用了这些 private 函数编译器不会报错因为可以访问 private但链接器会报错。这意味着错误发现时机被推迟到了链接期。而 delete能在编译期就发现错误更加安全。6.2 需要同时禁用拷贝构造和拷贝赋值吗是的如果只禁用其中一个另一个仍然可能被误用导致语义不一致。例如classWidget{public:Widget(constWidget)delete;// 禁用了拷贝构造// 但拷贝赋值仍然可用};Widget w1;Widget w2;w2w1;// 编译通过但语义上可能有问题6.3 移动语义怎么办在 C11 中如果你声明了拷贝构造/拷贝赋值为 delete编译器通常也不会自动生成移动构造/移动赋值。如果需要支持移动语义需要显式声明classFileHandle{public:FileHandle(FileHandleother)noexcept;// 移动构造FileHandleoperator(FileHandleother)noexcept;// 移动赋值FileHandle(constFileHandle)delete;FileHandleoperator(constFileHandle)delete;};七、总结做法适用场景推荐度private 不实现需要兼容 C98 的老项目历史方案Uncopyable 基类C98 中需要复用不可拷贝语义历史方案 deleteC11 及以上项目强烈推荐请记住编译器可以暗自为 class 创建默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。如果不想使用这些自动生成的函数就应该明确拒绝。在 C11 及以后使用 delete是最清晰、最安全的方式。在 C98 中将函数声明为private且不予实现或使用Uncopyable基类模式。拒绝编译器的好意有时候正是写出健壮代码的关键一步。希望这篇文章对你有所帮助欢迎在评论区交流讨论。参考阅读《Effective C》第三版Scott Meyers《C Primer》第五版关于 delete的章节