静态绑定
绑定指的是函数的调用
看下面的例子
class Base
{
public:
Base(int data = 10) :ma(data) {}
virtual void show() { cout << "Base::show()" << endl; }
virtual void show(int) { cout << "Base::show(int)" << endl; }
~Base() { cout << "~Base()" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20)
:Base(data), mb(data)
{}
void show() { cout << "Derive::show()" << endl; }
private:
int mb;
};
int main()
{
Derive d(50);
Base* pb = &d;
pb->show(); // 动态绑定
pb->show(10); // 动态绑定
cout << sizeof(Base) << endl; // 4
cout << sizeof(Derive) << endl; // 8
cout << typeid(pb).name() << endl; // class Base*
cout << typeid(*pb).name() << endl; // class Derive RTTI类型指针 --》指向类型字符串
return 0;
}
结果图:
虚函数
一个类添加了虚函数,对这个类有什么影响?
总结一:
- 如果类里面定义了虚函数,那么编译阶段,编译器会给这个类类型产生一个唯一的vftable虚函数表。虚函数表中主要存储的内容是RTTI指针(指向类型信息–>类型字符串)和虚函数地址;
- 当程序运行时,每一张虚函数表都会加载到内存的.rodata(只读数据区)区
总结二:
- 一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针指向对应类型的虚函数表vftable;
- 一个类型定义的n个对象,它们的vfptr指向的都是同一张虚函数表
总结三:
- 一个类里面虚函数的个数,不影响对象内存大小(vfptr),影响的是虚函数表的大小
总结四:
- 如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,就会自动被处理成虚函数,这两个函数也就是覆盖关系
虚函数表中哪个虚函数地址先放和虚函数的定义顺序有关
看上面两张图
虚析构函数
- 哪些函数不能实现成虚函数?
虚函数依赖:
1.虚函数能产生地址,存储在vftable当中
2.调用时,对象必须存在(vfptr->vftable->虚函数地址,而vfptr存储在对象的内存空间中),即依赖对象来调用构造函数
1.virtual+构造函数(不可以)
2.构造函数中(调用的任何函数,都是静态绑定的)调用虚函数,也不会发生静态绑定派生类对象的构造过程,先调用的是基类的构造函数,然后才调用派生类的构造函数
static静态成员方法(不可以)
静态成员方法不依赖对象
- 虚析构函数(析构函数调用时,对象存在!)
什么时候基类的析构函数必须实现成虚函数?
基类的指针(引用)指向堆上new出来的派生类对象的时候, 执行delete pb(基类的指针),它调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用,堆上的内存无法释放。
(基类指针如果指向栈上的派生类对象,那么基类的析构函数不用设置成虚函数,出函数作用域时,会自动调用派生类的析构函数和基类的析构函数)
需要注意一点:如果基类的析构函数是虚函数,那么其派生类的析构函数也会被自动处理成虚函数,并且在派生类的虚函数表中派生类的虚析构函数的地址(& Derive::~!Derive)将会覆盖由基类继承来的虚析构函数的地址(& Base::~Base)
class Base
{
public:
Base(int data = 10) :ma(data) { cout << "Base(int data = 10)" << endl; }
virtual void show() { cout << "Base::show()" << endl; }
//virtual ~Base() { cout << "~Base()" << endl; }
~Base() { cout << "~Base()" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20)
:Base(data)
,mb(data)
,ptr(new int(data))
{
cout << "Derive(int data = 20)" << endl;
}
void show() { cout << "Derive::show()" << endl; }
// 基类的析构函数是虚函数,那么派生类的析构函数自动成为虚函数
~Derive()
{
delete ptr;
ptr = nullptr;
cout << "~Derive()" << endl;
}
private:
int mb;
int* ptr;
};
int main()
{
Base* pb = new Derive(10);
pb->show(); // 动态绑定
delete pb; // 调用析构函数时是静态绑定
return 0;
}
可以看到,如果基类的析构函数没有设置成虚函数,这里派生类析构函数也是普通函数,那么在执行delete pb,调用对象析构函数时,发生的是静态绑定,即编译器看到pb是Base* 类型,就去看Base::~Base(),发现它是普通函数,于是直接进行静态绑定call Base::~Base(),而不会调用Derive的析构函数,这样堆上开辟的空间就无法被释放。
如果将基类的析构函数设置成虚函数,那么派生类的析构函数自动成为虚函数,并且在派生类的虚函数表中派生类的虚析构函数的地址(& Derive::~!Derive)将会覆盖由基类继承来的虚析构函数的地址(& Base::~Base),那么当执行delete pb的时候就会发生动态绑定,即编译器看到pb是Base* 类型,就去看Base::~Base(),发现它是虚函数,就会去看pb指向的内存的前4个字节(vfptr),根据vfptr去找到Derive类的虚函数表,根据表中的虚析构函数地址 (&Derive::~Derive())去执行相应的析构函数。
上图所示的两种覆盖方式,导致Derive类虚函数表中的虚函数地址的顺序不同
再谈动态绑定
- **是不是虚函数的调用一定就是动态绑定? **
解答:不是的。
(1)在类的构造函数中调用虚函数,也是静态绑定(构造函数就算调用其他函数虚函数,也不会发生动态绑定)
(2)必须由指针(引用)调用虚函数
class Base
{
public:
Base(int data = 10) :ma(data) { cout << "Base(int data = 10)" << endl; }
virtual void show() { cout << "Base::show()" << endl; }
virtual ~Base() { cout << "~Base()" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20)
:Base(data), mb(data)
{
cout << "Derive(int data = 20)" << endl;
}
void show() { cout << "Derive::show()" << endl; }
// 基类的析构函数是虚函数,那么派生类的析构函数自动成为虚函数
~Derive()
{
cout << "~Derive()" << endl;
}
private:
int mb;
};
- 通过对象本身调用虚函数,发生的是静态绑定
int main()
{
Base b;
Derive d;
#if 0
b.show();
d.show();
// 对象本身调用虚函数,是静态绑定!!,因为没有必要动态绑定,通过对象调用访问的一定是自己的虚函数表
/* 静态绑定
b.show();
00072B53 lea ecx,[b]
00072B56 call Base::show (071037h)
d.show();
00072B5B lea ecx,[d]
00072B5E call Derive::show (071073h)
*/
return 0;
}
- 通过指针或者引用变量来调用虚函数才会发生动态绑定!!
// 动态绑定
Base* rb1 = &b;
rb1->show();
Base* rb2 = &d;
rb2->show();
// 动态绑定(通过引用变量调用虚函数)
Base& rb3 = b;
rb3.show();
Base& rb4 = d;
rb4.show();
// 动态绑定(虚函数通过指针或者引用变量调用,才发生动态绑定)
Derive* pd1 = &d;
pd1->show();
Derive& rd1 = d;
rd1.show();