C++中继承及virtual小结

一、继承基础知识

C++中的继承

1.1继承的基本概念

类与类之间的关系

  • has-A,包含关系,用以描述一个类由多个“部件类”构成,实现has-A关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类。
  • use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现。(和组合不同)
  • is-A,即继承关系,关系具有传递性。

继承的特点

  • 子类拥有父类的所有属性和方法(除了构造函数和析构函数)。
  • 子类可以拥有父类没有的属性和方法。
  • 子类是一种特殊的父类,可以用子类来代替父类。
  • 子类对象可以当做父类对象使用。  

 1.2继承中的访问控制

三种继承方式对子类访问的影响

  • public继承:父类成员在子类中保持原有的访问级别(子类可以访问public和protected)。
  • private继承:父类成员在子类中变为private成员(虽然此时父类的成员在子类中体现为private修饰,但是父类的public和protected是允许访问的,因为是继承后改为private)。
  • protected继承
    • 父类中的public成员会变为protected级别。
    • 父类中的protected成员依然为protected级别。
    • 父类中的private成员依然为private级别。
  • 注意:父类中的private成员依然存在于子类中,但是却无法访问到。不论何种方式继承父类,子类都无法直接使用父类中的private成员。

父类如何设置子类的访问

  • 需要被外界访问的成员设置为public。
  • 只能在当前类中访问设置为private。
  • 只能在当前类和子类中访问,设置为protected。

1.3继承中的类型兼容性原则

继承中的同名成员

  当在继承中,如果父类的成员和子类的成员属性名称相同,我们可以通过作用域操作符来显式的使用父类的成员,如果我们不使用作用域操作符,默认使用的是子类的成员属性。

类型兼容性原则

  类型兼容性原则是指在需要父类对象的所有地方,都可以用公有继承的子类对象来替代。通过公有继承,子类获得了父类除构造和析构之外的所有属性和方法,这样子类就具有了父类的所有功能,凡是父类可以解决的问题,子类也一定可以解决。

1.4.继承中的构造和析构函数

*虚析构函数为了解决多态中存在的问题:内存泄漏基类的析构函数通常为虚函数,这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。如果析构函数是虚的,将调用相应对象类型的析构函数。因此,如果指针指向派生类对象,将调用派生类析构然后自动调用基类析构。

父类中的构造和析构执行顺序

  • 子类对象在创建时,会先调用父类的构造函数,如果父类还存在父类,则先调用父类的父类的构造函数,依次往上推理即可。
  • 父类构造函数执行结束后,执行子类的构造函数。
  • 当父类的构造函数不是C++默认提供的,则需要在子类的每一个构造函数上使用初始化列表的方式调用父类的构造函数。
  • 析构函数的调用顺序和构造函数的顺序相反

继承与组合(类中有另一个类对象)情况混搭下的构造析构函数调用顺序

  • 构造函数:先调用父类的构造函数,再调用成员变量的构造函数,最后调用自己的构造函数。
  • 析构函数:先调用自己的析构函数,再调用成员变量的析构函数,最后调用父类的析构函数。


二、继承中关于内存及指针

一个类所有的函数都是再code代码区中唯一的存放一份。而数据成员则是每个对象存储一份,并按照声明顺序依次存放。
2.1为什么 C++ 中,基类指针可以指向派生类对象?
可以指向,但是无法使用不存在于基类只存在于派生类的元素。(所以需要虚函数和纯虚函数
原因是这样的:
在内存中,一个基类类型的指针是覆盖N个单位长度的内存空间。
当其指向派生类的时候,由于派生类元素在内存中堆放是:前N个是基类的元素,N之后的是派生类的元素。
于是基类的指针就可以访问到基类也有的元素了,但是此时无法访问到派生类(就是N之后)的元素。

基类指针和子类指针之间相互赋值

(1)将子类指针赋值给基类指针时,不需要进行强制类型转换,C++编译器将自动进行类型转换。因为子类对象也是一个基类对象。

(2)将基类指针赋值给子类指针时,需要进行强制类型转换,C++编译器将不自动进行类型转换。因为基类对象不是一个子类对象。子类对象的自增部分是基类不具有的。

从子类对象强制类型转换为基类对象是允许的,而相反地要从基类对象强制转换成子类对象是错误的(编译不通过)。


void  ExamAnimal() 
   // 将子类指针直接赋给基类指针,不需要强制转换,C++编译器自动进行类型转换 
   // 因为fish对象也是一个animal对象 
   animal* pAn; 
   fish* pfh =  new  fish; 
   pAn = pfh; 
     
   delete  pfh; 
   pfh = NULL; 
     
   // 将基类指针直接赋给子类指针,需要强制转换,C++编译器不会自动进行类型转换 
   // 因为animal对象不是一个fish对象 
   fish* fh1; 
   animal* an1 =  new  animal; 
   // 修改处: 
   // 进行强制类型转化 
   fh1 = (fish*)an1; 
   
   delete  an1; 
   an1 = NULL; 
}

子类fish指针赋给基类animal指针时,内存的变化: 当我们构造fish类的对象时,首先要调用animal类的构造函数去构造animal类的构造函数,然后才调用fish类的构造函数完成自身部分的构造,从而拼接出一个完整的fish对象。当我们将fish类对象转换为animal类对象时,该对象就被认为是原对象整个内存模型的上半部分,也就是图中animal对象的内存部分。当我们利用类型转换后的对象指针去调用它的方法时,自然是调用它所在的内存中的方法。 将fish类对象赋给animal类对象时,会截取fish类对象自身的部分,剩下fish类对象中的animal部分。

基类animal对象包含的信息少,类fish对象包含的信息多,将信息少的对象直接转换为信息多的对象时(没有强制类型转换),显然是无法构造出多出的信息。在编译时,也会发生错误:error C2440: '=' : cannot convert from 'class animal *' to 'class fish *'。 这时,需要做强制类型转换。

2.2虚函数表


(基类中有两个虚函数fg,子类中继承了fg自己有一个虚函数d然后覆盖了一个虚函数f)
虚表指针(vptr):每个类有一个虚表指针,当利用一个基类的指针绑定基类或者派生类对象时,程序运行时调用某个虚函数成员,会根据对象的类型去初始化虚指针,从而虚表指针会从正确的虚函数表中寻找对应的函数进行动态绑定,因此可以达到从基类指针调用派生类成员的效果。

那么为什么需要虚指针和虚函数表来实现动态多态呢?因为无论是什么函数,包括类内的虚函数和非虚函数,都会储存在内存中的代码段。但是当编译器在编译时,就可以确定普通函数和非虚函数的入口地址,以及其调用的信息,所以这指的是常量指针。当遇到动态多态时,虚函数真正的入口地址的指针要在运行时根据对象的类型才能确定,所以要通过虚指针从虚函数表中找虚函数对应的入口地址。

当然,用基类指针绑定的子类对象,只能通过这个基类指针调用基类中的成员,因为作用域仅限于基类的子对象,子类新增的部分是看不见的。

更多关于虚函数表的信息见博客C++虚函数表解析

*2.3虚函数实现原理

【:-》首先:什么是函数指针?

  指针指向对象称为对象指针,指针除了指向对象还可以指向函数,函数的本质就是一段二进制代码,我们可以通过指针指向这段代码的开头,计算机就会从这个开头一直往下执行,直到函数结束,并且通过指令返回回来。函数的指针与普通的指针本质上是一样的,也是由四个基本的内存单元组成,存储着内存的地址,这个地址就是函数的首地址。

【:-》多态的实现原理

        虚函数表指针:类中除了定义的函数成员,还有一个成员是虚函数表指针(占四个基本内存单元),这个指针指向一个虚函数表的起始位置,这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数。

        当实例化一个该类的子类对象的时候,(如果)该类的子类并没有定义虚函数,但是却从父类中继承了虚函数,所以在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表是子类的虚函数表,但是记录的子类的虚函数地址却是与父类的是一样的。所以通过子类对象的虚函数表指针找到自己的虚函数表,在自己的虚函数表找到的要执行的函数指针也是父类的相应函数入口的地址。

        如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时子类定义了自己的(从父类那继承来的)相应函数,所以它的虚函数表当中管于这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,换句话说就是指向了自己定义的相应函数,这样如果用父类的指针,指向子类的对象,就会通过子类对象当中的虚函数表指针找到子类的虚函数表,从而通过子类的虚函数表找到子类的相应虚函数地址,而此时的地址已经是该函数自己定义的虚函数入口地址,而不是父类的相应虚函数入口地址,所以执行的将会是子类当中的虚函数。这就是多态的原理

函数的覆盖和隐藏

父类和子类出现同名函数称为隐藏。

  • 父类对象.函数函数名(...);     //调用父类的函数
  • 子类对象.函数名(...);           //调用子类的函数  
  • 子类对象.父类名::函数名(...);//子类调用从父类继承来的函数。

父类和子类出现同名虚函数称为覆盖

  • 父类指针=new 子类名(...);
  • 父类指针->函数名(...);//调用子类的虚函数。

虚析构函数的实现原理

[:->虚析构函数的特点:

  • 当我们在父类中通过virtual修饰析构函数之后,通过父类指针指向子类对象,通过delete接父类指针就可以释放掉子类对象

[:->理论前提:

  • 执行完子类的析构函数就会执行父类的析构函数

原理:

        如果父类当中定义了虚析构函数,那么父类的虚函数表当中就会有一个父类的虚析构函数的入口指针,指向的是父类的虚析构函数,子类虚函数表当中也会产生一个子类的虚析构函数的入口指针,指向的是子类的虚析构函数,这个时候使用父类的指针指向子类的对象,delete接父类指针,就会通过指向的子类的对象找到子类的虚函数表指针,从而找到虚函数表,再虚函数表中找到子类的虚析构函数,从而使得子类的析构函数得以执行,子类的析构函数执行之后系统会自动执行父类的虚析构函数。这个是虚析构函数的实现原理。

三、(virtual)抽象类和虚基类的定义及用途

1.1虚函数是用于多态中virtual修饰父类函数,确保父类指针调用子类对象时,运行子类函数的。

1.2纯虚函数是用来定义接口的,也就是基类中定义一个纯虚函数,基类不用实现,让子类来实现。

2.虚基类是用来在多继承中,如果父类继承自同一个父类,就只实例化一个父类(说的有点绕,就是只实例化一个爷爷的意思)

3.1.在类Base中加了Virtual关键字的函数就是虚拟函数,有一个纯虚函数的基类为抽象类。

virtual在函数中的使用限制

  • 普通函数不能是虚函数,也就是说这个函数必须是某一个类的成员函数,不可以是一个全局函数,否则会导致编译错误。
  • 静态成员函数不能是虚函数 static成员函数是和类同生共处的,他不属于任何对象,使用virtual也将导致错误。
  • 内联函数不能是虚函数 如果修饰内联函数 如果内联函数被virtual修饰,计算机会忽略inline使它变成存粹的虚函数。
  • 构造函数不能是虚函数,否则会出现编译错误。

为了让一个类成为抽象类,至少必须有一个纯虚函数。包含至少一个纯虚函数的类视为抽象类!

1 成员函数重载特征:
   a 相同的范围(在同一个类中)
   b 函数名字相同
   c 参数不同
   d virtual关键字可有可无
2 重写(覆盖)是指派生类函数覆盖基类函数,特征是:
   a 不同的范围,分别位于基类和派生类中
   b 函数的名字相同
   c 参数相同
   d 基类函数必须有virtual关键字
3 重定义(隐藏)是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
   a 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
   b 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。 如果父类的成员和子类的成员属性名称相同,我们可以通过作用域操作符来显式的使用父类的成员,如果我们不使用作用域操作符,默认使用的是子类的成员属性。
虚函数作用及上三条总结

基类和子类都有同一个函数,定义一个基类指针指向子类对象,用指针调用该函数,如果函数为非虚则调用基类函数,否则调用子类函数(第2条)。若指针与指向对象相同,则各自调用各自的函数(子类时第3条)。

《C++ Primer》第五版,P537 页:“当我面在派生类中覆盖某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数”

C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数


纯虚函数没有函数体,所以抽象类不允许实例化对象。抽象类子类只有把抽象类当中的所有的纯虚函数都做了实现才可以实例化对象。

如果在抽象类当中仅含有纯虚函数而不含其他任何东西,我们称之为接口类。没有任何数据成员,仅有成员函数,成员函数都是纯虚函数

3.2.如果只知道virtual加在函数前,那对virtual只了解了一半,virtual还有一个重要用法是virtual public,就是虚拟继承(virtual 与public顺序无关)

多继承

1.C++中的多继承:所谓的多继承就是指一个子类可以继承多个父类,子类可以获取多个父类的属性和方法。这种继承方式是不被推荐的,但是C++还是添加了,事实证明,多继承增加了代码的复杂度,而且任何可以通过多继承解决的问题都可以通过单继承的方式解决。多继承和单继承的基本知识是相同的。不需要再阐述,主要讲解下面的不同的地方。

2,多继承中的构造和析构:和单继承类似,还是首先执行父类的构造函数,此时有多个构造函数,则按照继承时的父类顺序来执行相应父类的构造函数,析构函数与此相反。

3.多继承中的二义性:一个类A,它有两个子类B1和B2,然后类C多继承自B1和B2,此时如果我们使用类A里面的属性,则根据上面的多继承的构造和析构,发现此时的类A会被创造两个对象,一个是B1一个是B2,此时使用A里面的属性则会出现编译器无法知道是使用B1的还是B2的。因此C++为我们提供了虚继承这个概念,即B1和B2虚继承自A,则在构造A对象的时候,只创建一个A的对象。

虚基类可以解决二义性的问题 ,就是解决多重多级继承造成的二义性问题

使用虚基类将改变C++解析二义性的方式。如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时如果没有用类名进行限定,将导致二义性,但如果使用的是虚基类,则这样做不一定会导致二义性

如果类有间接虚基类 ,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数但对于非虚基类非法。即C++在基类是虚的时,禁止信息通过中间类自动传递给基类

虚拟派生不是基类本身的一个显式特性,而是它与派生类的关系,如前面所说明的,虚拟继承提供了“按引用组合”。也就是说,对于子对象及其非静态成员的访问是间接进行的。这使得在多继承情况下,把多个虚拟基类子对象组合成派生类中的一个共享实例,从而提供了必要的灵活性。同时,即使一个基类是虚拟的,我们仍然可以通过该基类类型的指针或引用,来操纵派生类的对象

猜你喜欢

转载自blog.csdn.net/sinat_27652257/article/details/79810567