C++ 构造函数语义学

来源:互联网 发布:js判断空值 编辑:程序博客网 时间:2024/05/23 12:07

此文源自《深度探索C++对象模型》第二章构造函数语义学和《Effective Modern C++》item17-Understand special member function generation.

c++98中,当需要的时候(代码中使用了却没有显示的声明时),编译器会自动合成出这三大函数:默认构造函数,拷贝构造函数,赋值函数和析构函数。默认构造函数只有在没有声明任何构造函数的时候才会被合成出来(准确的来说,是成员对象含有构造函数,或基类含有构造函数,或者涉及到虚继承虚函数等虚基类表和虚函数表指针的设置时才会合成出来),而且编译器合成的这些函数都是隐式inline 和 public 且 nontrivial 的(除了这种情况:如果基类的析构函数被显示的声明为 virtual 的,那么派生类的析构函数也自动的变为 virtual 的,虽然函数签名不一样(编译之后再内部可能会被整合成相同的名称?) —— 见Effective Modern C++)


构造函数为 trivial 是可以不发生调用过程的,省掉了调用的开销。即使是一个空的构造函数(函数体为空,这样将是nontrivial的),也还是有调用开销的,会有调用的过程。也就是说,在需要的时候,编译器才去合成,造成开销。拷贝构造函数也是一样,需要的时候才会去合成,由于存在派生类对象拷贝给基类对象,所以对于有虚的(虚函数,虚继承),或者派生类子对象有拷贝构造函数,成员对象有拷贝构造函数(无论是定义的还是合成的),编译器才会合成.也就是说,对于构造函数,不是一定要有才能构造出对象来.


到了现在的C++11, 新增了两个特殊的构造函数 —— 移动拷贝函数和移动赋值函数。移动拷贝函数和移动赋值函数在需要的时候也会被按memberwise的语义合成出来,也就是对每个member都施加移动。如果是派生类对象,同样的,会将派生类对象中的基类部分施以移动构造。当然,如果没有资源可转移所有权,move的实际操作还是按copy 来进行。

典型的自定义构造函数的三定律:如果需要显示定义拷贝构造函数,说明类有需要手动管理的资源,那么在赋值函数中一样需要管理,在析构函数中需要对资源进行释放。(构造链和析构链)

如果显示的定义了拷贝操作(拷贝构造函数和赋值函数),说明默认的memberwise copy 不再合适,那么memberwise move也将是不合适的。反过来,如果显示的定义了一个移动操作(移动构造函数和赋值函数),说明默认的memberwise move不适合用来move对象,那么memberwise copy也将是不适合的。也就是说,如果定义了拷贝操作,编译器将不提供移动操作;如果定义了移动操作,编译器将不提供拷贝操作。另外,如果定了析构函数,根据Rule of Three(三大函数同时自定义或者都不定义), 表明是有资源要释放的,那么应该也要自定义拷贝操作。

所以总的来说,拷贝操作在需要时,如果没有自定义,而且没有自定义移动操作,编译器会提供memberwise copy的拷贝操作。移动操作在需要时,如果没有自定义,而且没有自定义拷贝操作和析构函数, 编译器会提供memberwise move的移动操作。默认构造函数在没有定义任何构造函数时,编译器会提供。析构函数在没有定义析构函数时,会提供。

另外模板形式的构造函数,不会影响上边的规则,即使模板能实例化出一个拷贝和移动构造函数,编译器提供与否的规则仍不变。

默认构造函数:

如果类中有class member objects,那么类的默认构造函数会调用每一个class member objects 的默认构造函数,对于内置类型,默认构造函数则只分配了空间,值是随机的。同样的,如果一个类派生自一个带有默认构造函数的基类,也就是派生类对象中包含base class subobjects(这里区别子对象和成员对象的说法),编译器提供的派生类的默认构造函数会先调用基类的默认构造函数。类的class member object如果没有默认构造函数,bass class subobjects 如果没有默认构造函数,那么该类必须自定义构造函数,且在初始化类表处调用base class subobjects (也就是基类)的构造函数和class member object的构造函数

对于含有虚函数的类,或者派生的基类中某一个含有虚函数(一般来说,为了确保安全,基类的析构函数要是virtual的,这样派生类的析构函数也是virtual的)。

默认构造函数都是nontrivial的,会发生调用的过程。trivial的优化为不发生调用。

拷贝操作:

对于内置类型(指针,整数,字符型等)成员,编译器提供的拷贝操作都是bitwise copy,也就是按位拷贝的,如果自定义的拷贝构造函数中,没有显示的拷贝内置这些内置类型成员,其值将是随机的。对于class member objects和base class subobjects, 编译器提供的拷贝构造函数会调用其对应的拷贝操作来初始化。多态是靠虚函数表来实现,在每个类对象中安插一个虚函数表指针,所有的类对象公用一张表。由于存在派生类对象拷贝给基类对象Base obj = Derived()(里氏替换原则),这里调用的是基类的拷贝构造函数,会发生切割,此时编译器必须在拷贝构造函数里,安插合适的代码来正确的初始化虚函数表指针vptr的值。所以对于有虚函数表指针的类对象,在拷贝构造函数里必须正确的初始化虚函数表指针vptr,而不是简单的去位拷贝一个值。vptr的发生在base class subobjects  的和class member objects 对象的构造之后,其他程序员提供的代码之前。


详细的可参见点击打开链接


对于MyClass obj(1024) ; MyClass obj = 1024; MyClass obj = Myclass(1024) 。语法上,后边两个语句明显的多调用了一次拷贝构造函数(实际编译器会采用NRV和优化技术,并不会有这个拷贝构造函数的调用),就像对于单参构造函数,实际上相当于在必要的时候int 类型可以转化为MyClass类型,但是将该单参构造函数设置为explicit 时,第二个语句便不可行,因为没有显示的调用构造函数。

class YourClass{int m_i;public:explicit YourClass(int i) : m_i(i){ cout << "single parameter constructor!" << endl; }YourClass(const YourClass & that){cout << "copy constructor" << endl;}void print(){cout << m_i << endl;}};int main(int argc, char *argv[]){//YourClass obj1 = 3;YourClass obj2(3);YourClass obj3 = YourClass(3);YourClass obj4 = obj2;obj4.print();   //obj4的m_i值是随机的cin.get();return 0;}


通过对象或者类的指针来显示调用构造函数时,需要这么写  obj.ClassName::ClassName() ;    p->ClassName::ClassName()

另外一般不要去显示的调用析构函数,在使用布局new运算符(C++中new的三重含义)时,此时编译器不会隐式的调用对象的析构函数。此时如果对象的构造函数中有分配内存(在析构函数中必然需要对应的delete 和 free 语句),才需要显示的去调用析构函数。


C++中所有的成员函数都可以加上域运算符来调用,包括构造函数和析构函数;一般的,由于存在多态,基类指针或者引用指向派生类对象(实际上指向的是派生类中的基类子对象),调用虚函数,调用的将是派生类的虚函数,可以通过在虚函数前加上基类的域运算符,这样调用的便是基类的虚函数。


另外,C++中构建对象的顺序与析构相反,所以成员的初始化总是按照其声明的先后顺序进行,所以在初始化列表中尽量遵循这个顺序防止出错。

C++11中的使用{} 的统一初始化,MyClass obj{1,2,3} 是调用对应的MyClass(int a, int b, int c)构造函数;而MyClass ob = {1,2,3} 则是调用 MyClass(std::initializer_list<int> a)构造函数。C++ 11 中的std::initializer_list<T> 构造函数具有最高的优先级。而且支持内置类型的隐式转化,也就是说在需要的时候 {1, true} 可以隐式的转化为 std::initializer_list<int> , 也可以转化为 std::initializer_list<double>。


对于虚基类,如果没有默认构造函数,继承树上的其他派生类的构造函数中,都需要在初始化列表中显示的调用虚基类的构造函数,不管是不是直接继承,这样做的目的的防止虚基类被重复初始化。






                  



0 0
原创粉丝点击