以前使用的引用的概念,都是指左值引用,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
为了提高程序运行效率,C++11中引入了右值引用
,右值引用也是别名,但其只能对右值引用
int Add(int a, int b)
{
return a + b;
}
int main()
{
const int&& ra = 10;
// 引用函数返回值,返回值是一个临时变量,为右值
int&& rRet = Add(10, 20);
return 0;
}
为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址
的称为左值
,只能放在=右边的,或者不能取地址
的称为右值
,但是也不一定完全正确
关于左值和右值的区分不是很好区分,一般认为:
左值 | 右值 | |
---|---|---|
特点 | 可以被取地址 | 一般情况下可以修改(const修饰不能修改) | 不能取地址 | 不能出现在 = 左边,不能被修改 |
例子 | 变量名或者解引用的指针 | C语言中的纯右值,比如:a+b, 100 | 将亡值,比如:表达式的中间结果、函数按照值的方式进行返回 |
前面学过的引用是左值引用,引用就是给变量取别名,那么左值引用就是给左值取别名,右值引用就是给右值取别名
左值引用 | 右值引用 | |
---|---|---|
表示 | 变量后面加&表示左值引用 | 变量后面加&&表示右值引用 |
一般情况 | 左值引用只能引用左值,不能引用右值 | 右值引用只能引用右值,不能引用左值 |
特殊情况 | const左值引用可以引用左值,也可以引用右值 | 右值引用可以引用move后的左值 |
左值std::move
后就会转移对象,此时对象的资源就会被转移走
,不能再使用被move后的对象
左值引用可以做参数
也可以做返回值
(用我们自己实现的string类来做演示)
mystring.h
左值引用做参数可以完美解决传参时的拷贝问题
传值
传递string对象到函数可以看出在传参的时候发生了深拷贝
传左值引用
的方式传参传参过程并没有发生深拷贝
左值引用做返回值也可以减少拷贝
但是,当函数的返回值出了函数作用域就会销毁时,就不能使用左值引用进行返回了,此时就会发生拷贝
比如说to_string方法的返回值就是一个临时对象,出了函数作用域就会销毁。所以只能使用传值返回,就会发生深拷贝
在这种情况下,本来是要发生两次拷贝构造,但是在一个调用中,连续的构造一般会被编译器优化成一次
为了解决临时对象的返回值传参深拷贝问题,就用移动语义来解决这个问题
移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。
引入右值引用,并不是直接使用右值引用减少拷贝,提高效率。而是深拷贝的类,提供移动构造
和移动赋值
来解决返回值深拷贝问题。
引入移动语义,这些类的对象进行传值返回或者参数为右值时,可以用移动构造和移动赋值,转移资源,避免深拷贝,提高效率
对于上面左值引用的短板,我们在深拷贝的类里实现一个移动构造,所谓移动构造
,就是把传入的右值资源,全部移动到当前对象,当前对象不去开辟新的空间拷贝构造,而是直接使用传入的右值引用对象的资源,减少了拷贝。
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
std::cout << "string(string&& s) -- 移动构造" << std::endl;
//直接交换传入右值引用的资源到当前对象
this->swap(s);
}
void swap(string& s){
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
有了移动构造,在上面to_string方法中进行返回
时,不会去拷贝构造临时对象,然后再把临时对象拷贝构造给ret
而是进行移动构造,构造一个临时对象,然后再把临时对象移动构造给ret,期间没有进行深拷贝
,而是进行资源的转移
,提高了效率
如果出现了下列场景
int main(){
mystring::string ret;
ret = mystring::to_string(1234);
return 0;
}
要对ret进行赋值操作
此时如果没有移动赋值,那么就会调用普通的赋值
// 赋值重载
string& operator=(const string& s){
string tmp(s);
swap(tmp);
return *this;
}
普通的赋值会先拷贝构造一个临时对象,然后把临时对象的资源交换给当前对象,最后临时对象出作用域销毁,期间会发生一次拷贝构造
这时给这个类实现一个移动赋值
// 移动赋值
string& operator=(string&& s){
std::cout << "string& operator=(string&& s) -- 移动赋值" << std::endl;
this->swap(s);
return *this;
}
此时再用to_string方法的返回值对ret进行赋值时,就会发生以下场景
这里会发生一次移动构造
和一次移动赋值
,由于不是一个操作,所以编译器无法进行优化
右值引用还可以在使用容器插入接口函数中,如果实参是右值,那么插入就会匹配右值引用版本的插入函数,转移右值的资源,而不是拷贝构造,减少拷贝,提高效率
比如说下面的使用场景
int main(){
std::list<mystring::string> v;
mystring::string s1("111");
v.push_back(s1);
v.push_back("2222");
v.push_back(std::move(s1));
return 0;
}
如果插入传入的是s1,那么会调用push_back的左值引用版本
,s1
会被拷贝构造插入到v中
如果传入的是一个右值,或者经过move后的s1,那么push_back会调用右值引用版本
,右值会被直接移动到v里,不会发生拷贝构造
右值引用的作用有一个就是给中间临时变量取别名
int main(){
string s1("hello");
string s2(" world");
string s3 = s1 + s2; // s3是用s1和s2拼接完成之后的结果拷贝构造的新对象
stirng&& s4 = s1 + s2; // s4就是s1和s2拼接完成之后结果的别名
return 0;
}
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数
对于模板参数中的&&
,不仅仅是引用右值,语法规定该中情况为万能引用,既能引用右值也能引用左值
但是当右值引用的对象作为实参传递时,属性会退化为左值
,只能匹配左值引用
就比如下列场景
void Fun(int& x) { std::cout << "左值引用" << std::endl; }
void Fun(const int& x) { std::cout << "const 左值引用" << std::endl; }
void Fun(int&& x) { std::cout << "右值引用" << std::endl; }
void Fun(const int&& x) { std::cout << "const 右值引用" << std::endl; }
template<typename T>
void PerfectForward(T&& t){
//万能引用,可以接收左值和右值
//但是当接收的t是右值时,再次向Fun传递t,t的右值属性就会退化为左值
Fun(t);
}
int main(){
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
因为t接收右值的传参后,t就有了空间用来保存传来的右值,t就退化成了左值,存在属性的混淆
如果想让传给Fun的t为右值,就需要在传参的时候进行完美转发
template<typename T>
void PerfectForward(T&& t){
//把t进行完美转发,这时Fun传入的就是保持了右值属性的t
Fun(std::forward<T>(t));
}
此时就能保持参数的右值属性
C++11之前的C++类中,有6个默认成员函数:
C++11新增了两个:移动构造函数和移动赋值运算符重载
关于移动构造函数和移动赋值,有以下生成规则
例如下列的类,如果实现了析构函数 、拷贝构造、拷贝赋值重载中的任意一个
,就不会
生成默认的移动语义
class Person{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
Person& operator=(const Person& p){
if (this != &p){
_name = p._name;
_age = p._age;
}
return *this;
}
~Person()
{}
private:
mystring::string _name;
int _age;
};
int main()
{
Person s1{ "zhangsan", 18 };
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
这时我们把析构函数 、拷贝构造、拷贝赋值重载全部屏蔽,那么这个类就会自动生成移动语义,默认生成的移动语义会调用到string的移动语义,实现这个类的移动构造和移动赋值
class Person{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
/*Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
Person& operator=(const Person& p){
if (this != &p){
_name = p._name;
_age = p._age;
}
return *this;
}
~Person()
{}*/
private:
mystring::string _name;
int _age;
};
int main(){
Person s1{ "zhangsan", 18 };
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
C++98/03,类模版和函数模版中只能含固定数量的模版参数
C++11新特性的可变参数模板能够创建可以接收可变参数的函数模板和类模板
可变参数函数模板示例:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数
我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数
由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值
我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式
来获取参数包中的每个参数
//停止函数
void ShowListArg(){
std::cout << std::endl;
}
//展开函数
template <class T, class ...Args>
void ShowListArg(T value, Args... args) {
std::cout << value << " ";
//递归展开
ShowListArg(args...);
}
//解析函数
template <class ...Args>
void ShowList(Args... args) {
ShowListArg(args...);
}
int main() {
ShowList();
ShowList(1, 2);
ShowList(1, 2, "num", 3);
return 0;
}
数组在实例化之前,会逐一获取到初始化列表中的参数,利用这一特性,可以把参数包的展开交给数组
template <class T>
int PrintArg(T t) {
std::cout << t << " " ;
return 0;
}
//展开函数
template<class ...Args>
void ShowList(Args ...args) {
int arr[] = { PrintArg(args)... };
std::cout << std::endl;
}
//匹配参数为0的情况
void ShowList()
{}
int main() {
ShowList();
ShowList(1, 2);
ShowList(1, 2, "num", 3);
return 0;
}
C++11在STL中加入了一个接口,emplace_back,功能和push_back基本一致,都是往容器里插入数据
template <class... Args>
void emplace_back (Args&&... args);
emplace_back和push_back有一些不同
不需要进行额外拷贝
int main(){
//带有拷贝构造和移动构造的mystring::string
std::list< std::pair<int, mystring::string> > mylist;
std::cout << "左值插入----------------------" << std::endl;
std::pair<int, mystring::string> kv(20, "sort");
mylist.emplace_back(kv);
std::cout << "右值插入----------------------" << std::endl;
mylist.emplace_back(std::move(kv));
std::cout << "可变参数包插入-----------------" << std::endl;
mylist.emplace_back(10, "sort");
return 0;
}
所以说emplace_back的优势
在于,如果传参时传入的是可变参数包
,那么在构造对象时会直接构造,不会拷贝对象,相比push_back有一定的效率提升。
但是如果仅仅是传左值或者传右值对象,那么两者的效率是一样
的。