从C语言到C++_22(继承)多继承与菱形继承+笔试选择题(上)

简介: 从C语言到C++_22(继承)多继承与菱形继承+笔试选择题

回顾一下面向对象三大特性:封装、继承、多态。(其它特性:反射、抽象...)

前面我们学了封装,封装带来了上面好处?:

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

② 迭代器设计,如果没有迭代器,容器访问只能暴露底层结构。 -> 使用复杂、

使用成本很高,对使用者要求极高。

封装了容器底层结构,不暴露底层结构的情况,提供统一的访问容器的方式,

降低使用成本,简化使用。

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

现在我们讲讲继承。


1. 继承

1.1 继承的概念

继承(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;
    int _age;
};

然后使用 "继承" 去把这些共有的东西运送给各个角色,先看语法:

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

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

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

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

(把成员函数也继承下来了)

#include<iostream>
using namespace std;
 
class Person 
{
public:
    void Print()
    {
        cout << "name: " << _name << endl;
        cout << "age:  " << _age << endl;
        cout << endl;
    }
protected:
    string _name = "user";
    string _tel;
    string _address;
    int _age = 18;
};
 
class Student : public Person
{
    string _stuID;  // 学号
};
 
class Teacher : public Person 
{
    string _wordID;  // 工号
};
 
int main()
{
    Person p;
    p.Print();
 
    Student s;
    s.Print();
 
    Teacher t;
    t.Print();
 
    return 0;
}


1.2  继承的定义格式

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

class Student : public Person // Student是子类(派生类),public是继承方式,Person是父类(基类)
{
    string _stuID;  // 学号
};

Student是子类(派生类),public是继承方式,Person是父类(基类)

把 Person 和 Student 看作是父子关系是比较容易理解的。

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

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


1.3 访问限定符和继承方式

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

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

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

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

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

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

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

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


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


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


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


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

1.4 继承中的赋值

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

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

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

注意事项:

父类对象不能赋值给子类对象

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

1.5 继承中的作用域

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

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

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

基类::基类成员

注意事项:

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

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

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

#include<iostream>
using namespace std;
 
class Person
{
protected:
    string _name = "李华";                // 姓名
    string _num = "56465456456464";  // 身份证号
};
 
class Student : public Person
{
public:
    void Print() 
    {
        cout << "姓名:" << _name << endl;
        cout << "身份证号:" << Person::_num << endl;  // 指定是Person的_num
        cout << "学号:" << _num << endl;  // 默认在自己作用域内找_num
    }
protected:
    string _num = "1007";  // 学号
};
 
int main(void)
{
    Student s1;
    s1.Print();
 
    return 0;
}

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

#include<iostream>
using namespace std;
 
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() 
{
  B b;
  b.func(10);
}

解读:函数重载要求在同一作用域,我们说了,子类和父类都有独立的作用域,

因为不是在同一作用域,B 中的 func 和 A 中的 func 不可能构成重载,正确答案是构成隐藏。

B 中的 func 和 A 中的 func 构成隐藏,成员函数满足函数名相同就构成隐藏。


2. 子类(派生类)的默认成员函数

默认成员函数复习链接:从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)_GR C的博客-CSDN博客


对于默认成员函数,如果不主动实现,编译器会自己生成一份。

那么这些默认成员函数在子类中,它们又是如何生成的?

2.1 子类的构造函数

① 父类成员需调用自己的构造完成初始化。 即子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。

② 如果 父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显式调用。

③ 子类对象初始化先调用父类构造再调子类构造。

代码演示:

#include <iostream>
using namespace std;
 
class Person 
{
public:
    Person(const char* name)
        : _name(name)
    {
        cout << "Person()" << endl;
    }
protected:
    string _name;
};
 
class Student : public Person 
{
public:
    Student(const char* name, int num)
        : Person(name)  // 父类成员,调用自己的构造完成初始化
        , _num(num)
    {
        cout << "Student()" << endl;
    }
 
protected:
    int _num;   // 学号
};
 
int main()
{
    Student s1("GR", 19);
 
    return 0;
}

调用父类构造函数初始化继承自父类的成员,自己再初始化自己的成员(规则参考普通类)。

析构、拷贝构造、赋值重载也是类似的。

思考:如何设计一个不能被继承的类?

将父类的构造函数私有化:

#include <iostream>
using namespace std;
 
class A
{
private:
    A()
    {}
 
protected:
  int _a;
};
 
class B : public A
{
 
};
 
int main()
{
    B b; // 只有实例化的时候才会报错
    return 0;
}

父类 A 的构造函数私有化后 B 就无法构造对象,因为 B 的构造函数必须要调用 A 的。

A a;  也不行了 ,A也没办法构造了,而且这只是C++98用的,

C++11提供了关键字 final 写在类的后面,表明这个类不能被继承:

#include <iostream>
using namespace std;
 
class A final
{
 
protected:
  int _a;
};
 
class B : public A // 错误  C3246 "B": 无法从 "A" 继承,因为它已声明为 "final"
{
 
};
 
int main()
{
    B b;
    return 0;
}

从C语言到C++_22(继承)多继承与菱形继承+笔试选择题(中):https://developer.aliyun.com/article/1521906

目录
相关文章
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
39 1
|
1月前
|
安全 编译器 程序员
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
18 0
|
1月前
|
编译器 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
30 0
|
8天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
35 4
|
9天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)