欢迎光临
我们一直在努力

第十一章 继承与派生

第十一章 继承与派生

1. 继承关系和派生类的概念

1.1 项目引入-人员管理第一次改版

先把翻车那版摊开。

#include <iostream>
#include <string>
using namespace std;

class Student {
public:
Student(string n, int a, int s) : name(n), age(a), score(s) {}
void show() {
cout << name << " " << age << "岁 " << score << "分" << endl;
}
private:
string name;
int age;
int score;
};

class Teacher {
public:
Teacher(string n, int a, int sa) : name(n), age(a), salary(sa) {}
void show() {
cout << name << " " << age << "岁 月薪" << salary << endl;
}
private:
string name;
int age;
int salary;
};

int main() {
Student stu("张三", 20, 78);
Teacher t("李四", 35, 8000);

stu.show(); // 张三 20岁 78分
t.show(); // 李四 35岁 月薪8000

return 0;
}

┌────────────────────────────────────────────┐
│ 抄来的家当 │
├────────────────────────────────────────────┤
│ 两个类,半副同一副面孔 │
│ │
│ class Student { │
│ string name; │
│ int age; │
│ int score; │
│ }; │
│ │
│ class Teacher { │
│ string name; │
│ int age; │
│ int salary; │
│ }; │
│ │
│ 姓名年龄抄了两遍,改一处得记着改另一处 │
└────────────────────────────────────────────┘

两个类的 show 函数,除了最后一行打印的字段不一样,前两行一模一样。name 和 age 这两个字段,两个类各存一份,各写一遍构造,各写一遍赋值。数据量小的时候这只是啰嗦,类一多就是事故温床。

改法的思路一句话,姓名年龄是个人就有,分数只有学生有,工资只有老师有。公共那部分提出来单独立一个类,Student 和 Teacher 伸手拿现成的。

#include <iostream>
#include <string>
using namespace std;

class Person {
public:
Person(string n, int a) : name(n), age(a) {}
void show() {
cout << name << " " << age << "岁";
}
protected:
string name;
int age;
};

class Student : public Person {
public:
Student(string n, int a, int s) : Person(n, a), score(s) {}
void show() {
Person::show();
cout << " " << score << "分" << endl;
}
private:
int score;
};

class Teacher : public Person {
public:
Teacher(string n, int a, int sa) : Person(n, a), salary(sa) {}
void show() {
Person::show();
cout << " 月薪" << salary << endl;
}
private:
int salary;
};

int main() {
Student stu("张三", 20, 78);
Teacher t("李四", 35, 8000);

stu.show(); // 张三 20岁 78分
t.show(); // 李四 35岁 月薪8000

return 0;
}

输出和原来一字不差,代码短了一截。Student 和 Teacher 后头那条尾巴,冒号加 public 加 Person,就是继承。Person 叫基类,也叫父类,Student 和 Teacher 叫派生类,也叫子类。

┌────────────────────────────────────┐
│ 继承这张关系图 │
├────────────────────────────────────┤
│ 一张关系图看亲疏 │
│ │
│ ┌──────────────────────────────┐ │
│ │ Person │ │
│ │ name age │ │
│ │ | │ │
│ │ +—+—+ │ │
│ │ | | │ │
│ │ Student Teacher │ │
│ │ score salary │ │
│ └──────────────────────────────┘ │
│ │
│ 学生老师都是人,公共的提出来 │
│ 公共的家当搬进 Person │
│ 各自的家当留在自己类里 │
└────────────────────────────────────┘

1.2 is a 的关系

继承不是随便认亲戚,前提是 is a。学生是一种人,老师也是一种人,这话通了才能继承。反过来的例子,引擎不是一种车,车轮也不是一种车,那是 has a,车拥有引擎,第十章讲的成员字段就是干这个的。is a 用继承,has a 用成员,这条线划清楚,类关系才不会乱。

继承落到代码上就三样好处。

公共字段基类写一遍,派生类直接拿,改年龄类型只动 Person 一处。公共函数同理,show 的前半段写在 Person 里,两个派生类各补各的后半段。还有个隐性好处,十种人,Person 加上十行尾巴就是十个类,不写继承就是十份全文。

派生类自己能添字段也能添函数,学生加个 score,老师加个 salary,互不打架。派生类还能改基类的函数,换个实现,Student 的 show 想连名带姓打出来,覆盖重写就行,第十章成员函数那套规矩原样管用。

2. 单一继承

2.1 派生类的定义

单一继承就是尾巴上接一个基类,写法固定,class 加派生类名,冒号,继承方式,基类名,一对花括号。

class Student : public Person {
private:
int score;
};

┌──────────────────────────────────────────────────┐
│ 三层访问权限 │
├──────────────────────────────────────────────────┤
│ 谁能摸到字段 │
│ │
│ +————–+————–+————–+ │
│ |权限 |类内 |类外 | │
│ +————–+————–+————–+ │
│ |private |可以 |不行 | │
│ |protected |可以 |不行 | │
│ |public |可以 |可以 | │
│ +————–+————–+————–+ │
│ │
│ 自己看,private 和 protected 一个样 │
│ 差别在派生类里头 │
│ │
│ +—————-+————————–+ │
│ |权限 |派生类里能碰吗 | │
│ +—————-+————————–+ │
│ |private |碰不到,得走窗口 | │
│ |protected |能,当自家的用 | │
│ |public |能,且跟着外泄 | │
│ +—————-+————————–+ │
└──────────────────────────────────────────────────┘

继承方式三种,public、protected、private。日常写 public 继承,九成九的场合够用,protected 和 private 继承冷门,知道有就行,本章不展开。

public 继承下,基类 public 成员到派生类还是 public,基类 protected 成员到派生类还是 protected,基类 private 成员到派生类还是 private,而且派生类够不着。这条传递规矩是理解访问权限的钥匙,下一节的表把它摆平。

2.2 三层访问权限

第十章讲过 private 和 public,本章补上第三种,protected。三种权限放一张表里看。

┌─────────────────────────────────────┐
│ 派生类怎么写 │
├─────────────────────────────────────┤
│ 一条尾巴接上基类 │
│ │
│ class Student : public Person { │
│ int score; │
│ }; │
│ │
│ 冒号后写继承方式 │
│ public 继承最常见 │
│ │
│ Student 自动拿到 │
│ name age │
│ 再加上自己的 score │
└─────────────────────────────────────┘

自己类里头看,protected 和 private 一模一样,都能摸。出了类门,两个都摸不着。差别全在派生类里头,private 字段派生类够不着,只能走基类留的窗口函数,protected 字段派生类能当自家的用,直接摸。

所以 protected 是专门给继承留的档位,基类作者的意思是,这东西外头人别碰,自家孩子可以拿。Person 的 name 和 age 写成 protected,Student 里能直接读,main 里摸不到,正合适。默认不写权限,class 里是 private,struct 里是 public,第十章那条规矩接着算。

2.3 派生类对象的构成

一个派生类对象,肚子里装着两截,基类那一截加上自己这一截。Student 对象前半截是 Person 的 name 和 age,后半截是自己的 score。

#include <iostream>
using namespace std;

class Base {
public:
int x;
};

class Derived : public Base {
public:
int y;
};

int main() {
cout << sizeof(Base) << " " << sizeof(Derived) << endl;

Derived d;
d.x = 1; // 基类的字段直接用
d.y = 2; // 自己的字段
cout << d.x << " " << d.y << endl;

return 0;
}

常见平台输出 4 8。Base 一个 int 四字节,Derived 两个 int 八字节,基类那截原样在里面。字段名不冲突时候,d.x 直接摸上去,和摸自己的字段一个手感,基类部分就是对象的一部分。换成含 string 的 Person,具体多大问 sizeof,编译器算得比手数准。

2.4 同名成员的隐藏

派生类可以和基类有同名成员,函数字段都行。同名时候规矩一条,派生类的遮基类的,近的赢。

#include <iostream>
#include <string>
using namespace std;

class Person {
public:
Person(string n, int a) : name(n), age(a) {}
void show() {
cout << name << " " << age << "岁" << endl;
}
protected:
string name;
int age;
};

class Student : public Person {
public:
Student(string n, int a, int s) : Person(n, a), score(s) {}
void show() {
cout << name << " " << age << "岁 " << score << "分" << endl;
}
private:
int score;
};

int main() {
Student stu("张三", 20, 78);
stu.show(); // 张三 20岁 78分

stu.Person::show(); // 张三 20岁,点名要基版

return 0;
}

┌─────────────────────────────────────┐
│ 同名遮罩 │
├─────────────────────────────────────┤
│ 同名成员,近的遮远的 │
│ │
│ ┌──────────────────────────────┐ │
│ │Person │ │
│ │ void show() │ │
│ │ 打印 姓名 年龄 │ │
│ └──────────────────────────────┘ │
│ | │
│ v │
│ ┌──────────────────────────────┐ │
│ │Student │ │
│ │ void show() │ │
│ │ 打印 姓名 年龄 分数 │ │
│ └──────────────────────────────┘ │
│ │
│ stu.show() 调的是 Student 那个 │
│ 想看基版的,Person::show() 点名 │
└─────────────────────────────────────┘

stu.show() 调的是 Student 自己那个,参数一样也一样遮,不玩重载那套。想看基类原版,双冒号点名,stu.Person::show()。字段同名也一个规矩,派生类声明一个和基类同名的字段,派生类里写的全是自己那个,基类的被遮得严严实实,也要 Person:: 点名才够得着。实际写代码,同名遮罩多数时候是意外的产物,起名留点心能少踩。

2.5 构造和析构的顺序

派生类对象出生,先叫基类构造,再叫派生类构造。老子先到位,儿子后说话。

#include <iostream>
#include <string>
using namespace std;

class Person {
public:
Person(string n, int a) : name(n), age(a) {
cout << "Person 构造 " << name << endl;
}
~Person() {
cout << "Person 析构 " << name << endl;
}
protected:
string name;
int age;
};

class Student : public Person {
public:
Student(string n, int a, int s) : Person(n, a), score(s) {
cout << "Student 构造 分数" << score << endl;
}
~Student() {
cout << "Student 析构 分数" << score << endl;
}
private:
int score;
};

int main() {
Student stu("张三", 20, 78);
return 0;
}

输出四行,Person 构造、Student 构造,函数结束 Student 析构、Person 析构。构造顺序 Person 打头 Student 收尾,析构整个反过来。

┌──────────────────────────────────┐
│ 构造顺着继承走 │
├──────────────────────────────────┤
│ 先老子后儿子 │
│ │
│ Student stu("张三", 20, 78); │
│ │
│ 第一步 Person 构造 │
│ 名字年龄先到位 │
│ 第二步 Student 构造 │
│ 分数后到位 │
│ │
│ 基类不吭声时候,调默认构造 │
└──────────────────────────────────┘

┌──────────────────────────────┐
│ 析构倒着走 │
├──────────────────────────────┤
│ 怎么叠的怎么撤 │
│ │
│ 构造 Person -> Student │
│ 析构 Student -> Person │
│ │
│ 派生类先收拾自己 │
│ 基类断后 │
│ │
│ 和第十章栈上对象一个脾气 │
│ 后进先出 │
└──────────────────────────────┘

派生类构造函数怎么叫基类那个,靠初始化列表点名。Student 构造函数的冒号后头写 Person(n, a),参数递进去。不写这一句,编译器默认去叫 Person 的默认构造,Person 只有带参构造时候直接报错。第十章说过,写了带参构造默认那个就退场,基类这儿同理。

析构函数不用点名,自动连着叫,派生类收拾完自己的,基类断后。第十章栈上对象后进先出那条,顺着继承链接着走。

3. 多重继承

3.1 定义和构造顺序

一条尾巴接一个基类叫单一继承,接两个叫多重继承。在职读书的学生,既是学生也是打工的,两副家当一起接。

#include <iostream>
#include <string>
using namespace std;

class Student {
public:
Student(int s) : score(s) {
cout << "Student 构造" << endl;
}
protected:
int score;
};

class Worker {
public:
Worker(int h) : hourly(h) {
cout << "Worker 构造" << endl;
}
protected:
int hourly;
};

class OnCampusStudent : public Student, public Worker {
public:
OnCampusStudent(int s, int h) : Student(s), Worker(h) {
cout << "OnCampusStudent 构造" << endl;
}

void income() {
cout << "时薪 " << hourly << " 考试 " << score << "分" << endl;
}
};

int main() {
OnCampusStudent ocs(78, 30);
ocs.income(); // 时薪 30 考试 78分

return 0;
}

┌────────────────────────────────────────┐
│ 多重继承 │
├────────────────────────────────────────┤
│ 一个人,两副家当 │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Student Worker │ │
│ │ 分数 工时薪 │ │
│ │ \\ / │ │
│ │ \\ / │ │
│ │ OnCampusStudent │ │
│ │ 分数加工时薪 │ │
│ └──────────────────────────────────┘ │
│ │
│ 在职读书的学生 │
│ 打工的里头藏着读书的 │
│ 两副家当一起继承 │
└────────────────────────────────────────┘

冒号后头并列写两个基类,用逗号隔开。派生类自动拿到两副家当,score 和 hourly 都能直接摸。输出三行构造,Student、Worker、OnCampusStudent 挨个来,顺序按冒号后的名单排队,谁写在前面谁先生,和派生类自己初始化列表里的顺序无关,只看出继承时候的排名。

┌───────────────────────────────────┐
│ 排队的规矩 │
├───────────────────────────────────┤
│ 按继承名单排队 │
│ │
│ class C : public A, public B; │
│ │
│ 构造顺序 │
│ │
│ 第一步 A 构造 │
│ 第二步 B 构造 │
│ 第三步 C 构造 │
│ │
│ 冒号后谁写在前面谁先生 │
│ 和成员初始化列表的顺序无关 │
└───────────────────────────────────┘

析构照旧反着来,OnCampusStudent、Worker、Student。第十章到本章一条铁律,构造从基类往派生类走,析构从派生类往基类退,多重继承只是基类从一个变两个,方向不变。

3.2 同名成员和二义性

多重继承有个坑先知道。两个基类有同名成员,派生类里直接写名字,编译器不知道你要哪个,报二义性。

class A {
public:
void show() { cout << "A" << endl; }
};

class B {
public:
void show() { cout << "B" << endl; }
};

class C : public A, public B {
};

int main() {
C c;
// c.show(); // 报错,不知道调哪个
c.A::show(); // A,点名要 A 版
c.B::show(); // B,点名要 B 版
return 0;
}

双冒号点名解决八成场面。真正麻烦的是菱形继承,两个基类又各自继承同一个类,派生类里那份公共家当有两份,用起来说不清是哪份。解法叫虚继承,virtual 加在继承方式前头,那份家当全类共享一份。虚继承本章只留个名,实际工程里用得少,等真碰上了再翻。

多重继承在工程里要慎重,两个基类各改各的,合起来容易打架。接口型基类用用无妨,带字段带实现的慎接。单一继承能解决的场合,别贪心接第二个。

4. 小结

┌─────────────────────────────────┐
│ 本章收口 │
├─────────────────────────────────┤
│ 继承这套家伙事儿 │
│ │
│ 继承 公共家当搬进基类 │
│ 派生 is a,添自己的字段 │
│ 权限 protected 留给自家人 │
│ 构造 先基后派生,析构反着 │
│ 隐藏 同名成员,近的赢 │
│ 多重 两个基类一起继承 │
│ │
│ 多态和虚函数在下一章 │
└─────────────────────────────────┘

继承的前提是 is a,车拥有引擎是 has a,那用成员字段。基类装公共家当,派生类接着用再添自己的,public 继承最常用。protected 是留给自家孩子的档位,派生类能摸,类外摸不到。派生类对象肚子里两截,基类那截加自己那截,字段直接用。同名成员近的遮远的,基版用双冒号点名。构造先基类后派生类,基类构造靠初始化列表点名递参,析构整个反过来。

多重继承接两个基类,构造按继承名单排队。同名成员二义性用双冒号解,菱形继承留给虚继承。接口型基类可以接,带实现的慎接。

速查卡和排错表在附录,写派生类卡壳时候扫一眼。

附录 A 速查卡

知识点写法说明
单一继承 class D : public B public 继承最常用
继承方式 public / protected / private 日常只写 public
调用基类构造 D() : B(1) 初始化列表点名
protected 字段 派生类能摸,类外不行 留给自家孩子
访问基类成员 obj.B::x 双冒号点名
同名遮罩 派生类声明即遮住 不玩重载
多重继承 class C : public A, public B 按名单排队构造
菱形继承 class C : virtual public A 公共家当留一份
is a 学生是一种人 才配用继承
has a 车有引擎 用成员字段

附录 B 排错表

常见现象病根药方
报错没有默认构造 基类只有带参构造 初始化列表写上基类构造
派生类摸不到基类字段 字段是 private 改 protected 或加窗口
调出的函数不对版 同名被派生类遮了 双冒号点名要基版
报错二义性 两个基类同名成员 A::show() 点名
构造顺序发懵 按继承名单走的 冒号后谁前谁先生
菱形继承字段说不清 公共家当有两份 virtual 继承
改了基类派生类报错 派生类依赖被删成员 检查各处用法再删
切片警告 派生类当基类传 传引用或指针,第十二章细说
赞(0)
未经允许不得转载:171主机测评 » 第十一章 继承与派生
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址