类的类成员的初始化

794 阅读2分钟

一个类中如果包含类成员,则一般会在构造函数中提供相关的参数来初始化类成员。在这个过程,不同的参数传递方式可能会导致不同的结果。本文主要讨论值传参、引用传参和const引用传参三种形式,以及是否使用移动语义来初始化类成员。文中并没有过多叙述移动语义相关概念,只是使用具体的实例来描述开发过程中遇到的问题。

使用的类

class tmpcls{
    int a = 5;

public:
    tmpcls(){
        cout<<"tmpcls::tmpcls()"<<endl;
    }

    tmpcls(const tmpcls&){
        cout<<"tmpcls::tmpcls(const tmpcls&)"<<endl;
    }

    tmpcls(tmpcls&&)noexcept{
        cout<<"tmpcls::tmpcls(tmpcls&&)"<<endl;
    }

    ~tmpcls(){
        cout<<"tmpcls::~tmpcls()"<<endl;
    }
};

class ctest{
    std::vector<tmpcls>vec;
    tmpcls tls;

public:
    ctest(){
        cout<<"ctest::ctest()"<<endl;;
    }

    //注意,以下函数重载形式的构造函数定义有些不能同时出现,
    //值传参
    ctest(std::vector<tmpcls>_v,tmpcls _t):vec(_v),tls(_t){
        cout<<"ctest::ctest(std::vector<tmpcls>,tmpcls)"<<endl;
    }

    //值传参,但初始化列表中使用std::move
    ctest(std::vector<tmpcls>_v,tmpcls _t):vec(std::move(_v)),tls(std::move(_t)){
        cout<<"ctest::ctest(std::vector<tmpcls>,tmpcls)"<<endl;
    }

    //引用传参
    ctest(std::vector<tmpcls>&_v,tmpcls &_t):vec(_v),tls(_t){
        cout<<"ctest::ctest(std::vector<tmpcls>&,tmpcls&)"<<endl;
    }

    //引用传参,但初始化列表中使用std::move
    ctest(std::vector<tmpcls>&_v,tmpcls &_t):vec(std::move(_v)),tls(std::move(_t)){
        cout<<"ctest::ctest(std::vector<tmpcls>&,tmpcls&)"<<endl;
    }

    //const引用传参
    ctest(const std::vector<tmpcls>&_v,const tmpcls &_t):vec(_v),tls(_t){
        cout<<"ctest::ctest(const std::vector<tmpcls>&,const tmpcls&)"<<endl;
    }

    //const引用传参,但初始化列表中使用std::move
    ctest(const std::vector<tmpcls>&_v,const tmpcls &_t):vec(std::move(_v)),tls(std::move(_t)){
        cout<<"ctest::ctest(const std::vector<tmpcls>&,const tmpcls&)"<<endl;
    }
};

vector自行扩容

std::vector容器在分配的内存不足以存放插入的数据时自动扩容。如果元素类型是类类型,在扩容时,已有的类元素会调用拷贝构造函数将它们复制到新内存中,再调用析构函数释放它们。这可以通过使用reserve函数来避免。注意不能使用resize,它会初始化分配的内存,导致额外调用了类tmpcls的默认构造函数。

#include <vector>

void main(){
    tmpcls t1,t2,t3;
    cout<<"----------------------"<<endl;
    std::vector<tmpcls>vec;
    vec.reserve(3); 
    vec.push_back(t1);
    vec.push_back(t2);
    vec.push_back(t3);
    cout<<"----------------------"<<endl;
}

1.JPG

从上图可以看出,使用了reservevector预先分配了足够空间后,减少了因扩容操作导致的拷贝构造函数的以及析构函数的多次调用。

类类型成员变量的初始化

void main(){
    /*
    ...省略了与上面main中相同的代码
    */
    ctest ct(vec,t1);   //调用类ctest的带参数的构造函数
}

此处,ctest类的构造函数中使用的参数传递形式包括:值传参、引用传参、const引用传参;初始化列表中使用的初始化形式包括:使用或不使用std::move。我们依次讨论各种组合形式。

值传参和引用传参

2.JPG

从上图中可以看出,值传参为参数1的vector中的三个元素以及参数2都产生了临时对象,而引用传参则没有。另外很明显,初始化列表中调用的是类tmpcls的拷贝构造函数。

引用传参但move

3.JPG

从上图中可以看出,在初始化列表中使用std::move后,没有再调用拷贝构造函数。从参数2的表现来看,应该是调用了移动构造函数,故参数1也应该使用了vector的移动构造函数,只是vector类模板的移动构造函数没有输出内容而已。但使用vector的移动构造函数后,导致实参变量vec的内容被清空,不管是size还是capacity都被清掉了。

值传参但move

4.JPG

从上图可以看出,值传参产生的临时对象直接被用来初始化类ctest的成员变量。同时,std::move导致调用了移动构造函数。由于产生了临时对象,故对实参变量没有影响。对于vec成员变量来说,调用移动构造函数后,vec接管了_v临时对象的内容,导致临时对象_vsizecapacity都变为0。对于tls成员变量来说,调用移动构造函数后,临时对象不再使用,所以被析构了。

vec_v说:你的内容对我有用(有三个元素),我拿了,你随意。所以_v临时对象的sizecapacity被清掉了。
tls_t说:你的内容对我有用(虽然是空类),我拿了,你随意。所以_t临时对象被析构了。

通过调试窗口可以帮助理解值传参但move形式:

4_1.JPG

const引用传参

5.JPG

根据上图,const引用传参和普通引用传参没有什么区别。

const引用传参但move

6.JPG

从上图可以看出,const引用传参的move形式与普通引用传参和const引用传参的输出是完全相同的。也就是说,const引用传参的move形式也使用了拷贝构造函数而非移动构造函数。仔细分析一下,类ctest的带参构造函数的初始化列表中,执行的代码大概是:

vector<tmpcls>vec = std::move(_v);
tmpcls tls = std::move(_t);

然而,_v_t都是const引用类型(左值引用),std::move强行将左值转换为右值,从而触发移动构造函数,但它不会丢弃const属性,这就要求vectortmpcls有const形式的移动构造函数。然而,vector和自定义类tmpcls都没有提供const版本的移动构造函数。C++规定,如果没有提供,就会使用拷贝构造函数。所以,const引用传参的move形式与const引用传参和普通引用传参的输出没什么区别。

总结

根据上文内容,如果实参不再使用,则使用引用传参且move的形式最佳;如果实参还要使用,则使用const引用传参最佳。虽然普通引用传参、const引用传参、以及const引用传参的move形式没什么区别,但我们一般不希望修改传入参数。