1データメンバーのバインドタイミング
1.1クラスでの型と変数のバインディングのタイミング
例えば:
#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クラスメンバー変数の配置とオフセット
デフォルトでは、一部のコンパイラーは、最大のメンバーが占めるバイト数でバイト・アライメントを実行します。これにより、より多くのメモリー・スペースが必要になりますが、操作効率が向上します。
例えば:
#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単一継承でのデータメンバーの分散
例えば:
#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);
}
上記の3種類のコードは、異なるコンパイラーでコンパイルされます(Linuxで変更する必要がある場合があります)。コンパイル結果は異なる場合があります。コンパイラーによってメモリーの最適化が異なるため、一部のコンパイラーはmesmetをクラスCに直接使用できます。オブジェクトが占めるスペースはクラスBサブオブジェクトにコピーされますが、一部のコンパイラは
3.2仮想関数を持つ派生クラスは仮想関数の親クラスを継承しません
例えば:
#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このポインタオフセットによる破壊エラー
例えば:
#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 2層構造の仮想継承
例えば:
#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 3層構造の仮想継承
例えば:
#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;
}