default 关键字强制生成默认构造函数

模拟面试开始

面试官: 我们知道,如果一个类我们不给它定义任何构造函数,编译器会自动为我们生成一个默认构造函数。类似的,编译器也可能会为我们生成拷贝构造函数、拷贝赋值运算符等。

在 C++11 之后,如果我们想显式地告诉编译器:“请为我生成一个默认的、最普通的版本”,我们可以使用 default 关键字。

请看下面这个例子:

1
2
3
4
5
6
7
8
9
class MyClass {
public:
    MyClass() = default; // 显式地要求编译器生成默认构造函数
    
    MyClass(int val) : data(val) {} // 用户自定义了别的构造函数

private:
    int data;
};

我的问题是: 在这个例子中,MyClass() = default; 这一行代码,有什么实际意义

换句话说,如果我们删掉 MyClass() = default; 这一行,会发生什么变化?这个类还能通过 MyClass obj; 这种方式创建对象吗?为什么?

回答:

  • 当用户自己定义了一种构造函数时,编译器就不会自动生成默认的无参数的构造函数。
  • MyClass() = default; 确保无参数的构造函数存在。
  • 如果删去这一行,那么 MyClass 这个类就无法通过 MyClass obj; 这种方式创建对象,因为编译器检测不到对应的构造函数。
  • 而增加上这一行,可以使得程序可以在无参数的情况下创建新的对象,等到对其内容有要求时,再做修改。

面试官 (总结与肯定): 你说的没错。这个问题的核心在于 C++ 的一条规则:

一旦用户为类定义了任何一种构造函数(无论带不带参数),编译器就会“罢工”,不再自动生成默认(无参)构造函数。

所以,在你给出的例子中:

  • 因为你已经定义了一个 MyClass(int val),编译器就不会再自动生成 MyClass() 了。
  • 如果我们删掉 MyClass() = default; 这一行,那么 MyClass 类就没有无参构造函数。此时,MyClass obj; 这样的代码就会编译失败,因为它找不到匹配的构造函数。

= default 的作用: MyClass() = default; 这一行,就是你(程序员)在向编译器明确地下指令

“我知道我已经写了别的构造函数,这通常会阻止你生成默认构造函数。但是,我仍然需要一个公开的、默认的无参构造函数,请你现在就为我生成一个。”

使用 = default 的好处:

  1. 意图明确:它清晰地表达了“我需要默认行为”的意图。
  2. 效率更高:由编译器生成的 = default 版本,通常比我们自己手写一个空的 {} 函数体效率更高。因为编译器可能会把它标记为 trivial (平凡的),从而进行更多的优化。
  3. 维持类的特性:在某些复杂的场景下,手写的 {} 版本可能会破坏类的 POD (Plain Old Data) 或 trivial 属性,而 = default 则不会。

你对 = default 的理解已经很到位了。


delete 关键字用于禁用函数

面试官 (引入 delete): 好的,我们知道了 = default 是用来 “强制生成” 默认版本的。

现在我们来看与它相对的另一个关键字:= delete;。它的作用正好相反,是用来 “明确禁用” 某个函数。

我们之前讨论过,对于一个包含裸指针的类,编译器自动生成的拷贝构造函数(执行浅拷贝)是危险的。在 C++98 时代,为了阻止别人拷贝这种对象,程序员们通常会耍一个小花招:把拷贝构造函数声明在 private 域中,并且只声明,不实现

1
2
3
4
5
6
// C++98 风格
class NonCopyable {
private:
    NonCopyable(const NonCopyable&);
    NonCopyable& operator=(const NonCopyable&);
};

这样,任何试图在类外调用拷贝构造函数的代码都会因为“私有成员不可访问”而编译失败。

我的问题是: C++11 引入的 = delete; 语法,是如何更清晰、更强大地解决“禁止拷贝”这个问题的?它和 C++98 的 private 技巧相比,有什么优势?

回答:

  • 更清晰: = delete; 语法能显式地告诉编译器:这个函数不允许被调用。
  • 更强大:相比“把拷贝函数声明在 private 域中的方法”,直接使用 delete 声明,能禁止来自所有地方的调用。不光能禁止来自类外的调用,还能禁止来自类内或者友元的调用。

具体的语法如下:

1
2
3
4
5
6
// C++11 风格:使用delete关键字。
class NonCopyable {
public:
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};

面试官 (总结与肯定): 你说的完全正确。

= delete vs C++98 private 技巧

  • 清晰性 (Clarity)

    • = delete:意图非常明确。任何看到 ... = delete; 的人,都会立刻明白“这个函数被禁用了”。
    • private 技巧:是一种“hack”。它利用了访问权限来间接达到目的。当编译失败时,给出的错误信息通常是“... is private”,这可能会让不熟悉这个技巧的开发者感到困惑,他可能会以为是权限问题,而不是这个函数本身就不该被调用。
  • 彻底性 (Completeness)

    • = delete:这是一种绝对的禁用。任何代码,包括成员函数友元函数,都不能调用被 = delete 的函数。
    • private 技巧无法阻止成员函数和友元函数调用它。虽然这在“禁止拷贝”的场景下不常见,但它暴露了这种技巧的不完备性。
  • 适用范围 (Applicability)

    • = delete:这是一个通用的机制。它不仅可以用来禁用特殊的成员函数,还可以用来禁用任何你想禁用的函数,包括普通函数
      1
      2
      3
      4
      5
      6
      
      // 禁止通过 int 调用
      void process(double d);
      void process(int i) = delete;
      
      process(3.14); // OK
      process(10);   // Error! 调用了被 delete 的函数
      
      这个特性在防止不期望的类型隐式转换时非常有用。
    • private 技巧:只能用于类的成员函数。

结论: 在现代 C++ 中,如果你想禁用某个由编译器自动生成的成员函数(比如拷贝构造),或者想禁止某个特定签名的函数被调用,永远都应该优先使用 = delete。它更清晰、更安全、也更通用。


总结

面试结束

今天我们系统地学习了 C++11 引入的两个用于控制特殊成员函数的重要关键字:

  • = default:用于在特定情况下,强制编译器生成默认的函数版本。
  • = delete:用于明确地禁用任何函数(包括特殊成员函数和普通函数)。

这两个关键字,让类的设计者能够更精确地掌控类的行为,写出意图更明确、也更安全的代码。你今天的表现非常出色,对这两个现代 C++ 特性的理解非常到位。