目录
1).左值引用类模板.左值引用类模板)
2).右值引用类模板--万能引用.右值引用类模板--万能引用)
一、列表初始化
在C++11中,我们开始支持用列表来初始化对象
1.支持内置类型
cpp
int a1 = { 2 };
int a2 = 2;
上面两种初始化,它们的效果实际都相同。
2.支持自定义类型
cpp
class BirthDay
{
public:
BirthDay(int y=1949, int m=10, int d=1)
:_year(y)
,_month(m)
,_day(d)
{
cout << "BirthDay(int y=1949, int m=10, int d=1)--默认构造" << endl;
}
BirthDay(const BirthDay& birday)
:_year(birday._year)
, _month(birday._month)
, _day(birday._day)
{
cout << "BirthDay(const BirthDay& birday)--拷贝构造" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
BirthDay a={ 2024,10,27 };
return 0;
}
在上面的例子中,我们就使用到了列表初始化。
BirthDay a={ 2024,10,27 }的本质其实是,先用{ 2024,10,27 }构造一个临时对象,再用这个临时对象去拷贝构造a。但是我们的编译器对这一步进行了优化,合二为一,省去了拷贝构造这一步。
可以看到这里只调用了构造函数,并没有调用拷贝构造。
在这里b引用的其实是{ 2014,10,27 }所构造出来的临时对象,而临时对象具有常性,所以要加const。
我们还可以直接去掉=来进行赋值:
可以发现有了列表初始化后,我们写代码会方便很多:
二、initializer-list
我们需要注意列表初始化和initializer_list的区别,虽然我们有了列表初始化,但是当我们想要对容器进行初始化还是不太方便,而initializer_list就可以用多个值对容器进行初始化:
而initializer_list其实是一个类模板,当我们用花括号列表去给一个容器,那么这就会被识别为initializer_list类型:
它的底层其实是数组,内部有两个指针指向开始和结束,因此它支持迭代器遍历,我们可以看看它所拥有的成员:
当一个容器支持initializer_list构造函数,那么就意味着它可以用多个值组成的花括号来初始化。而STL中的容器支持{x1,x2,x3...}这种形式来初始化,其实就是通过initializer_list的构造函数来实现的。
在这里我们需要注意下面两个写法,它们的效果虽然相同,但是语义有差别。
三、右值引用和移动语义
1.左值和右值
a)左值
左值是⼀个表示数据的表达式(如变量名或解引用的指针),存储在内存中。英文简写为lvalue,是left value的简写。在现代C++中,lvalue又被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象。
- 我们可以获取左值的地址,可以对其进行赋值。但是被const所修饰的左值只能取地址,不能赋值
- 左值可以出现在赋值符号的左边或者右边
b)右值
右值也是一个表示数据的表达式,比如字面值常量、表达式求值过程中创建的临时对象等,它的英文简写是rvalue,传统认为是right value的简写。在现代c++中,它也被解释为read value意为只读值,即可以提供数据值,但不能寻址不能修改。
在C++中,右值又被分为纯右值和将亡值:
纯右值:指的是那些不绑定到内存地址的临时值,其生命周期通常很短,通常在表达式计算结束时就被丢弃。
例如字面量:整数10、布尔值true以及调用一个函数非引用的返回值。
将亡值:将亡值表示的是那些即将被销毁但仍持有资源的对象。这些对象通常是通过右值引用返回的函数调用、std::move的返回值等表达式产生的。将亡值允许在对象被销毁之前,将其资源(如动态分配的内存、文件句柄等)高效地转移到另一个对象中,这是通过移动语义实现的。
- 右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,即不能被赋值修改。
- 不能取地址
可以看到,当我们对右值进行取地址时,编译器进行了报错,并且明确表示&取地址符号要求左值。
2.左值引用和右值引用
cpp
type& a;//左值引用
type&& b;//右值引用
a)左值引用
给左值起别名:
cpp
int main()
{
//p a b *p s s[0]皆为左值
int* p = new int(0);
int a = 1;
const int b = a;
*p = 10;
string s("12345");
s[0] = 'x';
//它们都能取地址
cout << &p << endl;
cout << &a << endl;
cout << &b << endl;
cout << &(*p) << endl;
cout << &s << endl;
cout << (void*)&s[0] << endl;
//左值引用
int*& rp = p;
int& ra = a;
const int& rb = b;
int& rpp = *p;
string& ps = s;
char& ps0 = s[0];
cout << endl;
}
b)右值引用
给右值起别名
cpp
int Add(int a, int b)
{
return a + b;
}
int main()
{
double x = 1.1, y = 2.2;
//以下皆为右值
/*
10;
x + y;
Add(1, 2);
string("123");
*/
//不能对右值进行取地址
/*cout << &10 << endl;
cout << &(x + y) << endl;
cout << &(Add(1, 2)) << endl;
cout << &string("123") << endl;*/
int&& r10 = 10;
double&& rxy = (x + y);
int&& radd = Add(1, 2);
string&& rstr = string("123");
return 0;
}
note:当我们给右值引用之后,这里的r10是右值引用,但是r10的属性是左值。这意味着,通过右值引用,我们可以做到修改右值。因为r10此时是右值,我们可以修改r10,也可以对它进行取地址。这一点会在后面有更详细的介绍和使用,我们先简单看一下:
可以看到这里我们能修改r10,也能对它进行取地址
c)总结
- 左值引用不能直接引用右值,但是const左值引用可以引用右值
- 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
- **值得注意的是,当对一个左值进行move的时候,结果是右值,但是这个左值还是左值,没有发生改变。**这就类似于,int a=0; double da=(double)a;这里的a还是int类型,只是进行了强转,结果是double类型,但是a本身的类型没有发生改变。
当我们直接用右值引用绑定左值时,编译器会进行报错
我们可以使用move对c进行强转,让右值引用可以绑定被move后的左值
这里的move其实是库里面的函数模板,本质是进行强制类型转换
- 当一个右值被右值引用绑定之后,右值引用的属性是左值
此时可以看到r10作为右值引用,但是r10的属性是左值,所以r10可以取地址也可以修改。
- 语法角度,左值引用和右值引用都是起别名,不例外开空间。但是在汇编底层的角度来看,左值引用和右值引用其实都是用指针来进行实现。
d)引用延长生命周期
匿名对象和临时对象,它们的生命周期只在当前行。从下面这个例子可以看到匿名对象会在当前行结束后被析构。
但是通过引用,我们可以延长它的生命周期:
右值引用延长生命周期
const左值引用延长生命周期
3.右值引用和移动语义的使用场景
通常左值引用使用于函数中传参和左值引用传返回值时减少拷贝,还可以修改实参和修改返回对象的价值。
但是在一些场景中,我们不能使用传左值引用返回来解决拷贝效率问题:
cpp
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return vv;
}
};
在上面这个例子中,我们不能使用传左值引用返回,也不能使用传右值引用返回来解决vv出了函数所带来的拷贝效率问题。
问题所在:
那么我们将如何解决这个问题呢?---移动构造和移动赋值
4.移动构造和移动赋值
移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引
⽤,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是⼀个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函
数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有 意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要"窃取"引⽤的
右值对象的资源,而不是像拷贝构造和拷⻉赋值那样去拷贝资源,从提高效率。下面的bit::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。
a)移动构造
当我们在用临时对象给目标对象拷贝构造的时候,这一过程其实是有点浪费的,因为临时对象拷贝完之后就会被销毁,那我们其实可以将临时对象的资源直接给我们的目标对象。怎么给呢?通过交换的方法:
我们用string来举一个例子加深理解:
我们原先是用局部对象拷贝构造给匿名对象,匿名对象拷贝构造给目标对象
但是通过移动构造我们可以直接省掉临时对象到目标对象的这一拷贝过程:
cpp
struct String
{
//...
// 这里省略String类的具体模拟实现
//....
void Swap( String& y)
{
std::swap(_size, y._size);
std::swap(_capacity, y._capacity);
std::swap(_arr, y._arr);
}
//移动构造
String(String&& temp)//temp可以为临时对象或者匿名对象
{
Swap(temp);
}
public:
int _size;
int _capacity;
int* _arr;
};
这里通过移动构造,我们可以省略掉临时对象到目标对象的拷贝构造:
此时临时对象有我们所需要的资源,而我们目标对象刚刚创建出来什么都没有,与临时对象交换也就是掠夺临时对象的资源。然后临时对象带着目标对象交换过来的空空如也去销毁。
这时候将右值引用的属性设置为左值的意义也体现出来了,这样我们就可以通过右值引用对右值进行修改。
此时我们没有通过拷贝构造再生成一份资源,而是直接掠夺临时对象的资源。
但其实在没有移动构造的情况下,我们也看不到两次拷贝构造,这是因为编译器对此进行了优化:
在vs2019的debug下,编译器会直接用局部对象去拷贝构造我们的目标对象
在release情况下,甚至连这一次拷贝构造也没有,直接就是一次目标对象的构造。
这个底层其实就是vv是arr的引用,所以可以做到只用一次构造不用多余的拷贝行为。
b)移动赋值
虽然编译器有优化,但是还有一种情况:
这种情况就不是调用拷贝构造而是赋值,赋值也需要进行拷贝,而此时赋值也可以用移动赋值解决拷贝问题:
在以前的c++版本中,下面的拷贝构造可以优化为直接构造,但是这个拷贝赋值还是优化不了
现在在C++11中,拷贝赋值就可以优化为移动赋值
此时就完全解决了对于右值产生的拷贝问题。
5.引用折叠
在C++中我们不能直接定义引用的引用,int& && ra=a。这样的写法是错误的,编译器会进行报错,通过模板或 typedef中的类型操作可以构成引用的引用。而在这里C++对引用的引用给了一个引用折叠的规则,即右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
a)typedef引用的引用--引用折叠
cpp
int main()
{
typedef int& lf;
typedef int&& rf;
int n = 0;
lf& r1 = n;//r1:左值引用的左值引用----左值引用
lf&& r2 = n;//r2:左值引用的右值引用----左值引用
rf& r3 = n;//r3:右值引用的左值引用----左值引用
rf&& r4 = move(n);//r4:右值引用的右值引用---右值引用
return 0;
}
b)类模板引用的引用--引用折叠
1).左值引用类模板
cpp
template<class T>
void f1(T& x)
{
}
int main()
{
int n = 0;
//没有发生折叠--实例化为void f1(int& x)
f1<int>(n);
f1<int>(0);//编译器报错,实例化为void f1(int& 0),左值引用不能引用右值
//发生了折叠--左值引用的左值引用--实例化为void f1(int& x)
f1<int&>(n);
f1<int&>(0);//编译器报错,实例化为void f1(int& 0),左值引用不能引用右值
//发生了折叠--右值引用的左值引用--实例化为void f1(int& x)
f1<int&&>(n);
f1<int&&>(0);//编译器报错,实例化为void f1(int& 0),左值引用不能引用右值
//发生了折叠--左值引用的左值引用--实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);//编译器不报错,因为const 左值引用可以绑定右值
//发生了折叠--右值引用的左值引用--实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);//编译器不报错,因为const 左值引用可以绑定右值
return 0;
}
2).右值引用类模板--万能引用
cpp
template<class T>
void f2(T&& x)
{
}
int main()
{
int n = 0;
//没有发生折叠--实例化为void f2(int&& x)
f2<int>(n);//编译器报错,实例化为void f2(int&& n),右值引用不能引用左值
f2<int>(0);
//发生了折叠--右值引用的左值引用--实例化为void f2(int& x)
f2<int&>(n);
f2<int&>(0);//编译器报错,实例化为void f2(int& 0),左值引用不能引用右值
//发生了折叠--右值引用的右值引用--实例化为void f2(int&& x)
f2<int&&>(n);//编译器报错,实例化为void f2(int&& x),右值引用不能引用左值
f2<int&&>(0);
//发生了折叠--右值引用的左值引用--实例化为void f2(const int& x)
f2<const int&>(n);
f2<const int&>(0);//编译器不报错,因为const 左值引用可以绑定右值
//发生了折叠--右值引用的右值引用--实例化为void f2(const int&& x)
f2<const int&&>(n);//编译器报错,实例化为void f2(const int&& n),const 右值引用不能引用左值
f2<const int&&>(0);
return 0;
}
c)引用的推导
1)
cpp
template<class T>
void Func(T&& t)
{
int a = 10;
T x = a;
x++;
cout << &a << endl;
cout << &x << endl;
}
int main()
{
Func(10);
return 0;
}
在上面的情况中,10为右值,则编译器会将T推导为int,此时就相当于int a=10;int b=10;打印它们的地址会出来不同的结果:
2)
cpp
template<class T>
void Func(T&& t)
{
int a = 10;
T x = a;
x++;
cout << &a << endl;
cout << &x << endl;
}
int main()
{
//Func(10);//右值,T推导为int
int b;
Func(b);
return 0;
}
在这种情况中,b为左值,因此T会被推到为int&,相当于int a=10;int& x=a;此时打印它们的地址会出来相同的结果。
3)
cpp
template<class T>
void Func(T&& t)
{
int a = 10;
T x = a;
x++;
cout << &a << endl;
cout << &x << endl;
}
int main()
{
int b;
//Func(10);//右值,T推导为int
//Func(b);//左值,T推导为int&
Func(move(b));
return 0;
}
b本来是左值,被move过后传给t的是右值类型,因此T会被推导为int,得到int x=a;打印结果会发现地址不同,并且x可以修改。
4)
cpp
int main()
{
int b;
//Func(10);//右值,T推导为int
//Func(b);//左值,T推导为int&
//Func(move(b));//右值,T推导为int
const int cb=1;
Func(cb);
return 0;
}
此时cb的类型为const 左值,那么T被推导为const int&,即const int& x=a;此时编译器会在x++处进行报错,因为x不能修改,但是打印它们的地址会发现相同的结果。
它们地址相同:
5)
cpp
int main()
{
int b;
//Func(10);//右值,T推导为int
//Func(b);//左值,T推导为int&
//Func(move(b));//右值,T推导为int
const int cb=1;
//Func(cb);//const左值,T被推导为const int&,不能修改但是可以求地址
Func(move(cb));
return 0;
}
在这种情况中,cb本是左值,但是move后传给Func函数是const int&&,则T会被推导为const int&&,这种情况x不能修改也不能取地址。
6.完美转发
先来看看下面这段代码:
cpp
void Fun(int& x) { cout <<x<< "左值引用" << endl; }
void Fun(const int& x) { cout<<x << "const 左值引用" << endl; }
void Fun(int&& x) { cout<<x << "右值引用" << endl; }
void Fun(const int&& x) { cout<<x << "const 右值引用" << endl; }
template<class T>
void Function(T&& t)
{
Fun(t);
}
int main()
{
Function(1);//在Function函数中实例化为右值引用,T为int,即int&&
int a = 2;
Function(a);//在Function函数中实例化为左值引用,T为int&,即int&
a++;
int& b = a;
Function(b);//在Function函数中实例化为左值引用,T为int&,即int&
int&& c = 4;
Function(move(c));//在Function函数中实例化为右值引用,T为int&&,即int&&
a+=2;
const int& d = a;
Function(d);//在Function函数中实例化为左值引用,T为const int&,即const int&
const int&& e = 6;
Function(move(e));//在Function函数中实例化为右值引用,T为const int&&,即const int&&
return 0;
}
我们再来看看结果:
结果是这6个引用全都变成了左值引用。
右值引用的变量,其属性是左值引用。
int&& ra=1;在这句代码中,ra是右值引用,但是ra的属性是左值,因此ra可以取地址和被修改。
理解了这一点,我们就能明白为什么上面6个引用最后全部变成了左值引用,这是因为不管t是左值引用还是右值引用,t的属性永远都是左值,因此将t向下传给另外一个函数时,编译器会自动推导为左值引用。
那么如何解决这一点呢?
编译器给出了完美转发
a)完美转发
完美转发其实是一个模板,如果是左值引用,它将直接返回左值引用不加任何修改。如果不是左值引用,它将返回右值引用。它主要还是通过引用折叠的方式来进行使用。
我们使用完美转发再来运行一下上面的代码试试
cpp
#include<utility>
void Fun(int& x) { cout <<x<< "左值引用" << endl; }
void Fun(const int& x) { cout<<x << "const 左值引用" << endl; }
void Fun(int&& x) { cout<<x << "右值引用" << endl; }
void Fun(const int&& x) { cout<<x << "const 右值引用" << endl; }
template<class T>
void Function(T&& t)
{
Fun(forward<T>(t));//完美转发
}
int main()
{
Function(1);//在Function函数中实例化为右值引用,T为int,即int&&
int a = 2;
Function(a);//在Function函数中实例化为左值引用,T为int&,即int&
a++;
int& b = a;
Function(b);//在Function函数中实例化为左值引用,T为int&,即int&
int&& c = 4;
Function(move(c));//在Function函数中实例化为右值引用,T为int&&,即int&&
a+=2;
const int& d = a;
Function(d);//在Function函数中实例化为左值引用,T为const int&,即const int&
const int&& e = 6;
Function(move(e));//在Function函数中实例化为右值引用,T为const int&&,即const int&&
return 0;
}
此时再来看看结果:
如果这篇文章有帮助到你,请留下您珍贵的点赞、收藏+评论,这对于我将是莫大的鼓励!学海无涯,共勉!😘😊😗💕💕😗😊😘