文章目录
- 介绍
- windows下堆栈相对位置
- 析构函数
- 复习override和final和重载/重定义/重写
- 抽象类
- 多态原理
- 回顾虚基表指针
- 单继承多态底层
- 打印虚函数表
- 多继承多态底层
- c++输出类成员函数地址
- 再次理解多态
- 早期绑定/晚期绑定
介绍
什么是多态
- 多态(Polymorphism)是面向对象编程中的一个重要概念,指的是同一种操作或方法可以在不同的对象上产生不同的行为。具体来说,多态是通过继承和虚函数实现的。多态:为不同数据类型的实体提供统一的接口
- 多态可以提高代码的灵活性和可扩展性。通过多态,我们可以编写通用的代码,而不必考虑对象的具体类型。这样可以使代码更加简洁、易于维护和扩展。
- 例如:同样是买票这种行为,普通人是全价买票,学生是半价买票,军人则是优先买票。这就是一种多态的体现
多态的构成条件
- 必须通过基类的指针或者引用调用虚函数
- 派生类必须对基类的虚函数进行重写(子类重写时virtual可有可无 建议有)
虚函数重写
派生类中有一个跟基类完全相同的虚函数(返回值类型、函数名字、参数列表完全相同,缺省值可以不同)
虚函数
-
虚函数用于实现运行时多态。虚函数在运行时根据对象的实际类型调用相应的函数。虚函数通过使用虚函数表来实现动态绑定。
-
在C++中,如果一个成员函数被声明为虚函数,那么它会被编译器标记为虚函数,并且在类的内存布局中会包含一个指向虚函数表的指针。虚函数表是一个存储虚函数地址的表格,它是在编译时由编译器生成的,用于实现动态绑定。每个包含虚函数的类都有自己的虚函数表,虚函数表中存储着该类的虚函数地址。
-
多态的实现原理:当一个对象调用虚函数时,编译器会通过对象的虚函数表指针找到该对象所属类的虚函数表,然后根据虚函数在类中的位置,找到对应的虚函数地址。这个过程称为动态绑定,它是在运行时确定的,而不是在编译时确定的。
-
虚函数可以被派生类重写,也可以被派生类继承并保留为虚函数。
-
virtual只能用于修饰普通成员函数,不能修饰静态成员函数,virtual和static不能共用。(最后解释)
-
virtual关键字只在声明时加上,虚函数在类外实现时不加virtual。(这点和static相同)
-
在重写基类虚函数时,派生类的虚函数不加virtual关键字,也可以构成重写。因为基类虚函数的接口被继承下来,在派生类中依旧保持虚函数属性。但是该种写法不是很规范,不建议这样使用。
-
可以被继承;可以被隐藏(重定义);可以被访问控制符修饰;
-
可以被声明为纯虚函数;可以被重写(覆盖);可以被动态绑定(进虚函数表);
两个例外:不严格按照重写条件也被认为是重写
- 协变 (基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(甚至可以是其他父子关系的指针或引用 )
// 1.1 (基类与派生类虚函数返回值类型不同)
class Animal
{
public:
virtual Animal* express()
{
cout << "我在疯狂动物叫" << endl;
return this;
}
};
class Dog :public Animal
{
public:
virtual Dog* express()
{
cout << "我在疯狂狗叫" << endl;
return this;
}
};
void func(Animal& animal)
{
animal.express();
}
int main()
{
Animal animal;
func(animal);
Dog dog;
func(dog);
return 0;
}
// 1.2(甚至可以是其他父子关系的指针或引用)
class Ox //牛
{
};
class Bull :public Ox//公牛
{
};
class Animal
{
public:
virtual Ox* express()
{
cout << "我在疯狂动物叫" << endl;
return nullptr;
}
};
class Dog :public Animal
{
public:
virtual Bull* express()
{
cout << "我在疯狂狗叫" << endl;
return nullptr;
}
};
void func(Animal& animal)
{
animal.express();
}
int main()
{
Animal animal;
func(animal);
Dog dog;
func(dog);
return 0;
}
- 析构函数的重写 (基类与派生类析构函数的名字不同)
基类与派生类析构函数名字不同构成重写的原因是,编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
基类的析构函数不为虚函数,派生类与基类的析构函数构成隐藏/重定义(子类与父类某函数名相同)。
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。
windows下堆栈相对位置
(35 封私信 / 8 条消息) 堆、栈的地址高低? 栈的增长方向? - 知乎 (zhihu.com)
Linux下进程地址空间
windows的进程地址空间
很明显,windows下栈的位置并不是严格按照Linux的!他甚至有时候还会比代码区低!
总结
- Windows的栈和Linux不一样。
- windwos的栈向哪个方向增长取决于编译器。解释看一下代码
void test()
{
int arr[3]{0, 1, 2};
}
int main()
{
test();
return 0;
}
被调用函数(callee)test的栈帧相对调用函数(caller)main的栈帧的位置反映了栈的增长方向:
如果被调用函数test的栈帧比调用函数main的栈帧在更低的地址,那么栈就是向下增长;反之则是向上增长。
而在一个栈帧内,局部变量是如何分布到栈帧里的(所谓栈帧布局,stack frame layout),这完全是编译器的自由。
即不是严格按照a[0] a[1] a[0] 创建的
析构函数
一些场景析构函数需要构成重写,重写的条件之一是函数名相同。一般情况下,编译器会对析构函数名进行特殊处理,处理成 destrutor(),所以父类析构函数不加 virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系。
没有虚析构导致的问题
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
Student()
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
// 1.0 s生命周期结束 调用student析构 而student的析构会自动调用父类析构 正确释放空间
Student s;
// 2.0.1 ptr1正确释放
Person* ptr1 = new Person;
delete ptr1;
// 2.0.2 ptr2 只调用person的析构 error!
Person* ptr2 = new Student;
delete ptr2;
return 0;
}
虚析构存在的必要性
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
//int* _ptr;
};
class Student : public Person
{
public:
Student()
{
cout << "Student()" << endl;
}
// 析构函数名底层为:destructor -- 构成虚函数重写
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* ptr1 = new Person;
delete ptr1;
// ptr2 是一个指向子类的父类指针 调用子类重写的析构函数 形成多态
// delete ptr2时会调用子类的析构函数 而子类的析构函数会自动调用父类的析构函数 ok!!
Person* ptr2 = new Student;
delete ptr2; //ptr2 -> destructer();
//operator delete(ptr2);
return 0;
}
纯虚析构
class Dad
{
public:
Dad()
{
cout << "Dad 构造函数调用!" << endl;
}
virtual void Name() = 0;
virtual ~Dad() = 0;
};
Dad::~Dad()
{
cout << "Dad 纯虚析构函数调用!" << endl;
}
class Son : public Dad
{
public:
Son(string name)
{
cout << "Son 构造函数调用!" << endl;
_name = new string(name);
}
virtual void Name()
{
cout << *_name << "是son的名字" << endl;
}
~Son()
{
cout << "Son 析构函数调用!" << endl;
if (this->_name != NULL)
{
delete _name;
_name = NULL;
}
}
public:
string* _name;
};
int main()
{
Dad* dad = new Son("Mike");
dad->Name();
delete dad;
return 0;
}
虚析构/纯虚析构
- 二者目的皆是能够【delete指向子类对象的父类指针】时正确调用析构函数。
- 纯虚析构适用于:当前基类作为一个抽象类,不想要实例化对象,只作为子类的父类,并且可以强制子类重写析构函数。
- 但是使用虚析构和纯虚析构需要注意:二者必须有函数实现–虚析构在类内即可完成函数实现–纯虚析构需要在类外完成。
复习override和final和重载/重定义/重写
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
final:修饰虚函数,表示该虚函数不能再被重写(不常用)【final修饰类 标识该类不能被继承】
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
抽象类
- 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
- 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
- 派生类继承抽象类后也不能实例化出对象。只有重写纯虚函数,派生类才能实例化出对象。
- 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
理解子类的虚函数表和接口继承
class A
{
public:
virtual void func(int value = 1)
{
cout << "A->" << value << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
virtual void func(int value = 0) override
{
cout << "B->" << value << endl;
}
};
int main()
{
//B* pb = new B; //B -> 1
//A* pb = new B; //B -> 1
//pb->test();
A* pa = new A;
pa->test(); // A->1
return 0;
}
B::func的缺省值和A::func不同 是否构成重写?此处构成重写!
函数重写(override)是指子类提供一个与父类相同的方法名、返回类型以及参数列表(包括参数的个数和类型)的实现。缺省值不同:参数缺省值属于接口内容,会被继承下来。
输出解释
B* pb = new B;
pb->test();
此时调用的是 B
对象中的 test
函数,而 test
函数的实现是从类 A
继承而来的:
virtual void test(A* this) // A* this = pb;
{
func();// this -> func();
}
参数缺省值属于接口内容,会被继承下来。==》默认参数绑定
深入理解
子类不重写test B对象虚基表里面:重写的func 未重写的test
如果子类不重写父类的所有虚函数 那么父类虚表指针指向父类的虚函数表 子类虚表指针也指向父类的虚函数表
但是vs下 不管是否重写 子类跟父类虚表都不是同一个
这样实现的理由:即便子类没有重写 但是子类有自己的虚函数时 单独创建一个虚表和父类分隔开 更有条理
子类虚函数表存储:重写的父类虚函数func 没有重写的父类虚函数test 自己的虚函数
结果相同,只不过切片赋值操作在定义基类指针p就已经发生了。调用test函数时是同类指针的普通赋值。
实际到底调用的是谁,不是看传的是父类指针还是子类指针,而是指针指向的对象是父类还是子类。指向谁调用的就是谁
多态原理
回顾虚基表指针
单继承多态底层
class Dad
{
public:
virtual void Cook()
{
cout << "佛跳墙" << endl;
}
virtual void Work()
{
cout << "Work" << endl;
}
int _a = 0;
};
class Son : public Dad
{
public:
virtual void Cook()
{
cout << "方便面" << endl;
}
int _b = 0;
};
void Test(Dad& p)
{
p.Cook();
}
int main()
{
Dad dad;
Test(dad);
Son son;
Test(son);
return 0;
}
打印虚函数表
class Dad
{
public:
virtual void BuyCar()
{
cout << "Dad::买车-宾利" << endl;
}
virtual void Func1()
{
cout << "Dad::Func1()" << endl;
}
};
class Son : public Dad
{
public:
virtual void BuyCar()
{
cout << "Son::买车-奔驰" << endl;
}
virtual void Func2()
{
cout << "Son::Func2()" << endl;
}
};
typedef void(*vftptr)();
void PrintVftable(vftptr* pt) //void PrintVftable(vftptr pt[])
{
for (size_t i = 0; *(pt + i) != nullptr; ++i)
{
printf("vft[%d]:%p->", i, pt[i]);
//1.直接访问
pt[i]();
//2.间接访问
//vftptr pf = pt[i]; f();
}
cout << endl;
}
int main()
{
Dad p1;
Dad p2;
Son s1;
Son s2;
//打印子类虚表
PrintVftable((vftptr*)*(int*)&s1);
PrintVftable((*(vftptr**)&s1));
//打印父类虚表
PrintVftable((vftptr*)*(int*)&p1);
PrintVftable((*(vftptr**)&p1));
return 0;
}
/*
typedef void(*VFPTR)();
void PrintVFTable(VFPTR *table, size_t n)
{
for(size_t i = 0; i<n; ++i)
{
printf("vftable[%lu]:%p -> ", i, table[i]);
table[i](); //函数指针强转成VFPTR,无视函数原型调用函数。
}
}
void Test1()
{
//打印Base和Derive两个类的虚函数表
Base b;
Derive d;
printf("Base虚函数表:%p\n", (int*)*(long long*)&b);
printf("Derive虚函数表:%p\n", (int*)*(long long*)&d);
cout << endl;
PrintVFTable((VFPTR*)*(long long*)&b, 2);//取出对象中的虚函数表指针传参
cout << endl;
PrintVFTable((VFPTR*)*(long long*)&d, 3);
}
*/
vs监视窗口存在bug,虚函数表中不能显示派生类自己定义的虚函数指针func2。
样例Ⅱ
class Person{
virtual void Buyticket(){
cout << "Person::Buyticket()" << endl;
}
virtual void Func1(){
cout << "Person::Func1()" << endl;
}
};
class Student:public Person{
virtual void Buyticket(){
cout << "Student::Buyticket()" << endl;
}
virtual void Func2(){
cout << "Student::Func2()" << endl;
}
};
typedef void(*VFPTR)();
void PrintVFTable(VFPTR *table, size_t n){
for(size_t i = 0; i<n; ++i)
{
printf("vftable[%lu]:%p -> ", i, table[i]);
table[i](); //函数指针强转成VFPTR,无视函数原型调用函数。
}
}
int main(){
Person p;
Person p1;
Student s;
Student s1;
//测试一:打印各对象虚函数表的地址
cout << "p: " << (VFPTR*)*(long long*)&p << endl;
cout << "p1: " << (VFPTR*)*(long long*)&p1 << endl;
cout << "s: " << (VFPTR*)*(long long*)&s << endl;
cout << "s1: " << (VFPTR*)*(long long*)&s1 << endl;
cout << endl;
//测试二:打印虚函数表中的虚函数地址,并调用虚函数
PrintVFTable((VFPTR*)*(long long*)&p, 2); //取对象开头的虚函数表指针传参
cout << endl;
PrintVFTable((VFPTR*)*(long long*)&s, 3);
return 0;
}
32位
64位
-
虚函数表的指针位于对象空间的开头前8个字节(64下),一个long long的大小;
-
同类型的对象p1,p2共用一个虚函数表(输出结果可见);不管是否完成重写,子类和父类的虚函数表都不是同一个。
-
单继承只有一个虚函数表。派生类对象的虚函数表指针保存在基类部分,派生类会继承基类的虚函数表(拷贝基类虚函数的地址);如果构成重写,就覆盖重写后的虚函数地址;
-
派生类自己定义的虚函数地址也要存入虚函数表。
多继承多态底层
class Base1{
virtual void Func1(){
cout << "Base1::Func1()" << endl;
}
virtual void Func2(){
cout << "Base1::Func2()" << endl;
}
};
class Base2{
virtual void Func1(){
cout << "Base2::Func1()" << endl;
}
virtual void Func2(){
cout << "Base2::Func2()" << endl;
}
};
class Derive:public Base1, public Base2{
virtual void Func1(){
cout << "Derive::Func1()" << endl;
}
virtual void Func3(){
cout << "Derive::Func3()" << endl;
}
};
//写法一:切片赋值
void Test1()
{
Derive d;
Base1 *pb1 = &d;
Base2 *pb2 = &d;
PrintVFTable((VFPTR*)*(long long*)pb1, 3);
cout << endl;
PrintVFTable((VFPTR*)*(long long*)pb2, 2);
}
//写法二:移动指针
void Test2()
{
Derive d;
PrintVFTable((VFPTR*)*(long long*)&d, 3);
cout << endl;
PrintVFTable((VFPTR*)*(long long*)( (char*)&d+sizeof(Base1) ), 2);
}
//显示虚表Ⅰ
PrintVftable((vftptr*)(*(int*)&s)); //int只能访问4个字节 在64位下不再适用
//PrintVftable((*(vftptr**)&s)); 高级写法
//显示虚表Ⅱ法一:
PrintVftable((vftptr*)(*(int*)( (char*)&s+sizeof(Dad1) )));
//PrintVftable((*(vftptr**)((char*)&s + sizeof(Dad1))));高级写法
//显示虚表tⅡ法二:
//Dad2* ptr = &s;
//PrintVftable((vftptr*)(*(int*)(ptr)));
//PrintVftable((*(vftptr**)ptr)); 高级写法
- 在多继承中,派生类对象中的每个基类部分都有自己的虚函数表。
- 派生类自己定义的虚函数地址存放在第一个基类的虚函数表中。
- 在多继承中,如果两个基类中有同名的虚函数,那么在派生类中必须重写它们,否则会导致二义性错误。重写版本会覆盖所有基类中的虚函数。(多重覆盖函数)
多继承函数调用
class Dad1
{
public:
virtual void func1()
{
cout << "Dad1::func1" << endl;
}
virtual void func2()
{
cout << "Dad1::func2" << endl;
}
private:
int a1 = 1;
};
class Dad2
{
public:
virtual void func1()
{
cout << "Dad2::func1" << endl;
}
virtual void func2()
{
cout << "Dad2::func2" << endl;
}
private:
int a2 = 2;
};
class Son : public Dad1, public Dad2
{
public:
virtual void func1()
{
cout << "Son::func1" << endl;
}
virtual void func3()
{
cout << "Son::func3" << endl;
}
private:
int aa = 3;
};
int main()
{
Dad1 d1;
Dad2 d2;
Son s;
//普通调用
s.func1();
//多态调用
Dad1* ptr1 = &s;
ptr1->func1();
Dad2* ptr2 = &s;
ptr2->func1();
return 0;
}
&Son::func1和汇编指令call的地址不同 但是调用的是同一个函数 为什么?
具体不明确,目前已知很有可能是因为&Son::func1输出的是Son::func1的没有对父类func1重写的地址。实际调用时该地址很有可能会像ptr2那样偏移一下去调用真正的func1。
int main()
{
Dad1 d1;
Dad2 d2;
Son s;
//显示虚表Ⅰ
PrintVftable((vftptr*)(*(int*)&s));
//显示虚表Ⅱ:
PrintVftable((vftptr*)(*(int*)((char*)&s + sizeof(Dad1))));
cout << "%p=&Son::func1:";
printf("%p\n", &Son::func1); //成员函数需要加&才能取到地址 普通函数名就可作为地址
void* pFunc1 = 0;
asm_cast(pFunc1, Son::func1);
std::cout << "汇编:" << pFunc1 << std::endl;
void* pFunc2 = union_cast<void*>(&Son::func1);
std::cout << "联合体:" << pFunc2 << std::endl;
//普通调用
s.func1();
//多态调用
Dad1* ptr1 = &s;
ptr1->func1();
Dad2* ptr2 = &s;
ptr2->func1();
return 0;
}
c++输出类成员函数地址
通过联合体的共享储存机制 模板的使用也使得该函数可迁移性更强
template<typename AddressType, typename FuncPtrType>
AddressType union_cast(FuncPtrType func_ptr) // 获取类内成员函数的函数地址
{
union
{
FuncPtrType f;
AddressType d;
}u;
u.f = func_ptr;
return u.d;
}
通过汇编取成员函数偏移得到地址 宏函数,通过 offset 语句取出 addr 的地址偏移量,将其值赋给 var 变量。
#define asm_cast(var,addr) \
{ \
__asm \
{ \
mov var, offset addr \
} \
}
测试
template<typename AddressType, typename FuncPtrType>
AddressType union_cast(FuncPtrType func_ptr) // 获取类内成员函数的函数地址
{
union
{
FuncPtrType f;
AddressType d;
}u;
u.f = func_ptr;
return u.d;
}
#define asm_cast(var,addr) \
{ \
__asm \
{ \
mov var, offset addr \
} \
}
class A
{
private:
int _val;
public:
A(int val)
:_val(val)
{
}
const int* getValAddress()
{
return &_val;
}
void func()
{
cout<<"test func"<<endl;
}
int getVal()
{
return _val;
}
};
void test()
{
A a1(10);
A a2(10);
// 检验相同类生成的不同对象对应 成员变量地址 是否一致 --->(不一致)
cout << "&(a1.val) = " << a1.getValAddress() << endl;
cout << "&(a2.val) = " << a2.getValAddress() << endl;
// 检验相同类生成的不同对象对应 成员函数地址 是否一致 --->(一致)
void* ptr1 = union_cast<void*>(&A::func);
void* ptr2 = 0;
asm_cast(ptr2, A::func);
// 打印成员函数指针的值
std::cout << "Address_1 of myMethod: " << ptr1 << std::endl;
std::cout << "Address_2 of myMethod: " << ptr2 << std::endl;
}
int main()
{
test();
return 0;
}
总结
- 虚函数表本质是一个存放虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(vs平台下)。
- 派生类对象的虚函数表指针保存在基类部分,派生类和基类的虚函数表不是同一个(地址不同)。
- 派生类会继承基类的虚函数表(虽然不是同一个虚函数表,但会拷贝基类虚函数的地址);如果构成重写,就覆盖重写后的虚函数地址;
- Func2继承下来后是虚函数,所以放进了虚函数表,Func3也继承下来了,但是不是虚函数,所以不会放进虚函数表。Func4是派生类自己定义的虚函数,也要进虚函数表。
- 虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚函数表中。
- 另外对象中存的不是虚函数表,存的是虚函数表指针。虚函数表是保存在只读常量区(代码段)中的。
- inline函数可以是虚函数,虚函数可以被声明为inline,但是是否真正内联取决于编译器的实现。当函数是虚函数时,如果进行多态调用(使用基类指针或引用来调用虚函数),inline就不起作用。因为多态调用在运行时决议,编译时无法确定地址就不能展开函数;如果不是多态调用(使用类的对象来调用时),同时满足inline条件就会展开函数。
- 静态成员函数不能是虚函数,因为静态成员函数没有this指针,使用 类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放入虚函数表。font>
- 构造函数不能是虚函数,因为构造函数中的虚函数表指针是在构造函数初始化列表阶段才初始化的,此时对象尚未完全建立。
- 析构函数可以是虚函数,并且最好将基类的析构函数定义为虚函数。这样当通过基类指针或引用来删除一个派生类对象时,会调用正确的析构函数并避免内存泄漏。虚析构函数通常用于处理多态对象的释放问题。
- 普通对象访问普通函数和访问虚函数的速度相同,直接调用函数就可以了,不需要查找虚函数表。指针对象或引用对象,由于可能存在多态性,需要根据实际类型查找虚函数表,稍微慢一些。
总结一下派生类的虚函数表生成:
a. 先将基类中的虚函数表内容拷贝一份到派生类虚函数表中
b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚函数表中基类的虚函数
c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚函数表的最后。
总结
- 类实例化的对象中的虚表指针在构造函数的初始化列表初始化。
- 虚表在编译阶段生成。
- 虚表存在于代码段。
- 虚函数表本质是一个存放虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(vs平台下)
带有虚函数的类大小
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
// 32位:8;
// 64位:16;(内存对齐)
再次理解多态
多态的构成条件,为什么?
答:运行时多态的概念就是在继承体系中以同一种方式执行同一种操作或方法,面对不同类型的对象产生不同的行为。
同一种方式,选择继承体系中的公共部分——基类,虚函数表的指针保存在基类部分的开头,必须通过基类的指针或者引用调用虚函数。
不同的行为,被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
多态的实现原理?
答:主要依靠虚函数和虚表来实现。当一个类中声明了虚函数时,编译器会为该类生成一个虚函数表(vtable),其中存储了虚函数的地址。每个对象都会有一个指向该虚函数表的指针,当调用虚函数时,通过指针在虚函数表中找到对应的函数地址并调用。
通过继承和重写基类的虚函数,派生类可以改变函数的实现,在运行时根据对象的实际类型来调用正确的函数。
早期绑定/晚期绑定
C++语言的多态性分为编译时的多态性和运行时的多态性,也叫早期绑定和晚期绑定
编译时的多态性又称为静态或早期绑定,通过函数重载来实现的,因为函数重载是一种静态多态性,它在编译时就能确定函数调用的地址。在调用重载函数时,编译器会根据实参的类型、个数和顺序来确定调用哪个函数。编译时多态适用于非虚函数和静态函数,因为它们的函数地址在编译时就已经确定了。编译时多态的优点是速度快,缺点是不支持运行时多态性。
运行时的多态性又称为动态或晚期绑定,是指在程序运行时根据对象的实际类型来确定函数调用的地址,从而实现多态性。运行时多态通过虚函数来实现。在调用虚函数时,会到指定对象的虚函数表中确定重写函数的地址,从而实现多态调用。运行时多态适用于虚函数和纯虚函数,因为它们的函数地址在运行时才能确定。运行时多态的优点是支持多态性,缺点是速度相对较慢。
注意:
使用对象名调用虚函数,或者通过派生类的引用或指针调用虚函数,也是普通调用,属于静态绑定。
虚函数可以动态绑定,也可以静态绑定。
虚函数是专为实现多态而设计的,如果不实现多态不要定义虚函数,否则会导致调用过程变慢。
当满足多态后,传父类调父类函数,传子类调子类函数,这是怎么实现的?
调用虚函数时,根据指针指向的对象,去改对象的虚函数表中调用这个函数。父类中的虚函数表就是父类自己的虚函数,而子类的虚函数表是自己重写的函数。故一个基类指针指向父调父,指向子调子。