🔷 第十章 类和对象
📄 一句话概括

构造函数是"对象出生时自动执行的那个函数"——它不需要你调用,对象一被造出来它就跑了。它的任务是把对象收拾成一个能用的状态(初始化成员变量),顺便可以检查参数的合法性。名字和类名完全相同、没有返回类型,就是它的两个识别特征。本节把它的基本形态(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; 那一行。记住这张脸——以后写类的时候,"我只写了带参构造,却用了 类名 变量;" 就是它。

六、常见误区

  1. "构造函数要写返回类型 void"——绝对不要。没有返回类型才是构造函数。写了 void Point() 它就变成普通函数了,对象出生时也不会跑它。
  2. "构造函数不认识,得手动调用"——不用手动。Point p(3, 4); 里的括号就是在调构造函数;也可以写 Point p = Point(3, 4);(效果一样,但多一次复制,10.2.2 会看到差别)。
  3. "不写构造函数,编译器会给我一个把成员清 0 的"——不会。编译器给的那个默认构造函数是"空"的——它什么都不做。所以没写构造函数时,成员变量还是垃圾值(这就是本节开头的例子)。真正"清 0"需要你自己写、或者用初始化列表。
  4. "类里必须有构造函数"——不是必须。一个构造函数都不写,编译器会借你一个(空的);一旦写了任何构造函数,就要自己把需要的那些都补齐(第三节那条规矩)。
  5. "构造函数里写了 return 就能提前退出"——语法上不算错,但强烈不建议。构造函数里写 return; 会让对象在半初始化状态下出生,后面用起来全是坑。正确做法:构造函数里只做"把状态设合法"这件事,不做需要中途放弃的逻辑。
自测三问(点开对答案) 1. 构造函数怎么认?两个特征是什么?——**名字与类名完全相同**、**没有返回类型**(连 `void` 都不写)。它在**对象被创建时自动执行**。 2. 为什么"写了带参构造函数之后,`Point p;` 会编译不过"?——因为你**亲手定义了构造方式**后,编译器**不再自动生成默认构造函数**。要用 `Point p;` 就得自己补一个无参版本。 3. 不写构造函数时,对象的成员变量是什么值?——**不确定的垃圾值**(局部对象在栈上,2.3 的坑)。因为编译器给的那个默认构造函数是**空的**,什么都不做。

相关笔记