构造函数是"对象出生时自动执行的那个函数"——它不需要你调用,对象一被造出来它就跑了。它的任务是把对象收拾成一个能用的状态(初始化成员变量),顺便可以检查参数的合法性。名字和类名完全相同、没有返回类型,就是它的两个识别特征。本节把它的基本形态(10.2.1)和特殊形态——复制构造函数(10.2.2)一起交代。
10.1.4 留下了一个问题:Point p; 声明之后,成员变量的值是"不确定的"(2.3 的垃圾值)。构造函数正是来补这个缺口的。 另外,本节会大量用到 6.5 的重载(构造函数可以有好几个版本,靠参数表区分)和 7.8.2 的引用传参(复制构造函数的参数必须是引用,这里会讲为什么)。
一、为什么需要构造函数
先看没有构造函数时的问题:
class Point {
private:
int x, y;
public:
void show() { cout << "(" << x << ", " << y << ")" << endl; }
};
int main() {
Point p;
p.show(); // ⚠️ 打印出什么?—— 不确定!可能是 (0,0),也可能是 (4201168, 32767)
return 0;
}
问题:p 的 x、y 从来没被赋过值,读出来是垃圾值(2.3 的老坑)。更糟的是——这种错误不报错。
还有一个更隐蔽的问题:类里的"规则"守不住。比如一个 BankAccount,余额必须在开户时就设定好,不能被落成垃圾值。如果没有构造函数,外层只能靠"记得手动调 init()"来保证——而"靠记得"就是不可靠。
构造函数解决的正是这两件事:
| 解决的问题 | 怎么解决 |
|---|---|
| 成员变量初始值是垃圾 | 对象一出生就自动跑,把成员变量设置好 |
| 初始化靠"记得调用" | 自动执行,想忘也忘不了 |
一句话:构造函数是"进门手续"——每个对象进这道门,都得先办完手续才能用。
二、构造函数的两个识别特征
class Point {
private:
int x, y;
public:
Point() { // ← 构造函数
x = 0;
y = 0;
}
};
| 特征 | 说明 |
|---|---|
| 函数名 = 类名 | Point 类的构造函数就叫 Point |
| 没有返回类型 | 连 void 都不写(写了 void Point() 就不是构造函数了,是普通函数) |
| 可以没有参数 | 这种叫"默认构造函数"(见下) |
可以被重载(6.5 的知识点直接用上):
Point() // 无参
Point(int a, int b) // 两个参数
Point(int a) // 一个参数
靠参数表区分,编译器自己挑。 于是就有了多种"出生方式":
Point p1; // 走 Point() → (0,0)
Point p2(3, 4); // 走 Point(3,4) → (3,4)
Point p3(5); // 走 Point(5) → 看你怎么写这条
看,10.1.4 里说的"带括号的对象声明"出现了——括号里就是给构造函数的实参。
三、三种构造函数(本章会陆续遇到)
| 种类 | 长相 | 什么时候自动跑 |
|---|---|---|
| 默认构造函数 | Point() |
Point p; 或 Point p{}; |
| 带参构造函数 | Point(int a, int b) |
Point p(3, 4); |
| 复制构造函数 | Point(const Point &other) |
Point q(p); 或 Point q = p;(10.2.2 专讲) |
关于"默认构造函数",有一条极其重要的规矩(本章最容易踩的坑之一):
只要你写了任何一个构造函数,编译器就"不再免费送你默认构造函数"了。
class Point {
private:
int x, y;
public:
Point(int a, int b) { x = a; y = b; } // 写了带参的
};
Point p1(3, 4); // ✅ 行
Point p2; // ❌ 编译错误!没有默认构造函数
报错长这样:no matching function for call to 'Point::Point()'。
这说明什么:你既然亲手定义了"怎么出生",编译器就不敢再替你猜一个默认的。如果你想要"两种出生方式都支持",就自己补一个无参版本:
Point() { x = 0; y = 0; } // 补一个
Point(int a, int b) { x = a; y = b; }
四、本节路线
| 小节 | 内容 |
|---|---|
| 10.2.1 构造函数概述 | 它的完整规则:初始化列表、默认参数、与 new 的关系、执行顺序 |
| 10.2.2 复制构造函数 | "拿一个对象造另一个对象"时自动跑;含实例02;浅拷贝 vs 深拷贝(提前埋个伏笔) |
五、动手 Demo:让 Point "一出生就合法"
用 10.1.4 的类补上构造函数,对比"有 / 无"的差别。
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
public:
Point() { // ① 无参:默认落在原点
x = 0; y = 0;
cout << "无参构造被调用 → (0, 0)" << endl;
}
Point(int a, int b) { // ② 带参:防止非法值
x = (a >= 0) ? a : 0; // 顺手做校验
y = (b >= 0) ? b : 0;
cout << "带参构造被调用 → (" << x << ", " << y << ")" << endl;
}
void show(const char *who) {
cout << who << " 现在是 (" << x << ", " << y << ")" << endl;
}
};
int main() {
Point p1; // 自动跑 Point()
p1.show("p1");
Point p2(3, 4); // 自动跑 Point(3, 4)
p2.show("p2");
Point p3(-9, 6); // 非法值被构造函数兜住
p3.show("p3");
// 注意:这里没有一行代码是"手动调用构造函数"
return 0;
}
看到什么算成功:
无参构造被调用 → (0, 0)
p1 现在是 (0, 0)
带参构造被调用 → (3, 4)
p2 现在是 (3, 4)
带参构造被调用 → (0, 6)
p3 现在是 (0, 6)
读结果:注意输出的顺序——"构造被调用"那句永远排在该对象第一次 show 之前,而我们从来没写过"调用构造函数"这样的代码。这就是"自动执行"。 另外 p3 的 x 被兜成了 0——构造函数顺手把校验也做了,规则从第一刻就守住。
30 秒实验:把 Point()(无参那个)整个删掉,重新编译。你会看到 no matching function for call to 'Point::Point()',而且报错会指向 Point p1; 那一行。记住这张脸——以后写类的时候,"我只写了带参构造,却用了 类名 变量;" 就是它。
六、常见误区
- "构造函数要写返回类型
void"——绝对不要。没有返回类型才是构造函数。写了void Point()它就变成普通函数了,对象出生时也不会跑它。 - "构造函数不认识,得手动调用"——不用手动。
Point p(3, 4);里的括号就是在调构造函数;也可以写Point p = Point(3, 4);(效果一样,但多一次复制,10.2.2 会看到差别)。 - "不写构造函数,编译器会给我一个把成员清 0 的"——不会。编译器给的那个默认构造函数是"空"的——它什么都不做。所以没写构造函数时,成员变量还是垃圾值(这就是本节开头的例子)。真正"清 0"需要你自己写、或者用初始化列表。
- "类里必须有构造函数"——不是必须。一个构造函数都不写,编译器会借你一个(空的);一旦写了任何构造函数,就要自己把需要的那些都补齐(第三节那条规矩)。
- "构造函数里写了
return就能提前退出"——语法上不算错,但强烈不建议。构造函数里写return;会让对象在半初始化状态下出生,后面用起来全是坑。正确做法:构造函数里只做"把状态设合法"这件事,不做需要中途放弃的逻辑。
自测三问(点开对答案)
1. 构造函数怎么认?两个特征是什么?——**名字与类名完全相同**、**没有返回类型**(连 `void` 都不写)。它在**对象被创建时自动执行**。 2. 为什么"写了带参构造函数之后,`Point p;` 会编译不过"?——因为你**亲手定义了构造方式**后,编译器**不再自动生成默认构造函数**。要用 `Point p;` 就得自己补一个无参版本。 3. 不写构造函数时,对象的成员变量是什么值?——**不确定的垃圾值**(局部对象在栈上,2.3 的坑)。因为编译器给的那个默认构造函数是**空的**,什么都不做。- ☐ 10.2.1 构造函数概述(下一节)
相关笔记
- 对象的声明 —— 上一节:声明对象(留下的"垃圾值"问题)
- 构造函数概述 —— 下一节:构造函数的完整规则
- 变量 —— "未初始化=垃圾值"的原始出处
- 重载函数 —— 构造函数可以重载,靠它
- 🔷 C++基础 · 索引 —— 全课路线图