C++知识点--多态-创新互联

C++知识点 – 多态

河西ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为成都创新互联的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:18980820575(备注:SSL证书合作)期待与您的合作!文章目录
  • C++知识点 -- 多态
  • 一、多态概念
    • 1.概念
  • 二、多态的定义及实现
    • 1、多态的构成条件
    • 2、虚函数
    • 3、虚函数的重写
    • 4.虚函数重写的特例
    • 5、不符合多态的场景
    • 6、C++11的override和final
    • 7、重载、覆盖(重写)、隐藏(重定义)的对比
    • 8、例题
  • 三、抽象类
  • 四、多态的原理
    • 1、虚函数表
    • 2、多态的原理
  • 五、单继承和多继承关系的虚函数表
    • 1、单继承中的虚表
    • 2、多继承中的虚表
  • 六、多态常见面试问题
    • 1.例题
    • 2.inline函数可以是虚函数吗
    • 3.静态成员函数可以是虚函数吗
    • 4.构造函数可以是虚函数吗
    • 5.析构函数可以是虚函数吗
    • 6.拷贝构造和赋值可以是虚函数吗
    • 7.对象访问普通函数快还是虚函数快
    • 8.虚函数表是在什么阶段生成的,存在哪里


一、多态概念 1.概念

多态就是完成某个行为时,不同对象去完成时会产生不同的状态。
比如买票,普通人买全价票,学生买半价票,军人优先买票。

二、多态的定义及实现 1、多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生不同的行为。
继承中构成多态还有两个条件:
1.必须通过基类的指针或引用去调用虚函数;
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写;

2、虚函数

用virtual关键字修饰的成员函数就是虚函数。
代码如下:

class Person
{public:
	virtual void BuyTicket()
	{cout<< "买票 - 全价"<< endl;
	}
};
3、虚函数的重写

派生类中的虚函数构成重写(覆盖)的条件有:函数名、参数和返回值类型相同,但是函数的实现不同;
如果不构成重写,就是隐藏关系;
代码如下:

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;      //重写
	}
};

void Func(Person& p)           //使用父类的引用调用虚函数
{p.BuyTicket();
}

void Test()
{Person p;
	Func(p);

	Student st;
	Func(st);

	Soldier sd;
	Func(sd);
}

以上代码就完整的构成了多态,其运行效果为:
在这里插入图片描述
不同的对象调用同一个虚函数,呈现出了不同的效果。

4.虚函数重写的特例

1.将子类中虚函数的virtual去掉

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

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

这样依然构成重写,子类中依然是虚函数,编译器认为先把父类的虚函数继承下来了,而且是接口继承,将函数接口完整继承下来了,子类中只是将函数的实现进行重写。

2.重写的协变
返回值类型可以不同,要求必须是父子关系的指针或引用;
代码如下:

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

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

以上的代码是会报错的,因为只满足了返回值类型不同,并不是父子关系的指针或引用,下面的代码才是协变:

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

class Student : public Person
{public:
	virtual Student* BuyTicket()       //返回值是有父子关系的指针或引用
	{cout<< "买票 - 半价"<< endl;
		return this;
	}
};

运行结果为:
在这里插入图片描述
上面的代码依然能够构成多态。

3.析构函数的重写
建议在继承中将析构函数定义为虚函数;

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

class Student : public Person
{public:
	virtual ~Student()      //子类的析构函数与父类的析构函数的函数名并不相同
	{cout<< "~Student()"<< endl;
	}
};
int main()
{Person* p1 = new Person;
	delete p1;

	Person* p2 = new Student;//父类指针指向子类对象,符合多态调用
	delete p2;
	
	return 0;
}

以上代码的运行结果为:
在这里插入图片描述
子类和父类的析构函数,参数类型和返回值类型都相同,编译器为了让他们构成重写,将析构函数名改写为destructor,所以,上述代码中析构函数完成了重写。
只有子类析构函数重写了父类的析构函数,这里才能正确调用,指针指向父类对象,调用父类的析构函数,指向子类对象就调用子类的析构函数。

如果析构函数不是虚函数:

class Person
{public:
	~Person()
	{cout<< "~Person()"<< endl;
	}
};

class Student : public Person
{public:
	~Student()      //子类的析构函数与父类的析构函数的函数名并不相同
	{cout<< "~Student()"<< endl;
	}
};
int main()
{Person* ptr1 = new Person;
	delete ptr1;

	Person* ptr2 = new Student;//父类指针指向子类对象,符合多态调用
	delete ptr2;
	
	return 0;
}

在这里插入图片描述
在子类delete时,调用的还是父类的析构函数:
在这里插入图片描述
这里是普通调用,不符合多态,在编译时就决定了;
ptr2是Person*类型的指针,call的是Person的析构函数;
ptr1希望调用父类的析构,ptr2希望调用子类的析构,所以把析构设计成符合多态的函数名。

5、不符合多态的场景

1.不是父类的指针或引用调用虚函数
代码如下:

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

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

void Func(Person p)
{p.BuyTicket();
}

运行结果为:
在这里插入图片描述
上述代码是不构成多态的。

2.不符合虚函数重写
2.1将父类虚函数的virtual去掉

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

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

void Func(Person& p)
{p.BuyTicket();
}

运行结果为:
在这里插入图片描述
是不符合虚函数重写的,自然就不构成多态。

2.2参数类型不同

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

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

void Func(Person& p)
{p.BuyTicket();
}

运行结果为:
在这里插入图片描述
同样不符合多态。

6、C++11的override和final

1.final:修饰虚函数,表示其不能再被重写(用的很少)

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

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

编译之后会报错:
在这里插入图片描述
2.override:检查派生类虚函数是否重写了某个基类的虚函数,若没有重写编译报错(常用)

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

class Student : public Person
{public:
	virtual void BuyTicket(char) override//参数类型不一致,未完成重写
	{cout<< "买票 - 全价"<< endl;
	}
};

上述代码子类的虚函数未完成重写,在后面加了override后,编译器就会报错:
在这里插入图片描述
override常用于检查子类虚函数重写的语法是否正确。

7、重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

8、例题

代码如下:

#includeusing namespace std;

class A
{public:
	virtual void func(int val = 1)
	{cout<< "A ->"<< val<< endl;
	}

	virtual void test()
	{func();
	}
};

class B : public A
{public:
	virtual void func(int val = 0)
	{cout<< "B ->"<< val<< endl;
	}
};

int main()
{B* p = new B;
	p->test();

	return 0;
}

以上代码的输出结果为:
在这里插入图片描述
分析:

  1. A为父类,B公有继承A,继承了A的func和test函数,其中A和B的func函数构成了虚函数重写(不要求参数的缺省值相同),因此构成了多态;
  2. main函数中,B指针p指向B对象,用p调用了test函数,p的类型是B,而test中this指针的类型是A*,p传给this,这里用父类指针指向子类对象,构成了切片;
  3. 这里this指针是A*类型的,用this调用func函数,符合父类指针调用虚函数,符合多态调用,多态调用时,指针指向那个类对象,就调用哪个类中的虚函数,显然p和this指向的都是子类对象,所以这里调用的是子类中的虚函数;
  4. 虚函数是接口继承,普通函数数实现继承;虚函数继承时,直接将父类的函数接口继承下来,与子类的接口是无关的,子类重写的是实现,这里的接口是父类的接口,val的缺省值是1,因此函数最终的输出结果为:B ->1,选B。

如果将代码改成以下形式:

class A
{public:
	virtual void func(int val)//去掉缺省值
	{cout<< "A ->"<< val<< endl;
	}

	virtual void test()
	{func(1);
	}
};

class B : public A
{public:
	void func(int val)
	{cout<< "B ->"<< val<< endl;
	}
};

int main()
{//Test();

	A* p = new B;//用父类的指针指向子类对象
	p->test();

	return 0;
}

子类和父类的func依然构成虚函数重写;
这里用父类的指针指向子类对象,发生了切片,但指向的还是子类的对象,所以调用的函数还是子类中的虚函数,结果还是:B ->1;
在这里插入图片描述
最终结果与p的指针类型无关,只与它指向的对象有关。

三、抽象类

在虚函数的后面写上 = 0,这个函数就是纯虚函数,包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,派生类继承抽象类后也不能实例化出对象,只有派生类重写了虚函数,才能实例化对象,纯虚函数规范了派生类必须重写,更好的体现出了接口继承。
代码如下:

class Car             //把不想实例化出对象的父类定义为抽象类
{public:
	virtual void Drive() = 0;
};

class Benz : public Car
{public:
	virtual void Drive(int)     //如果子类继承了抽象类却未完成虚函数重写,就会报错
	{cout<< "Benz - 舒适"<< endl;
	}
};

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

int main()
{Car c1;
	Benz c2;
	BMW c3;

	return 0;
}

在这里插入图片描述
1.抽象类一般用于定义接口,将不想实例化出对象的类定义为抽象类;
2.抽象函数强制子类完成虚函数的重写,不重写就无法实例化,而override是检查语法是否完成重写;

四、多态的原理 1、虚函数表

创建如下对象:

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

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

	return 0;
}

我么可以发现sizeof(b)的结果是8,再看b对象实例化后的成员
在这里插入图片描述
可以发现在成员_b的上面还有一个_vfptr的成员,这叫做虚函数表指针;带有虚函数的类对象,其成员中都有一个虚函数表指针,因为选虚函数要放到虚函数表中,也简称虚表。
将Base继承给子类,代码如下:

class Base
{public:
	virtual void func1()
	{cout<< "Base::func1"<< endl;
	}
	virtual void func2()             //加一个虚函数func2
	{cout<< "Base::func2"<< endl;
	}
	void func3()                     //加一个普通函数func3
	{cout<< "Base::func3"<< endl;
	}

private:
	int _b = 1;
};

class Derive : public Base
{public:
	virtual void func1()                //重写父类虚函数
	{cout<< "Derive::func1"<< endl;
	}
private:
	int _d = 2;
};

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

	Derive d;

	return 0;
}

通过监视窗口我们可以看到:
在这里插入图片描述
1.子类对象d中也有一个虚函数表指针,且和父类对象b的虚表指针不同,由于子类对func1完成了重写,虚表中的func1就是子类重写后的Detive::func1;
2.func2是虚函数,继承下来也会放进子类的虚表,而func3不是虚函数,不会放进虚表;
3.虚表本身是一个放函数指针的数组,一般情况最后会放一个nullptr(vs环境下);
4.虚表存放的是虚函数的函数指针,不是虚函数,虚函数跟普通函数一样,都存放在代码段。

2、多态的原理

在这里插入图片描述
在这里插入图片描述
通过对汇编代码的分析,我们可以总结出:
1.满足多态以后的函数调用,不是在编译时确定的,是运行起来以后再到对象中找的,程序运行时取对象中的虚表指针找到函数地址,再去调用;
2.普通函数的调用,是在编译链接时就确定函数的地址,运行时直接调用。

五、单继承和多继承关系的虚函数表 1、单继承中的虚表

代码如下:

class Base
{public:
	virtual void func1()
	{cout<< "Base::func1"<< endl;
	}
	virtual void func2()
	{cout<< "Base::func2"<< endl;
	}

private:
	int _b = 1;
};

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 _d = 2;
};

在这里插入图片描述
通过监视窗口看不见func3和func4,我们可以使用代码打印虚表中的函数:

class Base
{public:
	virtual void func1()
	{cout<< "Base::func1"<< endl;
	}
	virtual void func2()
	{cout<< "Base::func2"<< endl;
	}

private:
	int _b = 1;
};

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 _d = 2;
};

typedef void(*VFPTR) ();     //将指向返回值为void、没有参数的类型的函数的指针重定义为VFPTR
void PrintVTable(VFPTR vTable[])
{//依次取虚表中的指针打印并调用,调用就可以看出存的是哪个函数
	cout<< "虚表地址>"<< vTable<< endl;
	for (int i = 0; vTable[i] != nullptr; i++)
	{printf("第%d个虚函数地址:0x%x", i, vTable[i]);//打印地址
		VFPTR f = vTable[i];//用函数指针取出虚函数地址
		f();//调用
	}
	cout<< endl;
}

int main()
{Base b1;
	Base b2;
	Derive d;

	VFPTR* vTableb1 = (VFPTR*)(*((int*)&b1));//将b对象的地址取出,强转成int*,再解引用,就取出了b的头四个字节的数据,这个就是指向虚表的指针
										   //再强转成VFPTR*,因为虚表就是VFPTR类型的数组
	PrintVTable(vTableb1);

	VFPTR* vTableb2 = (VFPTR*)(*((int*)&b2));
	PrintVTable(vTableb2);


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

	return 0;
}

在这里插入图片描述
在这里插入图片描述
我么可以看出,在vs下:
1.同一个类型的对象,共用一个虚表(b1和b2);
2.不管是否完成重写名子类虚表和父类虚表都不是同一个;
3.单继承中,子类的所有虚函数,包括重写父类的虚函数和未重写的虚函数,都放在同一个虚表中。

2、多继承中的虚表

代码如下:

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

private:
	int _b1 = 1;
};

class Base2
{public:
	virtual void func1()
	{cout<< "Base2::func1"<< endl;
	}
	virtual void func2()
	{cout<< "Base2::func2"<< endl;
	}

private:
	int _b2 = 2;
};


class Derive : public Base1, public Base2
{public:
	virtual void func1()
	{cout<< "Derive::func1"<< endl;
	}
	virtual void func3()
	{cout<< "Derive::func3"<< endl;
	}

private:
	int _d = 3;
};

typedef void(*VFPTR) ();     //将指向返回值为void、没有参数的类型的函数的指针重定义为VFPTR
void PrintVTable(VFPTR vTable[])
{//依次取虚表中的指针打印并调用,调用就可以看出存的是哪个函数
	cout<< "虚表地址>"<< vTable<< endl;
	for (int i = 0; vTable[i] != nullptr; i++)
	{printf("第%d个虚函数地址:0x%x", i, vTable[i]);//打印地址
		VFPTR f = vTable[i];//用函数指针取出虚函数地址
		f();//调用
	}
	cout<< endl;
}

int main()
{Derive d;

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

	VFPTR* vTabled2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));//从Base2的虚表中取虚函数地址
	PrintVTable(vTabled2);


	return 0;
}

Derive多继承Base1和Base2,其中Derive重写了func1,而func1既是Base1的虚函数,也是Base2的虚函数,func3是Derive自己的虚函数,运行结果如下:
在这里插入图片描述
在这里插入图片描述

可以看出在多继承下:
1.子类中每一个继承的父类都有自己的虚表,存放父类中的虚函数;
2.子类中重写的虚函数会覆盖子类中父类虚表对应的虚函数,Base1和Base2中的func1都没覆盖为了Derive::func1;
3.子类中继承的Base1中的func1和Base2中的func1的地址不同,但它们都是Derive重写后的虚函数,最终调用的是同一个func1,只是中间多了一个步骤;
4.子类未重写的的虚函数放在第一个继承的父类的虚表中;
在这里插入图片描述

六、多态常见面试问题 1.例题

以下程序的输出结果是:
在这里插入图片描述
B和C都是虚继承A,D多继承B和C,所以B和C在D中共享一个A,所以B和C都不能去初始化D中的A对象,只能在D中单独进行A的初始化;
初始化是按照类声明的顺序来的,不是按照初始化列表的顺序,所以在D中先初始化A对象,在初始化B和C,这事就不会重复初始化A了,最后初始化D,所以答案选A。

2.inline函数可以是虚函数吗

可以,inline函数是没有地址的,而且inline只是对编译器的一个建议,当一个inline函数是虚函数时,在多态调用以后,inline就失效了,因为虚函数要放进虚表中。

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

不可以,static函数没有this指针,可以直接使用类名::函数名()的方式调用,而使用类名::函数名()的方式无法访问对象的虚表,因此静态成员函数无法放进虚表,虚函数是为了实现多态,多态运行时都是去虚表中找决议,静态成员函数都是在编译时就决议了,因此它是虚函数没有价值。

4.构造函数可以是虚函数吗

不可以,因为虚函数是为了实现多态调用,运行时去虚表中找对应的虚函数进行调用,对象中的虚表指针都是在构造函数初始化列表阶段才初始化的,构造函数是虚函数没有意义。

5.析构函数可以是虚函数吗

可以,并且最好把基类的析构函数定义为虚函数,详情参考 二-5-3。

6.拷贝构造和赋值可以是虚函数吗

拷贝构造不可以,因为拷贝构造也是构造函数,参考上面的构造函数;
赋值重载operator==()可以,但是没有实际价值。

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

如果虚函数不构成多态,是一样快的;
如果虚函数构成多态,调用普通函数比较快,因为构成多态调用虚函数时,运行中需要到虚表中去查找。

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

虚函数表是在编译阶段就生成好的,存在代码段(常量区);
构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针。

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


本文名称:C++知识点--多态-创新互联
URL链接:http://ybzwz.com/article/ddoego.html