【深度探索C++对象模型】(2)构造函数语意学

1.Default Constructor的构造操作

默认构造函数是在编译器需要的时候构建出来的,被合成的默认构造函数只执行编译器所需的动作。被合成的默认构造函数中只有base class subobjects以及member class objects会被初始化,而其他nonstatic data member(如整数、指针、数组)都不会初始化,因为他们是满足程序需要的。
有四种情况会使得编译器为未声明constructor的classes合成一个满足编译器需要的implicit nontrivial default constructor。对于其他情况又没有声明任何constructor的,实际上并不会合成。

1.1 带有Default Constructor的Member Class Object(一个类的成员类具有默认构造函数。)

若一个class没有constructor但有member object,而这些member object有default constructor,那么编译器就会为该class合成一个inline的default constructor,合成的constructor将会按照member class object声明顺序调用其member object。

class Foo {
public:
	Foo(){ /* ...*/ };
	Foo(int i){ /* ... */ };
}
class Bar{
public:
	Foo foo;
	char *str;
}

//Bar合成的default 构造函数很有可能是
inline Bar::Bar(){
	//Bar::str的初始化是程序员的责任
	foo.Foo::Foo();
}

由于合成的默认构造函数并不会初始化其他nonstatic data member(如整数、指针、数组),所以需要程序员来进行初始化操作,而编译器可以对程序员写的每个构造函数进行扩张,使其满足编译器需求。

//假如我们写一个默认构造函数
inline Bar::Bar(){
	str = 0;
}
//它会被扩张成
inline Bar::Bar(){
	foo.Foo::Foo();
	str = 0;
}

假如我们不要调用Member Class Object的默认构造函数,我们可以这样:

inline Bar::Bar() : foo( 1024 )  {//这里并不会改变扩张后的调用顺序
	str = 0;
}
//他会被扩张成
inline Bar::Bar(){
	foo.Foo::Foo( 1024 );
	str = 0;
}

1.2 带有Default Constructor的Base Class(一个派生类的基类具有默认构造函数。)

与1.1类似,若一个没有默认构造函数的class派生自一个有默认构造函数的base class,那么它将按照base classes的声明顺序调用上一层base classes的default constructor,也与1.1一样可以对每个constructor进行扩张操作。
1.2的操作将优先于1.1的操作进行。

1.3 带有一个Virtual Function的class(带有虚函数的类。 )

若class声明或继承一个virtual function,编译器会在编译期间合成default constructor或扩张所有的构造函数进行以下操作:

  1. 产生一个virtual function table (即vtbl),内放class的virtual functions地址。
  2. 在每个class object中合成一个额外的pointer member (即vptr),指向相关class vtbl地址。
    同时虚函数的虚拟调用操作将会重新改写为使用vptr和vtbl的条目。
class Widget {
public:
    virtual void flip() = 0;    //pure virtual
};
class Bell : public Widget{};
class Whistle : public Widget{};
void flip(const Widget& widget) { 	
	widget.flip(); //将被改写为
	//(*widget.vptr[ 1 ] )( &widget);
}
void foo(){
    Bell b;
    Whistle w;
    flip(b);
    filp(w);
}

1.4 带有Virtual Base Class 的 class(一个派生类,该派生类的继承体系中含有虚基类(虚继承)。)

必须使virtual base class在其每一个derived class object中的位置,可以在执行期准备妥当。
可以使编译器在class object构造使其安插一个指向虚基类的指针_vbcX,然后所有仅有reference或pointer来存取virtual base class的操作都可以通过相关指针完成。

class X { public: int i; }
class A : public virtual X { public: int j; }
class B : public virtual X { public: double d; }
class C : public A, public B { public : int k; }
//无法在编译期决定pa->X::i的位置
void foo(const A* pa) { pa->i = 1024; }
//可能会被编译器转变成
void foo(const A* pa) { pa->_vbcX->i = 1024; }
main{
    foo(new A);
    foo(new C);
}

1.5 一些误区的实际情况

  1. 并不是任何class没有定义default constructor就一定会合成一个出来。
  2. 合成出的default constructor也并不会设定class内每一个data member的默认值。

2 Copy Constructor的构造操作

有三种情况会使用copy constructor:

  1. 显式的对一个object做一个初始化操作.
  2. object通过传值交给函数。
  3. 函数返回一个非引用class object。

2.1 Default Memberwise Initialization(默认成员逐一初始化)

如果一个class没有提供任何的copy construct,那么在进行拷贝构造是,class内部是以default memberwise initialization的手法完成的。实际上就是bitwise copies(位逐次拷贝),即把class object中的所有data members按顺序一个一个拷贝到另一个object身上,如果有data members是类类型,那么就会递归地施行bitwise copies。

class String{
public:
	//.......无copy constructor
private:
	char *str;
	int len;
};

class Word {
public:
    Word(int i, String s) : _occurs(i), _word(s) {}
private:
    int _occurs;
    String _word;   //String object称为一个data member
};
Word word1(2, "word");
Word word2 = word1;

那么最后一行很有可能会是这样的,这不是copy constructor,而是default memberwise initialization!

word2._occurs = word1._occurs;
//word2._word = word1._word;
//递归展开
word2._word.str = word1._word.str;
word2._word.len = word1._word.len;

2.2 不要 Bitwise Copy Semantics

C++ Standard说若class没有声明一个copy constructor就会有隐式的声明或定义。实际上只有nontrivial的实力才会被合成在程序中,即只有class不展示出bitwise copy semantics的时候.例如

class String{
public:
	String( const char* );
	String( const String& );
private:
	char *str;
	int len;
};

class Word {
public:
    Word(int i, String s) : cnt(i), str(s) {}
private:
    int cnt;
    String str;   //String object称为一个data member
};

这种情况中Word没有展示出bitwise copy semantics,故会生成copy constructor:

inline Word::Word( const Word& wd){
	str.String::String( wd.str );
	cnt = wd.cnt;
}

因此,一个类的copy语意有三种情况:
- 存在copy constructor,使用copy constructor;
- 下面上面的四种情况,编译器帮助合成copy constructor;
- bitwise copy;

有四种情况表示class不展示出bitwise copy semantics:

  1. 当class中含有一个声明有copy constructor的member object时。(无论是显式还是被合成)
  2. 当class继承自有一个声明有copy constructor的base object时。(无论是显式还是被合成)
  3. 当class声明了virtual functions。
  4. 当class派生自一个继承链串,其中有virtual base classes时。

第一种和第二种情况不必多说,下面主要说第三第四种情况,当编译器导入一个vptr到class的时候,该class就不再展现bitwise semantics了。

2.2.1 重新设定Virtual Table的指针

现在,编译器需要合成一个copy constructor以求将vptr适当的初始化。假设有这样的继承关系:

class ZooAnimal {
public:
    ZooAnimal();
    virtual ~ZooAnimal();
    virtual void draw();
    virtual void animate();
};
class Bear : public ZooAnimal {
public:
    Bear();
    virtual ~Bear();
    void draw();    //virtual function
    void animate();
    virtual void dance();
};

当一个class object以其class的另一个class为初始值时,种情况都可以直接靠“bitwise copy semantics”来完成(除了pointer member)。、例如一个ZooAnimal class object以另一个ZooAnimal class object为初值或者Bear class object以另一个Bear class object为初值:

Bear yogi;
Bear winnie = yogi;

相同 class 的 objects 的 vtbl 相同。在constructor中,yogi的vptr被设定指向Bear class的virtual table。
在这里插入图片描述

当一个base class object以其derived class的object为初值时,合成的显式构造函数会设定object的vptr指向base class的virtual table,而不是之前从右手边的object拷贝。

void draw( const ZooAnimal& zoey ) { zoey.draw(); }
void foo(){
	ZooAnimal franny = yogi;//发生切割,franny的vptr指向ZooAnimate的vtbl而非Bear的
	draw(yogi);      //调用Bear::draw()
	draw(franny);    //调用ZooAnimate::draw()

2.2.2 处理Virtual Base Class Subobject

virtual base class需要特别处理,编译期必须让处于derived class object中的virtual base class subobject的位置在执行期就准备妥当。
假如有这样的继承关系:
在这里插入图片描述

class Raccoon : public virtual ZooAnimal {
public:
    Raccoon() {}
    Raccoon(int val) {}
};
class RedPanda : public Raccoon {
public:
    RedPanda() {}
    RedPanda(int val) {}
};

如果一个class object以其derived classes的某个object为初值(例如一个Raccoon object作为另一个Raccoon object的初值),“bitwise copy”就足够了。

Raccoon rocky;
Raccoon little_critter = rocky;

但是如果以derived object作为base object的初值,如以RedPanda object作为Raccoon object的初值,编译器必须判断**“能否正常执行存取ZooAnimal的subobject的动作”(进行切割),这种情况下编译器必须合成一个copy constructor,安插一些代码以设定virtual base class pointer/offset的初值**,对每个members执行必要的memberwise初始化以及执行其他的内存相关工作。

RedPanda little_red;
Raccoon little_critter = little_red;

在这里插入图片描述

对指针而言,“bitwise copy”可能够用,也可能不够用,因为编译器无法知道一个base class指针是否指向一个真正的base class object,或是指向一个derived class object。

3.程序转化语意学

#include <iostream>
using namespace  std;
//加载头文件
#include "X.h"
X foo(){
	X x_1;
	//对对象x_1进行处理的相关操作。
	return x_1;
}

两种正常假设:

  1. 每调用一次foo()函数,会返回一个对象x_1的值。
  2. .应该会调用类中的拷贝构造函数。

两个假设的正确性需要参看类X中的定义。

3.1 显式的初始化操作(Explicit Initialization)

若有下面程序

void foo_bar(){
	X x1(x0);
	X x2 = x0;
	X x3 = X(x0);
}

上面三种初始化操作显式的用x0初始化三个对象。但是在实际的编译器中可能会发生如下的转换。

  1. 重写定义,其中的初始化操作会被剥离 。
  2. 调用相关的拷贝构造函数。

转化后的代码有可能如下

void foo_bar(){
	X x1;  
	X x2;
	X x3;
	x1.X::X(x0); //调用拷贝构造函数。
	x2.X::X(x0);
	x3.X::X(x0);
	//可能在类X中会有类似的声明:
	//X::X(const X&);
}

3.2 参数的初始化

当将一个class object作为参数以传值方式给一个函数或作为一个非引用返回值,将会导入临时对象策略,调用copy constructor将它初始化,然后将临时对象交给对象(或返回),同时根据需要将参数或返回值改为引用。假如有代码

void foo( X x0 );
X xx;
foo(xx);

可能会转换如下:

//第一步
void foo( X& x0 );
X xx;
//第二步
X __temp0;
__temp0.X::X( xx );
foo( __temp0 );
//第三步
__temp0.X::~X();

3.3返回值的初始化

对于返回值做两阶段的转化:

  1. 加入一个类型为class object的引用的额外参数作为返回值。
  2. 在return前安插一个copy constructor。将要返回的结果拷贝给引用的额外参数。(故要求一定要有拷贝构造函数才启动)

假如有代码:

X bar(){
    X xx;
    return xx;
}
//1
X xx = bar();
//2
bar().mem_func();
//3
X (*pf)();
pf = bar();

很有可能改写如下:

void bar(X& __result){
    X xx;
    xx.X::X();
    __result.X::X(xx);
    return;
}
//1
X xx;
bar( xx );
//2
X __temp0;
(bar( __temp0 ), __temp0 ).mem_func();
//3
X (*pf)( X& );
pf = bar();

3.4 在编译器层次做优化(NVR优化)

对返回值进一步优化,并不是在最后将要返回的结果拷贝给引用的额外参数,而是直接使用额外参数作为要返回的对象进行操作。

X bar(){
    X xx;
    //对xx操作
    return xx;
}

很有可能改写如下:

void bar(X& __result){
    __result.X::X();
    //直接处理__result
    return;
}

3.5 是否需要copy constructor?

假如class需要大量的memberwise(深拷贝)初始化操作,例如以传值(by value)的方式传回object,那么提供一个copy constructor的explicit inline函数实例就非常合理了。

3.6成员们的初始化队伍(Member Initialization List)

下列四种情况必须使用成员初始化列表:

  1. 当初始化一个reference member时;
  2. 当初始化一个const member时;
  3. 当调用一个base class的constructor,而它拥有一组参数时;
  4. 当调用一个member class的constructor,而它拥有一组参数时;

实际上在四种情况下不用Initialization List仍然可以正确编译执行,但是效率低。

class Word {
    String _name;
    int _cnt;
public:
    Word() {
        _name = 0;
        _cnt = 0;
    }
};

在以上程序中Word constructor会产生临时String对象,初始化后再给_name,最后再摧毁临时对象:

Word::Word {
    _name.String::String();
    String temp = String(0);
    _name.String::operator=(temp);
    temp.String::~String();
    _cnt = 0;
}

正确高效的方法如下是

Word::Word() : _name(0), _cnt(0) {}

扩张后的结果为

Word::Word() {
    _name::String::String(0);
    _cnt = 0;
}

成员初始化列表中的初始化顺序是按照声明顺序来的,与initialization list顺序无关,并且initialization list的项目要先于explicit user code。

猜你喜欢

转载自blog.csdn.net/RaKiRaKiRa/article/details/84329728
今日推荐