
C 中 delete 与 default 关键字详解显式控制特殊成员函数一、引言从编译器自动生成到开发者显式控制在 C 中编译器会为每个类自动生成一系列特殊成员函数——默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符。这种自动生成机制大多数时候很方便但有时也会带来问题某些类不应该被拷贝(如管理独占资源的类)某些类不应该使用编译器生成的默认实现某些类的默认实现被抑制了但开发者希望恢复它C11 引入的 default和 delete正是为此而生。它们让开发者能够精确控制这些特殊成员函数的生成与禁止让代码意图更明确、更安全。二、核心概念速览| 维度 | default | delete ||------|-----------|----------|| 用途 | 显式要求编译器生成默认实现 | 显式禁止使用该函数 || 适用函数 | 六大特殊成员函数 | 任意函数(成员函数、全局函数) || 编译器行为 | 为该函数生成默认实现 | 调用该函数时产生编译错误 || 对类的影响 | 类仍然是平凡的/可拷贝的等(取决于函数) | 可阻止拷贝、移动、特定类型转换等 || 常见场景 | 恢复被抑制的默认函数 | 禁止拷贝(Movable but not Copyable) || 与不声明的区别 | 显式表达设计意图 | 不声明时可能被编译器自动生成 |三、 default显式要求生成默认实现3.1 为什么需要 default当开发者定义了任何构造函数(包括拷贝构造、移动构造等)编译器就不会再自动生成默认构造函数class Person { std::string name; int age; public: // 自定义了一个有参构造函数 Person(const std::string n, int a) : name(n), age(a) { } // 此时编译器不再自动生成默认构造函数 }; int main() { // Person p; // 编译错误没有默认构造函数 Person p(Alice, 30); // OK }使用 default可以要求编译器恢复生成默认构造函数class Person { std::string name; int age; public: // 显式要求编译器生成默认构造函数 Person() default; // 自定义有参构造函数 Person(const std::string n, int a) : name(n), age(a) { } }; int main() { Person p1; // OK: 使用默认构造函数 Person p2(Alice, 30); // OK: 使用有参构造函数 }3.2 default 的适用函数 default只能用于编译器能够自动生成的六大特殊成员函数| 函数 | 示例 ||------|------|| 默认构造函数 |ClassName() default;|| 析构函数 |~ClassName() default;|| 拷贝构造函数 |ClassName(const ClassName) default;|| 拷贝赋值运算符 |ClassName operator(const ClassName) default;|| 移动构造函数 |ClassName(ClassName) default;|| 移动赋值运算符 |ClassName operator(ClassName) default;|3.3 典型使用场景#include iostream class Resource { int* data; size_t size; public: // 有参构造 Resource(size_t sz) : data(new int[sz]), size(sz) { std::cout Constructor std::endl; } // 显式要求默认构造函数(即使有其他构造) Resource() default; // 显式要求析构函数默认实现 ~Resource() default; // 显式要求拷贝构造(即使有移动构造) Resource(const Resource) default; Resource operator(const Resource) default; // 显式要求移动构造 Resource(Resource) default; Resource operator(Resource) default; // 如果定义了析构函数(即使是 default) // C11 仍会抑制移动操作的自动生成 // 这里显式 default 可以恢复它们 };3.4 default 的位置class MyClass { public: // 在类内声明处使用 default (内联) MyClass() default; // 或者在类外定义处使用 default (非内联) MyClass(const MyClass other); }; // 类外定义 MyClass::MyClass(const MyClass other) default;四、 delete显式禁止使用函数4.1 基本语法class NonCopyable { public: NonCopyable() default; // 禁止拷贝构造 NonCopyable(const NonCopyable) delete; // 禁止拷贝赋值 NonCopyable operator(const NonCopyable) delete; // 允许移动 NonCopyable(NonCopyable) default; NonCopyable operator(NonCopyable) default; }; int main() { NonCopyable obj1; // NonCopyable obj2 obj1; // 编译错误拷贝构造被 delete // NonCopyable obj3(obj1); // 编译错误拷贝构造被 delete NonCopyable obj4 std::move(obj1); // OK: 移动构造可用 }4.2 delete 可以用于任意函数与 default不同 delete不仅限于特殊成员函数// 禁止特定参数的函数重载 void process(int value) { std::cout int: value std::endl; } void process(double value) delete; // 禁止 double 参数 void process(void* ptr) delete; // 禁止 void* 参数 int main() { process(42); // OK: int 版本 // process(3.14); // 编译错误double 版本被 delete // process(nullptr); // 编译错误void* 版本被 delete }4.3 典型应用场景场景一禁止拷贝(实现不可拷贝的 RAII 类)class FileHandle { FILE* fp; public: explicit FileHandle(const char* path) : fp(fopen(path, r)) { if (!fp) throw std::runtime_error(Cannot open file); } ~FileHandle() { if (fp) fclose(fp); } // 禁止拷贝——文件句柄不应被复制 FileHandle(const FileHandle) delete; FileHandle operator(const FileHandle) delete; // 允许移动 FileHandle(FileHandle other) noexcept : fp(other.fp) { other.fp nullptr; } FileHandle operator(FileHandle other) noexcept { if (this ! other) { if (fp) fclose(fp); fp other.fp; other.fp nullptr; } return *this; } };场景二禁止危险的隐式类型转换class SafeInt { int value; public: SafeInt(int v) : value(v) { } // 允许 int 构造但禁止浮点数(避免截断) SafeInt(double) delete; SafeInt(float) delete; int getValue() const { return value; } }; int main() { SafeInt a(42); // OK // SafeInt b(3.14); // 编译错误防止意外的精度丢失 }场景三禁止堆上分配(或栈上分配)class StackOnly { public: StackOnly() default; // 禁止 new 分配(将 operator new 声明为 delete) void* operator new(size_t) delete; void* operator new[](size_t) delete; }; class HeapOnly { public: HeapOnly() default; // 将析构函数设为 private 或 delete阻止栈上分配 // 但可以使用特定的 destroy 函数 private: ~HeapOnly() default; public: static void destroy(HeapOnly* p) { delete p; } }; int main() { StackOnly s; // OK: 栈上分配 // auto* p new StackOnly(); // 编译错误operator new 被 delete // HeapOnly h; // 编译错误析构函数不可访问 // auto* hp new HeapOnly(); // 编译错误operator new 被 delete }场景四禁止特定模板实例化templatetypename T void printValue(T value) { std::cout value std::endl; } // 禁止打印 char 类型(可能产生非预期输出) template void printValuechar(char value) delete; int main() { printValue(42); // OK printValue(Hello); // OK // printValue(A); // 编译错误 }五、 default 与 delete 的组合使用5.1 完整控制六种特殊成员函数class ControlledResource { int* data; size_t size; public: // 默认构造 ControlledResource() default; // 有参构造 explicit ControlledResource(size_t sz) : data(new int[sz]), size(sz) { } // 析构函数 ~ControlledResource() { delete[] data; } // 禁止拷贝——独占资源 ControlledResource(const ControlledResource) delete; ControlledResource operator(const ControlledResource) delete; // 允许移动 ControlledResource(ControlledResource other) noexcept : data(other.data), size(other.size) { other.data nullptr; other.size 0; } ControlledResource operator(ControlledResource other) noexcept { if (this ! other) { delete[] data; data other.data; size other.size; other.data nullptr; other.size 0; } return *this; } }; // 此类: 默认可构造、不可拷贝、可移动5.2 设计意图表达对比// 方案一纯 C98 风格(私有声明不实现) class NonCopyable98 { private: NonCopyable98(const NonCopyable98); // 仅声明不实现 NonCopyable98 operator(const NonCopyable98); // 仅声明不实现 public: NonCopyable98() { } }; // 问题错误信息出现在链接阶段且不清晰 // 方案二C11 现代风格 class NonCopyable11 { public: NonCopyable11() default; NonCopyable11(const NonCopyable11) delete; // 明确禁止 NonCopyable11 operator(const NonCopyable11) delete; // 明确禁止 }; // 优势编译期报错错误信息清晰设计意图一目了然六、特殊成员函数的自动生成规则理解编译器的自动生成规则才能更好地运用 default和 delete否是是否是否是否是否声明了任何构造函数?编译器自动生成全部默认构造/拷贝/移动/析构是否声明了拷贝操作?移动操作被抑制不会自动生成是否声明了移动操作?拷贝操作被 delete不会自动生成是否声明了析构函数?移动操作被抑制C11 规则自动生成未声明的特殊函数如需移动使用 default 显式恢复如需拷贝需手动实现如需移动使用 default 显式恢复关键规则如果定义了析构函数、拷贝构造或拷贝赋值编译器不会自动生成移动操作如果定义了移动操作拷贝操作会被隐式delete使用 default可以显式恢复被抑制的函数使用 delete可以显式禁止自动生成的函数七、最佳实践总结7.1 推荐做法// 实践一明确表达设计意图 class Copyable { public: Copyable() default; ~Copyable() default; Copyable(const Copyable) default; // 显式允许拷贝 Copyable operator(const Copyable) default; // 显式允许拷贝 Copyable(Copyable) default; // 显式允许移动 Copyable operator(Copyable) default; // 显式允许移动 }; // 实践二使用 delete 禁止拷贝 class NonCopyable { public: NonCopyable() default; NonCopyable(const NonCopyable) delete; NonCopyable operator(const NonCopyable) delete; }; // 实践三Movable but not Copyable class MovableOnly { public: MovableOnly() default; MovableOnly(const MovableOnly) delete; MovableOnly operator(const MovableOnly) delete; MovableOnly(MovableOnly) default; MovableOnly operator(MovableOnly) default; }; // 实践四继承 boost::noncopyable 的现代替代 // 不再需要直接用 delete7.2 常见错误// 错误一忘记 default 导致对象不可移动 class BadResource { int* data; public: explicit BadResource(size_t n) : data(new int[n]) { } ~BadResource() { delete[] data; } // 因为定义了析构函数移动操作被抑制 // 尽管没有定义拷贝操作但拷贝会生成 }; // BadResource 可拷贝但不可移动(违背直觉和性能期望) // 正确做法显式控制 class GoodResource { int* data; public: explicit GoodResource(size_t n) : data(new int[n]) { } ~GoodResource() { delete[] data; } // 禁止拷贝 GoodResource(const GoodResource) delete; GoodResource operator(const GoodResource) delete; // 显式要求移动 GoodResource(GoodResource other) noexcept : data(other.data) { other.data nullptr; } GoodResource operator(GoodResource other) noexcept { if (this ! other) { delete[] data; data other.data; other.data nullptr; } return *this; } };八、总结 default和 delete是 C11 引入的两个语法利器它们让特殊成员函数的控制变得简洁而明确 default显式要求编译器生成默认实现。主要用于在其他构造函数存在时恢复默认构造函数在定义了析构函数/拷贝操作后恢复被抑制的移动操作显式表达“这个函数使用默认行为即可”的设计意图 delete显式禁止使用某个函数。主要用于禁止拷贝构造/拷贝赋值(实现独占资源语义)禁止特定的隐式类型转换禁止特定模板实例化禁止特定的重载版本设计哲学这两个关键字体现了现代 C 的核心设计理念——让编译器成为你的盟友在编译期发现问题。 delete将原来只能在链接期或运行期暴露的错误提前到编译期 default让开发者能够精确指定需要编译器帮助的边界。与 Rule of Five 的关系 default和 delete是实现 Rule of Five 的首选工具。通过它们你可以简洁、精确地定义类的拷贝/移动行为是允许拷贝还是只允许移动是禁止拷贝还是默认可拷贝每一个特殊成员函数都可以被精确设置为 default(使用默认实现)、 delete(禁止)或手动实现。在现代 C 中如果你看到私有且只声明不定义的拷贝构造函数那是 C98 的遗留做法。新代码应该使用 delete来明确表达“此操作不可用”。这两种写法的区别不仅在于语法更在于错误检查的时机(编译期 vs 链接期)和设计意图的清晰度。