C++对象模型

C++对象模型综述

在C++中, 一个类中存在以下几种类型成员, 根据限定符的不同, 它们也会被置于不同的地方:

数据成员: 分为静态和非静态, 非静态数据成员存在于类内部, 非静态数据成员作为类的共享成员, 存在于一个全局数据块中.
函数成员: 分为静态/非静态和虚函数, 静态和非静态函数存在于函数外部, 虚函数由虚表指针指向的虚函数表管理.

1
2
3
4
5
6
7
8
9
class Object {
public:
int NonstaticFunc() { return _nonstatic_data; }
static int StaticFunc() { return static_data; }
virtual void VirtualFunc(){} // 虚函数的存在导致需要一个指向虚表的指针
private:
int _nonstatic_data; // 只有非静态数据存在于类对象模型中, 它们撑大了类
static int static_data;
};

以上 sizeof Object = sizeof int + sizeof vptr = 8.

构造函数语义 - The Semantics of Constructors

构造函数

构造函数(constructor)的任务是初始化对象的数据成员, 无论何时只要类的对象被创建, 就会执行构造函数.

所以构造函数必须存在, 但有时候我们没有定义构造函数, 对象也能被创建, 这是因为当没有显示的定义构造函数时, 在编译器需要时会隐式合成默认构造函数(default constructor).

从构造函数语义的角度来说, 类的数据成员分为两类: 一类依赖于程序手动初始化, 一类依赖于编译器自动初始化.

对于需要程序手动初始化的数据, 需要显示定义构造函数为其设定初始值, 若未定义构造函数, 那么这些数据将无法在构造期间被初始化, 其值未定义.
所以若类中仅有内建类型数据成员时, 编译器并不需要一个构造函数来做什么事, 尽管它会隐式创建一个默认构造函数, 但这类默认构造函数被看做是无用的(trivail), 对这种隐式默认构造函数大可不必放在心上.

1
2
3
4
5
class Object {
// 未定义构造函数, 编译器隐式声明默认构造函数
private:
int i; // 但i也没有机会被初始化, 其值未定义
};

我们需要关心的是有用的(nontrivial)隐式构造函数, 这类构造函数在编译器需要时被合成出来, 那么什么时候编译器需要一个默认构造函数呢?

有一下四种情况:

  1. 类的数据成员是一个自定义类型, 该类含有默认构造函数.
  2. 类继承自一个基类, 该基类含有默认构造函数.
  3. 类中存在虚函数.
  4. 类虚继承自一个虚基类.

下面分别探讨这四种情况, 为何这是编译器需要合成默认构造函数, 它在构造函数中做了什么?

类的数据成员是一个自定义类型, 该类含有默认构造函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SubObject {
public:
SubObject() = default; // 存在默认构造函数
SubObject(int i) :_i(i) {}
private:
int _i;
};

class Object {
public:
// 此时编译器合成一个默认构造函数, 因为构造_subobject是编译器的责任
// 合成的默认构造函数可能如下:
// Object() {
// _subobject.SubObject::SubObject();
// }
private:
SubObject _subobject;
int _i; // 但i任需要由程序显示初始化, 它是程序员的责任
};

但如果Object中已定义默认构造函数, 那么编译器将会扩张所需要的操作到程序定义的默认构造函数中:

1
2
3
4
5
6
7
8
9
class Object {
public:
Object():_i(0) {
// _subobject.SubObject::SubObject(); 此处由编译器扩张默认构造函数而来
}
private:
SubObject _subobject;
int _i;
};

若Object包含多个用户定义的对象, 那么编译器将按照成员声明的顺序, 来扩张所需要的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Object {
public:
Object():_i(0) {
// _subobject.SubObject::SubObject();
// _subobject2.SubObject2::SubObject2();
// _subobject3.SubObject3::SubObject3(); 在默认构造函数中按顺序扩充操作
// _i将按顺序在这时被初始化!
}
private:
SubObject _subobject;
SubObject2 _subobject2; // 假设存在SubObject2, 类似SubObject
SubObject3 _subobject3; // 假设存在SubObject3, 类似SubObject
int _i;
};

类继承自一个基类, 该基类含有默认构造函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
Base() :_i(0) {} // 基类默认构造函数
Base(int i): _i(i) {}

protected:
int _i;
};

class Derived: public Base {
public:
// Derived() : Base(){
// 编译器大概会为Base创建这样的默认构造函数, 来调用基类的默认构造函数
// }
};

类中存在虚函数.

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual void VitrualFunc() {}

protected:
int _i;
};
class Derived : public Base {
// Derived() : Base() {
// 构造出来vptr -> vtbl, vtbl用来管理虚函数的地址
// }
};

类虚继承自一个虚基类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
protected:
int _i;
};
class Derived1_1 : virtual public Base {

};
class Derived1_2 : virtual public Base {

};
class Derived2 : public Derived1_1, public Derived1_2 {
public:
// Derived2() : Base(), Derived1_1(), Derived1_2() {
// ... 生成一个间接指针, 来确定_i的位置
// }
};

拷贝构造函数

拷贝构造函数肯能在以下三种情况下发生: 1 显示的为另一个对象赋值; 2 作为参数; 3 作为返回值.

如果不存在拷贝构造函数, 编译器将会合成一个默认的拷贝构造函数, 同样的根据编译器是否需要一个拷贝构造函数, 将这个合成的拷贝构造函数分为无用的(trivial)和需要的(nontrivial), 其判断依据为拷贝的行为是深拷贝(书中所描述为memberwise copy semantics)还是浅拷贝(bitwise copy semantics), 浅拷贝被认为是无用的.

什么时候一个类合成的默认拷贝函数不会采用浅拷贝? 有以下四种情况:

  1. 类存在数据成员是一个自定义类型, 并且该类型显示定义了一个拷贝构造函数(无论该拷贝函数是显示定义还是隐式合成).
  2. 类继承自一个基类, 基类显示定义了一个拷贝构造函数(无论该拷贝函数是显示定义还是隐式合成).
  3. 类中存在虚函数.
  4. 类虚继承自虚基类.

前两种情况的缘由和构造函数中编译器为其合成默认构造函数的缘由相似.
而后两种则有所不同:

对于类中存在虚函数不会采用浅拷贝的原因是:

浅拷贝vptr会怎样? 这必定是一个错误, 编译器需要在合成的拷贝构造函数中合理的重新设置vptr, 并且将虚函数的地址置入vptr指向的vtbl中.

对于类虚继承自虚基类不会采用浅拷贝的原因是:

程序转化

显示的初始化操作:

1
2
3
4
Object obj;
Object obj1(obj);
Object obj2 = obj;
Object obj3 = Object();

=>

1
2
3
4
5
6
7
8
Object obj;

Object obj1;
obj1.Object::Object(obj);
Object obj2;
obj2.Object::Object(obj);
Object obj3; // 重新写一份定义, 其中初始化阶段会被剥除.
obj3.Object::Object(obj); // 进行一次拷贝构造

参数的初始化:

1
2
3
void func(Object arg_obj){

}

=>

1
2
3
4
5
6
Object tmp_obj;
tmp_obj.Object::Object(arg_obj); // 进行一次拷贝构造
void func(Object& ref_arg_obj){
// 原函数的参数转化为带引用的参数
}
func(tmp_obj);

返回值的初始化:

1
2
3
4
Object func(){
Object obj;
return obj;
}

=>

1
2
3
4
5
6
void func(Object& result){ // 将返回值转化为通过引用参数返回
Object obj;
obj.Object::Object();
result.Object::Object(obj); // 进行一次拷贝构造
return;
}

成员的初始化队伍

构造函数初始化列表:

在以下情况下需要使用初始化列表对数据成员进行初始化:

  1. 初始化一个ref成员.
  2. 初始化一个const成员.
  3. 调用基类的构造函数, 而它拥有一组参数时.
  4. 调用一个成员的构造函数, 而它拥有一组参数时.

注意:
在构造函数体内的初始化, 实际上不是真正意义上的初始化, 而是产生一个临时对象, 在将该对象拷贝给数据成员.
构造函数初始化列表的顺序没有意义, 初始化的顺序是按照成员的声明顺序进行的. 构造函数初始化列表的操作会被转化到构造函数块内, 它们的执行顺序在用户所写的代码之前.

Data语义学 - The Semantics of Data

Class布局