【C++】继承(二)多继承,虚函数,虚继承

1.单继承与多继承
2.菱形继承
3.虚继承---解决菱形继承的二义性和数据冗余的问题
4.虚函数与多态
5.继承体系同名函数的关系

1.单继承与多继承

 1.1单继承:
      一个子类只有一个直接父类。
 

class Person
class Student : public Person
class Monitor : public Student 


 1.2多继承:
      一个继承有俩个或俩个以上的直接父类。
 

class Teacher
class Student
class Assistant : public Studenr ,public Teacher


 

2.菱形继承
 有多继承存在就一定菱形继承存在,菱形继承是由多继承衍生出来的一种特殊的继承体系,也是C++坑的开始。。。
  2.1菱形继承模型:

    
 

2.2从代码的角度认识菱形继承
     2.2.1代码

class Student : public Person
{
protected:
	int _num;  //学号
};

class Teacher : public Person
{
protected:
	int _id;  //职工编号
};

class Assistant : public Student,public Teacher
{
protected:
	string _majorCourse;  //主修课程
};

int main()
{
	Assistant a;
	//a._name = "xxx";//这里因为a包含了俩分父类对象的_name,所以会产生二义性的问题,造成访问不明确的问题
	a.Student::_name = "Linmed";
	a.Teacher::_name = "dada";
	//使用指定作用域也只是解决了二义性的问题
	system("pause");
	return 0;
}


     2.2.2菱形继承对象模型


 

2.3菱形继承缺点:因为Assistant中有俩份Person成员,所以菱形继承有二义性和数据冗余的问题


3.虚继承---解决菱形继承的二义性和数据冗余的问题
  3.1虚继承实现方式

class Student : virtual public Person


  3.2虚继承解决了在菱形继承体系里子类对象包含多份父类对象的数据冗余和浪费空间的问题。
  3.3虚继承看起来好复杂,在实际运用中我们并不会定义这么复杂的继承体系,一般不到万不得已都不要定义菱形结构的继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗
  3.4站在内存的角度来看菱形继承与菱形虚拟继承对象模型
  在这里会使用俩段非常相似的代码来演示菱形继承与虚拟菱形继承的对象模型。
   在代码运行起来后仅仅关注监视窗口是看不出来什么东西的,所以需要打开内存窗口才可以更明确地了解到对象模型。
   3.4.1菱形继承

class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};

class C : public A
{
public:
	int _c;
};

class D : public B,public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	system("pause");
	return 0;
}

   站在内存角度探索菱形继承模型

3.4.2 菱形虚拟继承对象模型---------虚继承解决了菱形继承二义性和数据冗余的问题,那么他到底是怎么解决的呢?让我们一起来看看吧。首先上一段代码,这段代码与我上个代码唯一的区别就是加了关键字virtual。。。。

class A
{
public:
	int _a;
};

class B : virtual public A
{
public:
	int _b;
};

class C : virtual public A
{
public:
	int _c;
};

class D : public B,public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	system("pause");
	return 0;
}

那么我们站在内存角度一起来探索虚拟继承吧。运行起来后按F10,我们会发现A的成员_a的值跑到了最下面(一定要在最下面么?不,只是系统想这样子操作罢了)。因为B和C都继承了A,所以_a的值从1变成了2.如下图:


等F10走到最后就会很明显的看出来菱形虚拟继承对象模型,如下图:and看完图后请看绿色的字。。。

那么这俩个地址到底存的是什么东西呢?不妨让我们在内存中探索探索,因为我们的计算机是个小端,所以读取地址是得反着来。来打开这俩个地址看看吧。。请先结合表4来看图再看绿色的字。。。

操作完成之后就会发现虚基表中存储的是偏移量,指向虚基表的指针从虚基表中取到偏移量,然后通过偏移量会找到被多份子类包含的那个父类对象成员的值。通过这种方式就可以解决掉数据冗余和二义性问题。

。。。那么当面试官问到你菱形虚拟继承是如何解决菱形继承的俩大问题的时候。你就可以在纸上大致画出表四与表五,说明哪个是指向虚基表的指针哪个是虚基表,虚基表指针和虚基表又是拿来做什么的,然后他们又是通过什么样的方式实现的。父类A有俩个子类B和C,在菱形虚拟地址空间中,类B和C包含的东西除了子类自己创造的成员之外还有一个指针,这个指针是一个指向虚基表的指针,虚基表是一个表也可以认为是一个数组,通过虚基表指针指向的地址可以在虚基表中找出一个数,这个数其实是一个偏移量,是存放虚基表指针的地址相对于父类A成员所在地址的相对偏移量,通过这个偏移量就可以找到父类A的成员。很好的解决了数据冗余和二义性。至少在我现在使用的VS版本中是这样实现的。。但是一般情况下除非万不得已一定不要定义菱形结构的虚继承体系结构,因为它在解决数据冗余的情况下还造成了性能上的损耗。一方面这样子过来过去实在有点麻烦,另一方面为了在最下面存父类成员还会多开辟出来空间。

4.虚函数与多态
  4.1虚函数:
     类的成员函数前面加virtual关键字,则这个成员函数称为虚函数。虽然虚继承也用到关键字virtual,但这俩个关键字没有任何关系,只是用了同一个名字,就像是引用和取地址的符号&。
  4.2虚函数重写:
     当在子类中定义了一个与父类完全相同的虚函数时,则称这个子类重写(也称覆盖)了父类的这个虚函数。
  4.3多态
     当使用基类的指针或引用调用重写的虚函数时,当指向父类调用的就是父类的虚函数,指向子类的就是子类的虚函数。

class Person
{
public:
	virtual void Buytickets()   //虚函数
	{
		cout<<"买票--全价"<<endl;
	}
};

class Student : public Person   
{
public:
	virtual void Buytickets()  //虚函数
	{
		cout<<"买票--半价"<<endl;
	}
};

void Fun(Person &p)   
{
	p.Buytickets();
}  //多态;使用基类的指针或引用来调用重写的虚函数

int main()
{
	Person p;
	Student s;

	//多态调用
	Fun(p);   //指向父类调用的就是父类的虚函数
	Fun(s);    //指向子类调用的就是子类的虚函数
	
	//普通调用
	p.Buytickets();
	s.Buytickets();

	system("pause");
	return 0;
}


  4.4多态调用与普通调用
     4.4.1多态调用:具有灵活性,调用函数与对象有关,指向谁调用谁的虚函数。
     Fun(p);    //指向父类对象就调用父类的虚函数
     Fun(s);    //指向子类对象就调用子类的虚函数
       4.4.1.1多态调用的条件
           4.4.1.1.1必须有虚函数的重写
           4.4.1.1.2 p是父类的指针和引用
              

  void Fun(Person& p)                
  {                  
  p.Buyickets();    
  }


      4.4.2普通调用:跟类型有关(s.BuyTickets()),哪个类型的对象,调用谁的虚函数。
  4.5总结
     4.5.1.派生类重写基类的虚函数实现多态,要求函数名,参数列表,返回值完全相同。(协变除外)
     4.5.2.基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
     4.5.3只有类的成员函数才能定义为虚函数。
     4.5.4.静态成员函数不能定义为虚函数。(why?)
     4.5.5.如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。
     4.5.6构造函数不能为虚函数(why?),虽然可以将oparetor=定义为虚函数,但是最好不要将它定义为虚函数,因为容易引起引用混淆。
     4.5.7不要再构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。
     4.5.8.最好把析构函数声明为虚函数。(why?)
    对于总结中存在的问题会在后续博客中进行解释。。。。
5.继承体系同名函数的关系
    5.1重载
       在同一作用域,函数名相同/参数不同,返回值可以不同。
    5.2重写(覆盖)
        不同作用域(分别是基类和派生类),函数名相同/参数相同/返回值相同(协变另外),基类函数必须有virtual关键字,访问修饰符可以不同。
    5.3重定义(隐藏)
        不同作用域,(分别是基类和派生类),函数名相同,参数相不相同无所谓,在基类和派生类中只要不构成重写就是重定义。
    5.4重写针对的是虚函数;重定义针对的是成员变量和成员函数。 
        

猜你喜欢

转载自blog.csdn.net/hgelin/article/details/82762127