目录
前言
列表初始化
std::initializer_list
右值引用和移动拷贝
左值和右值
左值引用和右值引用的区别
万能引用(引用折叠)
完美转发
默认成员函数控制
列表初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
//也算是兼容C
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11扩大了{ }(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4] { 0 };
return 0;
}
创建对象时也可以使用列表初始化方式调用构造函数初始化:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); // old style
// C++11支持的列表初始化,下面两个会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
return 0;
}
std::initializer_list
initializer_list是C++11新增加的容器,底层就是一个数组。
int main()
{
//这里初始化其实是构造初始化,先不在意底层
initializer_list<int> l = {0, 1, 2, 34};
initializer_list<int>::iterator it = l.begin();
while (it != l.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
initializer_list平常并无作用,但是C++11对{}有特殊处理,它真正的应用场景是用于构造其它容器
//C++11为vector、map等容器都提供了initializer_list做参数的构造
//原理也很简单,先将{}转变成initializer_list,然后遍历initializer_list尾插即可
int main()
{
vector<int> v = { 0, 1, 2, 3, 4, 5 };
for (auto e : v) cout << e << " ";
cout << endl;
map<int, int> m = { {0, 1}, {1, 0}, {10, 9} };
for (auto kv : m)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
右值引用和移动拷贝
左值和右值
首先先分清什么是左值,什么是右值——不是在=左边的就叫左值,在=右边的就叫右值
左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。
最显著的特点就是左值可以被取地址,右值不能被取地址(是否真的存储)
左值引用就是给左值的引用,给左值取别名。
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
右值不能被取地址,例如字面常量、表达式返回值,函数返回值(左值引用返回不是右值,传值返回才是右值)
右值引用就是对右值的引用,给右值取别名
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
左值引用和右值引用的区别
1.左值引用只能引用左值,不能引用右值;右值引用同理
2.const 左值引用可以引用左值也可以引用右值,但是反过来就不行
3.右值引用可以引用move后的左值
左值引用的使用场景: 做参数和做返回值都可以提高效率
为什么要有右值引用呢,主要是为了弥补左值引用的不足
场景:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。
例如:zzb::string to_string(int value)函数中可以看到,这里只能使用传值返回,传引用返回当该函数栈帧被销毁的时候该位置有可能被其他变量所占用,存在很大的问题,但是传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造),效率很低
zzb::string to_string(int x)
{
bit::string ret;
while (x)
{
int val = x % 10;
x /= 10;
ret += ('0' + val);
}
内置类型的右值叫做纯右值,将自定义的右值称为将亡值
我们先来看下面这两种情况
int main()
{
bit::string s1 = bit::to_string(1234);
bit::string s2;
s2 = bit::to_string(2345);
return 0;
}
下面的那个:
str拷贝给临时对象,临时对象拷贝构造给ret2
自定义创建时调用一次构造函数
函数栈帧销毁的时候,创建临时对象的时候调用拷贝构造
返回值作为右值给左值赋值的时候又调用拷贝构造
上面的减少的一次可以理解为没有产生临时对象了,直接赋值给ret2,这时编译器优化后的结果
老一点的编译器可能没有这样优化
如果是浅拷贝的类那还没事,但如果是深拷贝的类,短短一个赋值操作就要深拷贝三次,代价太大了,反正这个函数栈帧里的空间都要被销毁的,如果把它拿过来直接用的话,是不是就很方便了
所以就有了移动构造和移动赋值,这时候右值引用的价值就体现出来了,可以区分左值和右值了
如果参数传的是将亡值,则直接将资源交换,不仅减少了拷贝,还将不要的资源转移到了即将要销毁的空间,过后自动销毁,一举两得
移动构造——不用开辟空间,直接交换得到目标值,目标即将被销毁的时候使用
右值引用是间接起作用的,对深拷贝的类有意义
左值引用是直接起作用,传引用返回
右值被右值引用后的属性是左值
右值不能修改,但是被右值引用之后需要被修改——>属性变成左值(不能修改怎么转移资源,也就无法实现移动构造和移动拷贝)
万能引用(引用折叠)
函数模板下才有用!
可以接受左值和右值,左值的时候就相当于将两个&折叠成一个,所以也叫引用折叠
完美转发
按照上面的写法写即可
可以保持原有属性(右值被右值引用后属性变成左值)
默认成员函数控制
这里为什么条件那么严格呢?
其实这三个一般都是绑定在一起的,因为写了析构一般都设计深拷贝,所以也就要自己写拷贝构造和拷贝赋值,不然会出现问题