Kodewick
第 23 课,共 30 课目录

继承

第 23 课,共 30 课 · 第 6 部分 · 继承和模板 · 约 20 分钟

活期账户和储蓄账户都有余额、都能存钱;储蓄账户另外还能算利息。写成两个各管各的类,余额和 deposit 就要写两遍,一份里修好的错,另一份里还在。继承让一样的部分只写一次,写在一个类里,另一个类在它上面接着盖:

main.cpp
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_ 上:

main.cpp
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**。类自己的函数和派生类的函数都能用,外面的代码还是不能用。

main.cpp
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)。让每个构造函数和析构函数都显示一行,就能看出先后:

main.cpp
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}

派生类改写基类的函数

派生类可以写一个和基类里同名的函数。经理的介绍和普通员工一样,再加上带几个人:

main.cpp
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& 的函数,两种都能用:

main.cpp
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}

复制成基类对象

基类的指针或引用,连着的还是整个对象。复制成一个基类对象就不一样了:Employee 类型的变量只放得下 Employee 那一块,多出来的部分被切掉。这叫切片(slicing):

main.cpp
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 里只有名字和月薪。要把派生类对象当基类用,传引用或指针,不要复制。

练一练

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

练习 1

书也是商品

写一个从 Product 派生的类 Book。它多一个作者;构造函数接收书名、价格和作者,它的 show 先显示商品那一行,再单独一行显示 作者: 和作者名。程序要显示下面四行。

要显示
三体:23 元
  作者:刘慈欣
活着:20 元
  作者:余华
exercise1.cpp
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}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:加速碰不到速度

SportsCar::boost 编译不过。改 Vehicle 里的一个词,让派生类能改 speed_,而 speed_ 对类外面还是藏着的(main 不能改它,所以不能改成 public)。程序要显示 130 180。

要显示
130 180
exercise2.cpp
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

入住和退房

给 Suite 写一个构造函数,接收房间号,显示 套房 101:迷你吧装满;再写一个析构函数,显示 套房 101:迷你吧清空(号码是它自己的)。Room 已经会显示自己的那几行。输出要一模一样。

要显示
房间 101 准备好了
套房 101:迷你吧装满
房间 102 准备好了
套房 102:迷你吧装满
熄灯
套房 102:迷你吧清空
房间 102 打扫好了
套房 101:迷你吧清空
房间 101 打扫好了
exercise3.cpp
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • class Derived : public Base 在一个类上面接着盖另一个类:派生类拿到基类的成员,再加上自己的。public 一定要写。
  • 基类的 private 成员,派生类里也不能用;protected 成员派生类能用,外面不能用。
  • 派生类的构造函数在初始化列表里建基类部分:: Animal(name)。建的时候先父后子;析构反过来,先子后父。
  • 派生类里同名的函数会隐藏基类的;Base::f() 调用基类的版本。
  • 派生类对象可以当 Base& 传、用 Base* 指着。通过它们调用的是基类的函数,即使派生类改写了;第 24 课解决这个问题。
  • 把派生类对象复制成基类对象,只留下基类那一块(切片)。