半小时掌握C++11之多态2

73 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情

静态绑定

绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译器

普通成员函数是静态绑定,缺省参数一般是静态绑定

动态绑定

绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期

虚函数是动态绑定

#include <iostream>
using namespace std;
class Person //人
{
  public:
  void fun()
   {
       cout << "全价票" << endl; //成人票全价
   }
};
class Student : public Person //学生
{
   public:
   void fun() //子类完成对父类虚函数的重写
   {
       cout << "半价票" << endl;//学生票半价
   }
};
class Soldier : public Person{
   public:
   void fun() 
   {
       cout << "军人票" << endl;
   }
};
​
int main()
{
   Person* p1 = new Person;
   p1->fun();
   p1 = new Student;
   p1->fun();
   p1 = new Soldier;
   p1->fun();
   return 0;
}
/*
全价票
全价票
全价票
*/

fun()是普通成员函数,是静态绑定的,所以到底执行哪个fun()成员函数取决于调用者的静态类型,p1的静态类型是Person*,所以调用的都是Person的fun()函数

所以不应该在子类中重新定义一个继承来的非虚函数,上面出现的结果不是我们想看到的

父类的析构函数一定要是虚析构函数

#include <iostream>
using namespace std;
class Person //人
{
  public:
  Person(){
      cout << "调用Person的构造函数" << endl;
   }
   ~Person(){
      cout << "调用Person的析构函数" << endl;
   }
   // virtual ~Person(){
   //    cout << "调用Person的析构函数" << endl;
   // }
};
class Student : public Person //学生
{
   public:
   Student(){
      cout << "调用Student的构造函数" << endl;
   }
   ~Student(){
      cout << "调用Student的析构函数" << endl;
   }
   
};
​
int main()
{
   Person* p1 = new Student();
   delete p1;
   return 0;
}
/*
调用Person的构造函数
调用Student的构造函数
调用Person的析构函数
*/

普通的类可以没有析构函数,但父类必须写一个析构函数,并且这个析构函数要是一个虚函数

虚函数会增加内存和执行效率上的开销,类里面定义一个虚函数,编译器就会为这个类增加虚函数表,在这个表里存放虚函数地址等信息

唯有这样,当delete一个指向子类对象的父类指针时,才能保证系统能够依次调用子类的析构函数和父类的析构函数,从而保证对象(父指针指向的子对象)内存被正确地释放。

虚函数表

#include <iostream>
using namespace std;
class A //人
{
public:
   virtual void f(){
      cout << "类A的虚函数f被调用" << endl;
   }
   virtual void g(){
      cout << "类A的虚函数g被调用" << endl;
   }
   virtual void h(){
      cout << "类A的虚函数h被调用" << endl;
   }
};
class Student : public Person //学生
{
public:
   virtual void g(){
      cout << "类B的虚函数g被调用" << endl;
   }
   
};
​
int main()
{
   A apple;
   A *pa = &apple;
   return 0;
}
​

A *pa = &apple;获取apple对象的地址,apple对象只有一个房间,就是虚函数指针。如果把父类的虚函数表地址拿出来和子类的虚函数表地址对比,就可以判断父子是否使用同一个虚函数表

比如A类f函数地址0x10,g函数地址0x20,h函数地址0x30

A的虚函数表里存0x10、0x20、0x30,虚函数表的地址为0xc0

A apple;apple地址为oxa0,里面存一个虚函数表的地址为0xc0

因为B类重写了g函数,g函数地址被重新写入,为0x80

那么B类f函数地址0x10,g函数地址0x80,h函数地址0x30

所以B的虚函数表里存0x10、0x80、0x30,虚函数表的地址为0xd0

B bpple;bpple地址为oxb0,里面存一个虚函数表的地址为0xd0

#include <iostream>
using namespace std;
class A //人
{
public:
   virtual void f(){
      cout << "类A的虚函数f被调用" << endl;
   }
   virtual void g(){
      cout << "类A的虚函数g被调用" << endl;
   }
   virtual void h(){
      cout << "类A的虚函数h被调用" << endl;
   }
};
class B : public A //学生
{
public:
   virtual void g(){
      cout << "类B的虚函数g被调用" << endl;
   }
   
};
​
int main()
{
   A apple;
   A* pa = &apple;
   A apple1;
   A* pa1 = &apple1;
   long* p1 = (long*)pa;
   long* vpa = (long*)(*p1);
   long* p3 = (long*)pa1;
   long* vpa1 = (long*)(*p3);
   typedef void (*Func)(void);
   Func fa = (Func)vpa[0];
   Func ga = (Func)vpa[1];
   Func ha = (Func)vpa[2];
   fa();
   ga();
   ha();
   cout<<"-------------"<<endl;
   B b1;
   B* pb = &b1;
   cout<<"sizeof(A):"<<sizeof(A)<<endl;
   cout<<"sizeof(B):"<<sizeof(B)<<endl;
   cout<<"-------------"<<endl;
   long* p2 = (long*)pb;
   long* vpb = (long*)(*p2);
   typedef void (*Func)(void);
   Func fb = (Func)vpb[0];
   Func gb = (Func)vpb[1];
   Func hb = (Func)vpb[2];
   fb();
   gb();
   hb();
   cout<<"-------------"<<endl;
   cout<<"父类的虚函数地址为:"<<vpa<<endl;
   cout<<"父类的虚函数地址为:"<<vpa1<<endl;
   cout<<"子类的虚函数地址为:"<<vpb<<endl;
   return 0;
}
​
/*
类A的虚函数f被调用
类A的虚函数g被调用
类A的虚函数h被调用
-------------
sizeof(A):8
sizeof(B):8
-------------
类A的虚函数f被调用
类B的虚函数g被调用
类A的虚函数h被调用
-------------
父类的虚函数地址为:0x557ec53a9d38
父类的虚函数地址为:0x557ec53a9d38
子类的虚函数地址为:0x557ec53a9d10
*/

class A、B的大小为8字节,存虚函数表的指针,虚函数表的地址

不同对象的虚函数地址是一样的

多继承中的虚函数表

总结

虚函数表是放在所有对象公共的院子里的,同一个对象的虚函数表指针盒子里放的都是同一个虚函数表抽屉

父类的虚函数表与子类的虚函数表的共同点是内容相同(重写的不同),但不是同一个虚函数表

同一个类的所有对象公用一个虚函数表

虚函数是带有virtual的函数,虚函数表是存放虚函数地址的指针数组,虚函数表指针指向这个数组。对象中存的是虚函数指针,不是虚函数表。

我们创建两个A对象,发现他们的虚函数指针相同,这说明他们的虚函数表属于类,不属于对象。所以虚函数表应该存在共有区。

堆?堆需要动态开辟,动态销毁,不合适。 虚函数表放在了全局数据段。

满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。 普通函数的调用是编译时就确定的。

子类已经覆盖了父类的虚函数的情况下,为什么子类还是可以调用“被覆盖”的父类的虚函数呢?

通过加作用域(正如你所尝试的),使得函数在编译时就绑定。