1 数据成员绑定时机
1.1 类中类型及变量绑定时机
EG:
#include <iostream>
using namespace std;
string c; //全局变量c
typedef string mytype;
class A
{
public:
mytype d; //mytype被解释为string类型
int test(mytype a) //成员参数中的mytype被解释成全局的mytype(string类型)
{
mytype b; //函数中的mytype被解释为int类型
return c;
}
public:
int c; //成员变量c
typedef int mytype;
mytype e; //mytype被解释为int类型
};
int main()
{
return 0;
}
建议:建议如果类中有typedef类型重命名放在类的开头,避免和外部类型重命名混淆
2 类成员变量的对齐及偏移
2.1 类成员变量的对齐及偏移
默认情况下一些编译器会以最大成员占用的字节数进行字节对齐,这样做虽然需要消耗更多的内存空间,但是可以提高运行效率
EG:
#include <iostream>
using namespace std;
string c; //全局变量c
typedef string mytype;
#pragma pack(1) //开始一字节对齐
class A
{
public:
int a;
double b;
char c;
};
#pragma pack() //恢复默认对齐方式
class B
{
public:
int a;
double b;
char c;
virtual void func(){}
};
int main()
{
//使用字节对齐
cout << "使用一字节对齐" << endl;
cout << "A类在一字节对齐下的大小:" << sizeof(A) << endl;
printf("A中a成员变量的偏移值%d\n", &A::a); //使用这种方式打印的是类成员变量的偏移量
printf("A中b成员变量的偏移值%d\n", &A::b);
printf("A中c成员变量的偏移值%d\n\n\n", &A::c);
//使用默认字节对齐,不同的编译器可能不一样,这里是VS2017,类中默认以占用字节数最大的成员变量对齐
cout << "使用默认对齐" << endl;
cout << "B类在默认对齐方式下的大小:" << sizeof(B) << endl;
printf("B中a成员变量的偏移值%d\n", &B::a);
printf("B中b成员变量的偏移值%d\n", &B::b);
printf("C中c成员变量的偏移值%d\n", &B::c);
return 0;
}
3 继承关系下的数据成员分布
3.1 单一继承下的数据成员分布
EG:
#include<iostream>
using namespace std;
class A
{
public:
int a;
char b;
};
class B:public A
{
public:
char c;
};
class C:public B
{
public:
char d;
};
int main()
{
cout << "A B C类的大小" << endl;
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
cout << "C类数据成员偏移量" << endl;
printf("C::a成员 %d\n", &C::a);
printf("C::b成员 %d\n", &C::b);
printf("C::c成员 %d\n", &C::c);
printf("C::d成员 %d\n", &C::d);
}
以上代码三个类在不同的编译器下(在Linux下可能需要修改)编译的结果可能不一样,由于不同的编译器对内存的优化不一样,有的编译器可以直接使用 mesmet 将C类子对象占用的空间复制给B类子对象,但是有的编译器不可以
3.2 一个有虚函数的派生类继承没有虚函数父类
EG:
#include<iostream>
using namespace std;
class A
{
public:
int a;
};
class B:public A
{
public:
virtual void func(){}
int b;
};
int main()
{
cout << "B类的大小" << sizeof(B) << endl;
printf("成员a的偏移量:%d\n", &B::a); //在VS2017中打印0
printf("成员b的偏移量:%d\n", &B::b);
B b;
//在这一行打断点,调试,打开内存,查看b处内存变化情况
b.a = 1;
b.b = 2;
return 0;
}
根据打印的偏移量的值,我们或许会认为在B类中,a成员变量在对象占用内存空间的最前面,其实不是,我们在调试状态下打开VS2017的内存窗口监视b实例的成员赋值情况发现vfptr还是在对象占用内存空间的最前面(至于这里为什莫不能但看偏移量,因为a成员是A类中的,打印a的偏移量是相对A类来计算的,这点需要注意一下)
3.3 this指针偏移引起的析构错误
EG:
#include<iostream>
using namespace std;
class A
{
public:
virtual void funcA()
{
cout << "funcA函数" << endl;
}
};
class B
{
public:
virtual void funcB()
{
cout << "funcB函数" << endl;
}
};
class C :public A, public B
{
public:
};
int main()
{
//B* b = new C();
//delete b; //如果上一行代码没有注释,那么这一行代码会报错,因为new C 返回给b的不是首地址,而是this偏移后的地址
A *a = new C();
delete a; //这行不会报错,因为new C返回给a的就是首地址,子类多继承this指针和第一个基类this指针指向的地址相同
return 0;
}
总结:不管多继承还是单继承,或者有没有虚函数等等,成员变量及虚函数指针的分布需要具体分析,不用强行记忆,而且同样的继承关系在不同平台的编译器也有可能会出现不同的情况
4 虚基类
4.1 虚基类的提出
EG:(没有使用虚继承)
#include<iostream>
using namespace std;
class A
{
public:
int a;
A(int item)
{
cout << "A的构造函数初始化" << endl;
}
};
class B1 :public A
{
public:
B1():A(3)
{
}
};
class B2:public A
{
public:
B2() :A(3)
{
}
};
class C :public B1, public B2
{
};
int main()
{
C c;
//菱形继承带来的内存重复消耗
cout << "C的大小" << sizeof(c) << endl;
//菱形继承带来的二义性问题
//c.a = 3; //二义性
c.B1::a = 3;
c.B2::a = 3;
//由于A没有无参构造函数,所以需要由其直接子类在初始化列表进行初始化(如果在C的初始化列表中进行初始化,会报错--不允许间接访问非虚拟基类)
return 0;
}
EG:(使用虚继承)
#include<iostream>
using namespace std;
class A
{
public:
int a;
A(int item)
{
cout << "A的构造函数初始化" << endl;
}
};
class B1:virtual public A
{
public:
B1():A(3)
{
}
};
class B2:virtual public A
{
public:
B2() :A(3)
{
}
};
class C :public B1, public B2
{
public:
C():A(3)
{
}
};
int main()
{
C c;
cout << "C的大小:" << sizeof(C) << endl;
c.a = 9; //不会出现二义性问题
//由于A类没有默认的无参构造函数,又由于引入了虚继承,所以如果C类没有使用初始化列表对A类进行初始化,那么会报错
//这里或许会有疑问,为什莫在B1和B2中也要使用初始化列表对A进行初始化,因为B1和B2也是A的子类,如果我们实例化一个B1和B2的子类那么必须这样做
return 0;
}
4.2 两层结构的虚继承
EG:
#include<iostream>
using namespace std;
class A1
{
public:
int a1;
};
class A2
{
public:
int a2;
};
class A3
{
public:
int a3;
};
class B:virtual public A1,virtual public A2,public A3
{
public:
int b;
};
int main()
{
cout << sizeof(B) << endl; //打印20
//以下代码在调试状态下查看内存布局
B b;
b.a1 = 1; //占用b的 13~16字节
b.a2 = 2; //占用b的 17~20字节
b.a3 = 3; //占用b的 1~4字节
b.b = 5; //占用b的 9~12字节
//根据上述代码推断出:b中含有一个vbptr(虚基类指针)vbtbl(虚基类表),占用b的5~8个字节
//虚基类表中的前四个字节,是虚基类指针相对于对象首地址的偏移(小于等于0),从第四个字节后开始没四个字节一个单位(每继承一个虚基类虚基类表中就多四个字节,用来标识对象中虚基类相对于虚基类指针的偏移量)
return 0;
}
4.3 三层结构的虚继承
EG:
#include<iostream>
using namespace std;
class A
{
public:
int a;
};
class B1 :virtual public A
{
public:
int b1;
};
class B2 :virtual public A
{
public:
int b2;
};
class C :public B1, public B2
{
public:
int c;
};
int main()
{
cout << sizeof(C) << endl; //打印 24
//以下代码调试内存分析
C c;
c.a = 1; //占用 c的21~24字节
c.b1 = 2; //占用 c的5~8字节
c.b2 = 3; //占用 c的13~16字节
c.c = 5; //占用 c的17~20字节
//c继承自B1的虚基类指针占用c的1~4字节
//c继承自B2的虚基类指针占用c的9~12字节
//查看反汇编代码可以看出,调用虚基类成员需要更多的时间消耗
//对于以上代码,由于继承自B1的虚基类指针指向的虚基类表中保存有虚基类成员的偏移量,所以使用虚基类中的成员时,没有用到继承自B2的虚基类指针,但是不代表继承子B2的虚基类指针没有用
//在以下代码中,继承自B2的虚基类指针发挥了作用
B2 b2 = c;
b2.a = 1; //通过查看继承自B2的虚基类指针指向的虚基类表查看虚基类中成员a的偏移量从而访问
return 0;
}