【C++】多态

多态

  • 多态的概念
  • 多态的定义
    • 继承中构成多态的条件
    • 虚函数
    • 虚函数的重写
      • 协变
      • 析构函数的重写
        • 普通调用与多态调用
      • 重写实现
    • C++11override和final
      • final关键字
      • override关键字
    • 重载、重写、隐藏的对比
  • 抽象类
    • 概念
    • 实现继承和接口继承
  • 多态的原理
    • 虚函数表
    • 多态的原理
    • 动态多态和静态多态
  • 单继承的虚函数表
  • 多继承的虚函数表
  • 菱形继承
  • 继承和多态的面试题
    • 概念题
    • 问答题

铁汁们,今天给大家分享一篇多态,来吧,开造⛳️

多态的概念

概念:多态是不同对象完成同一行为产生出不同的状态。即:多态是基类对象和派生类对象,去调用同一函数,产生不同的结果。

eg : 买票这一行为,成人买票为全价买票,学生买票为半价买票。Student类继承了Person类,Student对象买票要半价,Person对象买票要全价。

多态的定义

继承中构成多态的条件

  1. 虚函数的重写。被调用的函数必须是虚函数,子类必须对父类的虚函数进行重写。
  2. .必须是父类指针或者引用去调用虚函数
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

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

class Student : public Person {  //子类(继承)
public:
	virtual void price() //虚函数的重写
	{
		cout << "买票 - 学生 - 全价" << endl;
	}
};

void func(Person* p)
{
	p->price();  //父类指针去调用虚函数
}
int main()
{
	Person p; 
	func(&p); //父类对象

	Student s;
	func(&s);  //子类对象

	return 0;
}


虚函数

概念:被virtual 关键字修饰的类成员函数被称为虚函数。

class Student : public Person {  //子类(继承)
public:
	virtual void price() //虚函数
	{
		cout << "买票 - 学生 - 全价" << endl;
	}
};
  • static和virtual是不能同时使用的。

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

  • 实现多态是要付出代价的,如虚表,虚表指针等,所以不实现多态就不要有虚函数了。

虚函数的重写

  • 概念:重写也称为覆盖。父类和子类中有一个完全相同的虚函数,该虚函数必须函数名、参数类型、返回值类型完全相同(协变除外),称为子类的虚函数重写了父类的虚函数。

协变

  • 概念:父类和子类的虚函数可以返回值类型不相同,但返回值类型必须是父子类关系指针或者引用,也构成重写。它是重写的两个例外之一。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

//协变,返回值类型必须相同,若父类虚函数返回值为指针,则子类虚函数返回值也要为指针
class A {  };  //父类
class B : public A  //子类
{   };

class Person {  //父类
public:
	virtual A* price() 
	{
		cout << "A" << endl;
	}
};

class Student : public Person {  //子类(继承)
public:
	virtual B* price()
	{
		cout << "B" << endl;
	}
};

int main()
{
	
	return 0;
}
  • Tips:返回值类型必须相同,若父类虚函数返回值为指针(引用),则子类虚函数返回值也要为指针(引用),不能一个为指针,一个为引用。

析构函数的重写

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

/*编译器对析构函数做了特殊处理,编译后所有的析构函数名统一为destructor。父子类析构函数不加virtua就构成了隐藏关系。*/
class Person {  //父类
public:
	~Person() 
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person {  //子类(继承)
public:
	~Student()
	{
		cout << "~Student()" << endl;
	}
	
};

int main()
{
	/*函数调用(虚函数也包括在内)有两种方式: 多态调用和普通调用
	多态调用:首先两函数要满足多态构成的条件,才是多态调用 -》看指针或引用指向的对象类型
	普通调用:不满足多态构成的条件,就为普通调用 -》看当前调用者的类型*/
	Person* p = new Person;  
	delete p;  //delete:析构函数+free(p),此处析构函数的调用为普通调用
	
	Person* s = new Student; 
	delete s;  /*普通调用,只看当前调用者的类型,s为Person类型,会去调用父类的析构函数,
			   若子类中有资源申请,会造成内存泄漏*/

	return 0;
}

  • 编译器对析构函数做了特殊处理,编译后所有的析构函数名统一为destructor。父子类析构函数不加virtual就构成了隐藏关系。建议父子类析构函数都加上virtual。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

//编译器会将所有的析构函数名统一处理为destructor,父子类析构函数不加virtual构成隐藏关系
class Person {  //父类
public:
	virtual ~Person() 
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person {  //子类(继承)
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
	
};

int main()
{
	/*函数调用(虚函数也包括在内)有两种方式: 多态调用和普通调用
	多态调用:首先两函数要满足多态构成的条件,才是多态调用 -》看指针或引用指向的对象类型
	普通调用:不满足多态构成的条件,就为普通调用 -》看当前调用者的类型*/
	Person* p = new Person;  
	delete p;  //多态调用
	
	Person* s = new Student; 
	delete s; //多态调用

	return 0;
}

普通调用与多态调用
  1. 函数调用(虚函数也包括在内)有两种方式 : 多态调用和普通调用。

  2. 多态调用 : 两函数满足多态构成的条件,才是多态调用 -》看指针或引用指向的对象类型。重写为多态调用,父类指向哪个对象就调用哪个对象的函数,灵活。

  3. 普通调用 : 不满足多态构成的条件,就为普通调用 -》看当前调用者的类型。隐藏为普通调用,若要访问父类函数,可以指定父类作用域进行访问。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

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

class Student : public Person {  //子类(继承)
public:
	virtual void price() //虚函数的重写
	{
		cout << "买票 - 学生 - 全价" << endl;
	}
};

int main()
{
	Person* pq1 = new Person;  //多态调用,父类指向谁就调用谁,price重写
	pq1->price();
	Student* pq2 = new Student;
	pq2->price();

	cout << endl;

	Student* s1 = new Student;  //普通调用,看当前调用者的类型,price隐藏关系
	s1->price(); //子类price
	s1->Person::price();  //父类price,指定作用域访问

	Student s2; //普通调用
	s2.price();

	return 0;
}

重写实现

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

class Person {  //父类
public:
	void price() //父类不是虚函数
	{
		cout << "买票 - 成人 - 全价" << endl;
	}
};

class Student : public Person {  //子类(继承)
public:
	virtual void price() //子类是虚函数
	{
		cout << "买票 - 学生 - 全价" << endl;
	}
};

void func(Person* p)
{
	p->price();  //父类指针去调用父类的price函数
}

int main()
{
	Person p;
	func(&p); //父类对象-》-》不构成多态,普通调用

	Student s;
	func(&s);  //子类对象-》不构成多态,普通调用

	return 0;
}

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

/*重写实现:基类的函数是虚函数,子类的函数不是虚函数,也构成重写。继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性。但只将基类虚函数的接口(声明)继承下来,在重写基类虚函数的实现*/
class Person {  //父类
public:
	virtual void price() //父类是虚函数
	{
		cout << "买票 - 成人 - 全价" << endl;
	}
};

class Student : public Person {  //子类(继承)
public:
	void price() //子类不是虚函数
	{
		cout << "买票 - 学生 - 全价" << endl;
	}
};

void func(Person* p)
{
	p->price();  //父类指针去调用虚函数
}

int main()
{
	Person p;
	func(&p); //父类对象,构成多态,多态调用,调用父类的price

	Student s;
	func(&s);  //子类对象-》构成多态,多态调用,调用子类的price

	return 0;
}

  • 💡重写实现:基类的函数是虚函数,子类的函数不是虚函数,构成重写。继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性。但只将基类虚函数的接口(声明)继承下来,在重写基类虚函数的实现。一般建议两函数都加vritual。

C++11override和final

final关键字

  1. 实现一个类,该类不能被继承:将该类的构造函数设置为私有,导致子类无法实例化出对象。

  2. final关键字修饰一个,表示该类不能被继承

  3. final关键字修饰虚函数,表示该虚函数不能再被子类重写

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

class Person final{  //父类
public:
	virtual void price() 
	{
		cout << "买票 - 成人 - 全价" << endl;
	}
};

class Student : public Person {  //子类(继承)
public:
	void price() 
	{
		cout << "买票 - 学生 - 全价" << endl;
	}
};

int main()
{ 
	return 0;
}

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

class Person{  //父类
public:
	virtual void price() final 
	{
		cout << "买票 - 成人 - 全价" << endl;
	}
};

class Student : public Person {  //子类(继承)
public:
	void price() 
	{
		cout << "买票 - 学生 - 全价" << endl;
	}
};

int main()
{ 
	return 0;
}

override关键字

override检查派生类虚函数是否重写了基类虚函数,若没有重写编译器就会报错。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

class Person{  //父类
public:
	/*virtual void price()
	{
		cout << "买票 - 成人 - 全价" << endl;
	}*/
};

class Student : public Person {  //子类(继承)
public:
	virtual void price()override
	{
		cout << "买票 - 学生 - 全价" << endl;
	}
};

int main()
{ 
	return 0;
}

重载、重写、隐藏的对比

抽象类

概念

  1. 在虚函数后面加上=0,就构成了纯虚函数,包含纯虚函数的类称为抽象类,也叫做接口类,抽象类不能实例化对象;

  2. 纯虚函数强制了子类必须重写虚函数。因为子类继承父类,会将父类的纯虚函数继承下来,子类为抽象类,子类不能实例化出对象。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

class Person{  //抽象类,不能实例化出对象,强制了子类必须重写虚函数
public:
	virtual void price() = 0  //纯虚函数
	{
		cout << "买票 - 成人 - 全价" << endl;
	}
};

class Student : public Person {  //子类(继承)
public:
	void price() //子类重写了基类的虚函数,否则,子类会继承父类的纯虚函数,成为抽象类,不能实例恶化出对象
	{
		cout << "买票 - 学生 - 全价" << endl;
	}
};

int main()
{ 
	return 0;
}

实现继承和接口继承

  1. 实现继承:普通函数的继承是一种实现继承。子类继承了父类的函数,继承的是函数的实现和声明。

  2. 接口继承:虚函数的继承是一种接口继承。子类继承了父类的纯虚函数,继承的是纯虚函数的接口(声明),目的是为了重写,构成多态。

多态的原理

虚函数表

pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

class Base { 
public:
	virtual void func1()  //虚函数
	{
		cout << "Base : func1" << endl;
	}

private:
	int _b;
};

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

	return 0;
}

  • sizeof(Base)=8,在b对象中,除了有个_b成员,还存了个vfptr放在对象的最前面(与平台有关,vs平台下放在对象的最前面),对象中的vfptr称为虚表函数指针(v为virtual,f为function,ptr为指针),一个含有虚函数的类至少有一个虚函数表指针,虚函数表指针指向一张虚函数表,简称为虚表,虚函数的地址存在虚表中。
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

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

class Get : public Base {

	virtual void func1()  //重写了虚函数func1
	{
		cout << "Get : func1" << endl;
	}

private:
	int _g;
};

void f(Base* pb) 
{
	pb->func1(); //父类指针分别去调用指向对象的fun1
}

int main()
{
	Base b; //父类对象
	cout << sizeof(Base) << endl;

	Get g; //子类对象
	cout << sizeof(Get) << endl;

	//多态调用
	f(&b); 
	f(&g);

	return 0;
}


  • 派生类有两部分组成:父类的成员+自己独有的成员,派生类对象中存放了一个虚函数表指针,存放在父类部分中。

  • 重写也称为覆盖。重写是语法层的叫法,覆盖是原理层的叫法。派生类和基类对象中的虚表是不一样的,因为子类中的func1完成了重写,所以子类对象中虚表存放的是重写的Get::func1,即:虚函数的重写也叫做覆盖,覆盖就是虚函数表中虚函数得覆盖。

  • 子类继承父类,func2、func3均被继承下来了,因为func2为虚函数,且未被重写,所以它的地址被放到虚函数表中了,在基类和派生类虚表中func2的地址未改变,因为func3为普通函数,不会被放入虚函数表中。

  • 虚表中存放的虚函数的指针,即:它本质是一个存放函数指针数组。一般情况下,该数组最后面放了个空指针。

  • 派生类虚表的生成:先将父类虚表拷贝给子类。若子类中某个虚函数重写了父类的虚函数,则就用子类重写的虚函数来覆盖虚表中父类的虚函数。如果子类中新增了只属于自己的虚函数,就按照在子类中的声明次序,依次加到子类虚表的后面。

  • 同一类创建出的不同对象,对象中虚函数表指针是相同的,指向同一虚表。

多态的原理

问题:上面调用func1函数,传的是Base对象,调用的就是Base::func1,传的是Get对象,调用的就是Get: :func1,这是怎么做到的呢?

  • 多态调用:运行时去对象的虚表中找虚函数的地址,进行调用。

  • 普通调用:在编译时函数的地址就已经确定好了。

  • 虚函数和普通函数存放在常量区,即:代码段中。

  • 虚函数表存放在常量区,即:代码段中,在编译器时就已经确定。虚函数指针存在对象中,在运行时确定的,在构造函数的初始化列表中最先定义的。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

class Person {  //父类
public:
	virtual void A()
	{
		cout << "Person : A" << endl;
	}

	virtual void B()
	{
		cout << "Person : B" << endl;
	}
};

class Student : public Person {  //子类
public:
	virtual void A()
	{
		cout << "Student : A" << endl;
	}
};

int main()
{
	int i;
	printf("栈区:%p\n", &i);

	static int j;
	printf("静态区:%p\n", &j);

	int* p = (int*)malloc(sizeof(int) * 4);
	printf("堆区:%p\n", p);

	const char* q = "lala";
	printf("常量区:%p\n", q);

	Student s;
	Person* pq = &s;  //要取对象前四个字节,只能相似类型才能进行强转,把Person*->int*,在解引用,取到虚表的地址
	printf("Base虚表地址: %p\n", (*(int*)pq));

	return 0;
}

动态多态和静态多态

  1. 静态绑定:又称为前期绑定(早绑定),在编译期间才确定了程序的具体行为,也称为静态多态。eg :函数重载、函数模板。 函数重载允许在同一个作用域内定义多个函数名相同但参数类型或参数个数不同的函数,编译器在编译时根据调用时传递的参数类型或个数来确定具体调用哪个函数,底层是根据函数名修饰规则来确定的。 函数模板:编译器在编译时根据实参的类型推演实例化,确定模板参数的类型,产生具体类型的函数。

  2. 动态绑定:又称为后期绑定(晚绑定),在运行期间根据具体类型来确定程序的具体行为,调用具体的函数,也称为动态多态。主要依赖继承中虚函数的重写机制来实现,通过去指向对象的虚函数表中拿虚函数的地址,实现调用。

单继承的虚函数表

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

typedef void (*_vfptr)();

class Base {  //父类
public:
	virtual void func1()  //虚函数
	{
		cout << "Base : func1" << endl;
	}

	virtual void func2()  //虚函数
	{
		cout << "Base : func2" << endl;
	}

private:
	int _b;
};

class Get : public Base {  //子类 -单继承

	virtual void func1()  //重写了虚函数func1
	{
		cout << "Get : func1" << endl;
	}

	virtual void func3()  //自己新增的虚函数
	{
		cout << "Get : func3" << endl;
	}

	virtual void func4()  //自己新增的虚函数
	{
		cout << "Get : func4" << endl;
	}

private:
	int _g;
};

//打印虚函数表-》打印函数指针(虚函数)数组
void print_table(_vfptr* pf) 
{
	for (int i = 0; pf[i] != nullptr; i++) //一般虚表最后面会放个空指针
	{
		printf("第%d个虚函数的地址:%p: ", i, pf[i]);
		_vfptr f = pf[i];
		f();  //调用虚函数
	}
}

int main()
{
	Base bb1;
	Base* pb1 = &bb1;
	print_table((_vfptr*)(*(int*)pb1));  //取对象的头四个字节,即:虚表指针的值

	cout << endl << endl;

	Get bb2;
	Base* pb2 = &bb2;
	print_table((_vfptr*)(*(int*)pb2));
	return 0;
}


  • 对于单继承,父类有虚函数,子类重写了父类的虚函数,且没有单独定义虚函数,则父类和子类虚表中虚函数指针的个数相同,但虚表指针不同,指向不同的表。

多继承的虚函数表

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

typedef void (*_vfptr)();

class Base1 {  //父类1
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }

private:
	int b1;
};

class Base2 {  //父类2
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;
};

//打印虚函数表-》打印函数指针(虚函数)数组
void print_table(_vfptr* pf)
{
	for (int i = 0; pf[i] != nullptr; i++) //一般虚表最后面会放个空指针
	{
		printf("第%d个虚函数的地址:%p: ", i, pf[i]);
		_vfptr f = pf[i];
		f();  //调用虚函数
	}
}


int main() 
{
	Derive bb3;
	Base1* pb3 = &bb3;  //切片,指针的类型决定了他指向子类中父类那部分的大小
	print_table((_vfptr*)(*(int*)pb3));

	cout << endl << endl;

	Base2* pb4 = &bb3;  
	print_table((_vfptr*)(*(int*)pb4));

	return 0;
}

  • 对于多继承,子类继承了多少个父类,它就有几张虚表。

  • 多继承派上类未重写得虚函数得地址被放到了第一张虚表中。

  • 父类1* p1 = &子类、父类2* p2 = &子类,为切片,指针的类型代表着它指向子类中父类那一部分的大小。子类继承父类的顺序(与他们定义的顺序无关),就代表着它声明的顺序,决定在多继承中,它的虚表在子类中的顺序。

菱形继承

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using namespace std;

class A
{
public:
	virtual void func() { cout << "A::func" << endl; }

	A(int a)
		:_a(a)
	{ 
		cout << "A()" << endl;
	}

	int _a;
};

class B : public A {  //父类1
public:
	virtual void func() { cout << "B::func" << endl; }

	B(int b, int a)
		:_b(b)
		, A(a)
	{
		cout << "B()" << endl;
	}
	int _b;
};

class C : public A{  //父类2
public:
	virtual void func() { cout << "C::func" << endl; }

	C(int c, int a)
		:_c(c)
		,A(a)
	{
		cout << "C()" << endl;
	}

	int _c;
};

class D : public B, public C {  //多继承
public:
	
	D(int a, int b, int c, int d)
		:C(c, a)
		,B(b, a)
		,_d(d)
	{
		cout << "D()" << endl;
	}

	int _d;
};


int main()
{
	D dd(1, 2, 3, 4);

	B* bb = &dd;
	bb->func();

	C* cc = &dd;
	cc->func();

	return 0;

}

继承和多态的面试题

概念题

1.下面哪种面向对象的方法可以让你变得富有(A)
A: 继承 B: 封装 C: 多态 D: 抽象

解析:继承、多态、封装是C++面向对象编程的三大特性,具体介绍如下:
封装:是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。封装是类的特征之一,可以保护类的数据和方法不被外部程序破坏或修改,提高程序的安全性。
继承:是指从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。通过继承,我们可以利用已有的类来创建新的类,从而避免重复编写代码,提高代码的可重用性。
多态:是指一个方法可以有多种实现版本,即“一种定义,多种实现”。多态允许不同类的对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式。多态也称作动态绑定,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

2.( D )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定

解析:方法被称为函数,本题含义就是函数的定义与具体的对象无关,函数的调用与与具体对象有关,这就是多态, “一种定义,多种实现”。多态可以分为静态绑定和动态绑定,静态绑定有函数重载、函数模板等,动态绑定是虚函数的重写。

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

解析:C优先使用组合,而不是继承,不是面向对象设计的第二原则,是合成/聚合复用原则。

4.以下关于纯虚函数的说法,正确的是(A )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数

解析:B包含纯虚函数的类为抽象类,不能实例化出对象。 C纯虚函数强制了子类必须重写虚函数。 D纯虚函数只是在虚函数后面加了=0,可以随意定义。

5.关于虚函数的描述正确的是(B )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函
D:虚函数可以是一个static型的函数

解析:A如果父子类的虚函数要构成重写,必须要函数名、参数类型、返回值必须相同。 B内联函数不能使多态调用,但是我们在内联函数前加vritual,编译器不会报错,因为内敛只是一种建议,多态调用编译器会自动舍弃inline属性,该函数是就不是内联函数。普通调用内联函数在编译时展开,找不到内联函数的地址。必须太绝对了。 C虚函数可以不重写,要构成多态,子类的虚函数必须重写父类的虚函数。 Dstatic函数没有this指针,可以通过指定类域进行访问,突破了封装,虚函数需要放进虚表中,虚表只能通过虚表指针进行访问。

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

解析:A对于多继承,一个子类有多张虚表。B基类中有虚函数,如果子类中没有重写基类的虚函数,因为子类继承了父类,虚表中的虚函数的地址是一样的,但因为虚表指针属于对象的,同一类型的对象虚表指针相同,不同类型的对象虚表指针不同。 C虚表在编译期间就已经确定好了。 D一个类创建出的不同对象中虚表指针值相同,指向同一张虚表。

7.假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则(D)
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

解析:AB不同平台,对象中虚表指针存放的位置是不相同的,vs下虚表指针存放在对象的头四个字节。 C同一类型的对象虚表指针才相同,指向同一张表。 D子类继承了父类,父类的虚函数在子类中依旧保持这虚函数属性,但因为父子类型不同,虚表指针就不同。

8.下面程序输出结果是什么? (A)
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 B class D

#include<iostream>

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

9.多继承中指针偏移问题?下面说法正确的是(C )
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

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

10.以下程序输出结果是什么(B)
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

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

问答题

什么是多态?

答:多态是不同对象完成某个动作产生不同状态。多态分为动态多态和静态多态。静态多态是在编译期间确定程序的行为的,如:函数重载、函数模板。函数重载是在编译期间根据调用时传递参数的类型和个树来确定调用哪个具体的函数,底层是通过函数名修饰规则确定的。函数模板在编译期间根据实参的类型推演实例化出模板的类型,产生具体类型的函数。动态多态是在运行期间根据具体类型确定程序的行为,调用具体的函数,主要是通过继承中虚函数的重写实现的,去对象虚表中找虚函数的地址,完成调用。

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

答:重载是在同一作用域中,函数名相同、参数类型不同。重写是两函数分别在子类和父类作用域中,两函数必须是虚函数,必须三同,函数名、参数类型、返回值类型相同。隐藏是两函数分别在子类和父类作用域中,函数名相同,对于在基类和派生类中的同名函数如果不构成重写就构成隐藏。

多态的实现原理?

答:静态多态在编译时就已经确定好了函数的地址。动态多态是在就在运行时根据对象中虚表指针找到虚表,去虚表中找虚函数的地址,实现调用。根据去不同类的对象中存放的虚表指针,指向的不同虚表找虚函数地址,调用不同函数,从而实现了多态。

inline函数可以是虚函数吗?

答:不能。因为虚函数表的指针要放到虚表中,若为普通调用,内联会在编译期间就会展开,找不到函数的地址。但是在内联函数前加virtul关键字,编译器不会报错,因为内联只是一种建议,为多态调用时,编译器会忽略inline属性,这个函数就不是内联函数了。

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

答:不能。静态成员函数中无this指针,在类外可以通过 类型: :成员函数名 进行直接访问,虚表只能通过虚表指针进行访问,不能使用 类型: :成员函数名 来访问虚函数表,所以静态成员函数无法被放入虚函数表中。

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

答:不能。因为对象中的虚表指针是在构造函数的初始化列表中才初始化的,把构造函数的地址放入虚表中,无法找到虚表指针,进行访问虚表,所以构造函数不能放入虚表中。

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

答:可以,并且将析构函数定义虚函数。下面场景下才能调用到子类的析构函数,不会造成内存泄漏。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

//编译器会将所有的析构函数名统一处理为destructor,父子类析构函数不加virtual构成隐藏关系
class Person {  //父类
public:
	virtual ~Person() 
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person {  //子类(继承)
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
	
};

int main()
{
	Person* p = new Person;  
	delete p;  //多态调用
	
	Person* s = new Student; 
	delete s; //多态调用

	return 0;
}

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

答:如果为普通调用,不构成多态,则对象访问普通函数和访问虚函数的速度是一样快的。如果为多态调用,则对象访问普通函数快于访问虚函数的速度,因为构成多态,去对象中的虚表指针,指向的虚表中找虚函数的地址,时间消耗多。

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

答:虚函数表在编译阶段就已经生成了,存在代码段(常量区)中。

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

答:菱形继承会造成数据冗余和二义性的问题,通过指定作用域可以解决数据二义性,但无法解决数据冗余,而菱形虚继承可以解决数据冗余和二义性的问题。虚继承的原理是将对象中冗余的数据放到对象组的最下面(公共区域),让加了virtual的函数共享这块区域,为了方便找到该数据,就在对象中存放了虚基表指针,该指针可以找到一张虚基表,根据虚基表中存放的偏移量,根据偏移量就能够找到共享的数据了。

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

答:在虚函数的后面加上=0,就构成了纯虚函数,包含纯虚函数的类就叫做抽象类,抽象类不能实例化对象。抽象类规定了子类必须重写纯虚函数,不然子类就会继承了父类的纯虚函数,不能够实例化出对象了。抽象类也叫做接口类。

铁铁们,多态就到此结束啦,若博主有不好的地方,请指正,欢迎铁铁们留言,请动动你们的手给作者点个👍鼓励吧,你们的鼓励就是我的动力✨

版权声明:本文为博主作者:奶芙c原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/m0_74808907/article/details/137251458

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2024年4月22日
下一篇 2024年4月22日

相关推荐