• 以下讨论围绕下述例子展开:
Eg:
struct point
{
float x, y, z;
};
point global;
int main()
{
point local;
point * heap = new point;
*heap = global;
delete heap;
return 0;
}
• 观念上,编译器会为 point 声明一个 trivial 默认构造函数,一个trivial 析构函数,一个 trival 复制构造函数,以及一个 trival 赋值操作符函数
• 实际上,这些 trivial member 要不就是没定义,要不就是没有调用,程序行为与 C 中表现的一样
• 与 C 中的区别为:
• 在 C 中,全局变量 global 被视为 “临时性的定义(因为没有显示的初始化操作)”
,将被放在 BSS 段中
•在 C++ 中,全局变量 global 被视为完全定义的,由于类构造行为的隐式应用,因此,C++不支持“临时性定义”
• C与C++一个区别:BSS段在 C++ 中相对不重要
*注:也即是说,在 C++ 中所有的全局对象都被以“初始过的数据”来对待
• 例子中各个变量的性质:
• global : 初始化过的对象
• local : 既没有被构造也没有被析构
• heap = new point :传回的类对象: 没有被构造
• *heap = local : 观念上,触发了 trivial 赋值操作符函数做拷贝操作;事实上,赋值操作执行纯粹的位搬移操作
• delete heap : 观念上,触发了 trivial 析构函数;事实上,未定义或调用
• 该对象会分析类 point 的声明,并为其贴上 POD 的标签,之后的构造操作如上述,执行简单的位搬移操作
• 以下围绕以下例子讨论:
Eg:
class Point
{
public:
Point(int i = 0, int j = 0, int k = 0) :x(i), y(j), z(k){}
private:
int x, y, z;
};
该类中仅定义了默认构造函数,并未定义复制构造函数,赋值操作符函数与析构函数,这些函数时 trivial的,事实上,编译器根本没有定义这些函数
• 这种情况下,其余三种成员函数将不被定义,其对应操作采用“逐比特行为”
• 考虑类成员的显示初始化式
• 显示初始化式的使用要求:
1. 没有定义构造函数
2. 全体数据成员均为 public
3. 只能指定常量
• 显示初始化式的使用例子:
Point p = {1,2,3}; //假设数据成员 x,y,z 均是 public
• 显示初始化式与构造函数的效率问题:
Eg:
void test()
{
Point local1 = {1,2,3}; //假设数据成员 x,y,z 均是 public;
Point local2; //该语句将调用默认构造函数对 local2 进行初始化
//对local2 调用默认构造函数,相当于如下的 inline expansion
local2.x = 0
local2.y = 0
local2.z = 0
}
local1 的初始化:当函数的 activation record 放进程序堆栈时,上述显示初始化列表中的常量就可以被放入 local1 的内存中
local2 的初始化:对默认构造函数进行了内联展开,对初始化列表中的每一个成员逐一进行赋值操作
*说明:显示初始化式的效率比内联构造函数的高,但其存在很多使用要求与条件。
• 在编译器层面有一个优化机制:编译器会抽取构造函数中逐成员的常量指定操作中的常量值,并对相应成员直接进行显示初始化式中的初始化方式,即直接将常量值放入对应类成员的内存中,而不是将构造函数扩展为一系列的赋值指令。
Eg:
{
Point local; //将调用默认构造函数对其进行初始化,其中初始化式中有逐成员指定常量值的操作 x(0), y(0), z(0)
}
编译器优化调整,将不再扩展构造函数,改为:
{
Point local = {0,0,0};
}
• 例子中各个变量的属性:
point global;
point test()
{
point local;
point * heap = new point;
*heap = global;
delete heap;
return local;
}
• global:执行 point::point(0,0,0); 调用默认构造函数
• local:也要执行默认构造函数
进行初始化
• heap:被附加一个对默认函数的有条件的调用操作。如下:
point * heap = new point;
if(heap != 0)
heap->point::point(0,0,0);
• *heap = global:仍然是简单的位拷贝操作
• delete heap:不会导致析构函数的调用,因为没有显示的提供一个析构函数实例,并不会合成
• return local:简单的位拷贝操作
• 围绕以下例子进行讨论:
class Point
{
public:
Point(int i = 0, int j = 0, int k = 0) :x(i), y(j), z(k){}
virtual float func();
private:
int x, y, z;
};
• 在数据成员均是用数值来存储的情况下,默认构造函数,复制构造函数,赋值操作符函数,析构函数均使用默认语义(逐比特操作)是合理的
• 加入虚成员函数后,每一个类对象要多负担一个 vptr,同时会引起对某些未定义成员函数的合成,以及对某些已定义成员函数的膨胀:
1. 默认构造函数的膨胀
自定义的默认构造函数在完成了基类构造函数的调用之后,被附加了对 vptr 进行初始化的代码,然后才是程序员提供的代码
Eg:
point::point(point * this, int i = 0, int j = 0, int z = 0) : x(i),y(j),z(k)
{
//紧跟在基类的构造函数被调用后执行以下步骤:
this->_vptr_point = __vtbl_point;
//用户定义的代码:
this->x = i;
this->y = j;
this->z = k;
return this;
}
2. 编译器需要合成复制构造函数与赋值操作符函数(因为此处类中有虚函数,为之前讨论的需要合成的情况)
• 不能对有虚函数的类采用逐比特的复制构造函数,假设调用复制构造函数的类的参数是其派生类对象,则此时派生类对象中的 vptr 指向派生类的虚函数表,此时直接逐比特复制将会导致基类的 vptr 指向了派生类的虚函数表,是一个错误的 vptr 初始化,因此,编译器需要合成一个复制构造函数。
Eg:
point* point::point(point * this, const point & rhs)
{
//直接指定 point 的虚函数表的地址为 vptr 的值,而不是使用 rhs 的 vptr 的值来初始化
this->_vptr_point = __vtbl_point;
//将 rhs 对象中的数据成员的值逐比特拷贝到 this 对象
return this;
}
• C++标准要求编译器尽量延迟 nontrival member 的实际合成操作,直到真正遇到起使用场合
• 例子中各个变量的属性:
point global;
point test()
{
point local;
point * heap = new point;
*heap = global;
delete heap;
return local;
}
• global, local, heap 的初始化操作均调用默认构造函数进行初始化,delete heap 操作也是逐比特操作(不合成析构函数)
• *heap = global:该操作将触发复制构造函数的合成
• return local:由于复制构造函数的出现,将会初始 test() 函数发生变化,激发 NRV 优化
• 若存在大量传值返回操作,则应该定义复制构造函数,因为这会触发 NRV 优化,而触发了NRV 优化后,将不再需要调用复制构造函数