文章目录
- 🐵1. 什么是多态
- 🐶2. 构成多态的条件
- 🐩2.1 虚函数
- 🐩2.2 虚函数的重写
- 🐩2.3 final 和 override关键字
- 🐩2.4 重载、重写、重定义对比
- 🐱3. 虚函数表
- 🐯4. 多态的原理
- 🐎5. 多继承的虚表关系
- 🦬6. 抽象类
🐵1. 什么是多态
当下网络有个热门词汇叫“双标”,意思就是用不同的标准来衡量人或事,这是一个贬义词。而在编程世界中,这种“双标”,我们称之为多态,当然了这里的多态并不是贬义词,而是一种技术实现。
比如说某种商城有会员机制,将用户分为普通用户、普通会员、尊贵会员等
那买同种东西的时候,不同的用户等级会有着不同的价格,这就是一种多态行为
🐶2. 构成多态的条件
实现多态性的主要构成条件是使用虚函数和继承:
- 必须通过基类的指针或引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对虚函数进行重写
🐩2.1 虚函数
只有类的成员函数才能被定义为虚函数,格式如下:
class A
{//函数前面加上virtual 表面该成员函数为虚函数virtual void func() {}
};
🐩2.2 虚函数的重写
当派生类中有一个和基类完全相同的虚函数时,我们称这为虚函数的重写/覆盖
重写有三同,即:返回值类型、函数名、参数列表完全相同
class A
{
public://虚函数virtual void func() const{cout << "A->func()" << endl;}
};
class B :public A
{
public://虚函数重写virtual void func() const{cout << "B->func()" << endl;}
};
//多态调用传引用过去
void Print(const A& p)
{p.func();
}
int main()
{Print(A()); //A->func()Print(B()); //B->func()return 0;
}
在多态调用中,看的是指向的对象;而普通的函数调用,看的是当前的类型
虚函数的重写,还需注意几点:
-
虚函数父类必须加上
virtual
修饰,子类虚函数重写前面可以不加virtual
,但在实际中,还是建议加上 -
对于虚函数的重写,我们规定三同,但是有例外——协变
即基类与虚函数返回值类型不同,但是返回值类型必须是构成父子关系指针或者引用(同时是指针 或 同时是引用)
class A { public://虚函数virtual A* func() const{cout << "A->func()" << endl;return 0;} }; class B :public A { public://虚函数重写 B和A是父子关系virtual B* func() const{cout << "B->func()" << endl;return 0;} }; void Print(const A& p) {p.func(); } int main() {Print(A());Print(B());return 0; }
-
析构函数的重写,基类和派生类的析构函数名不同
class A { public://虚函数virtual ~A(){cout << "~A()" << endl;} }; class B :public A { public://虚函数重写virtual ~B(){cout << "~B()" << endl;} }; int main() {A* a1 = new A;A* a2 = new B;delete a1;delete a2;return 0; }
输出:
这里的原因是因为编译器对析构函数的名字做了处理,编译后名称统一处理为
destructor
,那为什么要将析构函数统一处理称destructor
呢?因为这里要让他们构成重写。如果不构成重写,就好出现类似这样的情况:class A { public:~A(){cout << "~A()" << endl;} }; class B :public A { public:~B(){delete ptr;cout << "~B()" << endl;} protected:int* ptr; }; int main() {A* a1 = new A;delete a1;a1 = new B;delete a1;return 0; }
输出发现,我们这里
new
了一个B对象,但是每次都是调用A的析构函数,这显然与我们的意愿不符,我们期望的是这个a1->destructor
形成的是多态调用,所以这样统一处理之后,就可以让他们构成重写
🐩2.3 final 和 override关键字
如果不想让这个虚函数被重写,可加上final
关键字修饰
当然了,final
也可以修饰类,让这个类不被继承,一般用于最终的类
如果要检查某个派生类是否重写了基类的某个虚函数,可用override
关键字修饰,如果没有重写,则编译报错
🐩2.4 重载、重写、重定义对比
🐱3. 虚函数表
class A
{
public:virtual void func(){cout << "func()" << endl;}
protected:int _a;
};
int main()
{cout << sizeof(A) << endl;
}
这段代码如果不加上virtual
,则输出的是4;但是加上virtual
之后,输出的是16(64位下,指针是8字节,然后内存对齐)
这是因为有了虚函数,这个类里面会多一个虚函数表的指针,这些表里面存的是虚函数的地址
但如果将这个虚函数没有被重写,那么派生类的虚函数表还是指向基类的虚函数;如果重写了,则指向重写的虚函数。
所以多态调用的时候,不管我们传的是基类和派生类,在内存里看到的都是父类;普通调用是在编译的时候就确定了地址,而多态调用时,运行时会到指向对象的虚表找函数的地址
动态绑定与静态绑定:
- 静态绑定:在编译时确定调用哪个函数或方法。这是在编译器根据变量的静态类型(声明类型)来决定调用哪个函数
- 动态绑定:在运行时根据对象的实际类型来确定调用哪个函数或方法。这是通过虚函数(在基类中声明为虚函数,子类进行重写)实现的。动态绑定适用于通过基类指针或引用调用虚函数的情况,确保调用正确的派生类函数
在这里虚表的地址,是存储在哪里的呢?我们通过这段代码来验证
class A
{
public:virtual void func(){cout << "A->func()" << endl;}virtual void Func(){cout << "A->Func()" << endl;}int _a;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
void Print(A a)
{a.func();
}
int main()
{A aa;B bb;int a = 0;printf("栈:%p\n", &a);static int b = 0;printf("静态区:%p\n", &b);int* p = new int;printf("堆:%p\n", p);const char* str = "hello";printf("常量区:%p\n", str);//前四个字节,一定是虚表的地址printf("虚表a:%p\n", *((int*)&aa));printf("虚表b:%p\n", *((int*)&bb));
}
输出发现虚表的地址和常量区的地址隔的较近,所以我们可以得出结论:虚表的地址存储在常量区
另外,我们在Vs的监视窗口只能查看3个虚函数的地址,但这不代表这,内存里面只有三个虚函数的地址,我们可通过这段代码进行验证:
class A
{
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}virtual void func3(){cout << "A->func3()" << endl;}
};
class B :public A
{virtual void func3(){cout << "B->func3()" << endl;}virtual void func4(){cout << "B->func4()" << endl;}
};
//函数指针命名
typedef void (*Func_Ptr)();
//打印函数指针数组
void PrintVFT(Func_Ptr table[])
{for (size_t i= 0; table[i]!=nullptr ; i++){printf("[%d]:%p->", i, table[i]);Func_Ptr f = table[i];f();}printf("\n");
}
int main()
{A a;B b;int vft1 = *((int*)&a);PrintVFT((Func_Ptr*)vft1);int vft2 = *((int*)&b);PrintVFT((Func_Ptr*)vft2);return 0;
}
🐯4. 多态的原理
有了虚表的概念,这我们就能理解,为什么构成多必须是通过基类的指针或引用调用虚函数。因为只有父类的虚表才能既能指向父类,又能指向子类。
那这里还有一个问题就是,为什么必须是指针或引用呢?
class A
{
public:virtual void func(){cout << "A->func()" << endl;}virtual void Func(){cout << "A->Func()" << endl;}int _a;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
void Print(A a)
{a.func();
}
int main()
{A a;a._a = 1;B b;b._a = 10;a = b;A* pa = &b;A& ref = b;
}
这段代码调试发现,子类赋值给父类,父类会进行切片,这里值会拷贝过去,但是虚表并不会拷贝;因为如果拷贝了虚表的话,这样父类对象中的虚表指向的是父类还是子类就混淆了
🐎5. 多继承的虚表关系
上面讲的内容,包括举得例子都是单继承的,所以就不再赘述。这里我们看一下多继承里面的虚表是怎样的
class A
{
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}
protected:int _a;
};
class B
{
public:virtual void func1(){cout << "B->func1()" << endl;}virtual void func2(){cout << "B->func2()" << endl;}
protected:int _b;
};
class C :public A, public B
{
public:virtual void func1(){cout << "C->func1()" << endl;}virtual void funcC(){cout << "C->funcC()" << endl;}
protected:int _c;
};
typedef void (*Func_Ptr)();
//打印函数指针数组
void PrintVFT(Func_Ptr table[])
{for (size_t i= 0; table[i]!=nullptr ; i++){printf("[%d]:%p->", i, table[i]);Func_Ptr f = table[i];f();}printf("\n");
}
int main()
{C c;cout<<sizeof(c)<<endl;int vft1 = *((int*)&c);//int vft2 = *((int*)(char*)&c + sizeof(A));B* ptr = &c;int vft2 = *((int*)ptr);PrintVFT((Func_Ptr*)vft1);PrintVFT((Func_Ptr*)vft2);
}
通过验证,我们可以发现,C类里面有两张虚表,一张是A的,一张是B的。而C里面的虚函数funcC()
的虚表,是存放在第一张虚表里面
但是,我们这里发现,重写的func1()
函数,明明是一样的,但是地址却不一样,我们这段代码转到汇编代码查看
int main()
{C c;A* ptr1 = &c;B* ptr2 = &c;ptr1->func1();ptr2->func1();return 0;
}
我们发现,ptr1
是直接调用找个func1()
,而ptr2
最终调用的地址和ptr1
是一样的,但是在jump
的,寄存器减了一个8,这个减8正好是c
的地址。ptr1
不用修改是因为正好指向了c
的起始地址,内存不看类型,只看地址
菱形继承这里就不讲了,很混乱~
🦬6. 抽象类
虚函数后面加上=0
,则这个函数为纯虚函数,包含了纯虚函数的类,叫做抽象类。
抽象类不能实例化出对象,之后继承的派生类也不能实例化对象,只能重写虚函数,派生类才能实例化出对象。这里规定了派生类必须重新虚函数,所以抽象类也叫接口类。
class A
{
public:virtual void func() = 0;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
class C :public A
{
public:virtual void func(){cout << "C->func()" << endl;}
};
void Func(A*a)
{a->func();
}
int main()
{Func(new B);Func(new C);return 0;
}
那么本期的分享就到这里咯,我们下期再见,如果还有下期的话。