一.多态的概念

多态的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)运行时多态(动态多态),在这里我们重点来说运行时多态。对于编译时多态(静态多态)主要就是我们所说的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运行时归为动态。

运行时多态可以理解为,在进行某一个动作时呈现的不同状态,例如叫声,狗与猫同时进行叫声这个动作,狗是汪,猫是喵,进行同一个动作,结果却不同.

二.多态的定义及实现

2.1 多态的构成条件

多态是⼀个继承关系的下的类对象,去调用同⼀函数,产生了不同的⾏为。比如Student继承了 Person。Person对象买票全价,Student对象优惠买票。

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

  • 必须为基类的指针或引用来调用虚函数
  • 被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖.

如下

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};
class Student:public Person 
{
public:
	virtual void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
};
void Func(Person& people)
{
	people.BuyTicket();
}
void test()
{
    Person Mike;
    Func(Mike);
    Student Johnson;
    Func(Johnson);
}

在Func函数中,我们发现ticket都是由p(基类的引用)来调用(满足第一个条件),在Person与Student中,两个BuyTicket都是虚函数,且完成了虚函数的重写(满足第二个条件)

调用结果如下

2.3虚函数

类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修 饰(如静态成员函数与友元函数)

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

2.4 虚函数的重写/覆盖

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

在派生类中,虚函数可以不加virtual,基类中的virtual会被继承下来,也会构成重写,但这样的写法不规范,不建议使用,了解即可.

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.1.4 多态场景的⼀个选择题,以下程序输出结果是什么()

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

结果为B.

首先我们需要知道,虚函数的重写可以理解为

virtual 基类的返回类型 函数名(基类的参数)
{
    派生类的函数内容
} 

因此缺省值是基类中的缺省值

2.5 override和final关键字

从上面可以看出C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运性时没有得到预期结 果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰.

2.5.1 override

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

例如该代码,在派生类中虚函数为Dirve,基类中为Drive,因此两者不构成重写,此时因为加上override编译就会报错.如下

因此我们可以将override可以理解为一个检查虚函数是否重写成功的关键字.

2.5.2 final

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

当基类中的函数加上final后,此成员函数则不可以被重写,如果重写编译器则会报错

如果不想让一个成员函数重写我们便可以使用final来修饰该成员函数

2.6 虚函数重写的⼀些其他问题

2.6.1协变(了解即可)

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

class Person
{
public:
	virtual Person* BuyTicket()
	{
		cout << "买票全价" << endl;
		return nullptr;
	}
};
class Student:public Person
{
public:
	virtual Student* BuyTicket()
	{
		cout << "买票半价" << endl;
		return nullptr;
	}
};
int main()
{
	Person p;
	Student s;
	return 0;
}

此外除了自身的基类与派生类外,其他类的派生类与基类也可如下

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

如上也是正确的.

2.6.2析构函数的重写

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

为什么要构成多态?看如下代码

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

如果不构成多态该段代码,如果去A中析构则会出现内存泄漏的问题.

三.纯虚函数和抽象类

虚函数后加上=0,则这个函数为纯虚函数,如下Drive()便是纯虚函数

class Car
{
public:
 virtual void Drive() = 0;
};

虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可.

因此包含纯虚函数的类叫做抽象类,因此Car即为抽象类

抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

演示:

抽象类不能实例化出对象:

class Car
{
public:
 virtual void Drive() = 0;
};
int main()
{
    //编译报错:error C2259: “Car”: ⽆法实例化抽象类 
    Car c;
    return 0;
}

如果派生类继承后不重写纯虚函数,那么派生类也是抽象类:

class Car
{
public:
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
};
int main()
{
	Benz b;
	return 0;
};

四.多态的原理

4.1虚函数表指针

首先我们看下方代码

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
class Base1
{
public:
	void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	Base1 b1;
	return 0;
}

在Base中有虚函数,在Base1中没有虚函数,实例化对象b与b1成员不一样,如下

除了_b和_ch成员,在b对象中还多⼀个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后⾯,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有⼀个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表.

4.2.多态是如何实现的

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

};
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
private:
	string _id;
};
void Func(Person& people)
{
	people.BuyTicket();
}
int main()
{
	Person p;
	Student s;
	Func(p);
	Func(s);
	return 0;
}

对于对象p与s都会存在一个虚表指针

详细解释如下

可以简单理解为,有虚函数的基类,会产生一个虚表指针,存储虚函数,如果子类对该虚函数的指针进行了重写,则会覆盖该虚函数的指针,因此在调用时产生了不同的效果

此外,每一个类无论实例化出多少个对象,都共用一个虚表指针.

int main()
{
	Person p;
	Person b;
	return 0;
}

我们可以看到虚表指针的地址是相同的.

为什么不可以通过子类对象来直接赋值给父类对象,从而触发多态?

因为复制时,只从子类对象中的数值进行拷贝给父类对象,_vfptr是在父类对象构造时便已经被编译器初始化指向父类中的_vfptr了.

4.3动态绑定与静态绑定

对不满足多态条件(指针或者引⽤+调⽤虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定.(即普通成员函数的调用)

 ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr] 
00EA2C94 call Student::Student (0EA153Ch)

例如上方,是在汇编下调用普通函数如何调用,编译器直接确定调用函数地址.

满⾜多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定.(即为多态的调用)

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

4.4虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同⼀张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表.
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
	virtual void func()
	{
		cout << "virtual void func()" << endl;
	}
private:
	string _name;

};
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
	virtual void func()
	{
		cout << "virtual func()" << endl;
	}
private:
	string _id;
};
int main()
{
	Person p;
	Person e;
	Student s;
	Student t;
	return 0;
}

此时基类中有两个虚函数,因此虚表指针中也会有两个虚函数的地址.

通过下方两张图我们可以看出对于同种类型的对象共用一张表,不同类型的对象有各自的虚表

  • 派生类由两部分构成,继承下来的基类和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派生类对象中的基类对象成员也独立的.
  • 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
	virtual void func()
	{
		cout << "virtual void func()" << endl;
	}
};
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票半价" << endl;
	}

};
int main()
{
	Person p;
	Student s;
	return 0;
}

通过对比,在第一个虚函数重写的情况下虚表第一个位置的地址被覆盖换成新的地址,而未重写的则继续为基类的地址

  • 虚函数表本质是⼀个存虚函数指针的指针数组,一般情况这个数组最后面放了⼀0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)

在32位的情况下,一个指针占4个字节.

 

  • 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段,只是虚函数的地址又存到了虚表中.
  • 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下面的代码可以对比验证⼀下。vs下是存在代码段(常量区)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐