一、多态的概念
1.1 编译时多态
编译时多态主要是函数重载和函数模板,它们实现多态主要是通过传不同类型的参数。之所以称为编译时多态,是因为参数的匹配是发生在编译阶段的,这种也被称为静态绑定。
1.2 运行时多态
运行时多态可以认为是通过对象来区分行为的,而编译时多态是通过类型的。就好像同样是'叫'这个行为,传猫过去就是'喵喵',传狗过去就是'汪汪'。
二、多态的定义和使用
2.1 多态的判定标准
- 要是用基类的引用或者指针来调用;
- 被调用的函数是虚函数;
- 调用函数需要构成重写。
我们这里着重讲一下为什么一定要用基类的指针或者是引用调用:
因为多态的内核就是'在运行时确定'。假如使用的是基类的对象,那么在编译的时候对象的类型就是明确的基类类型,这时候就起不到运行时确定的效果了。假设是使用指针或者是引用来调用,在运行时是不知道具体指向的类型的,这时候就需要去查找虚函数表,就可以实现'运行时确认'。
2.1.1 虚函数
在函数声明之前加上 virtual 关键字,这个函数就是虚函数。
virtual void func()
2.1.2 函数重写
函数重写需要构成以下条件:
- 两个函数的函数名、参数、返回值全部相等;
- 两个函数全都是虚函数。
补充:派生类中的函数不是虚函数也是可以的,但是基类中的函数一定是虚函数,此时相当于是将基类中函数的 virtual 关键字继承下来了,此时重写后的函数相当于是将基类的函数名和参数和派生类的函数实现拼在一起了。
2.2 相关的题目
以下程序输出结果是什么? 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;
}
在 main 函数中创建了一个 B 类型的对象,并将它的指针赋值给了 B*类型的指针 p。然后使用 p 来调用 test 函数,这实际上调用的是 B 中继承自 A 的部分中的 test 函数,但是在调用过程中传递给 test 函数中 this 指针的是 B 类型的指针。
根据多态的对象决定论,实际上访问的 func 函数是派生类 B 重写后的 func 函数。但是派生类的 func 函数并没有声明是虚函数,所以实际上重写函数是由基类中的函数的声明和派生类的实现组成的,相当于:
virtual void func(int val = 1) {
std::cout << "B->" << val << std::endl;
}
此时运行结果是 B->1,选择 B。
2.3 析构函数的重写
在继承体系中基类的指针是可以指向派生类的对象的,这时候传统的析构就出现问题了。
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];
};
// 只有派生类的析构函数重写了 Person 的析构函数,下面的 delete 对象调用析构函数,才能
// 构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。
int main() {
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
这时候应该怎么析构,我们显然是想将指针指向的对象全部析构掉,但是碍于指针是 A 基类类型的,如果不加以处理,将只会调用基类的析构函数,派生类并没有被有效析构,这显然是不符合我们的预期的。
这时候我们将析构函数置为虚函数,派生类和基类中的析构函数就构成了函数重写,此时再 delete 基类的指针,调用的就是派生类的析构函数,就可以将空间释放干净。
![虚函数表示意图]
可以看到:析构 p2 指向空间的时候调用了 B 的析构函数。
2.4 override 和 final 关键字
在继承体系中重写的判断条件是比较苛刻的,两个函数只要是出现一点不一样就会达不到重写的条件,这时候还不一定会报错,这是比较危险的(可能会实现不了预期效果)。这时候就引入了 override 关键字和 final 关键字,override 关键字用来检查是否构成重写,final 关键字用来声明这个函数不能被重写。
示例:
class A {
public:
virtual ~A() {
cout << "~A()" << endl;
}
virtual void func1() {}
virtual void func2() final {}
};
class B : public A {
public:
virtual ~B() {
cout << "~B()->delete:" << _p << endl;
delete _p;
}
virtual void func1(size_t i) override {}
virtual void func2() {}
protected:
int* _p = new int[10];
};
如图所示:会直接编译报错。
2.5 纯虚函数和抽象类
在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现)。
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
2.6 协变(不重要,了解即可)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class Person {
public:
virtual A* BuyTicket() {
cout << "买票 - 全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket() {
cout << "买票 - 打折" << endl;
return nullptr;
}
};
三、多态的底层实现
3.1 虚函数表简介
在以上情景中的类的内存中,都存在一个 _vfptr 指针,这就是虚函数表,也叫虚表,指向了类中的虚函数。
![虚函数表结构]
3.2 多态的实现
在满足多态后,要调用哪个函数并不是在编译时就通过指向对象确定的,而是需要在运行时到指向虚函数地址的虚函数表内确定虚函数的地址然后再调用,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
![多态调用流程]
class Person {
public:
virtual A* BuyTicket() {
cout << "买票 - 全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket() {
cout << "买票 - 打折" << endl;
return nullptr;
}
};
void Func(Person* ptr) {
ptr->BuyTicket();
}
int main() {
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
3.3 静态绑定和动态绑定
- 对不满足多态条件 (指针或者引用 + 调用虚函数) 的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定,函数重载和函数模板都是静态绑定。
- 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。
3.4 虚函数表详解
3.4.1 虚函数表的相关规则
- 同类型的对象的虚函数表是同一个(不直接将虚函数的函数指针存在对象中而是存在一个指针数组中就是为了节省空间);
- 派生类先将基类的虚函数表继承下来(但是是拷贝,指向的不是同一块空间);
- 构成了重写的函数会将原来的函数指针覆盖掉;
- 派生类的虚函数表中包含:(1) 基类的虚函数地址,(2) 派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三个部分;
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 0x00000000 标记。(这个 C++ 并没有进行规定,各个编译器自行定义的,vs 系列编译器会在后面放个 0x00000000 标记,g++ 系列编译不会放)
还有需要注意的是:
当虚函数继承了多个基类时,新增的虚函数,统一放在第一个基类的虚函数表中。
3.4.2 虚函数存放的位置问题(这里有坑)
大部分人看到这个问题可能想到的是:虚函数明显是存在虚函数里的。
实则不然,虚函数也是函数,是存在代码段中的,只不过是也在虚函数表中存了一份。
3.4.3 虚函数表存放的位置
这个 C++ 委员会并未明确规定存放位置,但是可以写程序验证。
class Base {
public:
virtual void func1() {
cout << "Base::func1" << endl;
}
virtual void func2() {
cout << "Base::func2" << endl;
}
void func5() {
cout << "Base::func5" << endl;
}
protected:
int a = 1;
};
class Derive : public Base {
public:
// 重写基类的 func1
virtual void func1() {
cout << "Derive::func1" << endl;
}
virtual void func3() {
cout << "Derive::func1" << endl;
}
void func4() {
cout << "Derive::func4" << endl;
}
protected:
int b = 2;
};
int main() {
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("Person 虚表地址:%p\n", *(int*)p3);
printf("Student 虚表地址:%p\n", *(int*)p4);
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);
return 0;
}
验证得:大概是在常量区。


