一、多态是如何体现的?
主要是通过虚函数和运行时类型识别机制。
1、虚函数表:每个包含至少一个虚函数的类都有一个虚函数表。这个表包含了该类及其所有基类的虚函数的地址。当创建派生类对象时,该对象的内存布局会包含一个指向其虚函数表的指针(vfptr)。注:基类有自己的vfptr,派生类连基类的vfptr一起继承。
2、动态绑定:当通过基类指针或引用调用虚函数时,编译器会在运行时通过vptr查找虚函数表,并确定要调用的实际函数(这取决于指针或引用所指向的对象的实际类型)。
3、多态性:由于动态绑定,基类指针或引用可以指向基类或派生类的对象,并调用相应的虚函数。
二、虚函数的底层实现
虚函数的底层是通过虚函数表实现的。当类中定义了虚函数之后,就会在对象的存储开始位置,多一个虚函数指针,该虚函数指针指向一张虚函数表,虚函数表中存储的是虚函数入口地址。
三、虚函数的限制
1、构造函数不能设为虚函数
构造函数的作用是创建对象,完成数据的初始化,而虚函数机制被激活的条件之一就是要先创建对象,有了对象才能表现出动态多态。如果将构造函数设为虚函数,此时构造还未执行完,对象还没创建出来,存在矛盾。
2、静态成员函数不能设为虚函数
虚函数的实际调用:this->vfptr->vtable->virtual function,但是静态成员函数没有this指针,所以无法访问到vfptr。
3、inline函数不能设为虚函数
因为inline函数在编译期间完成替换,而在编译期间无法展现动态多态机制,所以效果是冲突的,如果同时存在,inline失效。
4、普通函数不能设为虚函数
虚函数要解决的是对象多态的问题,与普通函数无关。
四、重载、隐藏、覆盖的区分
重载:发生在同一个类中,当函数名称相同时,函数参数类型、顺序、个数任一不同。.
隐藏:发生在基类派生类之间,函数名称相同时,就构成隐藏(参数不同也能构成隐藏)。它允许子类隐藏父类中的同名函数。
列如:
- #include
- using std::cout;
- using std::endl;
- class A
- {
- public:
- void print()
- {
- cout << "A" << endl;
- }
- };
- class B
- : public A
- {
- public:
- void print(int x)
- {
- cout << "B" << endl;
- }
- };
- int main()
- {
- B b;
- b.print();
- return 0;
- }
运行结果:
在例子中,B类中的print(int x) 隐藏了A类中的print()。如果使用B类型的对象调用print(),编译器会报错,因为没有找到匹配的函数。
覆盖:发生在基类和派生类之间,基类和派生类中同时定义相同的虚函数,覆盖的是虚函数表的入口地址,并不是覆盖函数本身。
- #include
- using std::cout;
- using std::endl;
- class A
- {
- public:
- virtual void print() const
- {
- cout << "A" << endl;
- }
- };
- class B
- : public A
- {
- public:
- void print() const override
- {
- cout << "B" << endl;
- }
- };
- int main()
- {
- B b;
- A *P = &b;
- P->print();
- return 0;
- }
运行结果:
五、析构函数设为虚函数
一、如果基类的析构函数不是虚函数,那么通过基类指针或引用调用析构函数时只会调用基类的析构函数,而不会调用派生类的析构函数。导致派生类部分没有被正确清理。为了避免这种情况,我,我们通常在基类中将析构函数声明为虚函数。
- #include
- using std::cout;
- using std::endl;
-
- class Base
- {
- public:
- Base()
- : _base(new int(10))
- {
- cout << "Base() " << endl;
- }
- virtual void display() const
- {
- cout << "*_base:" << *_base << endl;
- }
- virtual ~Base()
- {
- if (_base)
- {
- delete _base;
- _base = nullptr;
- }
- cout << "~Base()" << endl;
- }
-
- private:
- int *_base;
- };
-
- class Derived
- : public Base
- {
- public:
- Derived()
- : Base(), _derived(new int(20))
- {
- cout << "Derived()" << endl;
- }
- virtual void display() const override
- {
- cout << "*_derived:" << *_derived << endl;
- }
- ~Derived()
- {
- if (_derived)
- {
- delete _derived;
- _derived = nullptr;
- }
- cout << "~Derived()" << endl;
- }
-
- private:
- int *_derived;
- };
- int main()
- {
- Base *pbase = new Derived();
- pbase->display();
- delete pbase;
- return 0;
- }
运行结果:
当使用基类指针指向派生类对象并通过 delete 操作符删除该基类指针指向的对象时,如果基类的析构函数是虚函数,那么析构函数的多态性就会被触发。
在delete ptr;这一行会发生以下事情:
1、编译器首先会检查 ptr 所指向的对象的实际类型(在这种情况下是Derived类型)。
2、由于 Base 类的析构函数是虚函数,编译器会查找该对象的虚函数表(Vtable),以确定应该调用哪个析构函数。
3、编译器使用虚函数表找到Derived 类的析构函数,并首先调用它。
4、Derived 类的析构函数执行完毕后,会自动调用基类Base的析构函数。
这样,即使是通过基类指针ptr删除对象,也能确保Derived 类的析构函数被正确调用,从而避免了资源泄露等问题。
注:析构函数顺序:派生类析构函数的执行顺序与构造函数相反。首先调用派生类的析构函数,然后按照基类在继承列表中的顺序(如果有多个基类)或按照基类声明的顺序(如果有单个基类)调用基类的析构函数。这确保了在对象销毁时,首先清理派生类特有的资源,然后清理基类共有的资源。