C++多态

多态

  • [1. 多态的概念](#1. 多态的概念)
  • [2. 多态的定义及实现](#2. 多态的定义及实现)
    • [2.1 多态的构成条件](#2.1 多态的构成条件)
    • [2.2 虚函数](#2.2 虚函数)
      • [2.2.1 虚函数的重写/覆盖](#2.2.1 虚函数的重写/覆盖)
      • [2.2.2 多态场景的一个选择题](#2.2.2 多态场景的一个选择题)
      • [2.2.3 虚函数重写的一些问题](#2.2.3 虚函数重写的一些问题)
        • [2.2.3.1 协变](#2.2.3.1 协变)
        • [2.2.3.2 析构函数的重写](#2.2.3.2 析构函数的重写)
    • [2.3 C++11关键字:override 和 final](#2.3 C++11关键字:override 和 final)
    • [2.4 重载/重写/隐藏对比](#2.4 重载/重写/隐藏对比)
  • [3. 纯虚函数和抽象类](#3. 纯虚函数和抽象类)
  • [4. 多态的原理](#4. 多态的原理)
    • [4.1 虚函数表指针](#4.1 虚函数表指针)
    • [4.2 多态的原理](#4.2 多态的原理)
      • [4.2.1 多态是如何实现的](#4.2.1 多态是如何实现的)
      • [4.2.2 动态绑定与静态绑定](#4.2.2 动态绑定与静态绑定)
      • [4.2.3 虚函数表](#4.2.3 虚函数表)
  • [5.扩展: 单继承和多继承的虚函数表深入探索](#5.扩展: 单继承和多继承的虚函数表深入探索)
    • [5.1 单继承虚函数表深入探索](#5.1 单继承虚函数表深入探索)
    • [5.2 多继承虚函数表深入探索](#5.2 多继承虚函数表深入探索)
    • [5.3 继承章节遗留的问题](#5.3 继承章节遗留的问题)
  • [6. 继承和多态考察的常见问题](#6. 继承和多态考察的常见问题)

1. 多态的概念

多态 :通俗来说,就是多种形态,具体点就是去完成某个⾏为(函数),传不同的对象去完成时会产生不同的状态。多态分为编译时多态(静态多态)和运行时多态(动态多态)编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。这里我们重点讲运行时多态。

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

2. 多态的定义及实现

2.1 多态的构成条件

多态是一个继承关系下的类对象去调用同一函数产生了不同行为。例如以买票为例,Student对象继承了Person,Student对象买票半价,Person对象买票全价。

实现多态还有两个必须条件

  1. 必须通过基类的指针或引用进行调用虚函数。因为只有基类的指针或引用才能既指向基类对象⼜指向派⽣类对象。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行了重写/覆盖。
cpp 复制代码
class Person 
{
public:
	// 在成员函数加virtual就是虚函数
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
	// 只要基类中加了virtual,这里不加virtual也构成多态,但是建议加上virtual
	// 注意:派生类只对基类的虚函数重写了实现,即重写了函数体。
	/*virtual void BuyTicket() { cout << "买票-打折" << endl; }*/
	void BuyTicket() { cout << "买票-打折" << endl; }
};

void Func1(Person* ptr)
{
	// 这⾥可以看到虽然都是Person指针ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
} 

void Func2(Person& p)
{
	// 这⾥可以看到虽然都是Person引用p在调⽤BuyTicket
	// 但是跟p没关系,⽽是由p引用的对象决定的。
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	// 指针
	Func1(&ps);
	Func1(&st);

	// 引用
	Func2(ps);
	Func2(st);

	return 0;
}

2.2 虚函数

虚函数:被virtual修饰的类成员函数是虚函数,注意非成员函数不能加virtual修饰。

cpp 复制代码
class Person
{
public:
	// 虚函数
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

2.2.1 虚函数的重写/覆盖

虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即派生类的虚函数和基类的虚函数的函数名,参数列表的类型,返回值完全相同),那么就称派生类的虚函数重写了基类的虚函数。

  1. 虚函数的重写是语法层的概念,在原理层上又称覆盖。这里重写的是虚函数的实现,继承的是虚函数接口,如果把函数的声明比作壳,函数的实现比作核,那么派生类继承的是壳即声明,重写的是核即实现。
  2. 注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意埋这个坑,让你判断是否构成多态。
cpp 复制代码
class Animal
{
public :
	virtual void talk() const
	{}
};

class Dog : public Animal
{
public :
	virtual void talk() const
	{
		std::cout << "汪汪" << std::endl;
	}
};

class Cat : public Animal
{
public :
	virtual void talk() const
	{
		std::cout << "(>^ω^<)喵" << std::endl;
	}
};

void letsHear(const Animal& animal)
{
	animal.talk();
}

int main()
{
	Cat cat;
	Dog dog;
	letsHear(cat);
	letsHear(dog);

	return 0;
}

2.2.2 多态场景的一个选择题

以下程序输出结果是什么()

A: A->0

B:B->1

C:A->1

D:B->0

E:编译出错

F:以上都不正确

cpp 复制代码
class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
class B : public A
{
public :
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
	B* p = new B;
	
	p->test();

	return 0;
}

答案:B

派生类B继承了基类A,B的指针 p 先调用了基类A中的test()函数,然后this指针调用了func()函数,(因为是A类的this指针,所以其类型是A,A*是基类的指针,基类的指针调用虚函数且还满足其他条件,所以就构成了多态。)因为该this指针指向的对象是new出来的B,且基类和派生类的func虚函数构成了重写,所以调用的是B类的func函数。但重写是只重写实现,声明还是用的基类中虚函数的声明,所以B->1。

加深对多态的理解:

分析一下下面操作要选哪个选项

cpp 复制代码
p->func();

答案:D

p是派生类的指针,派生类的指针调用虚函数不构成多态,所以不会重写,因此调用的是派生类中的func,所以B->0

2.2.3 虚函数重写的一些问题

2.2.3.1 协变

协变:派生类重写基类的虚函数时,与基类虚函数的返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。

返回值类型可以是该基类对象和派生类对象指针或者引用

cpp 复制代码
class Person
{
public:
	virtual Person* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};

class Student : public Person
{
public:
	virtual Student* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

void Func(Person* ptr)
{
	ptr->BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);

	return 0;
}

返回值类型可以是其他基类对象和派生类对象指针或者引用

cpp 复制代码
class A {};

class B : public A {};

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

class Student : public Person 
{
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

void Func(Person* ptr)
{
	ptr->BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);

	return 0;
}
2.2.3.2 析构函数的重写

如果基类的析构函数加上virtual成为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,但实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。

下面的代码我们可以看到,如果~ A(),不加virtual,那么delete p2时就只调⽤了A的析构函数,没有调⽤B的析构函数,导致内存泄漏问题,因为~B()中在释放资源。

所以基类中的析构函数建议设计为虚函数。

基类的析构函数不加virtual产生的效果

cpp 复制代码
class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A 
{
public:
	virtual ~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;

	// 期望调用析构函数构成多态,从而正确调用对应的析构函数
	// 因为p1 和 p2 都是A类的指针,所以都调用A类的析构
	delete p1; // p1->~A() + operator delete(p1)

	delete p2; // p1->~A() + operator delete(p1)

	return 0;
}

基类的析构函数加virtual产生的效果

cpp 复制代码
class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A 
{
public:
	virtual ~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;

	// p1->destructor() + operator delete(p1)
	delete p1;

	// p2->destructor() + operator delete(p2)
	delete p2;

	return 0;
}

因为派生类的析构会自动调用基类的析构,所以才会多一个~A()

2.3 C++11关键字:override 和 final

override

从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的,在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。

注意:override是加在派生类虚函数声明后面的。

cpp 复制代码
// error C3668: "Benz::Drive": 包含重写说明符"override"的方法没有重写任何基类方法
// 两个虚函数名字不一样
class Car 
{
public:
	virtual void Dirve()
	{}
};

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

int main()
{
	return 0;
}

final

如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰基类的虚函数,同样加在声明后面。

cpp 复制代码
// error C3248: "Car::Drive": 声明为 "final" 的函数不能由 "Benz::Drive" 重写
class Car
{
public :
	virtual void Drive() final {}
};

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

int main()
{
	return 0;
}

2.4 重载/重写/隐藏对比

3. 纯虚函数和抽象类

在虚函数的后⾯写上 = 0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。

cpp 复制代码
// 抽象类
class Car
{
public :
	// 纯虚函数
	virtual void Drive() = 0;
};

// 抽象类
class Benz :public Car
{
public:
	// 不构成重写,所以该子类也包含父类中的纯虚函数,也是抽象类
	virtual void Drive(int x)
	{
		cout << "Benz-舒适" << endl;
	}
};

class BMW :public Car
{
public :
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};

int main()
{
	// 编译报错:error C2259: "Car": ⽆法实例化抽象类
	Car car;

	// error C2259: "Benz": 无法实例化抽象类
	Benz benz;

	Car* pBMW = new BMW;
	pBMW->Drive();

	return 0;
}

4. 多态的原理

4.1 虚函数表指针

下⾯编译为32位程序的运⾏结果是什么()

A. 编译报错

B. 运⾏报错

C. 8

D. 12

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;

	return 0;
}

答案:D

根据之前学的内存对齐可知Base类对象是8个字节,但是因为类中有虚函数,编译器会在头部或尾部(VS是头部)加一个虚函数表指针,所以是12字节。

vfptr实际是vftptr,v是virtual,f是function,t是table,ptr是pointer,合起来就是虚函数表指针。

⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

4.2 多态的原理

4.2.1 多态是如何实现的

从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调⽤Person::BuyTicket,ptr指向Student对象调⽤Student::BuyTicket的呢?通过下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。


4.2.2 动态绑定与静态绑定

  1. 对不满足多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
  2. 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
cpp 复制代码
	// ptr是指针 + BuyTicket是虚函数满⾜多态条件。
	// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址 
	ptr->BuyTicket();

	// 下面是该指令的汇编代码
00EF2001  mov         eax,dword ptr [ptr]  
00EF2004  mov         edx,dword ptr [eax]  
00EF2006  mov         esi,esp  
00EF2008  mov         ecx,dword ptr [ptr]  
00EF200B  mov         eax,dword ptr [edx]  
00EF200D  call        eax

	// BuyTicket不是虚函数,不满⾜多态条件。
	// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
	ptr->BuyTicket();
	// 下面是该指令的汇编代码
00EA2C91  mov         ecx,dword ptr [ptr]  
00EA2C94  call        Student::Student (0EA153Ch)

4.2.3 虚函数表

  1. 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。
  2. 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也是独⽴的。
  3. 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
cpp 复制代码
class A
{
public:
	int _a;
	virtual void func1() {};
};

class B : public A
{
public:
	int _b;
	virtual void func1() {};
};

class C : public A
{
public:
	int _c;

	virtual void func1() {};
};

int main()
{
	B b;
	C c;

	A a1;
	A a2;
	A a3;
}
  1. 派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分。
  2. 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)
  3. 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
cpp 复制代码
class Base 
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};

class Derive : public Base
{
public:
	// 重写基类的func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}
  1. 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)
cpp 复制代码
int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈: % p\n", &i);
	printf("静态区: % p\n", &j);
	printf("堆: % p\n", p1);
	printf("常量区: % p\n", p2);
	Base b;
	Derive d;
	Base * p3 = &b;
	Derive * p4 = &d;
	printf("Person虚表地址: % p\n", *(int*)p3);
	printf("Student虚表地址: % p\n", *(int*)p4);
	printf("虚函数地址: % p\n", &Base::func1);
	printf("普通函数地址: % p\n", &Base::func5);

	return 0;
}

根据打印的虚表地址和其他区的地址可知:虚表是存放在代码段(常量区)的。

5.扩展: 单继承和多继承的虚函数表深入探索

5.1 单继承虚函数表深入探索

vs编译器的监视窗⼝是经过特殊处理的,以它的⻆度给出了⼀个⽅便看的样⼦,但并不是本来的样⼦。多态部分我们讲了,虚函数指针都要放进虚函数表,这⾥我们通过监视窗⼝观察Derive对象,看不到func3和func4在虚表中,借助内存窗⼝可以看到⼀个地址,但是并不确认是不是func3和func4的地址。所以下⾯我们写了⼀份特殊代码,通过指针的⽅式,强制访问了虚函数表,调⽤了虚函数,确认继承中虚函数表中的真实内容。

cpp 复制代码
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; }
	void func5() { cout << "Derive::func5" << endl; }
private:
	int b;
};

typedef void(*VFPTR) (); // 函数指针

void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调⽤。调⽤就可以看出存的是哪个函数
	cout << " 虚表地址> " << vTable << endl;
	// 注意如果是在g++下⾯,这⾥就不能⽤nullptr去判断访问虚表结束了
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址: 0X%p,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f(); // 调用f指向的函数
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	// 32位程序的访问思路如下:
	// 需要注意的是如果是在64位下,指针是8byte,对应程序位置就需要进⾏更改
	// 思路:取出b、d对象的头4bytes,就是虚表的指针,虚函数表本质是⼀个存
	// 虚函数指针的指针数组,vs下这个数组最后⾯放了⼀个nullptr,g++下⾯最后没有nullptr
	// 1.先取b的地址,强转成⼀个int*的指针
	// 2.再解引⽤取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
	// 3.再强转成VFPTR *,因为虚表就是⼀个存VFPTR类型(虚函数指针类型)的数组。
	// 4.虚表指针传递给PrintVTable进⾏打印虚表
	// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不⼲净,
	// 虚表最后⾯没有放nullptr,导致越界,这是编译器的问题。我们只需要点⽬录栏的⽣成
	// 清理解决⽅案,再编译就好了。

	VFPTR * vTable1 = (VFPTR*)(*(int*)&b);
	PrintVTable(vTable1);

	VFPTR* vTable2 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTable2);

	return 0;
}

5.2 多继承虚函数表深入探索

  1. 跟前⾯单继承类似,多继承时Derive对象的虚表在监视窗⼝也观察不到部分虚函数的指针。所以我们⼀样可以借助上⾯的思路强制打印虚函数表。
  2. 需要注意的是多继承时,Derive中同时继承了Base1和Base2,内存中先继承的对象在前⾯,并且Derive中包含的Base1和Base2各有⼀张虚函数表,通过观察我们发现Derive没有重写的虚函数func3,选择放在先继承的Base1的虚函数表中。
  3. 另外需要注意的是,细⼼观察的话可以看到Derive对象中重写的Base1虚表的func1地址和重写Base2虚表的func1地址不⼀样,这是为什么呢?这个问题还⽐较复杂。需要我们分别对这两个函数进⾏
    多态调⽤,并翻阅对应的汇编代码进⾏分析,才能捋清楚问题所在。这⾥简单说⼀个结论就是本质Base2虚表中func1的地址并不是真实的func1的地址,⽽是封装过的func1地址,因为Base2指针p2指向Derive时,Base2部分在中间位置,向上转型时,指针会发⽣偏移,那么多态调⽤p2->func1()时,p2传递给this前需要把p2给修正回去指向Derive对象,因为func1是Derive重写的,⾥⾯this应该是指向Derive对象的。
cpp 复制代码
class Base1 
{
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};

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;
};

typedef void(*VFPTR) (); // 函数指针

void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址> " << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址: 0X%p,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	// 取Base2虚表的两种方法:
	//1
	Base2* b = &d;
	VFPTR* vTableb2 = (VFPTR*)(*(int*)b);
	// 2
	//VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);

	// 验证:Derive对象中重写的Base1虚表的func1地址和重写Base2虚表的func1地址不⼀样的原因
	Base1* p1 = &d;
	p1->func1();

	Base2* p2 = &d;
	p2->func1();

	d.func1();

	return 0;
}

5.3 继承章节遗留的问题

虚基表中除了后面存放距离基类成员变量的偏移量外,前面的空间其实是给虚表预留的位置,存放的是距离其虚表的偏移量

cpp 复制代码
class A
{
public:
	virtual void func1() {}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1() {}
	virtual void func2() {}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1() {}
	virtual void func3() {}
public:
	int _c;
};

class D : public B, public C
{
public:
	D()
		:_d(1)
	{
	}
	virtual void func1() {}
	virtual void func4() {}
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._a = 3;
	d._b = 4;
	d._c = 5;
	d._d = 6;

	return 0;
}

6. 继承和多态考察的常见问题

选择题

  1. 下面函数输出结果是( )
cpp 复制代码
class A
{
public: 
  virtual void f()
  {
    cout<<"A::f()"<<endl;
  }
};

class B : public A
{
private:
   virtual void f()
  {
    cout<<"B::f()"<<endl;
  }
};

int main()
{
	A* pa = (A*)new B;
	
	pa->f();
	
	return 0;
}

A.B::f()

B.A::f(),因为子类的f()函数是私有的

C.A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象

D.编译错误,私有的成员函数不能在类外调用

第一题:A

A.正确

B.虽然子类函数为私有,但是多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变,只是执行函数发生变化

C.不强制也可以直接赋值,因为赋值兼容规则作出了保证

D.编译正确

  1. 以下哪项说法时正确的( )
cpp 复制代码
class A
{
public:
  void f1(){cout<<"A::f1()"<<endl;}
  virtual void f2(){cout<<"A::f2()"<<endl;}
  virtual void f3(){cout<<"A::f3()"<<endl;}
};

class B : public A
{
public:
  virtual void f1(){cout<<"B::f1()"<<endl;}
  virtual void f2(){cout<<"B::f2()"<<endl;}
  void f3(){cout<<"B::f3()"<<endl;}
};

A.基类和子类的f1函数构成重写

B.基类和子类的f3函数没有构成重写,因为子类f3前没有增加virtual关键字

C.基类引用引用子类对象后,通过基类对象调用f2时,调用的是子类的f2

D.f2和f3都是重写,f1是重定义

第二题:D

A.错误,构成重写是子类重写父类的virtual函数,

B.f3构成重写,重写时子类可以不要求加virtual关键字

C. 选择题一定要扣字眼,题目前半句说的是基类引用 引用了子类对象,但是后半句调用虚函数时,说的是基类的对象调用f2,通过对象调用时编译期间就直接确定调用那个函数了,不会通过虚表以多态方式调用

D.正确

  1. 以下程序输出结果是( )
cpp 复制代码
class A
{
public:
  A ():m_iVal(0){test();}
  virtual void func() { std::cout<<m_iVal<<' ';}
  void test(){func();}
public:
  int m_iVal;
};

class B : public A
{
public:
  B(){test();}
  virtual void func()
  {
    ++m_iVal;
    std::cout<<m_iVal<<' ';
  }
};

int main(int argc ,char* argv[])
{
  A*p = new B;
  p->test();

  return 0;
}

A.1 0

B.0 1

C.0 1 2

D.2 1 0

E.不可预期

F. 以上都不对

第三题:C

分析:new B时先调用父类A的构造函数,执行test()函数,在调用func()函数,由于此时还处于对象构造阶段,多态机制还没有生效,所以,此时执行的func函数为父类的func函数,打印0,构造完父类后执行子类构造函数,又调用test函数,然后又执行func(),由于父类已经构造完毕,虚表已经生成,func满足多态的条件,所以调用子类的func函数,对成员m_iVal加1,进行打印,所以打印1, 最终通过父类指针p->test(),也是执行子类的func,所以会增加m_iVal的值,最终打印2, 所以答案为C 0 1 2

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

A.A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址

B.A类对象和B类对象前4个字节存储的都是虚表的地址

C.A类对象和B类对象前4个字节存储的虚表地址相同

D.A类和B类中的内容完全一样,但是A类和B类使用的不是同一张虚表

第四题:B

A.父类对象和子类对象的前4字节都是虚表地址

B.A类对象和B类对象前4个字节存储的都是虚表的地址,只是各自指向各自的虚表

C.不相同,各自有各自的虚表

D.A类和B类不是同一类内容不同

  1. 关于虚表说法正确的是( )

A.一个类只能有一张虚表

B.基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表

C.虚表是在运行期间动态生成的

D.一个类的不同对象共享该类的虚表

第五题:D

A.多继承的时候,就会可能有多张虚表

B.父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象

C.虚表是在编译期间生成的

D.一个类的不同对象共享该类的虚表,可以自行写代码验证

  1. 假设A为抽象类,下列声明( )是正确的

A.A fun(int);

B.A*p;

C.int fun(A);

D.A obj;

第六题:B

A.抽象类不能实例化对象,所以以对象返回是错误

B.抽象类可以定义指针,而且经常这样做,其目的就是用父类指针指向子类从而实现多态

C.参数为对象,所以错误

D.直接实例化对象,这是不允许的

  1. 关于多态,说法不正确的是( )

A.C++语言的多态性分为编译时的多态性和运行时的多态性

B.编译时的多态性可通过函数重载实现

C.运行时的多态性可通过模板和虚函数实现

D.实现运行时多态性的机制称为动态绑定

第七题:C

A.多态分为编译时多态和运行时多态,也叫早期绑定和晚期绑定

B.编译时多态是早期绑定,主要通过重载实现

C.模板属于编译时多态,故错误

D.运行时多态是动态绑定,也叫晚期绑定

  1. 关于不能设置成虚函数的说法正确的是( )

A.友元函数可以作为虚函数,因为友元函数出现在类中

B.成员函数都可以设置为虚函数

C.静态成员函数不能设置成虚函数,因为静态成员函数不能被重写

D.析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数

第八题:D

A.友元函数不属于成员函数,不能成为虚函数

B.静态成员函数就不能设置为虚函数

C.静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数

D.尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态

  1. 关于虚函数说法正确的是( )

A.被virtual修饰的函数称为虚函数

B.虚函数的作用是用来实现多态

C.虚函数在类中声明和类外定义时候,都必须加虚拟关键字

D.静态虚成员函数没有this指针

第九题:B

A.被virtual修饰的成员函数称为虚函数

B.正确

C.virtual关键字只在声明时加上,在类外实现时不能加

D.static和virtual是不能同时使用的

解答题

  1. 什么是多态?

多态分为静态多态和动态多态静态多态是在编译时就确认调用地址,如函数重载。动态多态是在运行时根据实际指向的对象去找到对应的函数进行调用。

  1. 什么是重载,重写(覆盖),重定义(隐藏)?

重载是函数重载:在同一个作用域下,函数名相同,参数不同的函数就构成重载;重写(覆盖):分别在基类和派生类的不同作用域下,派生类继承基类,基类中有虚函数,派生类和基类的虚函数符合三同(协变除外),即函数名相同,返回值相同(协变除外),参数列表相同,在这样的条件下,派生类对虚函数的实现进行编写的情况是重写(覆盖);重定义(隐藏):分别在基类和派生类的不同作用域下,派生类继承基类,派生类和基类中的函数只需要函数名相同即构成隐藏派生类和基类中的同名函数不构成重写(覆盖)就构成重定义(隐藏),派生类和基类的成员变量相同也构成重写。

  1. 多态实现的原理?

静态多态中的函数重载:函数名修饰规则,传入不同的参数根据函数名修饰规则去对应匹配参数匹配的函数进行调用;动态多态中的多态:虚函数表,对于基类的虚函数表中存放虚函数的地址,如果派生类对基类的虚函数进行了重写,那么对应派生类的重写的虚函数会覆盖到派生类中基类的虚函数表中,在运行时,根据基类的指针或引用实际指向的对象的虚函数表中寻找对应的虚函数的地址进行调用。

  1. inline函数可以是虚函数吗?

可以,不过此时编译器会忽略inline属性,这个函数不再是内敛inline,不会在调用的地方进行展开了,而是虚函数,因为虚函数要放到虚表中,inline的函数没有地址,所以也就不能放到虚表中,所以编译器为了虚函数可以放到虚表中会将inline的属性进行忽略。

  1. 静态成员函数可以是虚函数吗?

不可以,静态成员函数没有this指针,虚函数被放到虚表中,在对象中虚表通过虚函数指针进行访问,要访问对象中的虚函数指针又需要this指针,所以如果是静态成员函数由于没有this指针无法找到虚表中进行调用,所以也就无法实现出多态,所以无法是虚函数。

  1. 构造函数可以是虚函数吗?

不可以,因为对象中的虚函数表指针是在初始化列表初始化的,如果构造函数是虚函数,那么调用虚函数需要先有对象,使用对象中的虚函数表指针找到虚表中的地址才能进行调用,构造函数的作用是构造初始化对象,连对象都没有也就没有虚函数表指针,所以自然不可以。

  1. 析构函数可以是虚函数吗,什么场景下析构函数是虚函数?

可以,假设有两个类A和B,B类中有资源,B继承了A,A* a = new B,delete a;如果基类的析构函数不是虚函数,那就无法与派生类的析构函数构成重写,也就无法释放资源,从而导致内存泄漏,所以可能的话,尽量将析构函数定义为虚函数。

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

如果是这个对象是普通对象,那么编译器检查不构成多态,会直接call地址进行调用,所以访问是一样快;如果这个对象是基类的指针或引用,那么普通对象快,前提是构成多态,在运行的时候会去虚函数表中去查找对应虚函数的地址进行调用。

  1. 虚函数表是在什么阶段生成的,存放到哪里?

虚函数表是在编译阶段生成,因为编译阶段虚函数的地址都已经确定,所以编译器会根据虚函数的地址直接生成虚函数表,虚函数表存放到代码段(常量区)。

  1. 菱形继承的问题,虚继承的原理是什么?

菱形继承的问题是数据冗余和二义性问题,虚继承是将具有两个及以上基类的派生类对象中基类的相同的成员独立成一份公共成员,放到派生类对象存储空间的开头或结尾位置(在vs中是放置到派生类对象存储空间的结尾位置),在基类的原位置放置一个指针,这个指针叫做虚基表指针,这个指针指向虚基表,虚基表是用来找虚基类的,表中存储虚基表指针到公共成员的地址的偏移量,根据这个偏移量就可以找到公共成员。

  1. 什么是抽象类,抽象类的作用是什么?

包含纯虚函数且没有构成重写的类就叫做抽象类,抽象类的作用是强制继承抽象类的派生类重写虚函数,同样抽象类体现出了接口继承的关系。

相关推荐
两颗泡腾片5 小时前
C++提高编程学习--模板
c++·学习
你好!蒋韦杰-(烟雨平生)6 小时前
扫雷游戏C++
c++·单片机·游戏
monicaaaaan7 小时前
搜索二维矩阵Ⅱ C++
c++·线性代数·矩阵
zh_xuan8 小时前
duiLib 自定义资源目录
c++·ui
西红柿煎蛋8 小时前
C++11的可变参数模板 (Variadic Templates) 是如何工作的?如何使用递归解包一个参数包 (parameter pack)?
c++
源代码•宸8 小时前
深入浅出设计模式——创建型模式之原型模式 Prototype
c++·经验分享·设计模式·原型模式
晨曦学习日记9 小时前
Leetcode239:滑动窗口最大值,双端队列的实现!
数据结构·c++·算法
wait a minutes9 小时前
【c++】leetcode763 划分字母区间
开发语言·c++
菜还不练就废了9 小时前
7.25 C/C++蓝桥杯 |排序算法【下】
c语言·c++·排序算法
饭碗的彼岸one10 小时前
重生之我在10天内卷赢C++ - DAY 2
linux·开发语言·c++·笔记·算法·vim