avatar

目录
274 - default constructor

274 - Default Constructor

好的,同学你好!今天我们来学习 C++ 中的一个重要概念:默认构造函数 (default constructor),以及如何使用 = default 这种现代 C++ 语法来让编译器为我们生成它。

C++ 教程:轻松理解默认构造函数 (= default)

简要总结: 本节课我们将重点学习什么是默认构造函数,为什么在定义了其他构造函数后我们可能仍然需要它,以及如何使用 = default 关键字让编译器为我们自动生成一个默认构造函数。我们还会讨论构造函数的访问权限,比如 public (公有的)。


详细讲解

1. 什么是构造函数 (Constructor)?

在我们深入默认构造函数之前,我们先来回顾一下什么是构造函数。在 C++ 中,构造函数是一种特殊的成员函数 (member function),它会在我们创建类的一个新对象 (object) 时自动被调用。它的主要职责是初始化对象的数据成员 (data members),确保对象在创建之初就处于一个有效的、可用的状态。构造函数的名称与类名完全相同,并且它没有返回类型 (return type),连 void 都没有。

2. 什么是默认构造函数 (Default Constructor)?

默认构造函数是一种特殊的构造函数,它不接受任何参数。如果你在类定义中没有提供任何构造函数,C++ 编译器会自动为你生成一个隐式的 (implicit) 默认构造函数。这个由编译器生成的默认构造函数通常是一个空函数,它不做任何特定的初始化操作(对于基本数据类型,它们的值将是未定义的;对于类类型的成员,它们的默认构造函数会被调用)。

例如:

C++

`class MyClass { public: int x; // 编译器会自动生成一个 MyClass() {} 这样的默认构造函数 };

MyClass obj; // 这里会调用编译器生成的默认构造函数`

3. 编译器行为的变化:当你定义了自己的构造函数

这里有一个非常关键的点:一旦你在类中定义了任何一个构造函数(无论是带参数的还是不带参数的),编译器就不再自动生成那个隐式的默认构造函数了。

这是 C++ 的一个设计规则。编译器的逻辑是:“哦,程序员已经开始自己定义构造函数了,那么他/她可能对对象的创建有特定的要求,我就不再擅自添加默认的了。”

问题来了: 如果你定义了一个带参数的构造函数,比如用来初始化圆柱体的半径和高度:

C++

`class Cylinder { public: double base_radius; double height;

Code
1
2
3
4
5
6
7
8
9
10
// 带参数的构造函数
Cylinder(double r, double h) {
base_radius = r;
height = h;
std::cout << "带参数的构造函数被调用!半径 = " << base_radius << ", 高度 = " << height << std::endl;
}

double volume() {
return 3.1415926 * base_radius * base_radius * height;
}

};`

现在,如果你尝试创建一个不带参数的 Cylinder 对象,就会编译失败:

C++

// Cylinder cylinder1; // 编译错误!编译器找不到 Cylinder() 这样的构造函数 Cylinder cylinder2(10.0, 5.0); // 这个没问题

编译器会报错,因为它找不到一个不接受参数的 Cylinder() 构造函数。

4. 如何重新获得默认构造函数?

如果你定义了其他构造函数,但仍然希望能够创建不带参数的对象(即拥有一个默认构造函数),你有两种主要方法:

  • 方法一:手动定义一个空的默认构造函数 (旧方法)

    你可以自己显式地 (explicitly) 写一个不带参数的构造函数,函数体可以为空,或者在里面进行一些默认的初始化。

    C++

    `class Cylinder { public: double base_radius; double height;

    Code
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 手动定义的默认构造函数
    Cylinder() {
    base_radius = 1.0; // 默认半径
    height = 1.0; // 默认高度
    std::cout << "手动定义的默认构造函数被调用!" << std::endl;
    }

    Cylinder(double r, double h) {
    base_radius = r;
    height = h;
    std::cout << "带参数的构造函数被调用!半径 = " << base_radius << ", 高度 = " << height << std::endl;
    }

    double volume() {
    return 3.1415926 * base_radius * base_radius * height;
    }

    };

    Cylinder c1; // 现在可以了,调用我们手动定义的默认构造函数 Cylinder c2(2.0, 3.0);`

  • 方法二:使用 = default (现代 C++ 推荐方法)

    从 C++11 开始,引入了一种更简洁、更明确的方式来告诉编译器:“请为我生成那个默认版本的函数实现。” 这就是使用 = default 语法。对于默认构造函数,你可以这样做:

    C++

    `class Cylinder { public: double base_radius; double height;

    Code
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 告诉编译器生成默认构造函数
    Cylinder() = default;

    Cylinder(double r, double h) {
    base_radius = r; // 通常,如果有了 =default 的构造函数,我们期望它不做特别的初始化
    // 或者像之前那样,如果你想让默认构造函数有特定的默认值,
    // 那么你还是需要手动实现它,而不是用 =default。
    // =default 通常意味着“给我一个最基础、编译器认为合适的版本”
    // 对于这里的例子,如果用了 =default,base_radius 和 height
    // 将会是未初始化的(对于基本类型)或默认构造的(对于类类型)。
    // 如果我们希望它们有初始值,我们可以这样做:
    // double base_radius = 1.0;
    // double height = 1.0;
    // 然后 Cylinder() = default; 就会使用这些类内初始值。
    // 或者,更常见的是,如果用了=default,就不再期望它有特定的值,除非有类内初始化。
    // 为了清晰,如果用=default,我们假设它就是创建一个“空”的对象,其成员的值依赖于它们的类型和是否有类内初始化。
    // 在原始课程中,作者似乎是期望一个“空”的,不做事的默认构造函数。
    height = h;
    std::cout << "带参数的构造函数被调用!半径 = " << base_radius << ", 高度 = " << height << std::endl;
    }

    // 为了让 = default 行为更符合“空”的默认构造函数的预期,并且能够被演示
    // 我们假设成员变量在类定义时没有赋初始值,除非在带参构造函数中赋值
    // Cylinder() = default; // 将生成一个不做任何事情的构造函数,base_radius 和 height 将是未初始化的

    double volume() {
    return 3.1415926 * base_radius * base_radius * height;
    }

    };`

    使用 = default 有几个好处:

    • 意图明确: 它清楚地表达了你想要一个由编译器生成的默认行为。
    • 简洁: 代码更少。
    • 潜在的性能优势: 编译器生成的版本有时可能比手写的空函数体更高效(尽管对于简单的默认构造函数,这种差异通常可以忽略不计)。它也允许编译器将这种类型的构造函数视为“平凡的”(trivial),这在某些高级场景下有意义。

      注意成员初始化:

      如果你的类成员有类内初始值 (in-class initializers),那么 = default 生成的构造函数会使用这些初始值。

      C++

      `class CylinderWithDefaultInit { public: double base_radius = 1.0; // 类内初始值 double height = 1.0; // 类内初始值

      Code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      CylinderWithDefaultInit() = default; // 编译器生成的默认构造函数会使用上面的初始值

      CylinderWithDefaultInit(double r, double h) {
      base_radius = r;
      height = h;
      }

      void print_values() {
      std::cout << "半径: " << base_radius << ", 高度: " << height << std::endl;
      }

      };

      CylinderWithDefaultInit c_default; // c_default.base_radius 会是 1.0, c_default.height 会是 1.0 c_default.print_values();`

      如果成员没有类内初始值,且是基本数据类型,那么 = default 生成的构造函数不会初始化它们,它们的值将是未定义的。

5. 构造函数的访问权限:public vs private

构造函数和普通成员函数一样,也有访问权限修饰符 (access specifiers),如 public (公有的), private (私有的), 和 protected (受保护的)。

  • public (公有的): 如果构造函数是 public 的,那么类的外部代码(比如 main 函数)就可以调用它来创建对象。这是最常见的情况。

    C++

    `class MyGadget { public: MyGadget() = default; // 公有构造函数 // … };

    MyGadget gadget; // OK,可以调用`

  • private (私有的): 如果构造函数是 private 的,那么类的外部代码就不能直接调用它来创建对象了。这通常用于实现一些特殊的设计模式,比如单例模式 (Singleton Pattern),或者当类只希望通过特定的静态成员函数 (static member function) 来创建对象时。

    C++

    `class OnlyInternal { private: OnlyInternal() = default; // 私有构造函数 public: static OnlyInternal createInstance() { return OnlyInternal(); // 类的内部或友元可以调用 } };

    // OnlyInternal oi; // 编译错误!’OnlyInternal::OnlyInternal()’ is private OnlyInternal oi = OnlyInternal::createInstance(); // OK`

    在课程的例子中,如果将 Cylinder() = default; 声明为 private,那么在 main 函数中尝试 Cylinder cylinder1; 就会导致编译错误,因为 main 函数无权访问私有的构造函数。


代码示例

让我们回顾并整合一下课程中提到的 Cylinder 类的例子:

C++

`#include const double PI = 3.14159265358979323846;

class Cylinder { public: // 构造函数通常需要是公有的 // 成员变量 (member variables) double base_radius = 1.0; // 给定默认值,这样 =default 的构造函数会使用它们 double height = 1.0;

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 使用 = default 来让编译器生成默认构造函数
// 这样我们就可以创建 Cylinder c1; 这样的对象了
Cylinder() = default;

// 2. 带参数的构造函数
Cylinder(double radius_param, double height_param) {
base_radius = radius_param;
height = height_param;
std::cout << "带参数的构造函数被调用。半径: " << base_radius << ", 高度: " << height << std::endl;
}

// 成员函数 (member function)
double volume() {
return PI * base_radius * base_radius * height;
}

};

int main() { // 情况一:使用默认构造函数创建对象 // 由于 Cylinder() = default; 并且成员有类内初始值, // cylinder1 会有 base_radius = 1.0 和 height = 1.0 Cylinder cylinder1; std::cout << “cylinder1 (默认构造) 的体积: “ << cylinder1.volume() << std::endl; std::cout << “cylinder1 的半径: “ << cylinder1.base_radius << “, 高度: “ << cylinder1.height << std::endl;

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 情况二:使用带参数的构造函数创建对象
Cylinder cylinder2(2.0, 5.0);
std::cout << "cylinder2 (带参构造) 的体积: " << cylinder2.volume() << std::endl;

// 如果我们把 Cylinder() = default; 注释掉或删除,
// 并且 Cylinder 类中存在其他构造函数(如 Cylinder(double, double)),
// 那么 Cylinder cylinder1; 这一行将会导致编译错误。
// 因为编译器不再自动生成默认构造函数。

// 演示如果构造函数是 private 的情况
// 假设我们将 Cylinder() = default; 移到 private 区:
/*
class Cylinder {
private:
Cylinder() = default; // 现在是私有的
public:
double base_radius = 1.0;
double height = 1.0;
Cylinder(double r, double h) : base_radius(r), height(h) {}
// ...
};
// Cylinder c_private; // 这将导致编译错误: 'Cylinder::Cylinder()' is private
*/

return 0;

}`

代码解释步骤:

  1. 包含头文件和定义常量: iostream 用于输入输出,PI 是圆周率。
  2. 定义 Cylinder 类:
    • public: 访问修饰符表明接下来的成员是公有的。
    • double base_radius = 1.0;double height = 1.0; 是类的成员变量,并给予了类内初始值。这意味着如果对象是通过默认构造函数创建的,这些成员将自动获得这些值。
    • Cylinder() = default; 这是核心。我们告诉编译器:“请为我生成一个标准的默认构造函数。” 因为成员有类内初始值,这个默认构造函数会使用它们。
    • Cylinder(double radius_param, double height_param) 是一个带参数的构造函数,它允许我们在创建对象时提供自定义的半径和高度。
    • double volume() 是一个计算圆柱体积的成员函数。
  3. main 函数:
    • Cylinder cylinder1;:这行代码现在是合法的,因为它会调用由 = default 生成的默认构造函数。cylinder1.base_radius 将是 1.0cylinder1.height 将是 1.0
    • std::cout << "cylinder1 (默认构造) 的体积: " << cylinder1.volume() << std::endl;:打印 cylinder1 的体积。
    • Cylinder cylinder2(2.0, 5.0);:这行代码调用带参数的构造函数,创建 cylinder2 对象,其半径为 2.0,高度为 5.0
    • 注释部分演示了如果默认构造函数缺失或为私有时会发生什么。

QA 闪卡 (QA Flash Cards)

  1. 问: 什么是默认构造函数 (default constructor)?

    答: 一个不接受任何参数的构造函数。

  2. 问: 如果我没有在类中定义任何构造函数,会发生什么?

    答: 编译器会自动为你生成一个公有的、隐式的默认构造函数。

  3. 问: 如果我定义了一个带参数的构造函数,编译器还会自动生成默认构造函数吗?

    答: 不会。一旦你定义了任何构造函数,编译器就不再自动生成默认构造函数。

  4. 问: 如何在定义了其他构造函数后,仍然让类拥有一个默认构造函数(使用现代C++方法)?

    答: 使用 YourClassName() = default; 语法。

  5. 问: Cylinder() = default; 这行代码是什么意思?

    答: 它指示编译器为 Cylinder 类生成一个默认的、不做特殊操作的构造函数。如果类成员有类内初始值,这些值会被使用。

  6. 问: 构造函数通常应该是什么访问权限 (access specifier)才能在类外部创建对象?

    答: public (公有的)。

  7. 问: 如果一个类的默认构造函数是 private (私有的),我可以在 main 函数中直接创建该类的对象吗(例如 MyClass obj;)?

    答: 不可以,会导致编译错误,因为 main 函数无权访问私有构造函数。


常见误解或易犯错误

  1. 忘记 = default 或手动定义: 当程序员定义了一个带参数的构造函数后,常常忘记如果还需要无参数构造对象的能力,就必须显式地使用 = default 或手动提供一个默认构造函数。然后尝试 MyClass obj; 时就会遇到编译错误。

  2. = default 与自定义逻辑混淆: = default 的意思是“给我编译器生成的版本”。如果你希望默认构造函数执行一些特定的自定义初始化逻辑(而不仅仅是依赖类内初始化),那么你应该手动编写构造函数体,而不是使用 = default

    • 例如,如果想在默认构造时打印日志或设置一些不方便通过类内初始化器设置的值,就需要手动写:

      C++

      class Logger { public: Logger() { // 手动实现,非 =default std::cout << "Logger object created by default constructor." << std::endl; // 可能还有其他初始化代码 } };

  3. 访问权限问题: 忘记将构造函数(包括 = default 的构造函数)声明为 public,导致在类外部无法创建对象。编译器会提示构造函数不可访问 (inaccessible) 或私有 (private)。

  4. 对未初始化成员的假设: 如果使用 = default 且成员变量没有类内初始值,那么基本数据类型的成员将不会被初始化(它们的值是未定义的)。依赖这些未定义的值会导致程序行为不可预测。

    • 错误示范:

      C++

      class BadExample { public: int value; // 没有类内初始化 BadExample() = default; }; BadExample ex; // std::cout << ex.value; // ex.value 的值是未定义的!

  5. 认为 = default 总是最好的: 虽然 = default 很方便,但如第2点所述,如果默认构造需要特定逻辑,则必须手动实现。= default 适用于你接受编译器提供的标准行为的场景。


编码练习

现在,这里有一个 Point 类的代码片段。你需要修改它,使得:

  1. 即使我们有带参数的构造函数 Point(int x_val, int y_val),我们仍然可以创建一个不带参数的 Point 对象 (例如 Point p1;)。
  2. 使用现代 C++ 的 = default 语法来实现这个默认构造函数。
  3. 确保新添加的默认构造函数是公有的。
  4. 为了让默认构造的对象有确定的初始值,请给成员变量 xy 添加类内初始值,都设为 0

C++

`#include class Point { // TODO: 为 x 和 y 添加类内初始值,都设为 0

public: int x;`

好的,我们继续!


7. 对练习的解答与评分

由于你没有直接提供代码,我就直接给出这个练习的正确答案和解释。如果你自己尝试了,可以对照一下看看思路是否一致。

练习的正确代码:

C++

`#include class Point { public: // 确保成员和构造函数有合适的访问权限 // 1. 为 x 和 y 添加类内初始值,都设为 0 int x = 0; int y = 0;

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
// 2. 在这里添加代码,使用 = default 来声明一个公有的默认构造函数
Point() = default;

// 带参数的构造函数
Point(int x_val, int y_val) {
x = x_val;
y = y_val;
std::cout << "带参数的 Point 构造函数被调用: x = " << x << ", y = " << y << std::endl;
}

void display() {
std::cout << "Point(" << x << ", " << y << ")" << std::endl;
}

};

int main() { // 测试默认构造函数 Point p1; // 应该使用默认构造函数,x和y应该因为类内初始化而为0 std::cout << “p1 (默认构造): “; p1.display();

Code
1
2
3
4
5
6
// 测试带参数的构造函数
Point p2(10, 20);
std::cout << "p2 (带参构造): ";
p2.display();

return 0;

}`

评分和解释:

假设你按照以下方式完成了修改(或者你的思路与此一致),那么你做得非常好!

  1. 添加类内初始值 (In-class initializers):

    C++

    int x = 0; int y = 0;

    • 为什么这么做? 当我们使用 Point() = default; 时,编译器生成的默认构造函数会检查成员变量是否有类内初始值。如果有,这些初始值就会被用来初始化对象的成员。这样,通过默认构造函数创建的 Point 对象 (如 Point p1;) 就会自动拥有 x=0y=0 的状态,而不是未定义的值。这是现代 C++ 中推荐的做法,可以确保对象成员总是有良好定义的初始状态。
  2. 声明公有的默认构造函数使用 = default:

    C++

    public: // 确保在 public 区 Point() = default;

    • 为什么这么做?
      • Point():定义了一个名为 Point 的构造函数,它没有参数,这正是默认构造函数的形式。
      • = default:告诉编译器:“请你为我生成这个默认构造函数的标准实现版本。” 因为我们已经有了带参数的构造函数 Point(int x_val, int y_val),编译器不会再自动提供隐式的默认构造函数,所以我们必须显式地要求一个。使用 = default 是最简洁、最能体现意图的方式。
      • public::将默认构造函数放在 public 区域,使得类的外部代码(例如 main 函数)可以调用它来创建 Point 对象,如 Point p1;。如果它是 privateprotected 的,那么 Point p1; 这样的语句在 main 中就会编译失败。

总结一下关键点:

  • 一旦定义了任何自定义构造函数,就不会再有隐式的默认构造函数。
  • = default 用于请求编译器生成默认版本的特殊成员函数(构造函数、析构函数、拷贝/移动操作等)。
  • 类内初始化 (int x = 0;) 与 = default 的默认构造函数配合良好,可以为通过默认构造函数创建的对象提供明确的初始值。
  • 构造函数的访问权限(通常是 public)决定了它在哪里可以被调用。

8. 扩展学习与思考建议

非常好!你已经掌握了默认构造函数和 = default 的基本用法。为了进一步加深理解和拓展你的 C++ 知识,你可以思考和研究以下问题:

  1. 析构函数 (Destructor) 与 = default
    • 了解什么是析构函数 (~ClassName())。
    • 析构函数可以被 = default 吗?在什么情况下你会这样做?(提示:通常是的,特别是当类不管理需要手动释放的资源时。)
  2. 拷贝构造函数 (Copy Constructor) 与 = default
    • 什么是拷贝构造函数 (ClassName(const ClassName& other))?它什么时候被调用?
    • 如果你不提供拷贝构造函数,编译器会自动生成一个吗?它的行为是什么?
    • 什么时候你会使用 ClassName(const ClassName& other) = default;?什么时候你会选择 = delete?(= delete 是另一个有用的关键字,表示禁止编译器生成某个函数。)
  3. 拷贝赋值运算符 (Copy Assignment Operator) 与 = default
    • 什么是拷贝赋值运算符 (ClassName& operator=(const ClassName& other))?
    • 与拷贝构造函数类似,研究编译器行为以及 = default= delete 的应用。
  4. 移动构造函数 (Move Constructor) 与移动赋值运算符 (Move Assignment Operator)(C++11及以后):
    • 这些是更高级的概念,与右值引用 (rvalue reference) 和资源所有权转移相关。了解它们也可以使用 = default= delete
  5. “平凡的” (Trivial) 默认构造函数:
    • 搜索一下什么是 “trivial default constructor”。了解一个类在什么条件下其默认构造函数是平凡的。使用 = default 有时可以帮助生成平凡的特殊成员函数,这在某些性能敏感或底层编程场景下有意义。
  6. 构造函数中的 explicit 关键字:
    • 了解单参数构造函数(除了拷贝/移动构造函数)默认可以用于隐式类型转换。
    • 学习如何使用 explicit 关键字来阻止这种隐式转换,以及为什么这通常是个好主意。例如:explicit Point(int val) : x(val), y(val) {}
  7. 委托构造函数 (Delegating Constructors) (C++11及以后):
    • 一个构造函数可以调用同一个类中的另一个构造函数来完成部分或全部初始化工作。这如何与默认构造函数或 = default 配合(或不配合)?

通过研究这些相关主题,你将对 C++ 中的类、对象创建、资源管理以及编译器提供的便利功能有更全面的理解。

继续努力,C++ 的世界非常广阔且充满乐趣!如果你在学习这些新内容时遇到任何问题,随时可以提问。


评论