C++-继承

继承简介

      继承机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生出来的新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程,以前我们接触的复用都是函数复用,继承是类设计层次的复用。

      派生类中的成员,包含两大部分:

  1. 一类是从基类继承过来的,一类是自己增加的成员。
  2. 从基类继承过过来的表现其共性,而新增的成员体现了其个性。

继承语法

在这里插入图片描述

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

在这里插入图片描述

结论

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

基类与派生类对象赋值转换

  1. 派生类对象可以赋值给基类的对象、基类的指针、基类的引用。这里有个形象的说法叫做切片或者切割。寓意是把派生类中父类的那部分切来赋值过去
  2. 基类对象不能赋值给派生类对象
  3. 基类的指针可以通过强制类型转换赋值给派生类的指针,但是必须该基类的指针必须是指向派生类对象才是安全的,如果基类是多态类型,可以使用RTTI的dynamic_cast来进行识别后进行安全转换

继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫做隐藏,也叫重定义(在子类成员函数中,可以使用基类::基类成员 显式访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际继承体系中最好不要定义同名成员。

派生类的默认成员函数

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。

实现一个不能继承的类

// C++98中将构造函数私有化,派生类中调不到基类的构造函数,则无法继承。
class NoInherit
{
public:
	static NonInherit GetInstance()
	{
		return NoInherit();	
	}
private:
	NonInherit()
	{}	
}
// C++11给出了新的关键字final进制继承
class NonInherit final
{};

菱形继承

      两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。
在这里插入图片描述
这种继承所带来的问题:

  1. 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。(可通过指定调用那个基类的方式来解决)
  2. 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。(采用虚基类解决该问题)

菱形继承的例子

class Animal
{
public:
	int m_Age;
};

//虚基类 Sheep
class Sheep :virtual public Animal
{
};
//虚基类 Tuo
class Tuo :virtual public Animal
{
};

class SheepTuo :public Sheep, public Tuo
{

};

当我们采用虚继承之后,子类对象的内存示意图如下图所示
在这里插入图片描述

  1. Animal 菱形最顶层的类,内存布局图没有发生改变,称之为虚基类
  2. Sheep和Tuo通过虚继承的方式派生自Animal,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。
  3. SheepTuo派生于Sheep和Tuo,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。
          由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。现在模型就变成了Sheep、 Tuo以及SheepTuo三个类对象共享了一份Animal数据。
          当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。
  4. 虚拟继承可以解决菱形继承的二义性问题,需要注意的是,虚拟继承不要在其他地方去使用。
  5. 在虚拟继承的情况下,基类不管在继承串链中被派生多少次,永远只会存在一个实例。
  6. 自从C++2.0导入了虚基类之后,那么就需要一种间接表示基类的方法,通常的办法是加入一个虚基类指针
    下面我们尝试通过上图来直接访问对象中的成员属性。

void test01()
{
	SheepTuo st;
	st.Sheep::m_Age = 10;
	st.Tuo::m_Age = 20;

	cout << st.Sheep::m_Age << endl;
	cout << st.Tuo::m_Age << endl;
	cout << st.m_Age << endl; //可以直接访问,原因已经没有二义性的可能了,只有一份m_Age
}

//通过地址 找到 偏移量
//内部工作原理
void test02()
{
	SheepTuo st;
	st.m_Age = 100;

	//找到Sheep的偏移量操作, 这里比较重要的是,我们可以认为在虚基表中开头的0和1代表数组的下标
	cout << *(int*)((int*)*(int *)&st + 1) << endl;

	//找到Tuo的偏移量
	cout << *((int *)((int *)*((int *)&st + 1) + 1)) << endl;
	
	//输出Age
	cout << ((Animal*)((char *)&st + *(int*)((int*)*(int *)&st + 1)))->m_Age << endl;

}

总结

  1. 很多人说C++语法复杂,其中多继承就是一个体现,有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度以及性能上都有问题
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如java

多态

多态是指使用相同的函数名来访问函数不同的实现方法,可以简单概括为“一种接口,多种方法”。

C++支持编译时多态(也叫静态多态)和运行时多态(也叫动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。

静态多态与动态多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态多态(编译时多态),就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定,是动态多态(运行时多态)。

构成多态的条件

  1. 调用函数的对象必须是指针或者引用
  2. 被调用的函数必须是虚函数,且完成了虚函数的重写

什么是封装

        将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互。
        一个类的大小,实际就是该类中“成员变量”之和,当然也要进行内存对齐,注意空类的大小是1。

析构函数的重写问题

        基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数,这里他们的函数名不同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名字同一处理成destructor,这也说明了用来实现多态的基类的析构函数最好是虚函数

接口继承和实现继承

        普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现,虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口,所以如果不实现多态,就不要将函数定义为虚函数。

重载、覆盖(重写)、隐藏(重定义)的区别

        重载:两个函数在同一个作用域,函数名相同,但是参数列表不同
        重写(覆盖):两个函数分别在基类和派生类的作用域中,函数名、参数、返回值都必须相同,并且这两个函数必须都是虚函数。覆盖就是虚表指中的虚函数的覆盖,重写是语法的叫法,覆盖是原理层的叫法
        重定义(隐藏):两个函数分别在基类和派生类的作用域,函数名相同,两个分别位于基类和派生类的同名函数不构成重写就是重定义。

抽象类

        在虚函数后加上=0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

inline函数可以是虚函数吗

        不能,因为inline函数没有地址,无法把地址放到虚函数表中

静态成员可以是虚函数吗

静态成员可以是虚函数吗
        不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表

构造函数可以是虚函数吗

        不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的

析构函数可以是虚函数吗

        可以,并且最好把基类的析构函数定义成虚函数

对象访问普通函数快还是虚函数快

        如果是普通对象,是一样快的,如果是指针对象或者是引用对象,则调用普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

解释一下虚函数,继承、虚继承

虚函数就是在成员函数的前面加上virtual,继承机制可以帮助我们在一个已有类的基础上添加新的属性和方法从而构造一个新的类,所以继承就是实现代码复用的一种手段,实现了类层次的代码复用。虚继承就是为了解决菱形继承问题,这里涉及到虚基类,所谓的虚基类就是位于中间的类,虚继承的结果就是在最下层的类中只有一份基类数据,不存在空间浪费以及调用的二义性。底层是通过在虚基类和子类中增加了一个虚基类指针。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值