1 对象之间的复制
同一个类的对象之间是可以进行复制的,即将一个对象的数据成员赋值给另外一个对象的相应数据成员。
定义 = 运算符 就是重载 “operator =” 成员函数
(1)如果在两个已创建的对象之间赋值,调用= 运算符
MyClass s1,s2;
....
s1 = s2 ;//将s2 的所有数据成员赋值给s1的相应的数据成员
(2)如果创建新的对象并赋值,调用拷贝构造函数
Class A
{
int m;
public:
A(){} //默认构造函数
A(int n) { m = n;} //重载构造函数
A(const A &b) {} //拷贝构造函数
A & operator = (const A &b) {} // 拷贝构造函数
~A{} //析构函数
}
void main()
{
A a(2),b(a),c;
A d = a;
}
A a(2) 调用重载构造函数;b(a) 调拷贝构造函数 创建对象b;c 调用默认构造函数 ;d = a 调用拷贝构造函数创建对象d 即调用 operator = ;
1.1 浅拷贝
当两个对象之间进行复制时,若完成复制后它们还共享某些资源(内存空间),其中一个对象的销毁会影响另一个对象,这种对象之间的复制称为浅复制。
class Student
{
int no;
char *pname;
Student() {} //默认构造函数
Student(int n,char *) { //重载构造函数
no = n;
pname = new char [10]; //用new 分配内存空间
strcpy(pname,p);
}
Student(Student &s){ //拷贝构造函数
no = s.no;
pname = s.pname;
}
}
void main()
{
Student s(10,"Mary"),t(s);
}
在main 函数中建立一个对象s,通过拷贝构造函数由s建立t对象。拷贝之后,两个对象的 数据成员 pname 均指向相同的内存空间。
1.2 深拷贝
当两个对象之间进行复制时,当复制完成后它们不会共享任何资源(内存空间),其中一个对象的销毁不会影响到另一个对象,这种对象之间的复制称为深复制。
class Student
{
int no;
char *pname;
Student() {} //默认构造函数
Student(int n,char *) { //重载构造函数
no = n;
pname = new char [10]; //用new 分配内存空间
strcpy(pname,p);
}
Student(Student &s){ //拷贝构造函数
no = s.no;
pname = new char[strlen(s.pname) + 1];
strcpy(pname, s.pname);
}
}
void main()
{
Student s(10,"Mary"),t(s);
}
由对象s复制产生对象t,两个对象的pname成员分别指向不同的内存单元。
2 右值引用
2.1 左值与右值
1、在赋值表达式中,左值是指表达式结束依然存在的持久对象,右值指的是表达时结束后就不存在的临时对象。
左值举例:
- 函数名和变量名
- 返回左值引用的函数调用
- 前置自增/自减运算符连接的表达式++i/–i (++i对i加1后再赋给i,最终的返回值就是i,所以,++i的结果是具名的,名字就是i)
- 由赋值运算符或复合赋值运算符连接的表达式(a=b、a+=b、a%=b)
- 解引用表达式*p
- 字符串字面值”abc”
2、判断左值与右值的方法:可以对表达式取地址&的为左值,不可以对表达式取地址&的则为右值。
3、右值又分将亡值(xvalue),纯右值。
(1)纯右值:
- 描述: 满足下列条件之一的:
- 1)本身就是赤裸裸的、纯粹的字面值,如3、false;
- 2)求值结果相当于字面值或是一个不具名的临时对象。
- 举例:
- 除字符串字面值以外的字面值
- 返回非引用类型的函数调用
- 后置自增/自减运算符连接的表达式i++/i– (对于i++而言,是先对i进行一次拷贝,将得到的副本作为返回结果,然后再对i加1,由于i++的结果是对i加1前i的一份拷贝,所以它是不具名的)
- 算术表达式(a+b、a&b、a<b) 注释:a+b得到的是不具名的临时对象,而a&&b和a==b的结果非true即false,相当于字面值
(2)将亡值:
在C++11中,用左值去初始化一个对象或为一个已有对象赋值时,会调用拷贝构造函数或拷贝赋值运算符来拷贝资源,而当用一个右值(包括纯右值和将亡值)来初始化或赋值时,会调用移动构造函数或移动赋值运算符来移动资源,从而避免拷贝,提高效率。当该右值完成初始化或赋值的任务时,它的资源已经移动给了被初始化者或被赋值者,同时该右值也将会马上被销毁(析构)。也就是说,当一个右值准备完成初始化或赋值任务时,它已经“将亡”了。这种右值常用来完成移动构造或移动赋值的特殊任务,扮演着“将亡”的角色,所以C++11给这类右值起了一个新的名字——将亡值
举例:
- 将要被移动的对象
- T&& 函数的返回值
- std::move返回值
- 转换为T&&的类型的转换函数的返回值
2.2 右值引用
1、右值引用
&&(右值引用):右值引用是对一个右值进行引用的类型,右值不具名,只能通过引用的方式找到。
右值引用指向的是将要销毁的对象,也就是前面提到的a+2产出的临时对象,在对象销毁之前该引用可以接管其资源
2、universal references
T&& 不一定为右值,它绑定的类型是未定的,可以为右值,也可以为左值。
template<typename T>
void f(T&& param)
f(10);//10是右值
int x = 10;
f(x);//x是左值
param的类型取决于它的初始化,&& 被一个左值初始化,则为左值。
参考文献:
C++11 中的左值、右值和将亡值_guoxiaojie_415的博客-CSDN博客
3 std::move
3.1 std::move
std::move 将一个左值转换为右值引用类型。move 是将对象的状态或所有权从一个对象移动到另一个对象,只是转移,没有内存的拷贝。
int a = 1;
int&& b = std::move(a);//左值转换为右值
输出结果:
a = 1
b =1
std::move 的实现:
template<typename T>
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
详解C++移动语义std::move()_子木呀的博客-CSDN博客_c++ std::move
4 完美转发
4.1 完美转发
定义:完美转发是指函数模板在向其他函数传递自身形参时,如果相应实参是左值,则被转发为左值;同理如果相应实参为右值,则被转换为右值。
std::forward 函数为转发函数,不管参数时T&& 这种未定的引用还是明确的左值引用或右值引用,它都会按照参数本来的类型转发。
void PrintT(int &t)
{
cout << "lvalue" << endl;
}
void PrintT(T &&t)
{
cout << "rvalue" << endl;
}
template<typename T>
void TestForward(T && v)
{
PrintT(v);
PrintT(std::forward<T>(v));
PrintT(std::move(v));
}
Test()
{
TestForward(1);
int x =1;
TestForward(x);
TestForward(std::forward<int>(x));
}
输出结果
lvalue
rvalue
rvalue
(1)分析:
TestForward(1),1是右值,T&& 被初始化为一个右值,调用PrintT(v) 时,v是一个具名变量被转换为左值,输出左值;
PrintT(std::forward<T>(v)),v还是右值,则调用函数void TestForward(T && v),输出右值;
调用PrintT(std::move(v)),v还是右值,输出右值
lvalue
lvalue
rvalue
(2)分析
v是左值,调用PrintT(v) 和 PrintT(std::forward<T>(v))输出左值;
调用PrintT(std::move(v)),.左值转换为右值,输出右值
lvalue
rvalue
rvalue
(3)分析
同(2)
完整用例:
#include <iostream>
#include <memory>
#include <utility>
#include <array>
struct A {
A(int&& n) { std::cout << "rvalue overload, n=" << n << "\n"; }
A(int& n) { std::cout << "lvalue overload, n=" << n << "\n"; }
};
class B {
public:
template<class T1, class T2, class T3>
B(T1&& t1, T2&& t2, T3&& t3) :
a1_{std::forward<T1>(t1)},
a2_{std::forward<T2>(t2)},
a3_{std::forward<T3>(t3)}
{
}
private:
A a1_, a2_, a3_;
};
template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)));
}
template<class T, class... U>
std::unique_ptr<T> make_unique(U&&... u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)...));
}
int main()
{
auto p1 = make_unique1<A>(2); // rvalue
int i = 1;
auto p2 = make_unique1<A>(i); // lvalue
std::cout << "B\n";
auto t = make_unique<B>(2, i, 3);
}
//输出:
rvalue overload, n=2
lvalue overload, n=1
B
rvalue overload, n=2
lvalue overload, n=1
rvalue overload, n=3
4.3 std::move 与std::forward
std::move和std::forward本质就是一个转换函数,std::move执行到右值的无条件转换,std::forward执行到右值的有条件转换,在参数都是右值时,二者就是等价的。其实std::move和std::forward就是在C++11基本规则之上封装的语法糖。
5 万能函数包装器
右值引用、完美转发、可变参数模板可编写一个万能的函数包装器。
5.1 可变参数模板
1、可变参数模板概念
C++11 中的可变参数模板允许模板定义中包含0到任意个模板参数。声明可变参数模板时,需要在typename或class 后面带上省略号“...”。
省略号的作用:
- 声明一个参数包,这个参数包可以包含0到任意个模板参数;
- 在模板定义的右边,可以将参数包展开成一个一个独立的参数
2、可变参数模板函数
template<class ...T>
void f(T... args)
{
}
有两种展开参数包的方法:
- 递归函数方式展开参数包
- 逗号表达式和初始化列表方式展开参数包
(1)递归函数方式展开参数包
通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。递归终止函数是用来终止递归的。
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <algorithm>
using namespace::std;
//递归终止函数
void print()
{
cout << "empty" << endl;
}
//展开函数
template <class T,class ... Args>
void print(T head,Args... rest)
{
cout << "parameter " << head << endl;
print(rest...);//递归调用自己,每一次调用参数包中的参数就会减少一个,直到所有的参数展开位置
//当没有参数是,则调用终止函数
}
int main()
{
print(1,2,3,4);
return 0;
}
(2)逗号表达式和初始化列表方式展开参数包
省略
参考:
【1】C++11 std::move和std::forward:C++11 std::move和std::forward - 简书
【2】C++11新特性之 std::forward(完美转发):C++11新特性之 std::forward(完美转发) - yfceshi - 博客园