【C++】多态

发布时间:2023年12月26日

目录

一. 多态的概念

二. 多态的定义及实现

1.多态的构成条件

2?虚函数

3虚函数的重写

虚函数重写的两个例外:

1. 协变(父类与子类虚函数返回值类型不同)

2. 析构函数的重写(父类与子类析构函数的名字不同)

三.C++11 override 和 final

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

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

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

五.抽象类

接口继承和实现继承

六.多态的原理

虚函数表

实现多态的原理

动态多态与静态多态

为什么父类对象调用虚函数不能多态?

七.单继承与多继承子类虚函数表

多继承中的虚函数表

八.多态的常见问题

1. 什么是多态?

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

3.多态的实现原理

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

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

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

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

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

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


一. 多态的概念

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态 如公交刷卡行为:成年人全价,学生半价。

二. 多态的定义及实现

1.多态的构成条件

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

那么在继承中要构成多态还有两个条件

1. 必须通过 父类 指针或者引用 调用 虚函数
2. 被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写

这里再次强调多态的条件:

必须使父类的指针或引用调用虚函数。 父类的对象调用虚函数不是多态!!!子类必须重写调用的虚函数

2?虚函数

虚函数:即virtual修饰的类成员函数称为虚函数(注:这里的vitrtual与虚继承里的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()  //对person里的虚函数进行重写
	{
		cout << "半价" << endl;
	}
};

void fun(person& s)
{
	s.buyTicket();
}

int main()
{
	person a;
	student b;
	fun(a);
	fun(b);
	return 0;
}

注:1.这里虚函数重写,重写的是函数实行方式(函数里面的内容),并不会改变原有的函数名与参数。

#include<iostream>
using namespace std;

class A
{
public:
	virtual void print(int a = 1)
	{
		cout << a << endl;
	}
};

class B :public A
{
public:
	void print(int b = 2)
	{
		cout << b << endl;
	}
};

int main()
{
	A a;
	B b;
	A& parent = a;
	A& parent2 = b;
	parent.print();
	parent2.print();
	return 0;
}

2.重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议 这样使用

虚函数重写的两个例外:

1. 协变(父类与子类虚函数返回值类型不同)
子类重写父类虚函数时,与父类虚函数返回值类型不同。即父类虚函数返回父类对象的指
针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。(了解)
 
class Person 
{
public:
    virtual Person* f() 
    {
        return new Person;
    }
};


class Student : public Person 
{
public:
    virtual Student* f() 
    {
        return new Student;
    }
};
2. 析构函数的重写(父类与子类析构函数的名字不同)
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加 virtual 关键字,
都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成 destructor

class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;
delete p1;
 delete p2;
 return 0;
}

三.C++11 override 和 final

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

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

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

同时:final还可以放在类名后修饰类,被修饰的类无法被其他类继承

注:final要修饰的是虚函数,不是虚函数会报错。

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

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

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

? ? ?

五.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象 子类继承后也不能实例化出对象,只有重写纯虚函数,子类 才能实例化出对象纯虚函数规范了子类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 virtual void Drive()
 {
 cout << "Benz-舒适" << endl;
 }
};
class BMW :public Car
{
public:
 virtual void Drive()
 {
 cout << "BMW-操控" << endl;
 }
};
void Test()
{
Car* pBenz = new Benz;
 pBenz->Drive();
 Car* pBMW = new BMW;
 pBMW->Drive();
}

注:虽然抽象类不能定义对象,但其还是可以定义指针的。

接口继承和实现继承

普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,子类继承的是父类虚函数的接口,接口的类作用限定符和缺省参数还是继承父类的,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

六.多态的原理

虚函数表

这里我们先看这段代码:

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

int main()
{
	cout << sizeof(B);
	return 0;
}

结果:

这里我们发现B的大小为8并不为4,那么这个类里除了存放了一个整形还存放了什么吗?这里我们可以通过调试的监视框口。

发现类里面还存了一个_vfptr的指针(

对象中的这个指针我们叫做虚函数表指针 (v virtual f 代表 function) 。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,因此_vfptr的指针类型位函数指针,虚函数表也简称虚表,。那么子类中这个表放了些什么呢?我们下面写这个代码?
#include<iostream>
using namespace std;

class A
{
public:
	virtual void fun1()
	{
		cout << "A--fun1" << endl;
	}
	virtual void fun2()
	{
		cout << "A--fun2" << endl;
	}
private:
	int _a = 1;
};

class B :public A
{
public:
	void fun1()
	{
		cout << "B--fun1" << endl;
	}
	void fun3()
	{
		cout << "B--fun3" << endl;
	}
private:
	int _b = 2;
};

int main()
{
	A a;
	B b;
	return 0;
}

然后调试:

A的虚函数表内容:

B虚函数表内容:

通过观察和测试,我们可以发现:

1.子类对象中也有一个虚表指针。虚表里的内容一部分继承父类的,一部分是子类重写父类虚函数的。

2. 父类 对象和子类 对象虚表是不一样的,这里我们发现f un1 完成了重写,所以子类 的虚表
中存的是重写的 A::fun1 ,所以虚函数的重写也叫作覆盖 ,覆盖就是指虚表中虚函数
的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3.另外fun2继承下来后是虚函数,所以放进了虚表,子类func3也是虚函数也放到了虚表中了,

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

?

注:

同类的对象:共享一张虚表

虚表在代码区,虚函数也在代码区

代码大致验证:

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

void fun()
{
	cout << "fun" << endl;
}

int main()
{
	B b1;
	cout << sizeof(B)<<endl;

	static int a = 1;
	int b = 2;
	int* c = new int;
	const char* d  = "hello";
	printf("静态区:%p\n", &a);
	printf("栈:%p\n", b);
	printf("堆区:%p\n", c);
	printf("代码区:%p\n", d);
	printf("普通代:%p\n", fun);
	printf("虚表地址:%p\n", *((int*)&b1));
	printf("虚函数地址:%p\n", &B::print);
	return 0;
}
子类的虚表生成
1. 先将父类中的虚表内容拷贝一份到子类虚表中。
2. 如果子类重写了基类中某个虚函数,用子类自己的虚函数覆盖虚表中父类的虚函数
3. 子类自己新增加的虚函数按其在子类中的声明次序增加到派生类虚表的最后

实现多态的原理

首先我们要知道,对象调用虚函数的大致过程:

通过虚表指针,找到虚表,然后再虚表上找到对应虚函数的地址,调用。

同时我们知道父类的指针或引用指向子类时,会指向对应子类中父类的那一部分(恰巧这部分包含了对应的子类虚表)。

再上面我们说过子类虚函数重写时会进行虚函数的覆盖。

所以当父类对象的指针或引用指向父类时,调用的时父类的虚表。

指向子类时,调用子类的虚表。

由于子类的虚表是拷贝父类的虚表,并进行了虚函数的重写,改变了虚表的内容,这导致了多态。

这里结合一个例子说明:

class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
 Func(Mike);
Student Johnson;
Func(Johnson);
 return 0;
}

1. 观察下图的红色箭头我们看到, p 是指向 mike 对象时, p->BuyTicket mike 的虚表中找到虚
函数是 Person::BuyTicket
2. 观察下图的蓝色箭头我们看到, p 是指向 johnson 对象时, p->BuyTicket johson 的虚表中
找到虚函数是 Student::BuyTicket
3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

动态多态与静态多态

静态多态 在程序编译期间确定了程序的行为,比如:函数重载
动态多态 。是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数。

为什么父类对象调用虚函数不能多态?

我们知道实现多态的重点是虚表的改变与调用的是父类还是子类的虚表。

而再C++中规定,子类赋值给父类时,通过切片给父类赋值,这个过程中并不会将子类的虚表赋值给父类对象,所以父类对象的虚表还是原来的,调用虚函数时还是找父类的虚表。因此不能实现多态。

那为什么子类的虚表为什么不给赋值给父类呢?

这里我们可以通过反证法举一个特殊的例子说明:

当父类的析构函数是虚函数,并且子类也进行了重写。

这时如果子类的虚表赋值给父类,这是当使用delete释放父类对象时,会调用子类的析构函数。那么父类对象在析构时,就会出现大问题。

七.单继承与多继承子类虚函数表

单继承子类的虚表:先将父类的虚表拷贝下来,修改重写的虚函数,在把自己的虚函数加入

这里我们看到子类的虚函数表里并没有少了两个函数指针,这是为什么呢?

这里我们要了解,我们从监视框口看到的并不是真实的内容,而是编译器修饰过的。这里我们要从调试的内存框口中去看。

这里我们根据虚函数表最后一个存放nullptr可以判断,虚函数表中有四个函数地址。

这里我们还可以以一种更加直观的方法观察:

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


typedef 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 b;
	Derive d;
	// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
	//指针的指针数组,这个数组最后面放了一个nullptr
		// 1.先取b的地址,强转成一个int*的指针
		// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
		// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
		// 4.虚表指针传递给PrintVTable进行打印虚表
		// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
		//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再
		//编译就好了。
		VFPTR * vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}

多继承中的虚函数表

子类分别将父类的虚表拷贝下来,若子类重写虚函数,就将对应的虚表改写。

多继承子类的未重写的虚函数放在第一个继承父类部分的虚函数表中

八.多态的常见问题

1. 什么是多态?

多态是指不同对象完成同一个件事的结果不同,如:坐车,学生半价,成人全价。

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

函数重载:在同一作用域内,函数名命相同,参数的个数,类型,数量,以及参数的顺序不同。

重写:继承中。两个函数分别在子类与父类里,且函数名,函数参数,返回值相同,且父类的函数被virtual修饰。

重定义(隐藏):在继承中,两个函数一个在父类一个在子类里,函数名相同。

3.多态的实现原理

构成条件:父类的指针或引用调用虚函数,子类对虚函数必须进行重写

实现原理:子类进行虚函数重写会改变虚表,而当父类指向父类是,调用虚函数,要调用父类的虚表,父类指向子类是,调用虚函数,要调用子类的虚表。由于子类一改写虚表,所以导致父类指向不同类的对象是,调用同一虚函数,会有不同的结果。

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

可以是虚函数,调用时,如果不构成多态,这个函数就保持inline的属性。如果构成多态,就不具备inline属性,因为多态要在运行时去找对象的虚函数表里虚函数,所以在编译时,不能使用inline展开函数。

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

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

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

不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。(先有鸡还是先有蛋的问题)

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

可以,当父类针织指向一个用new开辟子类的空间时,最好把基类的析构函数定义成虚函数

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

如果不构成多态,即使虚函数,也是在编译阶段确定调用地址的,速度一样快。如果构成多态,编译器在运行中通过对象去虚函数表中确定虚函数的调用地址,这个时候就是普通函数快。

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

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。纯虚函数规范了子类必须重写,另外纯虚函数更体现出了接口继承,子类会继承虚函数的函数名,缺省参数,但不会继承实现

文章来源:https://blog.csdn.net/qq_63440853/article/details/135196281
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。