构造函数和析构函数
上一课的温度对象一开始都是 0 度,要另外调用 set_celsius 才有值。忘了调用,就带着一个不对的值往下跑。构造函数解决这个问题:创建对象的时候就把值给它。
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 成员:
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; 这样什么都不给就不行了:
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、负号放在分子上:
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。不管对象怎么创建,出生时都已经合乎规矩。
析构函数
有生就有死。对象死的时候会自动调用析构函数:名字是 ~ 加类名,没有参数、没有返回类型。让构造函数和析构函数各显示一行,就能看见对象什么时候生、什么时候死:
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 课自己向系统要内存时,析构函数就是收拾的地方。
作用域结束,倒着拆
对象在声明它的那一对花括号(作用域)结束时死。几个对象一起死时,后创建的先死,像搭积木,最后放上去的最先拿下来:
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}
b 写在循环体里,它的作用域就是这一圈。每一圈开始时创建,这一圈结束时就死了,下一圈再创建一个新的。
vector 里的对象
装在 vector 里的对象归 vector 管:pop_back 去掉一个,那个就死;vector 自己死时,里面剩下的全部跟着死。std::vector<Seat> row(3); 用默认构造函数做出 3 个:
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 怎么变大。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
长方形的构造函数
✓给 Rectangle 写一个构造函数:用初始化列表收下宽和高,再显示一行 新长方形:3 x 4。程序要显示下面两行。
新长方形:3 x 4 面积:12
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}
修好它:零钱包从零开始
✓Wallet w; 编译不过。给 Wallet 加一个默认构造函数,让钱包从 0 分开始。程序要显示 350 分。
350 分
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}
开会的人走了
✓给 Member 写一个析构函数,显示「名字 离开会议室」,让输出和要求的一样。想想谁先走。
小林 来开会 阿明 来开会 散会 阿明 离开会议室 小林 离开会议室
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}
这一课学会了
- 构造函数和类同名、没有返回类型,创建对象时自动执行:
Account acc{"小林", 100};。 - 初始化列表
: owner_(owner), balance_(balance)让成员一出生就有值;const成员只能这样初始化。成员按声明的顺序初始化。 - 不带参数的是默认构造函数。自己写了任何构造函数,编译器就不再生成默认的;需要就自己写一个。构造函数可以重载,也是守规矩的好地方。
- 析构函数是
~类名(),对象死时自动执行;string、vector靠它把内存还回去。 - 对象在自己的作用域结束时死,同一个作用域里后创建的先死;循环体里的对象每一圈生一次、死一次。
vector里的对象在pop_back或vector死时死。