文章目录
- 1.列表初始化
- 1.1 C++98传统的{}
- 1.2 C++11中的{}
- 1.3 C++11中的std::initializer_list
- 2.右值引用和移动语义
- 2.1左值和右值
- 2.2左值引用和右值引用
- 2.3 引用延长生命周期
- 2.4左值和右值的参数匹配问题
- 2.5右值引用和移动语义的使用场景
- 2.5.1左值引用主要使用场景
- 2.5.2移动构造和移动赋值
- 2.5.3右值引用和移动语义解决传值返回问题
- 2.5.3.1右值对象构造,只有拷贝构造,没有移动构造的场景->拷贝构造
- 2.5.3.2右值对象构造,有拷贝构造,也有移动构造的场景->移动构造
- 2.5.3.3右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景->拷贝构造和拷贝赋值
- 2.5.3.4右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景->移动构造和移动赋值
- 2.5.3.5 移动构造和移动赋值的意义
- 3.面试经常问到的
1.列表初始化
1.1 C++98传统的{}
C++98的{}主要支持数组和结构体的初始化
struct Hello
{
int _a;
int _b;
};
int main()
{
int a[] = { 1,2,3,4,5 };
int b[5] = { 0 };
Hello c = { 1,2 };
return 0;
}
1.2 C++11中的{}
- C++11规定了一切对象都可以用{}初始化,{}初始化也叫列表初始化
- 内置类型可以用{}初始化
- C++98支持单参数的类型转换,也可以不用{}
Date d3 = { 2025 };// C++11
Date d4 = 2025;// C++98
string s = "11111";
// 单参数的支持隐式类型转换
// 不支持,只有{}初始化才能省略=
// Date d7 2025;
vector<Date> v;
v.push_back(d5);// 有名对象
v.push_back(Date( 2025,1,2 ));// 匿名对象
v.push_back({ 2025,1,1 });// {}
map<string, string> dict;
dict.insert({ "string","字符串" });
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 1. 内置类型
int x1 = { 2 };
// 2.自定义类型,构造,隐式类型转换,构造 + 拷贝构造
// 优化为了直接构造
Date d1 = { 2025,1,12 };
// 直接调用构造
Date d10(2025,1,1);
// d2引用的是{2025,1,15}构造的临时对象
const Date& d2 = {2025,1,15};
// 可以省略掉=
Hello p{ 1,2 };
int a{ 2 };
Date d5{ 2025,1,1 };
const Date& d6{ 2025,1,2 };
return 0;
}
1.3 C++11中的std::initializer_list
- ⼀个vector对象,我想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持,因为参数的个数不同
vector v1 ={1,2,3};
vector v2 = {1,2,3,4,5};- C++11库中提出了⼀个std::initializer_list的类底层是一个数组,将数据拷贝过来,std::initializer_list中有两个指针,分别指向数组的开始和结束,
- std::initializer_list支持迭代器遍历
- 有了std::initializer_list就可以进行多个值的初始化,比如{x1,x2,x3…}
vector<int> v1 = {1,2,3,4};
vector<int> v2 = {10,20,30,1,2,3,4};
const vector<int>& v3 = {10,20,30,1,2,3,4};
// {}初始化可以省略=
vector<int> v1{1,2,3,4};
vector<int> v2{10,20,30,1,2,3,4};
const vector<int>& v3{10,20,30,1,2,3,4};
// initializer_list 构造
vector<int> v4({1,2,3,4,5,6});
// a数组的指针和il1的两个指针都在栈上,数组也在栈上
// a数组和这两个指针的位置非常接近
initializer_list<int> il1 = {1,2,3};
int a[] = {1,2,3};
- map的initializer_list的初始化
外层是initializer_list,内层是pair的键值对的初始化
// initializer_list + {}pair初始化的隐式类型转换
map<string, string> dict = { {"sort","kai"},{"string","men"} };
2.右值引用和移动语义
1. C++98中的引用是左值引用,C++11中有了右值引用
type & x 左值引用
type && y 右值引用
2.1左值和右值
1.左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
2.右值也是⼀个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
3.区分左值和右值的重要特点就是是否可以取地址
int main()
{
// 1.左值 p,b,c,*p,s,s[0]
// 变量
int* p = new int(0);
int b = 1;
const int c = b;
// 解引用
*p = 10;
// 有名对象
string s("111111");
// 函数的返回类型是引用
s[0] = 'x';
// 2.右值 10 x + y fmin(x,y) string("111")
// 右值通常在寄存器中或者#define直接替换了
// 所以不能够直接取地址
int x = 0, y = 0;
// 字面量常量
// 10
// 表达式的返回值
x + y;
// 函数的返回值是存在寄存器中的或是临时对象的拷贝
fmin(x, y);
// 匿名对象
string("111");
return 0;
}
2.2左值引用和右值引用
1. Type& r1 = x; Type&& rr1 = y;
第一个语句就是左值引用,左值引用就是给左值取别名;第二个就是右值引用,右值引用就是给右值取别名
左值引用取别名
int*& r1 = p;
int& r2 = b;
int& r6 = *p;
const int& r3 = c;
string& r4 = s;
char& r5 = s[0];
右值引用取别名
int&& p1 = 10;
int&& p2 = x + y;
int&& p3 = fmin(x, y);
string&& p4 = string("111");
2. 左值引用不能直接引用右值,但是const左值引用可以引用右值,因为右值通常具有常性
对上面的解释
1.右值到左值要权限放大(但是权限不能放大)
2.权限可以平移
// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
3.右值引用不能直接引用左值,但是右值引用可以引用move(左值)
// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
// move的底层其实是强制类型转换
// 把左值强转为右值(s有名对象为左值)
string&& rrx5 = (string&&)s;
move实际是一个函数模版,其实也涉及到了引用折叠,后面会细讲
// (左值)强转之后不会改变本身的属性
// 强转之后再使用左值,还是左值本身的属性
// 用的只是临时对象
// b、r1、rr1都是变量表达式,都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;
int i = 1;
int* pi = (int*)i;
i还是int类型不会改变类型
4. 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值
左值引用的属性是左值
右值引用的属性也是左值
int&& rr1 = 10;
引用完rr1的属性变为左值
rr1的属性是左值,所以不能被右值引用绑定,除非move一下
int& rr6 = rr1;// 不报错
int&& rrx6 = rr1;// 报错
int&& rrx6 = move(rr1);
5. 在语法层面,左值引用和右值引用都是取别名,不开空间;在底层都是指针实现的
2.3 引用延长生命周期
1. 可以延长临时对象和匿名对象的生命周期,临时对象和匿名对象的生命周期只在当前的一行
std::string s1 = "Test";
const左值引用延长生命周期
const std::string& r1 = s1 + s1;
右值引用延长生命周期
std::string&& r2 = s1 + s1;
延长到r1和r2使用完
2.4左值和右值的参数匹配问题
1.C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
template<class T>
void func(const T& x)
{}
传左值,权限缩小,左值传左值
传右值,权限平移,右值传const左值
2. C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会
匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用),也就是编译器会调用最匹配的类型,如果没有右值引用,会调用const左值引用
void f(int& x)
{
std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&)
f(std::move(i)); // 调用 f(int&&)
return 0;
}
3. 右值引用变量在用于表达式时属性是左值
右值引用本身的属性是左值
int&& x = 1;
f(x);调用f(int& x)
f(std::move(x));调用f(int&& x)
2.5右值引用和移动语义的使用场景
2.5.1左值引用主要使用场景
1.左值引用的主要场景是在函数中,左值引用传参和左值引用传返回值减少拷贝,同时左值引用传参在函数中修改形参可以改变实参,传引用返回可以修改返回对象
2.左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如addStrings和generate函数里面返回的是局部对象,C++98中的解决方案只能是被迫使用输出型参数解决(传值返回)。
3.那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。
class Solution
{
public:
// 传值返回需要拷⻉
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
};
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;
}
};
2.5.2移动构造和移动赋值
右值引用如何解决返回的对象是局部变量的问题?
1. 移动构造函数是一种构造函数,类似拷贝构造,要求第一个参数必须是类类型的引用,第一个参数必须是右值引用,拷贝构造是左值引用,如果有其他参数,必须有缺省值
2. 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用
3. 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,它的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率
void swap(string& ss)
{
::swap(_str, ss._str);
::swap(_size, ss._size);
::swap(_capacity, ss._capacity);
}
// 移动构造
string(string&& s)
{
// 右值引用本身具有左值的属性
// 因为要和this交换,左值可以修改值,右值不可以修改值
cout << "string(string&& s) -> 移动构造" << endl;
swap(s);
// 交换需要的资源,转移掠夺你的资源
}
int main()
{
string s1("111");
string s2 = s1;
string s3 = string("222");
// s1本来的地址是xxxa510,现在s5的地址是a510
// 说明移动构造掠夺了s1的资源给了s5
string s5 = move(s1);
return 0;
}
2.5.3右值引用和移动语义解决传值返回问题
2.5.3.1右值对象构造,只有拷贝构造,没有移动构造的场景->拷贝构造
1. vs2019debug下,左边为不优化的场景,传值返回会发生拷贝构造,会产生临时对象,临时对象再拷贝构造ret,右边的场景是直接优化为一次拷贝构造
2.vs2019release和vs2022下,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。本质就是str对象本质是ret对象的引用,底层是指针,str对象和ret对象的地址是一样的,所以就没有拷贝,没有构造,只有引用了
3. Linux下也是和2022一样的,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造优化,就是两次拷贝构造
2.5.3.2右值对象构造,有拷贝构造,也有移动构造的场景->移动构造
1. 传值返回会被编译器识别为右值,图2展示了vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。
2. 需要注意的是在vs2019的release和vs2022的debug和release,会直接将str对象的构造,str移动构造临时对象,临时对象移动构造ret对象,合三为一,变为直接构造。
2.5.3.3右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景->拷贝构造和拷贝赋值
1. 左图不优化,一次拷贝构造,一次拷贝赋值,右图优化为直接拷贝赋值,str是临时对象的别名,直接用临时对象拷贝赋值ret
2.5.3.4右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景->移动构造和移动赋值
1. 需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。(临时对象的生命周期只存在于当前行,赋值完之后就会销毁)
2. 如果是传值返回并且是右值对象,会移动你的资源,不用拷贝了
2.5.3.5 移动构造和移动赋值的意义
1. 深拷贝,使用移动构造和移动赋值,不需要进行大量的拷贝,只需要交换指针即可
2.浅拷贝,使用移动构造和移动赋值,就是锦上添花,其实传值返回要拷贝的很少,比如Date日期类,拷贝12个字节,pair< int , int >拷贝8个字节
3.面试经常问到的
1. 左值引用和右值引用的最终目的是减少拷贝提高效率
2.左值引用还可以修改参数和返回值,比如输出型参数(在函数体内修改参数可以影响实参)和operator
3. 左值引用的不足:
部分函数返回场景,只能传值返回,不能左值引用返回,比如当前函数的局部对象,出了当前函数的作用域,生命周期就到了,就销毁了,不能左值引用返回,只能传值返回
解决方案1,2,3:
1.不用返回值,用输出型参数解决问题,不足:牺牲了可读性
2.编译器的优化
3.右值引用和移动语义
更早的编译器只能使用这种输出型参数,避免大量拷贝数据
class Solution
{
public:
void generate(int numRows, vector<vector<int>>& vv)
{
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];
}
}
}
};
int main()
{
// vector<vector<int>> ret = Solution().generate(100);
vector<vector<int>> ret;
Solution().generate(100, ret);
// 输出型参数可以解决这个问题,传引用过去,就不需要拷贝了
return 0;
}