《C++ 多态详解:从原理到虚函数表》
1.多态的概念
这里我们主要讲动态多态
1.多态的构成条件:
多态是⼀个继承关系的下的类对象,去调用同⼀函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票。
2.实现动态多态的两个条件:
2.1必须是基类的指针或者引用调用虚函数
2.2被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖
说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派⽣类之间才能有不同的函数,多态的不同形态效果才能达到。
3.题目,输出结果是什么
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();
p->func();
return 0;
}
我来解析一下,就是先是创建了一个B类的对象,然后这个对象调用了test()函数,发现自己类没有,就去父类找,然后找到了,在父类中,test()括号里面其实还有隐藏的this,这个this接受了B类对象,然后在函数中,this->func()这个时候this就变成了b类对象,然后又因为func()构成了重写,所以最后调用父类的声明,子类的函数实现,所以答案就是B->1
第二个就是对象调用函数,所以就是B->0
4.重写一定要返回类型跟函数名跟参数都要相等吗,不,其实返回类型可以不同,但是有条件,叫做协变 派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们了解⼀下即可。
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;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
5.析构函数的重写
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "~B()" << endl;
delete _n;
}
protected:
int* _n = new int[10];
};
int main()
{
/*A a1;
B b1;*/
A* p1 = new A;
A* p2 = new B;
// 多态调用,指向谁,调用谁的析构函数
// p1->destructor() + operator delete(p1)
delete p1;
delete p2;
return 0;
}
6.override跟final
override就是强制子类重写
final就是不想要派生类重写这个虚函数
//class Car
//{
//public:
// // 纯虚函数
// // 1、抽象类不能实例化对象
// // 2、强制子类重写
// virtual void Drive()
// {
//
// }
//};
//
//class Benz :public Car
//{
//public:
// virtual void Drive()override
// {
// cout << "Benz-舒适" << endl;
// }
//};
//
//
//int main()
//{
// return 0;
//}
class Car
{
public:
// 纯虚函数
// 1、抽象类不能实例化对象
// 2、强制子类重写
virtual void Drive()final
{
}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
return 0;
}
7.纯虚函数
在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。
下面写纯虚函数,一定程度上强制子类重写,如果不重写的话那么子类继承了父类的纯虚函数那么子类也变成了抽象函数,变成抽象函数就不能实例化对象,也就没有意义
class Car
{
public:
// 纯虚函数
// 1、抽象类不能实例化对象
// 2、强制子类重写
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;
}
};
int main()
{
//Car c;
//Benz b;
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
return 0;
}
8.多态的原理
上⾯题⽬运⾏结果12bytes,除了_b和_ch成员,还多⼀个__vfptr放在对象的前⾯(注意有些平台可能会放到对象的最后⾯,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表
8.1多态是如何实现的
从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调用Person::BuyTicket,ptr指向Student对象调用Student::BuyTicket的呢?通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。第⼀张图,ptr指向的Person对象,调用的是Person的虚函数;第⼆张图,ptr指向的Student对象,调用的是Student的虚函数



8.2动态绑定与静态绑定
对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。
8.3虚函数表
• 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同⼀张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
•
派⽣类由两部分构成,继承下来的基类和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
•
派⽣类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。也就相当于是基类先拷贝一份给子类,子类收到后先保存,如果子类有重写的虚函数,那么就会覆盖父类拷贝下来的虚函数
基类 A 虚函数表(编译时)
┌─────────────────┐
│ A::func1() 地址 │
│ A::func2() 地址 │
│ A::~A() 地址 │
└─────────────────┘
派生类 B(继承 A,重写 func1,新增 func3)
步骤1:先拷贝 A 的虚表
┌─────────────────┐
│ A::func1() 地址 │ ← 拷贝来的
│ A::func2() 地址 │ ← 拷贝来的
│ A::~A() 地址 │ ← 拷贝来的
└─────────────────┘
步骤2:覆盖重写的函数
┌─────────────────┐
│ B::func1() 地址 │ ← 覆盖 ✅
│ A::func2() 地址 │
│ A::~A() 地址 │ ← 如果重写析构,这里也会被覆盖
└─────────────────┘
步骤3:添加新虚函数(如果有)
┌─────────────────┐
│ B::func1() 地址 │
│ A::func2() 地址 │
│ A::~A() 地址 │
│ B::func3() 地址 │ ← 新加的,放在后面
└─────────────────┘
•
派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三个部分。
•
虚函数表本质是一个存虚函数指针的指针数组,⼀般情况这个数组最后面放了⼀个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)
•
虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
•
虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下面的代码可以对比验证⼀下。vs下是存在代码段(常量区)

这里Derive中没有看到func3函数,这个vs监视窗⼝看不到,可以通过内存窗口查看
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()
{
Base b;
Derive d;
return 0;
}
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;
}

可以看到虚表地址在vs环境下是放在常量区
更多推荐

所有评论(0)