【C++要笑着学】继承 | 子类默认成员函数 | 单继承与多继承 | 钻石继承 | 虚拟继承 | 继承和组合(一)

简介: 本系列 C++ 教学博客的基础知识已经告一段落了,下面的章节我会先把面向对象三大特性讲完,然后穿插一些数据结构的教学以方便我们继续讲解 STL 的 map 和 set。对于面向对象三大特性 —— 封装、继承、多态,我们已经在之前讲解过封装了,本章将开始讲解继承,详细探讨多继承引发的钻石继承问题,并用虚继承解决钻石继承问题。阅读本章需要掌握访问限定符以及默认成员函数的知识,如果阅读过程中感到有些许生疏建议先去复习一下。

💭 写在前面


本系列 C++ 教学博客的基础知识已经告一段落了,下面的章节我会先把面向对象三大特性讲完,然后穿插一些数据结构的教学以方便我们继续讲解 STL 的 map 和 set。对于面向对象三大特性 —— 封装、继承、多态,我们已经在之前讲解过封装了,本章将开始讲解继承,详细探讨多继承引发的钻石继承问题,并用虚继承解决钻石继承问题。阅读本章需要掌握访问限定符以及默认成员函数的知识,如果阅读过程中感到有些许生疏建议先去复习一下。

Ⅰ. 继承(inheritance)


0x00 知识回顾

回顾一下面向对象三大特性:封装、继承、多态。


面向对象还有其它特性:反射、抽象。


① C++ Stack 类设计和 C 设计 Stack 对比,封装更好、访问限定符 + 类   狭义。


② 迭代器设计,如果没有迭代器,容器访问只能暴露底层结构。 -> 使用复杂、使用成本很高,对使用者要求极高。


封装了容器底层结构,不暴露底层结构的情况,提供统一的访问容器的方式,降低使用成本,简化使用。


③ stack/queue/priority_queue 的设计 —— 适配器模式。


今天我们的主角是继承。


0x01 继承的概念

继承(inheritance)机制是面向对象程序设计,使代码可以复用的最重要的手段。


它允许程序员在保持原有类特性的基础上进行扩展,以增加功能。这样产生新的类,称为派生类。


继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。


以前我们接触的复用都是函数复用,而继承是类设计层次的复用。


💭 举例:比如我们要设计一个图书管理系统,每个角色的权限是不同的。


角色类:学生、老师、保安、保洁、后勤…… 为了区分这些角色,我们就要设计一些类出来:

class Student {
    string _name;
    string _tel;
    string _address;
    int _age;
    // ...
    string _stuID;  // 学号
};
class Teacher {
    string _name;
    string _tel;
    string _address;
    int _age;
    // ...
    string _wordID;  // 工号
};
...


不难发现其存在大量冗余部分,有些信息是公共的,有些信息是每个角色独有的。


对于有些数据和方法是每个角色都具有的,我们每次都写一边,这就导致设计重复了。


我们说了代码是要讲究复用的,我们要想办法去做一个 "提取" ,把共有的成员变量提取出来。


💡 解决方案:设计一个 Person 类

// 把大家共有的东西写进来
class Person {
    string _name;
    string _tel;
    string _address;
    string _age;
};

然后使用 "继承" 去把这些大家公有的东西运送给各个角色,先看操作:

class Person {
    /* 共有的信息 */
    string _name;
    string _tel;
    string _address;
    string _age;
};
/* Student 公有继承了 Person */
class Student : public Person {
    string _stuID ;  // 学号
};
/* Teacher 公有继承了 Person */
class Teacher : public Person {
    string _wordID;  // 工号
};

这就是继承。在需要称为子类的类的类名后加上冒号,并跟上继承方式和父类类名即可。


比如说我们这里希望让 Student 以 public 的继承方式继承自 Person。


💬 为了能够演示继承的效果,我们给 Person 类加上个 Print 打印函数:

class Person {
public:
    void Print() {
        cout << "name: " << _name << endl;
        cout << "age:  " << _age << endl;
        cout << endl;
    }
    /* 共有的信息 */
    string _name = "user";
    string _tel;
    string _address;
    string _age = "null";
};
/* Student 公有继承了 Person */
class Student : public Person {
    string _stuID ;  // 学号
};
/* Teacher 公有继承了 Person */
class Teacher : public Person {
    string _wordID;  // 工号
};
int main(void)
{
    Person p;
    p.Print();
    Student s;
    s.Print();
    Teacher t;
    t.Print();
    return 0;
}

🚩 运行结果:

9d0b23f0645f76c82344245f239b0a66_58b2f747644d405c937b5ea99e3d8400.png


0x02 继承的定义格式

我们还是拿刚才的 Person 和 Student 举例:


派生类  继承方式  基类
         👇       👇     👇
class Student : public Person {
public:
    string _stuID;  // 学号
};

Student 是 子类,我们也称之为派生类。Person 是父类,我们也称之为 基类。


个人觉得,把 Person 和 Student 看作是父子关系是比较容易理解的。


子承父业,孩子 Student 从父亲 Person 那里继承一些 "资产" ,


这里的继承方式是 public,即公有继承,还有其他的一些继承方式。


(这里我们先做一个铺垫,复习和补充一下访问限定符的知识)


0x03 访问限定符:public / protected / private

🔗 链接:【C++要笑着学】访问限定符


🎈 知识回顾:对之前没讲的 protected 进行补充


三种访问限定符,分别是 public(公有)、protected(保护)、private(私有)。


这一听名字就能知道,公有就是随便玩,保护和私有就是藏起来一点点不让你随便玩得到。

7ff23bd0c74a7f24a43554de63f56726_baea67530dc249e1a5d30d5879dffe0f.png



① public 修饰的成员,可以在类外面随便访问(直接访问)。


② protected 和 private 修饰的成员,不能在类外随便访问。


③ 定义成 protected 可以让父类成员不能在类外直接访问,但可以在子类中访问。


public、protected、private 不仅仅是访问限定符,它们也可以表示继承的三种继承方式:

59bfab86835e7bd8bedd8eb5213d08b7_12aacd28daff4bf8bdd12411c5dc518c.png


0x04 继承基类成员访问方式的变化

三种访问限定符和三种继承方式相碰撞,就产生了  种情况:

b4bb17d7d097a1d35cdadd4fa332189f_c38b067e7bd149b9a928574d0a4550c7.png

① 父类的 private 成员在子类种无论以何种方式继承都是不可见的。 这里的不可见指的是父类的私有成员还是被继承到了子类对象中,但是语法上限制了子类对象不管在类里面还是类外卖呢都不能去访问父类的 private 成员。


② 父类 private 成员在子类种不能被访问,如果父类成员不想在类外被直接访问,但是想让它们在子类中能被访问,可定义为 protected。 不难看出,保护成员限定符是因继承才出现的。

62bec58e469f534b3431fe3b36229550_360e426bd97e47b6a8f569fbb5b16f55.png

③ 实际上,上面的表格我们通过观察不难发现,父类的私有成员在子类都是不可见的,父类的其他成员在子类的访问方式 == Min(成员在父类的访问限定符,继承方式):

image.png

④ 使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public,但是最好还是显式的写出继承方式,提高代码可读性。


⑤ 一共 9 种组合,实际上是大佬们早期设计的时候想复杂了,实际中父类成员基本都是保护和公有,继承方式基本都是用公有继承,几乎很少使用 protected / private 继承。 而且也不提倡使用 protected / private 继承,因为 protected / private 继承下来的成员都只能在子类里使用,实际扩展维护性不强。

class Person {
public:
    void Print() {
        cout << "name: " << _name << endl;
    }
protected:
    /* 共有的信息 */
    string _name;   
private:
    string _age;
};
// class Student : protected Person {
// class Student : private Person {
class Student : public Person {
protected:
    string _stuID;  // 学号
};

0x05 父类和子类对象赋值转换

子类对象可以赋值给父类的对象、父类的指针、父类的引用:

class Person {
protected:
    string _name;   
    string _age;
};
class Student : public Person {
public:
    string _stuID;  // 学号
};
int main(void)
{
    Student s;
    // 子类对象可以赋值给父类对象/指针/引用
    Person p = s;
    Person* pp = &s;
    Person& rp = s;
    return 0;
}

这种操作我们称之为 "切割"(或切片),寓意是把子类中父类的那部分切过来赋值过去。  

b33abc11164e2c4a0bc07366fe066323_1e3895b555fe4d209ea4f669d33cebfa.png


📌 注意事项:


① 父类对象不能赋值给子类对象(儿子不能抢父亲的钱)

Student s;  // 子类
Person p;   // 父类
s = p; ❌

② 父类的指针可以通过强转赋值给子类的指针,但是必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用 RTTI(Run-Time Type Information,即运行时类型识别)的 dynamic_cast 来进行识别后进行安全转换。

Student s;
Person* pp = &s;
// 父类的指针可以通过强制类型转换赋值给子类的指针
pp = &s;
Student* ps1 = (Student*)pp;
ps1->_stuID = 10001;
pp = &p;
Student* ps2 = (Student*)pp;  // 这种情况虽然可以,但是会存在越界访问问题
ps2->_stuID = 20002;

0x06 继承中的作用域

继承体系中的父类和子类都有独立的作用域,如果子类和父类有同名成员,


此时子类成员会屏蔽父类对同名成员的直接访问,这种情况叫做 "隐藏" (也叫重定义)。


💭 在子类成员函数中,可以使用如下方式进行显式访问:


基类::基类成员

📌 注意事项:


① 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。


② 实际运用中在继承体系里最好不要定义同名的成员。父类成员名称不要和子类成员名称冲突。


💬 代码演示:父类和子类的成员函数同名的场景(注意父类和子类的 _num)

class Person {
protected:
    string _name = "小明";                // 姓名
    string _num = "320103xxxxxxxxxx14";  // 身份证号
};
class Student : public Person {
public:
    void Print() {
        cout << "姓名:" << _name << endl;
        cout << "身份证号: " << Person::_num << endl;  // 指定是Person的_num
        cout << "学号:" << _num << endl;  // 默认在自己作用域内找_num
    }
protected:
    string _num = "10001";  // 学号
};
int main(void)
{
    Student s1;
    s1.Print();
    return 0;
}

🚩 运行结果:

704408ed542be5b4a53fbaff50c6f2eb_740d6e85ad8d4005a7f47cab66346132.png

❓ 思考:观察下列代码,A::func 和 B::func 的关系是重载还是隐藏?

class A {
public:
  void func() {
  cout << "func()" << endl;
  }
};
class B : public A {
public:
  void func(int i) {
  A::func();
  cout << "func(int i) -> " << i << endl;
  }
};
int main(void) {
  B b;
  b.func(10);
}

🚩 运行结果:

4d15206a2d8d681a92c6bf0ae5f9148d_0a1d8392bef74439a77ab12e161ed762.png

💡 解读:函数重载要求在同一作用域,我们说了,子类和父类都有独立的作用域,因为不是在同一作用域,B 中的 func 和 A 中的 func 不可能构成重载,正确答案是构成隐藏。B 中的 func 和 A 中的 func 构成隐藏,成员函数满足函数名相同就构成隐藏。


(从语言的设计角度来说,如果出现同名直接报错,就没这么多事了)


0x07 继承与友元

友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员!

class Student;
class Person {
public:
  friend void Display(const Person& p, const Student& s);
protected:
  string _name; // 姓名
};
class Student : public Person {
protected:
  int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
  cout << p._name << endl;
  cout << s._stuNum << endl;   ❌
}
void main()
{
  Person p;
  Student s;
  Display(p, s);
}

🚩 运行结果:

“Student::_stuNum”: 无法访问 protected 成员(在“Student”类中声明)


0x08 继承与静态成员

父类定义了 static 静态成员,则整个继承体系里面中有一个这样的成员。


可以理解为共享,父类的静态成员可以在子类共享,父类和子类都能去访问它。


无论派生出多少个子类,都只有一个 static 成员实例:

class Person {
public:
  Person() {
  ++_count;
  }
protected:
  string _name; // 姓名
public:
  static int _count; // 统计人的个数
};
int Person::_count = 0;
class Student : public Person {
protected:
  int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
  string _seminarCourse; // 研究科目
};
void TestPerson()
{
  Student s1;
  Student s2;
  Student s3;
  Graduate s4;
  Person s;
  cout << "大家都可以访问" << endl;
  cout << "人数 : " << Person::_count << endl;
  cout << "人数 : " << Student::_count << endl;
  cout << "人数 : " << s4._count << endl;
  cout << "大家也都可以变动" << endl;
  s3._count = 0;
  cout << "人数 : " << Person::_count << endl;
  cout << "并且他们的地址也都是一样的,因为所有继承体系中只有一个" << endl;
  cout << "人数 : " << &Person::_count << endl;
  cout << "人数 : " << &Student::_count << endl;
  cout << "人数 : " << &s4._count << endl;
}

🚩 运行结果:

1f50dc5cc186837da75ec5e32c6b6a05_e54b11bb0dcd4a099b6607552c0c6075.png

相关文章
|
3天前
|
Java C++
C++的学习之路:21、继承(2)
C++的学习之路:21、继承(2)
13 0
|
28天前
|
C++
8. C++继承
8. C++继承
22 0
|
28天前
|
安全 Java 编译器
C++:继承
C++:继承
31 0
|
15小时前
|
设计模式 编译器 数据安全/隐私保护
C++ 多级继承与多重继承:代码组织与灵活性的平衡
C++的多级和多重继承允许类从多个基类继承,促进代码重用和组织。优点包括代码效率和灵活性,但复杂性、菱形继承问题(导致命名冲突和歧义)以及对基类修改的脆弱性是潜在缺点。建议使用接口继承或组合来避免菱形继承。访问控制规则遵循公有、私有和受保护继承的原则。在使用这些继承形式时,需谨慎权衡优缺点。
8 1
|
9天前
|
C++
【C++成长记】C++入门 | 类和对象(下) |Static成员、 友元
【C++成长记】C++入门 | 类和对象(下) |Static成员、 友元
|
9天前
|
存储 编译器 C++
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
|
14天前
|
编译器 C语言 C++
【C++初阶(九)】C++模版(初阶)----函数模版与类模版
【C++初阶(九)】C++模版(初阶)----函数模版与类模版
18 0
|
24天前
|
NoSQL C++
c++中包含string成员的结构体拷贝导致的double free问题
c++中包含string成员的结构体拷贝导致的double free问题
8 0
|
24天前
|
存储 缓存 C++
C++链表常用的函数编写(增查删改)内附完整程序
C++链表常用的函数编写(增查删改)内附完整程序
|
26天前
|
存储 安全 编译器
【C++】类的六大默认成员函数及其特性(万字详解)
【C++】类的六大默认成员函数及其特性(万字详解)
35 3