C++ 构造函数语义学与 const rvalue reference

此文源自《深度探索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>。

对于虚基类,如果没有默认构造函数,继承树上的其他派生类的构造函数中,都需要在初始化列表中显示的调用虚基类的构造函数,不管是不是直接继承,这样做的目的的防止虚基类被重复初始化。虚继承本来就是为了防止菱形继承中派生类内存不只包含一个基类子对象。

无数的C++书籍教导,Use const whenever you need,但是对于右值引用类型呢?const rvalue reference 其实基本不会用到。

class Foo
{
    int* m_p;
public:
    Foo(int i)
    {
        m_p = new int(i);
    };
    Foo(const Foo& other)
    {
        m_p = new int;
        *m_p = *other.m_p;
        std::cout << "Foo&\n";

    }
    Foo(Foo && other)
    {
        this->m_p = other.m_p;
       other.m_p = nullptr;
       std::cout << "move Foo&&\n";
    }
    Foo& operator=(const Foo& other)
    {
        if (this != &other)
        {
            *this->m_p = *other.m_p;
        }
        std::cout << "operator Foo\n";
        return *this;
    }
    Foo& operator=(Foo&& other)
    {
              if(this != &other)     //关于这里是否需要做指针的检查
             {
         delete this->m_p;
        this->m_p = other.m_p;
        other.m_p = nullptr;
              }
        std::cout << "move operator&&\n";
        return *this;
    }
    void m_func()
    {
        std::cout << "m_func : " << m_p << " : " << *m_p << "\n";
    }
       ~Foo(){delete m_p;}
};

Foo Foofunc()
{
    std::cout << "*********\n";
    Foo obj(1);
    obj.m_func();
    std::cout << "*********\n";
    return obj;
}

void he(const Foo&&)
{
    std::cout << "he\n";
}

const int g()
{
    int i = 0;
    return i;
}

int main(){
    Foo x(4);
    x.m_func();
    x = Foofunc();
    x.m_func();
    std::cout << "______________________\n";
    Foo y = (Foofunc() = Foofunc());
    std::cout << "______________________\n";
    y.m_func();
    return 0;
}

 

左边是VS2013的结果,右边是 Mingw gcc 7.1.0的结果。从结果可以看出,x = Foofunc(); 这句在 Mingw gcc 7.1.0 中进行了返回值优化,没有移动拷贝构造的过程(将return后的值拷贝构造传出函数外 C++中的RVO与NVR优化 ),而VS中还存在。

在链式赋值中,Foo y = (Foofunc() = Foofunc()) ,最后一步给y 赋值,注意调用的是非移动版本的赋值构造函数。这是因为 operator= 重载的返回值是 Foo& 引用类型,对于左值引用,无名的一样是左值。但是无名的右值引用是右值,有名的右值引用才是左值。

判断左值的两个方法:1、有名 2、能够取地址。

const rvalue reference 当然能绑定到 const rvalue 上,不过C++11发明的移动语义,本就为了窃取所有权的(需要modify),所以移动拷贝构造函数,移动赋值构造函数的参数,不像非移动版本,是没有const 修饰的)。

上边代码中对 other.m_p 进行了修改,也说明other 不能为 const.

const rvalue 出现的场合有两种:
1、函数的返回值是 const,当然函数的返回值不是引用和指针,却被const 修饰,没有任何价值

const Foo g();
2、对于普通的 const 左值,被move之后,就是 const rvalue .
const Foo obj{1};
//那么 move(obj)的类型就是 const Foo&&
std::cout << std::is_same<decltype(std::move(obj)), const Foo&&>::value;

所以,按照C++的设计,一般没有 const rvalue reference 的使用场合的。

最后,对于移动赋值构造函数中,是否需要像非移动版本一样进行 this 指针的检查,即 self-assignment 自赋值,在 stackoverflow 上有讨论,高票答案是不必,而是采用 assert 断言的方式。

其实对于移动拷贝构造函数,还真有可能self-assignment,如:

std::move(x) = std::move(x);
这样在上边移动赋值构造函数的内存管理部分,就会出错,所以还是检查的好。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值