Kodewick
第 16 课,共 30 课目录

构造函数和析构函数

第 16 课,共 30 课 · 第 4 部分 · 类 · 约 15 分钟

上一课的温度对象一开始都是 0 度,要另外调用 set_celsius 才有值。忘了调用,就带着一个不对的值往下跑。构造函数解决这个问题:创建对象的时候就把值给它。

main.cpp
1#include <iostream>2#include <string>3 4class Account {5public:6    Account(const std::string& owner, int balance) : owner_(owner), balance_(balance) {7        std::cout << "开户:" << owner_ << '\n';8    }9 10    void show() const {11        std::cout << owner_ << " 的余额:" << balance_ << '\n';12    }13 14private:15    std::string owner_;16    int balance_;17};18 19int main() {20    Account acc{"小林", 100};21    acc.show();22    return 0;23}
输出
开户:小林
小林 的余额:100

构造函数是一个和类同名、没有返回类型(连 void 都不写)的成员函数。Account acc{"小林", 100}; 创建对象时,花括号里的值交给构造函数,它自动执行,不用(也不能)自己去调用。点单步:执行到这一行时,先跳进构造函数,acc 的两个字段填好以后才回到 main。

初始化列表

构造函数括号后面、花括号前面的 : owner_(owner), balance_(balance) 叫初始化列表:成员在出生的那一刻就拿到值。也可以在花括号里赋值,可那样成员是先出生、后改值,多做了一步;有的成员还根本不让改,比如 const 成员:

main.cpp
1#include <iostream>2 3class Ticket {4public:5    Ticket(int number) {6        number_ = number;7    }8 9    int number() const {10        return number_;11    }12 13private:14    const int number_;15};16 17int main() {18    Ticket t{7};19    std::cout << t.number() << '\n';20    return 0;21}
输出
main.cpp: In constructor 'Ticket::Ticket(int)':
main.cpp:6:17: error: assignment of read-only member 'Ticket::number_'
    6 |         number_ = number;
      |         ~~~~~~~~^~~~~~~~

assignment of read-only member 'Ticket::number_':number_ 是 const,出生以后就不能再赋值了。写成 Ticket(int number) : number_(number) {} 就好了。这门课的构造函数都用初始化列表。

注意: 成员是按类里声明的顺序初始化的,不是按初始化列表里写的顺序。初始化列表最好照声明的顺序写,免得一个成员用到另一个还没初始化的成员。

默认构造函数

类一旦有了自己写的构造函数,Fraction f; 这样什么都不给就不行了:

main.cpp
1#include <iostream>2 3class Fraction {4public:5    Fraction(int num, int den) : num_(num), den_(den) {}6 7    void show() const {8        std::cout << num_ << "/" << den_ << '\n';9    }10 11private:12    int num_;13    int den_;14};15 16int main() {17    Fraction f;18    f.show();19    return 0;20}
输出
main.cpp: In function 'int main()':
main.cpp:17:14: error: no matching function for call to 'Fraction::Fraction()'
   17 |     Fraction f;
      |              ^

no matching function for call to 'Fraction::Fraction()':找不到不带参数的构造函数。不带参数的构造函数叫默认构造函数。一个构造函数都没写时,编译器替你生成一个;自己写了任何一个,它就不管了。

构造函数也能重载(第 9 课),想要哪几种就写哪几种。构造函数还是守规矩的好地方,比如分母不能是 0、负号放在分子上:

main.cpp
1#include <iostream>2 3class Fraction {4public:5    Fraction() : num_(0), den_(1) {}6 7    Fraction(int num, int den) : num_(num), den_(den) {8        if (den_ == 0) {9            den_ = 1;10        }11        if (den_ < 0) {12            num_ = -num_;13            den_ = -den_;14        }15    }16 17    void show() const {18        std::cout << num_ << "/" << den_ << '\n';19    }20 21private:22    int num_;23    int den_;24};25 26int main() {27    Fraction zero;28    Fraction half{1, 2};29    Fraction odd{3, -4};30    zero.show();31    half.show();32    odd.show();33    return 0;34}
输出
0/1
1/2
-3/4

Fraction zero; 用默认构造函数,得到 0/1;odd{3, -4} 进了构造函数就变成 -3/4。不管对象怎么创建,出生时都已经合乎规矩。

析构函数

有生就有死。对象死的时候会自动调用析构函数:名字是 ~ 加类名,没有参数、没有返回类型。让构造函数和析构函数各显示一行,就能看见对象什么时候生、什么时候死:

main.cpp
1#include <iostream>2#include <string>3 4class Guest {5public:6    Guest(const std::string& name) : name_(name) {7        std::cout << name_ << " 进门\n";8    }9 10    ~Guest() {11        std::cout << name_ << " 出门\n";12    }13 14private:15    std::string name_;16};17 18int main() {19    std::cout << "开门\n";20    Guest a{"小林"};21    std::cout << "关门\n";22    return 0;23}
输出
开门
小林 进门
关门
小林 出门

「关门」显示以后,main 到了结尾,a 死了,析构函数显示「小林 出门」。点单步,看 return 0; 之后执行跳进了 ~Guest()。

平时很少要自己写析构函数。std::string、std::vector 都有自己的析构函数,死的时候把占的内存还回去,所以用它们从来不用操心收拾。等到第 20、21 课自己向系统要内存时,析构函数就是收拾的地方。

作用域结束,倒着拆

对象在声明它的那一对花括号(作用域)结束时死。几个对象一起死时,后创建的先死,像搭积木,最后放上去的最先拿下来:

main.cpp
1#include <iostream>2#include <string>3 4class Guest {5public:6    Guest(const std::string& name) : name_(name) {7        std::cout << name_ << " 进门\n";8    }9 10    ~Guest() {11        std::cout << name_ << " 出门\n";12    }13 14private:15    std::string name_;16};17 18int main() {19    Guest a{"小林"};20    {21        Guest b{"阿明"};22        Guest c{"小美"};23        std::cout << "--- 里面的花括号结束\n";24    }25    std::cout << "--- main 结束\n";26    return 0;27}
输出
小林 进门
阿明 进门
小美 进门
--- 里面的花括号结束
小美 出门
阿明 出门
--- main 结束
小林 出门

b 和 c 在里面那对花括号结束时就死了,c 后来,先走;a 一直活到 main 结束。请点单步一行一行走,看画面上的对象出现和消失。

函数里的对象也一样,函数返回时就死;循环体里的对象,每转一圈都会生一次、死一次:

先猜一猜

这段程序显示什么?

1#include <iostream>2 3class Box {4public:5    Box(int n) : n_(n) {6        std::cout << "+" << n_ << " ";7    }8 9    ~Box() {10        std::cout << "-" << n_ << " ";11    }12 13private:14    int n_;15};16 17int main() {18    for (int i = 1; i <= 2; i++) {19        Box b{i};20    }21    std::cout << '\n';22    return 0;23}

vector 里的对象

装在 vector 里的对象归 vector 管:pop_back 去掉一个,那个就死;vector 自己死时,里面剩下的全部跟着死。std::vector<Seat> row(3); 用默认构造函数做出 3 个:

main.cpp
1#include <iostream>2#include <vector>3 4class Seat {5public:6    Seat() {7        std::cout << "摆好一把椅子\n";8    }9 10    ~Seat() {11        std::cout << "收起一把椅子\n";12    }13};14 15int main() {16    std::vector<Seat> row(3);17    std::cout << "有 " << row.size() << " 把\n";18    row.pop_back();19    std::cout << "还剩 " << row.size() << " 把\n";20    return 0;21}
输出
摆好一把椅子
摆好一把椅子
摆好一把椅子
有 3 把
收起一把椅子
还剩 2 把
收起一把椅子
收起一把椅子

先摆好 3 把;pop_back 收起 1 把;main 结束,row 死了,剩下的 2 把跟着收起。vector 里的元素按什么顺序析构,标准没有规定(g++ 是从前往后),别依赖它。

注意: 用 push_back 往 vector 里放对象时,你会看到多出来的构造和析构:放进去的是一份复制或「搬」过去的东西,vector 变大时还会整体搬家。第 18 课讲复制和移动,第 28 课讲 vector 怎么变大。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

长方形的构造函数

给 Rectangle 写一个构造函数:用初始化列表收下宽和高,再显示一行 新长方形:3 x 4。程序要显示下面两行。

要显示
新长方形:3 x 4
面积:12
exercise1.cpp
1#include <iostream>2 3class Rectangle {4public:5 6    int area() const {7        return width_ * height_;8    }9 10private:11    int width_;12    int height_;13};14 15int main() {16    Rectangle r{3, 4};17    std::cout << "面积:" << r.area() << '\n';18    return 0;19}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:零钱包从零开始

Wallet w; 编译不过。给 Wallet 加一个默认构造函数,让钱包从 0 分开始。程序要显示 350 分。

要显示
350 分
exercise2.cpp
1#include <iostream>2 3class Wallet {4public:5    Wallet(int cents) : cents_(cents) {}6 7    void add(int cents) {8        cents_ += cents;9    }10 11    int cents() const {12        return cents_;13    }14 15private:16    int cents_;17};18 19int main() {20    Wallet w;21    w.add(200);22    w.add(150);23    std::cout << w.cents() << " 分\n";24    return 0;25}
输出
点「运行」或按 Ctrl+Enter
练习 3

开会的人走了

给 Member 写一个析构函数,显示「名字 离开会议室」,让输出和要求的一样。想想谁先走。

要显示
小林 来开会
阿明 来开会
散会
阿明 离开会议室
小林 离开会议室
exercise3.cpp
1#include <iostream>2#include <string>3 4class Member {5public:6    Member(const std::string& name) : name_(name) {7        std::cout << name_ << " 来开会\n";8    }9 10private:11    std::string name_;12};13 14int main() {15    Member a{"小林"};16    Member b{"阿明"};17    std::cout << "散会\n";18    return 0;19}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 构造函数和类同名、没有返回类型,创建对象时自动执行:Account acc{"小林", 100};。
  • 初始化列表 : owner_(owner), balance_(balance) 让成员一出生就有值;const 成员只能这样初始化。成员按声明的顺序初始化。
  • 不带参数的是默认构造函数。自己写了任何构造函数,编译器就不再生成默认的;需要就自己写一个。构造函数可以重载,也是守规矩的好地方。
  • 析构函数是 ~类名(),对象死时自动执行;string、vector 靠它把内存还回去。
  • 对象在自己的作用域结束时死,同一个作用域里后创建的先死;循环体里的对象每一圈生一次、死一次。
  • vector 里的对象在 pop_back 或 vector 死时死。