普通成员函数
类中普通成员函数的访问方式是通过调用者的类型访问
test:该测试存在巧合:两个类中对应位置的变量类型相同
class A
{
int a;
char c;
public:
A(){cout<<"A()"<<endl;}
~A(){cout<<"~A()"<<endl;}
public:
void fun()
{
cout<<"A_fun()"<<endl;
}
};
class B:public A
{
int a;
char c;
public:
B(){cout<<"B()"<<endl;}
~B(){cout<<"~B()"<<endl;}
public:
void fun()
{
cout<<"B_fun()"<<endl;
}
};
int main(int argv,char argc[])
{
A *pa,a;
B *pb,b;
pa = &a; pa->fun();//假定此处第10行
pa = &b; pa->fun();//11
pb = &b; pb->fun();//12
pb = (B*)&a; pb->fun();//13
return 0;
}
得到结果:
第10行和12行,都是自己的指针指向自己的对象调用fun()函数,调用自身的函数,不难理解。11行用Al类指针指向了B类对象(基类指针可以指向派生类对象),调用的是A类的函数。而14行是B类指针指向了A类对象,在c++语法中是不允许的,所以我们通过强制类型转换为B*类型,pb->fun();调用的是B类函数。
原因:
可以这样理解…现在,在我们的代码区有两个fun()函数,在调用时程序将两个函数编译成了有不同参数,也就是当前对象的指针this,
A类fun函数:
fun(A * const this);
B类fun函数:
fun(B * const this);
相当于做了函数重载,来区分两个同名函数,将pa->fun();编译成fun(pa);将pb->fun();编译成fun(pb);。所以得出结论:普通成员函数的访问是通过调用者类型来访问的
在来看一下他们的同名数据如何调用…
用于测试,就不管数据的封装性了,将成员变量都写成public,做以下更改,给成员变量赋值,并将其打印
得到结果:
第10行和12行不难理解,而第11行,打印的a是A类中的。
计算对象a的内存大小为8字节,b为16字节,其中8字节继承与A类,因为pa指针只能指向8字节大小,pa=&b所得到的是内存中前面的8字节(也就是b对象中无名的A类对象内存),后面的8字节会无法保存被丢弃,所以打印的变量a是对象b中无名的A类对象的变量a
画内存图解释…
而13行,pb = (B*)&a; 打印的值却是一个未知值?是因为pb所能指的是16字节大小的内存,前面8字节保存继承下来的值(分别是10,和字符A),后面8字节保存自己的值,因为是将A强制转换为B,此时后面的8字节是没有数据的,所以打印是未知值,这也是c++语法为什么允许这样做的原因!
pb=(B*)&a;内存图如下…
虚函数的访问方式
虚函数是通过虚表访问
如果类中有虚函数,那么这个类会多出4个字节,这4个字节保存的虚表地址,也就是指向一个函数指针数组的指针,保存在内存前4个字节。虚函数可以被继承,但虚表本身不会,派生类可以重写覆盖基类虚函数。如果类中有虚函数,那么该类的析构函数需要写成虚析构,否则在堆区申请的派生类对象,delete时无法被释放。
test:
class A
{
int a;
char c;
public:
virtual void fun1()
{
cout << "A_fun1()" << endl;
}
virtual void fun2()
{
cout << "A_fun2()" << endl;
}
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
};
int main(int argv, char argc[])
{
A *pa;
B b;
pa=&b;
pa->fun1();
pa->fun2();
return 0;
}
结果为:
B类公有继承与A类,则B的虚函数表中也有fun1(),fun2()。B类中只重写了fun1()函数,从A中继承下来的fun1()函数被覆盖,fun2()没有。
通过virtual多出的4字节指针访问虚函数表
int main(int argv, char argc[])
{
typedef void(*pFun)();//函数指针类型取别名
A m;
pFun p;
for (int i = 0; i < 2; i++)
{
p = (pFun)*((int *)*(int *)(&m) + i);
p();
}
return 0;
}
得到结果:
对象的首地址的前4字节是一个指向一个函数指针数组的指针,可以通过偏移指向数组首地址进行访问虚函数。得到对象地址 &m,将这个地址看成是指向int类型的指针 (int *)&m,得到一个十六进制的整数地址,该地址保存的是函数指针数组首地址 *(int *)(&m),将这个地址看成是指向int类型的指针,然后做偏移( (int )(int *)(&m))+i,再解引用得到虚函数的首地址,将其转为函数指针(pFun),再对函数指针进行解引用(即调用虚函数),p();
模拟内存图
{虚函数按书写顺序写入虚表,类中的成员变量也是同理}