C++指北,多态详解
1.多态的概念
多态,简称多种形态。
多态被分为编译时的多态,静态多态;运行时的多态,动态多态。
静态多态,通过函数重载、运算符重载和函数模板,在编译时根据参数类型确定调用的具体函数。
动态多态,运行时,通过基类指针/引用调用虚函数,根据对象实际类型执行对应的派生类方法,实现多态。
2.多态的定义
多态是一个继承关系下的类对象,在调用同一函数下,产生了不同情况的行为。
同一接口,产生不同行为。
2.1多态的构成条件
实现多态的两个必要重要条件。
1.必须是基类的指针或引用。
因为只有基类的指针/引用才能既指向基类又指向派生类对象。
2.被调用的函数必须是基类的虚函数,且该虚函数完成了重写/覆盖。
因为只有达到重写/隐藏时,才能实现不同的功能。
2.2虚函数,virtual
在类成员函数前加一个关键字virtual修饰,就变成虚函数了,非成员函数不能加。
//简单多态的实现
class person
{
public:
//虚函数,用于实现运行时的多态,动态多态
virtual void buytick() { cout << "person" << endl; }
};
2.3虚函数的重写/隐藏/协变
虚函数的重写/覆盖,当派生类有一个与基类完全相同的虚函数,函数名,返回类型,参数类型,称为派生类的虚函数重写/覆盖了基类的虚函数。
派生类对虚函数的重写不依赖于关键字virtual的显式重复声明,因为当基类声明了一个虚函数时,派生类会自动继承该函数的虚特性。
即便子类不写虚函数,派生类会直接继承基类虚函数
//简单多态的实现
class person
{
public:
//虚函数,用于实现运行时的多态,动态多态
virtual void buytick() { cout << "person" << endl; }
};
class student :public person
{
public:
//基于基类的虚函数与子类的虚函数达到三重,构成重写状态
//即便子类不写虚函数,派生类会直接继承基类虚函数
virtual void buytick() { cout << "student" << endl; }
};
接下来看一道题。
下列程序输出的结果是什么
a.A->0 b.B->0 c.A->1 d.B->1 e.编译出错
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是A的子类,两个类中的func构成重写关系,并且B继承了A中的test虚函数,当通过 B 类型指针p调用test()时,由于 B 未重写test,实际执行的是 A 的test函数。
又因为p是B*类型的指针,指向的对象是B对象,因此test函数内部会调用B对象中的func函数。
协变,指的是基类虚函数返回类型与派生类重写虚函数返回类型不一致。
即基类虚函数返回基类的指针/引用,派生类重写虚函数返回派生类指针/引用。
2.4动态失效的三个场景
当虚函数被static修饰时,无this指针无法访问虚函数表,不支持多态。
虚函数在构造/析构中调用,构造时虚表指针未完全初始化,析构时虚表指针已失效,此时调用虚函数会执行当前类(构造 / 析构所在类)的版本,而非实际对象类型的版本。
虚函数被inline修饰,inline仅为编译器建议,若虚函数被重写,编译器会忽略inline,但若未被重写且通过非多态方式调用,可能按普通内联函数处理,不触发多态。
2.5关键字override与final
关键字override是用来检查是否正确重写的,因为三重出错导致无法重写是不会在编译期间报出而是在运行当中。
class student :public person
{
public:
virtual void buytick() override { cout << "student" << endl; }
};
final在继承中有所提到是用来声明一个类为最终类无法继承,既然不能继承那么也能让派生类不能重写这个虚函数,毕竟重写的首要条件便是继承。
class person final
{
public:
virtual void buytick() { cout << "person" << endl; }
};
2.6重载/继承/隐藏/重写/覆盖
重载,在同一作用域下函数名相同,参数不同构成函数重载。
继承,在public继承下,子类对于父类的非私有成员的复用及拓展。
隐藏,在继承体系当中,子类与父类只要具有相同函数名就会隐藏父类的函数,若要使用则需要显式声明父类的类域,成员变量也是一样的。
重写/覆盖,两个函数分别在继承体系下父类和子类的两个不同的作用域下,父子类中虚函数,函数名,参数类型,返回值相同,或满足协变的称为重写。
2.7析构函数的重写
在继承篇中我们提到,析构函数名经过编译器特殊处理统一称为destructed()。
基类的析构函数为虚函数时,所以在派生类中析构函数只要定义,无论是否加关键字virtual修饰它都能与之构成重写关系。
int main()
{
//a父类,b子类
a* p1 = new a;
a* p2 = new b;
delete p1;
delete p2;
return 0;
}
3.纯虚函数与抽象类
纯虚函数就是虚函数后面加上个=0,它是可以被定义实现的。
包含纯虚函数的叫做抽象类,它不能实例化,如果派生类继承后不重写,该派生类也是抽象类,某种意义上就是在强迫你重写虚函数
class person
{
public:
virtual void buytick() =0;
};
4.多态的原理
//简单多态的实现
class person
{
public:
//虚函数,用于实现运行时的多态,动态多态
virtual void buytick() { cout << "person" << endl; }
};
class student :public person
{
public:
//基于基类的虚函数与子类的虚函数达到三重,构成重写状态
//即便子类不写虚函数,派生类会直接继承基类的虚函数
virtual void buytick() { cout << "student" << endl; }
};
void fun(person* ptr)
{
//这里可以看到指针ptr在调用buytick但是跟ptr无关,而是由ptr指向的对象决定
ptr->buytick();
}
int main()
{
person a;
student b;
//通过不同对象
fun(&a);
fun(&b);
return 0;
}
4.1虚函数表
虚函数表是一个数组,专门用来存储虚函数指针的,是一个指针数组,也可以叫做函数指针数组。
虚函数表是用来存储对象中所有虚函数的地址,同类型的对象共用同一张虚函数表,不同类型的对象即便是在继承体系下用不同的虚函数表。
派生类由继承下来的基类部分和自己构成,一般继承下来的基类部分是有着自己的虚函数表指针,但与基类的所指向的并不是同一个。
派生类中重写基类的虚函数,在虚函数表中对应的虚函数地址就会被覆盖为重写后虚函数地址。
派生类的虚函数表中包含,基类的虚函数地址,被覆盖后的虚函数地址,派生类自己的虚函数地址。
在VScode编译器中编译器一般会给虚函数表这个指针数组后面放一个标记位0x00000000,类似它对数组越界检查哨兵位机制。
虚函数与普通函数一样编译后是一段指令,会被存储在代码段中,只是虚函数地址会被存储在虚函数表中。
VScode编译器中,虚函数表被存储在代码段中。
以下这段代码可以对比验证。
#include<iostream>
using namespace std;
class person
{
public:
virtual void func()
{
cout<<"person"<<endl;
}
};
class student:public person
{
public:
virtual void func()
{
cout<<"student"<<endl;
}
};
void func(person* ptr)
{
int x=10;
ptr->func();
}
int main()
{
person a;
student b;
func(&a);
func(&b);
int x=10;
static int y=1;
person* a1=&a;
student* b1=&b;
cout<<&x<<endl;
cout<<&y<<endl;
cout<<"虚表指针"<<*(int*)a1<<endl;
cout<<"虚表指针"<<*(int*)b1<<endl;
cout<<&person::func<<endl;
cout<<&student::func<<endl;
return 0;
}
4.2虚函数表指针
来看程序输出结果为多少
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;
}
因为一个类中所有的虚函数地址都要放在虚函数表中,而虚函数表的地址又通常是起始于对象头部,所以除了常规的成员_b,_ch以及内存对齐外我们还要计算这个虚函数表指针的大小。
5.静态绑定与动态绑定
对不满⾜多态条件(指针/引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤ 函数的地址,叫做静态绑定。
满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数 的地址,也就做动态绑定。
更多推荐


所有评论(0)