2. 默认构造函数:编译器自动生成的默认构造函数、自定义默认构造函数、=default与=delete
好,咱们来聊聊默认构造函数。这东西看起来简单,但坑不少。我见过不少新手,甚至有些老手,都在这里栽过跟头。
说白了,默认构造函数就是那种不需要你传任何参数就能调用的构造函数。它有两种形态:一种是编译器悄悄给你生成的,另一种是你自己手写的。
2.1 编译器自动生成的默认构造函数
什么时候编译器会帮你生成一个默认构造函数?答案是:当你一个构造函数都没写的时候。
举个例子:
class Point {
public:
int x;
int y;
};
int main() {
Point p; // 这里调用了编译器生成的默认构造函数
return 0;
}
你看,我没写任何构造函数,但代码能编译通过。为什么?因为编译器偷偷给我生成了一个。这个生成的构造函数,行为很简单——它什么都不做。对,就是什么都不做。x 和 y 的值是未初始化的,是垃圾值。
注意:编译器生成的默认构造函数,不会初始化内置类型的成员变量。int、double、指针这些,统统是未定义的值。我在项目中就遇到过,有人以为编译器会帮忙清零,结果线上出了诡异的bug,查了一整天才发现是成员变量没初始化。
那什么时候编译器不会生成默认构造函数?
- 你定义了任何构造函数(哪怕带参数的)
- 类中有 const 或引用类型的成员,且你没有提供默认初始化
- 基类或成员对象的默认构造函数不可访问(比如是私有的)
嗯,这里要注意第二点。我曾经帮一个同事排查编译错误,他写了个类,里面有个 const int 成员,然后想用默认构造。编译器直接报错。为什么?因为 const 成员必须在初始化列表中初始化,编译器生成的默认构造不会帮你做这件事。
2.2 自定义默认构造函数
很多时候,编译器生成的那个不够用。你需要自己写一个。
class Point {
public:
Point() : x(0), y(0) {} // 自定义默认构造函数
int x;
int y;
};
这样写,p.x 和 p.y 就保证是 0 了。你想想看,这比依赖编译器行为要安全得多。
我个人习惯是:只要类中有内置类型的成员,我一定会写默认构造函数,或者在声明时直接给默认值:
class Point {
public:
int x = 0;
int y = 0;
};
C++11 之后,这种写法也很常见。编译器生成的默认构造会使用这些默认值。省事又安全。
2.3 =default 与 =delete
这两个关键字是 C++11 引入的。说实话,刚出来的时候我觉得没什么用,后来用多了才发现——真香。
=default:显式要求编译器生成默认版本
有时候你写了其他构造函数,但还想要默认构造函数。这时候可以用 =default:
class Point {
public:
Point() = default; // 让编译器生成默认构造
Point(int x, int y) : x_(x), y_(y) {}
private:
int x_ = 0;
int y_ = 0;
};
这里有个细节:如果你用 =default,编译器生成的默认构造是 trivial 的。什么意思?就是它跟 C 语言的结构体初始化一样高效,没有任何额外开销。但如果你自己写一个空的构造函数,哪怕里面什么都不做,它也不是 trivial 的。
小技巧:如果你只是想让默认构造存在,而且不需要做任何特殊操作,就用 =default。它比你自己写一个空函数体更高效。我在做性能敏感的系统时,特别在意这个。
=delete:禁止生成默认构造函数
有些类,你就不想让别人默认构造。比如一个代表数据库连接的类,没有连接信息怎么构造?
class DatabaseConnection {
public:
DatabaseConnection() = delete; // 禁止默认构造
DatabaseConnection(const std::string& connStr) : connStr_(connStr) {}
private:
std::string connStr_;
};
这样写,如果有人试图 DatabaseConnection db;,编译器直接报错。比在运行时抛异常要好得多——编译期就能发现问题。
我曾经在一个遗留项目中看到有人用 private 构造函数来禁止默认构造,然后写个注释说「不要用这个」。嗯,这种做法太古老了。用 =delete 才是现代 C++ 的方式,意图明确,报错信息也清晰。
2.4 知识体系图
下面这张图帮你理清默认构造函数的核心逻辑:
2.5 总结一下
关于默认构造函数,记住这几条就够了:
- 编译器自动生成:省事但有风险,内置类型不初始化
- 自定义:完全控制,但要注意 trivial 性
- =default:想要编译器生成但又要显式声明时用,高效
- =delete:明确禁止默认构造,比 private 声明优雅得多
我的建议:除非你的类真的不需要默认构造(比如所有成员都必须通过参数初始化),否则尽量提供一个。哪怕用 =default 也行。这样在使用容器(如 vector<T>)时会方便很多。我见过太多人因为没提供默认构造,导致后面用 vector 时各种别扭。
好了,默认构造函数就聊到这里。记住,写 C++ 代码,多想想「这个类能不能默认构造?应不应该默认构造?」想清楚了,代码的质量会上一个台阶。