C++中的继承机制

继承

面向对象三大特性:封装,继承,多态。继承这里时刻牢记一个类继承了父类时,这个类中==既有自己的成员,也有来自父类的成员!==如何统筹协调这两类成员就是继承的性质。

这些面向对象的特性是针对所有 面向对象程序语言 的!并不是特指C++,这里只是学习C++中面向对象特性的实现。

1. 再次理解封装

封装的第一层理解:封装成类后加上访问限定符,是一种==更严格的管理方式!==同时也可以很好的做到解耦。

封装的第二层理解:STL迭代器的设计。对容器的底层结构进行封装,在不暴露底层数据结构的情况下,给用户提供统一的访问方式,降低使用成本。

封装的第三层理解:STL中的适配器模式。通过对底层容器的封装适配得到需要的其他容器;通过对正向迭代器的封装得到反向迭代器。

2. 继承的基本知识

1. 基本概念

继承是面向对象程序设计中代码复用的重要手段!是 类 设计层次的复用。

// 每个人都有name和tel,就把这些共有的属性提取出来单独设计一个类,其他的类继承我!
class Person        // 父类/基类
{
protected:
  string name_;
  string tel_;
}
class Student : public Person   // 子类/派生类
{
public:
  int stuId_;
  // ...
}
class Teacher : public Person   // 子类/派生类
{
public:
  int TeachId_;
  // ...
}

2. 继承方式

继承方式也有三种:publicprotectedprivate

基类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected 成员派生类的private 成员
基类的protected 成员派生类的protected 成员派生类的protected 成员派生类的private 成员
基类的private成 员在派生类中不可见在派生类中不可见在派生类中不可 见

不可见是指基类的私有成员还是被继承到了派生类中,但是语法上限制==派生类对象不管在类里面还是类外面都无法访问基类的私有成员。==基类的private成员就是为了不让派生类访问修改。

一般而言,基类的访问限定符都是public/protected,继承方式都是public

3. 基类和派生类对象的赋值转换

public继承中派生类的对象可以赋值给基类的对象/基类的指针/基类的引用,并且中间不会产生临时变量。就是把派生类中基类的那部分赋给基类的对象,这叫做切割/切片。这个过程是天然支持的,没有类型转换。

基类的对象不能赋值给派生类的对象。

但是当基类的指针是指向派生类时,这个基类的指针可以 强转后赋值给派生类的指针/引用。

4. 继承中的作用域

基类和派生类有自己独立的作用域。

当基类和派生类中有同名成员(包括成员变量和成员函数)时,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)。对于成员函数也是一样,只要基类和派生类函数名相同就构成隐藏。

隐藏是在不同的作用域中,而函数重载是在同一作用域中。

3. 子类的默认成员函数

在调用子类的构造/拷贝构造函数时,会自动调用父类的构造/拷贝构造函数对父类的成员进行初始化,然后调用子类的构造/拷贝构造函数进行初始化!

在调用子类的析构函数时,为了保证析构顺序会先调用子类的析构函数清理子类的资源,然后自动调用父类的析构函数清理父类的资源!不需要显示调用(栈帧后进先出)

在这里插入图片描述

而对于赋值重载,则在子类的赋值重载函数中需要显示的调用父类的赋值重载函数。

注意一下:子类和父类的赋值重载函数构成了隐藏 (函数名相同),所以调用的时候要指定作用域!另外,析构也构成隐藏—实际析构函数是覆盖,参考多态章节。

4. 继承与友元

友元关系不能被继承!也就是说,父类的友元函数不是子类的友元函数,无法访问子类的私有和保护成员。

5. 继承中的静态成员

静态成员在整个继承体系中只有一份,基类和所有的派生类的对象共享,这些对象共用同一个静态成员。

6. 继承与组合的区别

继承是一种 **"is-a"**的关系,可以认为派生类就是一个基类,在基类的基础上多了派生类自己的成员。

而组合是指在一个类中有另一个类对象,这是一种 **"has-a" **的关系—我里面有一个你,也可以做到类的复用。

// 组合示例
class A
{
  // ...
protected:
  int a_;
};
class B : public A
{
  // ...
protected:
  int b_;
  // B类里有一个A类。
  A classA_;
};

这两种方式都是代码复用的做法。但是继承破坏了封装—public继承下 子类也可以访问修改父类的protected成员!所以认为继承是一种白盒复用;而组合就不会破坏代码的封装—B类不能访问修改A类的protected成员,所以认为组合是一种黑盒复用。

而且继承机制中基类和派生类的耦合度更强,组合机制中两个类之间的耦合度就要低一些。

在实际中能用组合就尽量用组合!但是有些情况下只能用继承那就用继承,而且继承配合多态可以使代码的复用性,灵活性更好!

组合的典型应用:STL中的容器适配器,如stackqueue中就有一个deque类,还配合了模板达到泛型编程。

7. 菱形继承与菱形虚拟继承

1. 菱形继承的产生与带来的问题

首先有多继承,比如D类继承B,C两个类!这时若B类和C类都继承自同一个类,就会导致菱形继承。见下图:

在这里插入图片描述

// 菱形继承示例
class A
{
  // ...
protected:
  int a_;
};
class B : public A
{
  // ...
protected:
  int b_;
};
class C : public A
{
  // ...
protected:
  int c_;
};
class D : public B,public C
{
 	// ...
protected:
  int d_;
};

菱形继承带来的问题:D类中又两份A类的数据—一份是继承B类得到的,一份是继承C类得到的!这就造成了数据冗余二义性的问题!(有两份A类的数据—数据冗余,D类访问A类的数据时必须指明访问的是哪一个A类的数据—二义性)

在这里插入图片描述

2. 菱形继承问题的解决—菱形虚拟继承

采用菱形虚拟继承就可以解决上述问题!

// 菱形虚拟继承示例
class A
{
  // ...
protected:
  int a_;
};
// 虚继承后B对象的存储结构就变了!不直接存A类的数据,而是存虚基表指针
// 当然A类的数据还在B类中,但是是通过虚基表访问的,不是直接访问的!!
class B : virtual public A   // 在这两个位置进行虚继承
{
  // ...
protected:
  int b_;
};
class C : virtual public A   // 在这两个位置进行虚继承
{
  // ...
protected:
  int c_;
};
class D : public B,public C
{
 	// ...
protected:
  int d_;
};

在虚继承中,D类里的B类和C类不再存A类的数据,而是存一个虚基表指针,指向虚基表,虚基表里存放A类数据的偏移量,通过偏移量就可以找到同一份A类的数据!!这就解决了数据冗余和二义性的问题!

简单来说,就是把冗余的基类数据放到一个公共的区域(还在D类里),第一代派生类继承时不存基类的具体数据,而是存一个指针可以找到这个公共区域。其实这个基类的数据变成了临界资源!

在这里插入图片描述

在VS下观察如下:
在这里插入图片描述

思路:多继承–>菱形继承–>数据冗余和二义性问题–>菱形虚拟继承解决–>具体解决思路(虚基表指针,虚基表存偏移量)

**虚继承后,B对象和C对象的存储结构就已经改变了!!**使用虚基表的方式找A对象。

由于虚继承的复杂,所以实际中尽量避免使用多继承(多继承是菱形继承的根本原因)!!C++标准库中的I/O流就是菱形继承!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月球上的星星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值