继承
活期账户和储蓄账户都有余额、都能存钱;储蓄账户另外还能算利息。写成两个各管各的类,余额和 deposit 就要写两遍,一份里修好的错,另一份里还在。继承让一样的部分只写一次,写在一个类里,另一个类在它上面接着盖:
1#include <iostream>2 3class Account {4public:5 void deposit(int amount) {6 balance_ += amount;7 }8 9 int balance() const {10 return balance_;11 }12 13private:14 int balance_ = 0;15};16 17class SavingsAccount : public Account {18public:19 void add_interest(int percent) {20 deposit(balance() * percent / 100);21 }22};23 24int main() {25 SavingsAccount s;26 s.deposit(200);27 s.add_interest(5);28 std::cout << "余额:" << s.balance() << '\n';29 return 0;30}
余额:210
SavingsAccount 里没有写 deposit、balance、balance_,s.deposit(200) 却能用:这些都是从 Account 那里继承来的。它只写了自己新加的 add_interest。点单步:s 里面先是一块 <Account>,balance_ 就在里面,存钱时变的是这一块。
基类和派生类
class SavingsAccount : public Account 读作「SavingsAccount 是一种 Account,再加上这里写的」。Account 叫基类(也叫父类),SavingsAccount 叫派生类(也叫子类)。一个基类可以有好几个派生类:活期账户、储蓄账户、儿童账户都可以建在 Account 上。
派生类拿到了基类的所有成员,可基类的 private 成员还是基类自己的:派生类的成员函数也碰不到。试试把利息直接加到 balance_ 上:
1#include <iostream>2 3class Account {4public:5 void deposit(int amount) {6 balance_ += amount;7 }8 9 int balance() const {10 return balance_;11 }12 13private:14 int balance_ = 0;15};16 17class SavingsAccount : public Account {18public:19 void add_interest(int percent) {20 balance_ += balance_ * percent / 100;21 }22};23 24int main() {25 SavingsAccount s;26 s.deposit(200);27 s.add_interest(5);28 std::cout << "余额:" << s.balance() << '\n';29 return 0;30}
main.cpp: In member function 'void SavingsAccount::add_interest(int)':
main.cpp:20:9: error: 'int Account::balance_' is private within this context
20 | balance_ += balance_ * percent / 100;
| ^~~~~~~~
main.cpp:14:9: note: declared private here
14 | int balance_ = 0;
| ^~~~~~~~'int Account::balance_' is private within this context。private 的意思是只有 Account 自己的成员函数能用,派生类也不行。所以第一个例子绕了一下,用的是 public 的 deposit 和 balance。
注意: : public Account 里的 public 一定要写。不写的话,class 是按 private 方式继承,那是另一回事(这门课不用)。
protected
public 和 private 中间还有一级:**protected**。类自己的函数和派生类的函数都能用,外面的代码还是不能用。
1#include <iostream>2 3class Account {4public:5 void deposit(int amount) {6 balance_ += amount;7 }8 9 int balance() const {10 return balance_;11 }12 13protected:14 int balance_ = 0;15};16 17class SavingsAccount : public Account {18public:19 void add_interest(int percent) {20 balance_ += balance_ * percent / 100;21 }22};23 24int main() {25 SavingsAccount s;26 s.deposit(200);27 s.add_interest(5);28 s.balance_ = 1000000;29 std::cout << "余额:" << s.balance() << '\n';30 return 0;31}
main.cpp: In function 'int main()':
main.cpp:28:7: error: 'int Account::balance_' is protected within this context
28 | s.balance_ = 1000000;
| ^~~~~~~~
main.cpp:14:9: note: declared protected here
14 | int balance_ = 0;
| ^~~~~~~~这回 add_interest 编译通过了。错在第 28 行,在 main 里:'int Account::balance_' is protected within this context。对 main 来说,protected 和 private 一样。删掉这一行,就显示「余额:210」。
小技巧: protected 的数据,以后谁写的派生类都能改,没有 private 安全。像第一个例子那样用 public 成员函数就够的时候,数据还是放 private;派生类真正需要的才放 protected。
构造:先父后子
派生类的对象里面有一块基类的部分,这一块由基类的构造函数来建。派生类的构造函数在初始化列表里用基类的名字调用它:: Animal(name)。让每个构造函数和析构函数都显示一行,就能看出先后:
1#include <iostream>2#include <string>3 4class Animal {5public:6 Animal(const std::string& name) : name_(name) {7 std::cout << "动物 " << name_ << " 建好了\n";8 }9 10 ~Animal() {11 std::cout << "动物 " << name_ << " 拆掉了\n";12 }13 14protected:15 std::string name_;16};17 18class Dog : public Animal {19public:20 Dog(const std::string& name, const std::string& breed) : Animal(name), breed_(breed) {21 std::cout << "狗 " << name_ << "(" << breed_ << ")建好了\n";22 }23 24 ~Dog() {25 std::cout << "狗 " << name_ << " 拆掉了\n";26 }27 28private:29 std::string breed_;30};31 32int main() {33 Dog d{"旺财", "比格犬"};34 std::cout << "--- main 结束\n";35 return 0;36}
动物 旺财 建好了 狗 旺财(比格犬)建好了 --- main 结束 狗 旺财 拆掉了 动物 旺财 拆掉了
建的时候先父后子:先建 Animal 那一块,再建 Dog 自己的成员,最后执行 Dog 构造函数的函数体,这时用 name_ 已经没问题了。拆的时候反过来:先 Dog 的析构函数,再 Animal 的。像盖房子:先打地基,最后上屋顶;拆的时候先拆屋顶。在第 33 行点单步,看程序进了 Dog 的构造函数,又跳到 Animal 的,再回来。
如果初始化列表里没写 Animal(name),编译器就用 Animal() 来建基类部分,这里没有这个构造函数,于是报错 no matching function for call to 'Animal::Animal()'。
这段程序显示什么?
1#include <iostream>2 3class Vehicle {4public:5 Vehicle() {6 std::cout << "V+ ";7 }8 9 ~Vehicle() {10 std::cout << "V- ";11 }12};13 14class Car : public Vehicle {15public:16 Car() {17 std::cout << "C+ ";18 }19 20 ~Car() {21 std::cout << "C- ";22 }23};24 25int main() {26 Car c;27 return 0;28}
Car() 没有写基类的构造函数,就用 Vehicle(),而且它在 Car 的函数体之前执行。c 死的时候顺序反过来:先 Car 的析构函数,再 Vehicle 的。
派生类改写基类的函数
派生类可以写一个和基类里同名的函数。经理的介绍和普通员工一样,再加上带几个人:
1#include <iostream>2#include <string>3 4class Employee {5public:6 Employee(const std::string& name, int salary) : name_(name), salary_(salary) {}7 8 void describe() const {9 std::cout << name_ << ",月薪 " << salary_ << '\n';10 }11 12protected:13 std::string name_;14 int salary_;15};16 17class Manager : public Employee {18public:19 Manager(const std::string& name, int salary, int team) : Employee(name, salary), team_(team) {}20 21 void describe() const {22 Employee::describe();23 std::cout << " 带 " << team_ << " 个人\n";24 }25 26private:27 int team_;28};29 30int main() {31 Employee e{"小林", 4000};32 Manager m{"老王", 6000, 5};33 e.describe();34 m.describe();35 return 0;36}
小林,月薪 4000 老王,月薪 6000 带 5 个人
m.describe() 找到的是 Manager 的 describe,它把 Employee 里的那个隐藏了。在它里面,Employee::describe() 写全名调用基类的版本,第一行就不用再写一遍。如果漏了 Employee::,describe() 调用的是 Manager 的版本,也就是自己,一遍又一遍,停不下来。
派生类对象能当基类用
经理也是员工,所以要员工的地方,都能放经理。参数是 const Employee& 的函数,两种都能用:
1#include <iostream>2#include <string>3 4class Employee {5public:6 Employee(const std::string& name, int salary) : name_(name), salary_(salary) {}7 8 const std::string& name() const {9 return name_;10 }11 12protected:13 std::string name_;14 int salary_;15};16 17class Manager : public Employee {18public:19 Manager(const std::string& name, int salary, int team) : Employee(name, salary), team_(team) {}20 21private:22 int team_;23};24 25void show_badge(const Employee& e) {26 std::cout << "[ " << e.name() << " ]\n";27}28 29int main() {30 Employee e{"小林", 4000};31 Manager m{"老王", 6000, 5};32 show_badge(e);33 show_badge(m);34 Employee* p = &m;35 std::cout << p->name() << '\n';36 return 0;37}
[ 小林 ] [ 老王 ] 老王
一个 show_badge 管所有员工:引用 e 只看见 m 里 Employee 的那一块。指针也一样:Employee* p = &m;。反过来不行:不是每个员工都是经理,所以 Manager* q = &e; 是编译错误。
只看见 Employee 那一块,有一个问题:
这段程序显示什么?
1#include <iostream>2#include <string>3 4class Employee {5public:6 void describe() const {7 std::cout << "员工\n";8 }9};10 11class Manager : public Employee {12public:13 void describe() const {14 std::cout << "经理\n";15 }16};17 18int main() {19 Manager m;20 Employee* p = &m;21 p->describe();22 return 0;23}
p 是 Employee*,编译器按指针的类型挑函数,不看它指着的是什么对象。所以执行的是 Employee::describe,虽然 p 指着的是 Manager。下一课的 virtual 就是来改这一点的。
复制成基类对象
基类的指针或引用,连着的还是整个对象。复制成一个基类对象就不一样了:Employee 类型的变量只放得下 Employee 那一块,多出来的部分被切掉。这叫切片(slicing):
1#include <iostream>2#include <string>3 4class Employee {5public:6 Employee(const std::string& name, int salary) : name_(name), salary_(salary) {}7 8 void describe() const {9 std::cout << name_ << ",月薪 " << salary_ << '\n';10 }11 12protected:13 std::string name_;14 int salary_;15};16 17class Manager : public Employee {18public:19 Manager(const std::string& name, int salary, int team) : Employee(name, salary), team_(team) {}20 21private:22 int team_;23};24 25int main() {26 Manager m{"老王", 6000, 5};27 Employee copy = m;28 copy.describe();29 return 0;30}
老王,月薪 6000
copy 是一个普通的 Employee,有老王的名字和月薪,team_ 没有跟过来。点单步:m 里有一块 <Employee> 和 team_,copy 里只有名字和月薪。要把派生类对象当基类用,传引用或指针,不要复制。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
书也是商品
✓写一个从 Product 派生的类 Book。它多一个作者;构造函数接收书名、价格和作者,它的 show 先显示商品那一行,再单独一行显示 作者: 和作者名。程序要显示下面四行。
三体:23 元 作者:刘慈欣 活着:20 元 作者:余华
1#include <iostream>2#include <string>3 4class Product {5public:6 Product(const std::string& name, int price) : name_(name), price_(price) {}7 8 void show() const {9 std::cout << name_ << ":" << price_ << " 元\n";10 }11 12private:13 std::string name_;14 int price_;15};16 17// 在这里写 Book 类18 19int main() {20 Book a{"三体", 23, "刘慈欣"};21 Book b{"活着", 20, "余华"};22 a.show();23 b.show();24 return 0;25}
修好它:加速碰不到速度
✓SportsCar::boost 编译不过。改 Vehicle 里的一个词,让派生类能改 speed_,而 speed_ 对类外面还是藏着的(main 不能改它,所以不能改成 public)。程序要显示 130 180。
130 180
1#include <iostream>2 3class Vehicle {4public:5 Vehicle(int speed) : speed_(speed) {}6 7 int speed() const {8 return speed_;9 }10 11private:12 int speed_;13};14 15class SportsCar : public Vehicle {16public:17 SportsCar(int speed) : Vehicle(speed) {}18 19 void boost() {20 speed_ += 30;21 }22};23 24int main() {25 SportsCar a{100};26 SportsCar b{120};27 a.boost();28 b.boost();29 b.boost();30 std::cout << a.speed() << " " << b.speed() << '\n';31 return 0;32}
入住和退房
✓给 Suite 写一个构造函数,接收房间号,显示 套房 101:迷你吧装满;再写一个析构函数,显示 套房 101:迷你吧清空(号码是它自己的)。Room 已经会显示自己的那几行。输出要一模一样。
房间 101 准备好了 套房 101:迷你吧装满 房间 102 准备好了 套房 102:迷你吧装满 熄灯 套房 102:迷你吧清空 房间 102 打扫好了 套房 101:迷你吧清空 房间 101 打扫好了
1#include <iostream>2 3class Room {4public:5 Room(int number) : number_(number) {6 std::cout << "房间 " << number_ << " 准备好了\n";7 }8 9 ~Room() {10 std::cout << "房间 " << number_ << " 打扫好了\n";11 }12 13protected:14 int number_;15};16 17class Suite : public Room {18public:19};20 21int main() {22 Suite a{101};23 Suite b{102};24 std::cout << "熄灯\n";25 return 0;26}
这一课学会了
class Derived : public Base在一个类上面接着盖另一个类:派生类拿到基类的成员,再加上自己的。public一定要写。- 基类的
private成员,派生类里也不能用;protected成员派生类能用,外面不能用。 - 派生类的构造函数在初始化列表里建基类部分:
: Animal(name)。建的时候先父后子;析构反过来,先子后父。 - 派生类里同名的函数会隐藏基类的;
Base::f()调用基类的版本。 - 派生类对象可以当
Base&传、用Base*指着。通过它们调用的是基类的函数,即使派生类改写了;第 24 课解决这个问题。 - 把派生类对象复制成基类对象,只留下基类那一块(切片)。