一个类中如果包含类成员,则一般会在构造函数中提供相关的参数来初始化类成员。在这个过程,不同的参数传递方式可能会导致不同的结果。本文主要讨论值传参、引用传参和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;
}
从上图可以看出,使用了reserve为vector预先分配了足够空间后,减少了因扩容操作导致的拷贝构造函数的以及析构函数的多次调用。
类类型成员变量的初始化
void main(){
/*
...省略了与上面main中相同的代码
*/
ctest ct(vec,t1); //调用类ctest的带参数的构造函数
}
此处,ctest类的构造函数中使用的参数传递形式包括:值传参、引用传参、const引用传参;初始化列表中使用的初始化形式包括:使用或不使用std::move。我们依次讨论各种组合形式。
值传参和引用传参
从上图中可以看出,值传参为参数1的vector中的三个元素以及参数2都产生了临时对象,而引用传参则没有。另外很明显,初始化列表中调用的是类tmpcls的拷贝构造函数。
引用传参但move
从上图中可以看出,在初始化列表中使用std::move后,没有再调用拷贝构造函数。从参数2的表现来看,应该是调用了移动构造函数,故参数1也应该使用了vector的移动构造函数,只是vector类模板的移动构造函数没有输出内容而已。但使用vector的移动构造函数后,导致实参变量vec的内容被清空,不管是size还是capacity都被清掉了。
值传参但move
从上图可以看出,值传参产生的临时对象直接被用来初始化类ctest的成员变量。同时,std::move导致调用了移动构造函数。由于产生了临时对象,故对实参变量没有影响。对于vec成员变量来说,调用移动构造函数后,vec接管了_v临时对象的内容,导致临时对象_v的size和capacity都变为0。对于tls成员变量来说,调用移动构造函数后,临时对象不再使用,所以被析构了。
vec对_v说:你的内容对我有用(有三个元素),我拿了,你随意。所以_v临时对象的size和capacity被清掉了。
tls对_t说:你的内容对我有用(虽然是空类),我拿了,你随意。所以_t临时对象被析构了。
通过调试窗口可以帮助理解值传参但move形式:
const引用传参
根据上图,const引用传参和普通引用传参没有什么区别。
const引用传参但move
从上图可以看出,const引用传参的move形式与普通引用传参和const引用传参的输出是完全相同的。也就是说,const引用传参的move形式也使用了拷贝构造函数而非移动构造函数。仔细分析一下,类ctest的带参构造函数的初始化列表中,执行的代码大概是:
vector<tmpcls>vec = std::move(_v);
tmpcls tls = std::move(_t);
然而,_v和_t都是const引用类型(左值引用),std::move强行将左值转换为右值,从而触发移动构造函数,但它不会丢弃const属性,这就要求vector和tmpcls有const形式的移动构造函数。然而,vector和自定义类tmpcls都没有提供const版本的移动构造函数。C++规定,如果没有提供,就会使用拷贝构造函数。所以,const引用传参的move形式与const引用传参和普通引用传参的输出没什么区别。
总结
根据上文内容,如果实参不再使用,则使用引用传参且move的形式最佳;如果实参还要使用,则使用const引用传参最佳。虽然普通引用传参、const引用传参、以及const引用传参的move形式没什么区别,但我们一般不希望修改传入参数。