一、拷贝构造函数
1、浅拷贝
当类没有定义拷贝构造函数的时候,编译器会默认提供一个,这个拷贝函数是浅拷贝。如果该类中含有指针,程序结束后会出现重复释放的问题。见下面的例子:
class Test
{
public:
int *p;
Test(){ p=new int; };
~Test(){ delete p; };
};
void main()
{
Test t1;//默认构造函数构造对象t1
Test t2(t1);//默认复制构造函数,此时t1.p与t2.p指针指向同一地址
Test t3 = t1;//默认复制构造函数,此时t1.p、t2.p与t3.p指针指向同一地址
}
2、深拷贝
为了解决浅拷贝造成的指针成员重复释放的问题,可以自定义深拷贝构造函数。针对指针类成员,为新对象分配新的空间,对源对象中指针指向的值进行重新构造。见下面例子:
class Test
{
public:
int *p;
Test(const Test &t)
{
p = new int (*(t.p));//为对象分配新的内存,并使用源对象中的值进行重新构造
}
Test(){ p=new int; };
~Test(){ delete p; };
};
二、移动构造函数
1、基础概念介绍
左值:表示可寻址。是保存在内存中,具有确切地址,并能取地址,进行访问,修改等操作的表达式(变量)。
右值:表示可读不可寻址。是保存在内存中,或者寄存器中,不知道也无法获得其确切地址,在得到计算表达式结果后就销毁的临时表达式。
右值引用:表示即将过期但还没过期的值。
右值是能够赋值给左值,但是左值不能赋值给右值。
右值引用主要作用是解决大对象在作为函数返回值返回时的深度拷贝问题,以及大对象之间的快速复制。
int &a = 1;//错误,1是右值,a是左值引用,无法直接初始化,需要用左值初始化;
int a = 1; //正确,a是左值不是左值引用,可以被赋值;
int const &a = 1;//正确,a是常量左值引用,右值可以绑定到常左值引用;
int &&a = 1;//正确,这是C++11中的右值引用,a为左值;
2、函数的返回值一般是右值,也可能是左值:
- 当返回值为函数内部定义的局部变量时,函数返回值为右值,因为函数内部该变量已被销毁。
- 当返回值为指针类型或引用类型的参数时,或者全局变量时,函数返回值为左值。
//返回右值的常规函数
int fun(){//函数中a是局部变量,会销毁,所以返回值是右值,只能临时使用。
int a = 1;
return a;
}
//返回左值的函数
int &fun(int &a){//函数将输入的引用返回,从始至终都是这个a
a++;
return a;
}
//以上函数可以这么用
fun(a) = 4;//因为fun返回左值,所以fun可以被赋值
//错误的返回左值案例
int &fun(){//不要将局部变量的引用返回,因为返回后局部变量被销毁
int a = 3;
return a;
}
3、移动构造函数
(1)传入临时对象的引用,(2)将引用绑定到目标对象,(3)临时对象的指针变量赋值为空。此过程中没有进行对象的复制拷贝。临时对象进行析构释放时,由于临时对象指针为空,C++内部允许对nullptr指针进行释放,不会影响到目标对象,不会出现同一指针重复释放的情况。见下面例子:
class Test
{
public:
int *p;
Test(Test &&t) //移动构造函数,***注意此处传入参数是右值引用
{
p = t.p;
t.p = nullptr;//将临时对象的指针赋值为空
cout<<"copy construct"<<endl;
}
Test(const Test &t) //拷贝构造函数
{
p = new int (*(t.p));
cout<<"move construct"<<endl;
}
Test(){ p=new int; cout<<"construct"<<endl; };
~Test(){ delete p; cout<<"disconstruct"<<endl; };
};
Test getTest()
{
return Test();
}
void main()
{
{
Test t = getTest();
}
}
上述例子中传入参数为右值引用T&&,一般情况下传入参数为左值引用T&,***但是由于一般情况下函数的返回值都为右值,如果使用右值函数返回值用于初始化新对象时,此时由于函数返回值为右值,无法调用参数为左值引用的移动构造函数,只能调用拷贝构造函数,增加了拷贝次数。
class Test
{
public:
int *p;
Test(const Test &t)
{
p = new int (*(t.p)); cout<<"copy construct"<<endl;
}
Test(){ p=new int; cout<<"construct"<<endl; };
~Test(){ delete p; cout<<"destruct"<<endl; };
};
Test getTest()
{
return Test();//函数内部默认构造函数函数构造对象,属于局部对象
}
void main()
{
{
//函数返回值为右值,此时无法调用左值引用参数的移动构造函数,只能调用拷贝构造函数
Test t = getTest();
}
}
4、移动语义std::move()
move作用是可以将一个左值转换成右值引用,从而可以调用C++11的拷贝构造函数。由于在C++11中的移动构造函数的入参是右值引用,因此当传入左值时,无法调用该移动构造函数,需要借助move将左值转换成右值引用。
5、完美转发std::forward()
完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给当前函数模板中的另外一个函数。
因此,为了实现完美转发,除了使用万能引用之外,我们还要用到std::forward(C++11),它在传参的过程中保留对象的原生类型属性。这样右值引用在传递过程中就能够保持右值的属性。
void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const左值引用" << endl; }
void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t) // 万能引用
{
Func(std::forward<T>(t)); // 根据参数t的类型去匹配合适的重载函数
}
int main()
{
int a = 4; // 左值
PerfectForward(a);
const int b = 8; // const左值
PerfectForward(b);
PerfectForward(10); // 10是右值
const int c = 13;
PerfectForward(std::move(c)); // const左值被move后变成const右值
return 0;
}
运行结果如下: