栏目分类:
子分类:
返回
文库吧用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
文库吧 > IT > 软件开发 > 后端开发 > C/C++/C#

C++ 【多态】

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++ 【多态】

文章目录
    • 1.多态的概念
      • 1.1概念
      • 1.2静态的多态与动态的多态
    • 2.多态的定义及实现
      • 2.1多态的构成条件
      • 2.2虚函数
      • 2.3虚函数的重写
        • 2.3.1函数重写的两个例外:
        • 2.3.2 多态设计的缺陷
      • 2.4. C++11 override和final
        • 2.4.1 final关键字
        • 2.4.2 override关键字
      • 2.5 重载、覆盖(重写)、隐藏(重定义)的对比
    • 3. 抽象类
      • 3.1 概念
      • 3.2 接口继承和实现继承
    • 4.多态的原理
      • 4.1虚函数表
      • 4.2多态的原理
    • 5. 动态绑定和静态绑定
    • 5.单继承和多继承关系的虚函数表
      • 5.1 单继承中的虚函数表
      • 5.1.1 通过内存窗口查看虚表
        • 5.1.2 通过代码打印虚表
      • 5.2 多继承中的虚函数表
      • 5.3. 菱形继承、菱形虚拟继承
    • 6. 继承和多态常见的面试问题
      • 6.1 填空题:
      • 6.2 问答题

1.多态的概念 1.1概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票 。

1.2静态的多态与动态的多态

多态:多种形态

静态的多态:函数重载,看起来调用同一个函数有不同行为。静态:原理是编译时实现。

动态的多态:一个父类的引用或指针取调用一个函数,传递不同的对象,会调用不同的函数。动态:原理运行时实现。本质:不同的人去做同一件事情,结果不同。

下面我们来讲解动态的多态;

2.多态的定义及实现 2.1多态的构成条件

多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要想构成多态需要满足两个条件:

  1. 必须通过基类的指针或者引用调用虚函数。

  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

    两个条件缺一不可!!

2.2虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

需要注意的是:

  1. 只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual。
  2. 虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性。
2.3虚函数的重写

虚函数的重写也叫做虚函数的覆盖,若派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同),此时我们称该派生类的虚函数重写了基类的虚函数。

例如,我们以下Student和Soldier两个子类重写了父类Person的虚函数。

//父类
class Person
{
public:
	//父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	//子类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
//子类
class Soldier : public Person
{
public:
	//子类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "优先-买票" << endl;
	}
};

现在我们就可以通过父类Person的指针或者引用调用虚函数BuyTicket,此时不同类型的对象,调用的就是不同的函数,产生的也是不同的结果,进而实现了函数调用的多种形态。

void Func(Person& p)
{
	//通过父类的引用调用虚函数
	p.BuyTicket();
}
void Func(Person* p)
{
	//通过父类的指针调用虚函数
	p->BuyTicket();
}
int main()
{
	Person p;   //普通人
	Student st; //学生
	Soldier sd; //军人

	Func(p);  //买票-全价
	Func(st); //买票-半价
	Func(sd); //优先买票

	Func(&p);  //买票-全价
	Func(&st); //买票-半价
	Func(&sd); //优先买票
	return 0;
}

注意: 在重写基类虚函数时,派生类的虚函数不加virtual关键字也可以构成重写,主要原因是因为继承后基类的虚函数被继承下来了,在派生类中依旧保持虚函数属性。但是这种写法不是很规范,因此建议在派生类的虚函数前也加上virtual关键字。

2.3.1函数重写的两个例外:

1.协变(基类与派生类虚函数返回值类型不同)

重写必须满足三同(函数名,返回值,参数列表),但是返回值是不同时称为协变。
即基类1虚函数返回基类2对象的指针或者引用,派生类1虚函数返回派生类2对象的指针或者引用时,称为协变。(了解)
协变虽然返回值不同但是构成多态条件之一。
例如:

class A{};
class B : public A {};
class Person {
public:
   // virtual B* f() {return new B;} 不构成协变
	virtual A* f() {return new A;}
};
class Student : public Person {
public:
  //  virtual A* f() {return new A;} 不构成协变
	virtual B* f() {return new B;}
};

注意:基类 只能返回基类的,父类只能返回父类的。

2.析构函数的重写 (基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor 。

class Person {
public:
	//~Person() { cout << "~Person()" << endl; }
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	  //~Student() { cout << "~Student()" << endl; }
	  virtual  ~Student() { cout << "~Student()" << endl; }
};
int main()
{
// 普通对象,析构函数是否虚函数,是否完成重写,都正确调用了
     Person p;
     Student s;
     //	运行结果:
	//	~Person()
     // ~Student()
	//	~Person()
}

不构成多态时导致内存泄漏的场景:

a、 不构成多态情况:

int main()
{ 	
	Person  p1 = new Person;
	Person  p2 = new Student; 
	delete p1;// ~Person()+ free(p1)
	delete p2;// ~Person()+ free(p2)
	//	运行结果:
	//	~Person() 
  //	~Student()
}

这里没有析构Student类的成员,特殊情况时,必须要构成多态才能完成正确析构,清理资源。

b、构成多态(正确)情况:

int main()
{
	Person * p1 = new Person;
	Person * p2 = new Student; 
	delete p1;// ~Person()+ free(p1)
	delete p2;// ~Student()+ free(父类成员) + A::~Person() + free(基类成员);
	//	运行结果:
	//	~Person()
     // ~Student()
	//	~Person()
}

总结上述:

1、构成多态,根p的类型没有关系,传的那个类型的对象,调用的就是这个类型的虚函数----根对象有关。

2、不构成多态,调用就是p类型的函数—根类型有关。
3、动态申请的继承对象,如果给了父类指针管理,那么需要析构函数是虚函数。

2.3.2 多态设计的缺陷

被调用的函数必须是虚函数,虚函数的重写允许,两个都是虚函数或者父类是虚函数,在满足三同,就构成重写。其实这个是c++不是很规范的地方,当然我们建议两个都写virtual。本质上,子类重写的虚函数,可以不加virtual是因为析构函数,大佬的设计初衷,父类析构函数加上virtual,那么就不存在不构成多态的情况,没调用子类析构函数,导致内存泄漏场景。
如下所示:
虽然子类没写virtual,但是他是先继承了父类的虚函数的属性,再完成重写。那么他也算是虚函数,并且也继承了public属性。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }

	virtual ~Person() { cout << "~Person()" << endl; }
};

class Student : public Person {

// 虽然子类没写virtual,但是他是先继承了父类的虚函数的属性,再完成重写。那么他也算是虚函数
	void BuyTicket() { cout << "买票-半价" << endl; }
public:
	~Student() { cout << "~Student()" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{

	Person ps;
	Student st;
	Func(ps);//"买票-全价"
	Func(st);//"买票-半价"
	return 0;
}

建议,我们自己写的时候,都加上virtual,肯定没毛病!!!

2.4. C++11 override和final 2.4.1 final关键字

从上面可以看出,C++对函数重写的要求比较严格,有些情况下由于疏忽可能会导致函数名的字母次序写反而无法构成重写,而这种错误在编译期间是不会报错的,直到在程序运行时没有得到预期结果再来进行调试会得不偿失,因此,C++11提供了final和override两个关键字,可以帮助用户检测是否重写。

final:修饰虚函数,表示该虚函数不能再被重写。

class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() {cout << "Benz-舒适" << endl;}// 报错
};

设计一个不能被继承的类?

c++98

class A {
private:
	A(int a)
		:_a(a)
	{}
public:
	static A GreatBase(const int a=0)
	{
		return A(a);
	}
	int _a;
};
class B:public A
{
	
};
int main()
{
	B b;//编译报错, A类的构造为私有,B类继承了A类但是无法调用A类的构造,这样就限制了A类被继承
	A a = A::GreatBase(1);//因为A类构造是私有的,类外无法调用构造,在类里定义静态的函数进行调用构造
	return 0;
}

c++11 ,利用final关键字直接限制,简单明了

// c++11 直接限制
class A final
{
private:
	A(int a)
		:_a(a)
	{}
public:
	int _a;
};
class B :public A // 报错 
{};
2.4.2 override关键字

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
	virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
2.5 重载、覆盖(重写)、隐藏(重定义)的对比

3. 抽象类 3.1 概念

1、在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

//抽象类(接口类)
class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
	void fun();
	int  _a;
};
int main()
{
	Car c; //抽象类不能实例化出对象,error
	Car* p=nullptr;
	p->Drive();//虚函数,对虚表指针解引用,对该对象成员解引用,空指针解引用问题,可以实现纯虚函数但是调用会崩溃。error
	p->fun();// fun()在公共代码段,没有解引用。没有空指针问题
	return 0;
}

2、派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

注意:纯虚函数一般只声明,不实现------实现了也没有价值。抽象—在现实世界中没有对应的实物。一个类型,如果一般在现实世界中,没有具体的对应实物就定义成抽象类比较好。

纯虚函数与override的区别是什么?

纯虚函数的类,本质上强制要求子类去完成虚函数重写。override知识在语法上检查是否完成重写。

3.2 接口继承和实现继承

实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。

接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。

建议: 所以如果不实现多态,就不要把函数定义成虚函数。

4.多态的原理

上述已经讲解了多态的实现,只要符合两个条件就构成多态,多态按照对象去调用根类型无关,不构成多态就按照类型去调用。

关学习实现是不够的,我们还要知道多态的原理。

我想在学习上述的内容会有这样的疑问-----

为什么多态是根据对象来调用函数?

为什么必须要有那两个多态条件才能构成多态?

在学习多态的原理之前,必须知道

符合多态的两个条件:

  1. 必须通过基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
4.1虚函数表

这里常考一道笔试题:sizeof(Base)是多少?

class Base
{
public:
virtual void Func1()
{
	cout << "Func1()" << endl;
}
private:
	int _b = 1;
};

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),

对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

如下图对象存储模型:

那么派生类中这个表放了些什么呢?

下面Base类Func1和Func2是虚函数,Func3普通函数,Derive类继承了Base类,并且Derive类里的Func1重写了Base类的Func1。

#include 
using namespace std;
//父类
class Base
{
public:
	//虚函数
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	//虚函数
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	//普通成员函数
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
//子类
class Derive : public Base
{
public:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过观察和测试,我们发现了以下几点问题:

1、B对象虚表里存放了两个虚函数地址,这是必然的,因为Base类里有两个虚函数。

2、基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3、另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

4、虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

5、总结一下派生类的虚表生成

​ a.先将基类中的虚表内容拷贝一份到派生类虚表中

​ b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

​ c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

虚表是什么阶段初始化的?虚函数存在哪的?虚表存在哪的?

答:虚表实际上是在构造函数初始化列表阶段进行初始化的。虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

​那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的,Linux g++下大家自己去验证?

int j = 0;
int main()
{
	Base b;
	Base* p = &b;
	printf("vfptr:%pn", *((int*)p)); //000FDCAC
	int i = 0;
	printf("栈上地址:%pn", &i);       //005CFE24
	printf("数据段地址:%pn", &j);     //0010038C

	int* k = new int;
	printf("堆上地址:%pn", k);       //00A6CA00
	char* cp = "hello world";
	printf("代码段地址:%pn", cp);    //000FDCB4
	return 0;
}

通过验证,虚表确实存在代码段。上述我给大家介绍了虚表,虚表还有很多细节----。

6、每个类的虚表只有一份,同类型的对象,虚表指针是相同的指向同一张虚表。

class A
{
public:
	virtual void fun()
	{
		cout << "A fun()n";
	}
};
int main()
{
	A a;
	A aa;
	A aaa;
	return 0;
}

7、 虚表不能被切片

class A
{
public:
	virtual void fun()
	{
		cout << "A fun()n";
	}
	int _a;
};
class B : public A
{
public:
	virtual void fun()
	{
		cout << "B fun()n";
	}
	int _b;
};
int main()
{
	B b;
	A a = b;
	return 0;
}

我们要知道每个类的虚表只有一份,并且子类切片给父类时,子类的虚表指针是不会被切片,如果把子类的虚表指针传给父类,那么麻烦大了,如果是子类的切片并且重写了析构虚函数,那么父类对象在析构时就调用了子类的析构函数。但是对象的指针和引用虚表指针是可以切片。

4.2多态的原理

上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的
Person::BuyTicket,传Student调用的是Student::BuyTicket

#include 
using namespace std;
//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
    int _b=1;
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
    int _d=2;
};
int main()
{
	Person Mike;
	Student Johnson;
	Person* p1 = &Mike;
	Person* p2 = &Johnson;
	p1->BuyTicket(); //买票-全价
	p2->BuyTicket(); //买票-半价
	return 0;
}

围绕此图分析便可得到多态的原理:

  1. 父类指针p1指向Mike对象,p1->BuyTicket在Mike的虚表中找到的虚函数就是Person::BuyTicket。
  2. 父类指针p2指向Johnson对象,p2>BuyTicket在Johnson的虚表中找到的虚函数就是Student::BuyTicket。

这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

现在想想多态构成的两个条件,一是完成虚函数的重写,二是必须使用父类的指针或者引用去调用虚函数。必须完成虚函数的重写是因为我们需要完成子类虚表当中虚函数地址的覆盖,那为什么必须使用父类的指针或者引用去调用虚函数呢?为什么使用父类对象去调用虚函数达不到多态的效果呢?

a、父类的指针或者引用去调用虚函数

	Person* p1 = &Mike;
	Person* p2 = &Johnson;

执行这两条语句时,发生切片,这里的切片,把p1只能指向Mike对象的Person部分,p2也同样如此。如图:

因此,在运行时,虽然都是用同样的步骤去虚表里找函数再调用,但是因为虚表指针指向不同的虚表,最后也就调用不同的函数了。之前我们学习了,重写,虚表,切片都是为了学习理解多态打基础。

b、使用父类对象去调用虚函数

Person p1 = Mike;
Person p2 = Johnson;

我们使用Person对象去接收继承对象,如果把虚函数表指针切片给p1,p2对象,那么在析构时p2对象会调用BuyTicket类的析构函数,调错了析构函数,所以虚表不会表切片。切片的过程除了虚表指针的基类的成员都会被切片,虚表指针指向同类型的虚表。如图所示:所以使用父类对象去调用虚函数其他方面的问题不能使用,所以编译器不会选择使用父类对象去调用虚函数,这不是多态的条件之一。

总结一下:

  1. 构成多态,指向谁就调用谁的虚函数,跟对象有关。
  2. 不构成多态,对象类型是什么就调用谁的虚函数,跟类型有关。
5. 动态绑定和静态绑定

静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。

动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

对于下列代码:

//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

我们若是按照如下方式调用BuyTicket函数,则不构成多态,函数的调用是在编译时确定的。

	Student Johnson;
	Person p = Johnson; //不构成多态
	p.BuyTicket();

p.BuyTicket();指令的汇编代码
首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址

mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
... 

我们若是按照如下方式调用BuyTicket函数,则构成多态,函数的调用是在运行时确定的。

	Student Johnson;
	Person& p = Johnson; //构成多态
	p.BuyTicket();

p.BuyTicket();指令的汇编代码的意思去虚表中找函数地址在进行调用

p->BuyTicket();
// p中存的是mike对象的指针,将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp

这样就很好的体现了静态绑定是在编译时确定的,而动态绑定是在运行时确定的。

如果是虚函数那么就一定去虚表里找吗?

只有符合多态的条件,编译器才会到虚函数表中取找,在调用!

总结一下上述的全部内容:

多态的最核心的部分,多态是多种形态,不构成多态编译时决议,构成多态运行时决议,重写(覆盖);

5.单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

5.1 单继承中的虚函数表
//基类
class Base
{
public:
	virtual void func1() { cout << "Base::func1()" << endl; }
	virtual void func2() { cout << "Base::func2()" << endl; }
private:
	int _a;
};
//派生类
class Derive : public Base
{
public:
	virtual void func1() { cout << "Derive::func1()" << endl; }
	virtual void func3() { cout << "Derive::func3()" << endl; }
	virtual void func4() { cout << "Derive::func4()" << endl; }
private:
	int _b;
};

上述虚表总结5已经讲过了,这里不讲解,
在vs监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

5.1.1 通过内存窗口查看虚表

注意:vs下的监视窗口的虚表里的函数地址并不是函数的真正地址,而是需要jmp多层以后才能找到,在这期间编译器会做一些准备工作。

5.1.2 通过代码打印虚表
typedef void(*VR_PTR)();
void Print_Table(VR_PTR* ptr)// 函数指针数组
{
	printf("虚表地址:%pn", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
		ptr[i](); //使用虚函数地址调用虚函数
	}
	printf("n");
}
//基类
class Base
{
public:
	virtual void func1() { cout << "Base::func1()" << endl; }
	virtual void func2() { cout << "Base::func2()" << endl; }
private:
	int _a;
};
//派生类
class Derive : public Base
{
public:
	virtual void func1() { cout << "Derive::func1()" << endl; }
	virtual void func3() { cout << "Derive::func3()" << endl; }
	virtual void func4() { cout << "Derive::func4()" << endl; }
private:
	int _b;
};
int main()
{	
    Print_Table((VR_PTR*)(*(int*)&b)); 32位下
    Print_Table((VR_PTR*)(*(long long *)&b)); 64位下
    // 还有一种写法是 利用宏_WIN64+条件编译
     Base b;
    #ifdef _WIN64
    Print_Table((VR_PTR*)(*(long long *)&b));
     #endif 
   	Print_Table((VR_PTR*)(*(int*)&b));
    
     // 下面的写法是通用写法
	Base b;
	Print_Table((VR_PTR*)(*(void**)&b));
	Derive d;
	Print_Table((VR_PTR*)(*(void**)&d));
	return 0;
}

运行结果:

5.2 多继承中的虚函数表
//基类1
class Base1
{
public:
	virtual void func1() { cout << "Base1::func1()" << endl; }
	virtual void func2() { cout << "Base1::func2()" << endl; }
private:
	int _b1;
};
//基类2
class Base2
{
public:
	virtual void func1() { cout << "Base2::func1()" << endl; }
	virtual void func2() { cout << "Base2::func2()" << endl; }
private:
	int _b2;
};
//多继承派生类
class Derive : public Base1, public Base2
{
public:
	virtual void func1() { cout << "Derive::func1()" << endl; }
	virtual void func3() { cout << "Derive::func3()" << endl; }
private:
	int _d1;
};
int main()
{
	Base1 b1;
	Print_Table((VR_PTR*)(*(void**)&b1));
	Base2 b2;
	Print_Table((VR_PTR*)(*(void**)&b2));

	Derive d;
	Print_Table((VR_PTR*)(*(void**)&d));
    
	Print_Table((VR_PTR*)(*(void**)((char *)&d+sizeof(Base1))));
	return 0;
}

通过观察:多继承d对象,Derive::fun3()的虚函数只放放在第一张继承基类部分的虚函数表中

5.3. 菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的宝宝,可以去看下面的两篇链接文章。

c++虚函数表解析

c++对象的内存布局

虚继承与重写结构:

6. 继承和多态常见的面试问题 6.1 填空题:

1、下面哪种面向对象的方法可以让你变得富有()

A.继承 B.封装 C.多态 D.抽象

2、()是面向对象程序设计语言中的一种机制,这种机制实现了方法的定义与具体的对象无关,而方法的调用则可以关联于具体的对象。

A.继承 B.模板 C.对象的自身引用 D.动态绑定

3、关于面向对象设计中的继承和组合,下面说法错误的是()

A.继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用。
B.组合的对象不需要关系各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用。
C.优先使用继承,而不是组合,是面向对象设计的第二原则。
D.继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现。

4、以下关于纯虚函数的说法,正确的是()

A.声明纯虚函数的类不能实例化对象
B.声明纯虚函数的类是虚基类
C.子类必须实现基类的纯虚函数
D.纯虚函数必须是空函数

5、关于虚函数的描述正确的是()

A.派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B.内联函数不能是虚函数
C.派生类必须重新定义基类的虚函数
D.虚函数可以是一个static型的函数

6、关于虚表的说法正确的是()

A.一个类只能有一张虚表
B.基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C.虚表是在运行期间动态生成的
D.一个类的不同对象共享该类的虚表

7、假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则()

A.A类对象的前4个字节存储虚表地址,B类对象的前4个字节不是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚基表的地址
C.A类对象和B类对象前4个字节存储的虚表地址相同
D.A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

8、下面程序输出结果是什么?

#include 
using namespace std;
class A
{
public:
	A(char* s) { cout << s << endl; }
	~A() {};
};
class B : virtual public A
{
public:
	B(char* s1, char* s2)
		:A(s1)
	{
		cout << s2 << endl;
	}
};
class C : virtual public A
{
public:
	C(char* s1, char* s2)
		:A(s1)
	{
		cout << s2 << endl;
	}
};
class D : public B, public C
{
public:
	D(char* s1, char* s2, char* s3, char* s4)
		:B(s1, s2)
		, C(s1, s3)
		, A(s1)
	{
		cout << s4 << endl;
	}
};
int main()
{
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

A.class A class B class C class D
B.class D class B class C class A
C.class D class C class B class A
D.class A class C class CBemsp;class D

9、下面说法正确的是?(多继承中指针的偏移问题)

class Base1
{
public:
	int _b1;
};
class Base2
{
public:
	int _b2;
};
class Derive : public Base1, public Base2
{
public:
	int _d;
};
int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

A.p1 == p2 == p3
B.p1 < p2 < p3
C.p1 == p3 != p2
D.p1 != p2 != p3

10、以下程序输出结果是什么?

#include 
using namespace std;
class A
{
public:
	virtual void func(int val = 1)
	{
		cout << "A->" << val << endl;
	}
	virtual void test()
	{
		func();
	}
};
class B : public A
{
public:
	void func(int val = 0)
	{
		cout << "B->" << val << endl;
	}
};
int main()
{
	B* p = new B;
	p->test();
	return 0;
}

A.A->0 B.B->1 C.A->1 D.B->0
E.编译错误 F.以上都不正确

答案

1A6D
2D7D
3C8A
4A9C
5B10B
6.2 问答题

1、什么是多态?

多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。多态又分为静态的多态和动态的多态。

2、什么是重载、重写(覆盖)、重定义(隐藏)?

重写(覆盖)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名、参数、返回值都必须相同(协变例外),且这两个函数都是虚函数。

重定义(隐藏)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名相同。若两个基类和派生类的同名函数不构成重写就是重定义。

重载是指两个函数在同一作用域,这两个函数的函数名相同,参数不同。

3、多态的实现原理?

构成多态的父类对象和子类对象的成员当中都包含一个虚表指针,这个虚表指针指向一个虚表,虚表当中存储的是该类对应的虚函数地址。因此,当父类指针指向父类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是父类当中对应的虚函数;当父类指针指向子类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是子类当中对应的虚函数。

4、inline函数可以是虚函数吗?

可以,调用时,如果不构成多态,这个函数保持inline属性如果构成多态,这个函数就没有inline属性了,因为调用是到对象的虚函数表中找到虚函数地址,实现调用无法使用inline属性。
例如:

注意:在类里定义的函数默认是有内联属性的,而类里只有声明,如果要内联属性,那么需要在声明前加 inline 关键字。

// 假设B重写了A类的func虚函数。
int main()
{
	A aa;
	a.func();// 不构成多态,保持内联属性。
	
	B bb;
	bb.func(); // 构成多态,没有内联属性了
}

5、静态成员函数可以是虚函数吗?

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

6、构造函数可以是虚函数吗?

不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的,也就是有了对象才能访问到虚表。

7、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

可以,并且最好把基类的析构函数定义成虚函数。
内存泄漏的场景:

int main()
{
	Person * p1 = new Person;
	Person * p2 = new Student; 
	delete p1;// ~Person()+ free(p1)
	delete p2;// ~Student()+ free(父类成员) + A::~Person() + free(基类成员);
	//	运行结果:
	//	~Person()
     // ~Student()
	//	~Person()
}

8、对象访问普通函数快还是虚函数更快?

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

9、虚函数表是在什么阶段生成的?存在哪的?

虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

10、C++菱形继承的问题?虚继承的原理?

什么是抽象类?抽线类的作用?

抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

转载请注明:文章转载自 www.wk8.com.cn
本文地址:https://www.wk8.com.cn/it/1038273.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 wk8.com.cn

ICP备案号:晋ICP备2021003244-6号