1.单纯的类
比较简单的类,即不包含虚函数和虚基类的类
代码:
#include <iostream>
#include <time.h >
using namespace std;
class X
{
public:
int x;
int y;
int z;
X()
{
memset(this, 0, sizeof(X));
cout << "构造函数被执行" << endl;
}
X(const X &tm)
{
memcpy(this, &tm, sizeof(X));
cout << "拷贝构造函数被执行" << endl;
}
};
int main()
{
X x0; //调用构造函数
x0.x = 100;
x0.y = 200;
x0.z = 300;
X x1(x0); //调用拷贝构造函数
cout << "x1.x=" << x1.x << " x1.y=" << x1.y << " x1.z=" << x1.z << endl;
}
构造函数使用memset初始化当前对象的值
拷贝函数使用这种memcpy将形参对象的内存直接拷贝到本对象所在内存当中来
使用上述写法构造对象和拷贝构造对象可以提高效率
但是上述写法只适合这种单纯的类
如果类并不单纯,那么在构造函数中使用如上所示的memset或者拷贝构造函数中使用如上所示的memcpy方法,那么就会出现程序崩溃的情形;
某些情况下,编译器会往类内部增加一些我们看不见 但真实存在的成员变量(隐藏成员变量),有了这种变量的类,就不单纯了;同时,这种隐藏的成员变量的增加(使用) 或者赋值的时机,往往都是在 执行构造函数或者拷贝构造函数的函数体之前进行。
那么你如果使用memset,memcpy,很可能把编译器给隐藏变量的值你就给清空了,要么覆盖了;
比如你类中增加了虚函数,系统默认往类对象中增加虚函数表指针,这个虚函数表指针就是隐藏的成员变量。
2.含有虚函数或虚基类的不单纯的类
代码1:
#include <iostream>
using namespace std;
class X
{
public:
int x;
int y;
int z;
X()
{
//编译器角度 伪码;
//vptr = vtbl; //下边的memset会把vptr(虚函数表指针)清0
memset(this, 0, sizeof(X));
cout << "构造函数被执行" << endl;
}
X(const X &tm)
{
memcpy(this, &tm, sizeof(X));
cout << "拷贝构造函数被执行" << endl;
}
virtual ~X()
{
cout << "析构函数被执行" << endl;
}
virtual void virfunc()
{
cout << "虚函数virfunc()被执行" << endl;
}
void ptfunc()
{
cout << "普通函数ptfunc()被执行" << endl;
}
};
int main()
{
int i = 9;
printf("i的地址 = %p\n", &i);
X x0;
printf("ptfunc()的地址=%p\n", &X::ptfunc); //打印正常的成员函数地址。
//long *pvptrpar = (long *)(&x0);
//long *vptrpar = (long *)(*pvptrpar);
//printf("virfunc的地址 = %p\n", vptrpar[1]);//虚函数virfunc地址
x0.ptfunc();
x0.virfunc();
}
反汇编代码:
运行结果:
第一次:
第二次:
可以发现两次执行对于普通函数的调用地址都相同,而变量i的地址不同
另外虚函数表指针被memset清空为0,但虚函数表指针为null居然可以成功调用虚函数
解释:
虚函数,主要解决的问题是父类指针或引用指向子类对象这种情况
静态联编 :我们编译的时候就能确定调用哪个函数。把调用语句和被调用函数绑定到一起;
动态联编:是在程序运行时,根据时机情况,动态的把调用语句和被调用函数绑定到一起,动态联编一般旨有在多态和虚函数情况下才存在。
可以发现代码1中x0.ptfunc()和x0.virfunc()都是在编译的时候就确定好了函数的调用地址,属于静态联编,不是多态,此时并没有通过虚函数表调用虚函数
所以这种调用情况下虚函数的调用跟普通函数的调用基本没什么区别
代码2:
#include <iostream>
using namespace std;
class X
{
public:
int x;
int y;
int z;
X()
{
//编译器角度 伪码;
//vptr = vtbl; //下边的memset会把vptr(虚函数表指针)清0
memset(this, 0, sizeof(X));
cout << "构造函数被执行" << endl;
}
X(const X &tm)
{
memcpy(this, &tm, sizeof(X));
cout << "拷贝构造函数被执行" << endl;
}
virtual ~X()
{
cout << "析构函数被执行" << endl;
}
virtual void virfunc()
{
cout << "虚函数virfunc()被执行" << endl;
}
void ptfunc()
{
cout << "普通函数ptfunc()被执行" << endl;
}
};
int main()
{
X *pX0 = new X();
pX0->ptfunc();//正常调用
//pX0->virfunc();//无法正常调用
X x0;
X &xy = *pX0;
xy.virfunc();//无法正常调用
X &xy2 = x0;
xy2.virfunc();//无法正常调用
delete pX0; //无法正常调用
}
反汇编代码:
运行结果:
使用指针或引用调用虚函数时变得无法正常执行了虚函数的调用,报出空指针异常
解释:
使用指针或引用调用虚函数时,编译器会帮我们通过虚函数表调用虚函数,属于动态联编,因为memset将虚函数表指针置0,所以出现空指针异常
虚函数、多态的概念是专门针对指针和引用的
多态的表现方式就是通过虚函数表指针拿到虚函数表,来调用虚函数,一般多态是要基于继承关系的,即父类指针指向子类对象或父类引用指向子类对象,而这里无继承的情况下通过指针和引用调用虚函数也属于多态的范畴,因为都是通过虚函数表指针拿到虚函数表,来调用虚函数