持续创作,加速成长!这是我参与「掘金日新计划 · 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对象,发现他们的虚函数指针相同,这说明他们的虚函数表属于类,不属于对象。所以虚函数表应该存在共有区。
堆?堆需要动态开辟,动态销毁,不合适。 虚函数表放在了全局数据段。
满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。 普通函数的调用是编译时就确定的。
子类已经覆盖了父类的虚函数的情况下,为什么子类还是可以调用“被覆盖”的父类的虚函数呢?
通过加作用域(正如你所尝试的),使得函数在编译时就绑定。