C++11右值与列表初始化
1.列表初始化
C++98传统的{}
C++98中一般数组和结构体可以用{}进行初始化。
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中的{}
C++11以后统一初始化方式,想要实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化
内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后就只有构造
{}初始化过程可以省略=
代码示例
#include<iostream>
#include<vector>
using namespace std;
struct Point
{
int _x;
int _y;
};
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()
{
//C++11支持
//内置类型支持
int x1 = { 2 };
//自定义类型支持
//这里本质是用{2025,1,1}构造一个Date临时对象
//临时对象再去拷贝构造d1,编译器优化后会合二为一变成{2025,1,1}直接构造
Date d1 = { 2025,1,1 };
//这里d2引用的是{2024,7,25}构造的临时对象
const Date& d2 = { 2024,7,25 };
//C++98支持单参数类型转换,也可以不用{}
Date d3 = { 2025 };
Date d4 = 2025;
//可以省略掉=
Point p1{ 1,2 };
int x2{ 2 };
Date d6{ 2024,7,25 };
const Date& d7{ 2024,7,25 };
//只有{}初始化才能省略=
//不支持 Date d8 2025
vector<Date> v;
v.push_back(Date(2025, 1, 1));
v.push_back(d1);
//对于匿名对象和匿名对象传参,这里{}相对好
v.push_back({ 2025,1,1 });
return 0;
}
C++11中std::initializer_list
上面初始化很方便,当对于容器的初始化还是不方便,像vector对象,想用N个值去构造初始化,就得实现很多个构造函数才能支持
vector<int> v1 = {1,2,3};vector<int> v2 = {1,2,3,4,5};
// STL 中的容器都增加了⼀个 initializer_list 的构造vector (initializer_list<value_type> il, const allocator_type& alloc =allocator_type ());list (initializer_list<value_type> il, const allocator_type& alloc =allocator_type ());map (initializer_list<value_type> il, const key_compare& comp =key_compare (), const allocator_type& alloc = allocator_type ());// ...template < class T >class vector {public :typedef T* iterator;vector (initializer_list<T> l){for ( auto e : l)push_back (e)}private :iterator _start = nullptr ;iterator _finish = nullptr ;iterator _endofstorage = nullptr ;};// 另外,容器的赋值也⽀持 initializer_list 的版本vector& operator = (initializer_list<value_type> il);map& operator = (initializer_list<value_type> il);# include <vector># include <string># include <map>using namespace std;int main (){std::initializer_list< int > mylist;mylist = { 10 , 20 , 30 };cout << sizeof (mylist) << endl;// 这⾥ begin 和 end 返回的值 initializer_list 对象中存的两个指针// 这两个指针的值跟 i 的地址跟接近,说明数组存在栈上int i = 0 ;cout << mylist. begin () << endl;cout << mylist. end () << endl;cout << &i << endl;// {}列表中可以有任意多个值// 这两个写法语义上还是有差别的,第⼀个 v1 是直接构造,// 第⼆个 v2 是构造临时对象 + 临时对象拷⻉ v2+ 优化为直接构造vector< int > v1 ({ 1 , 2 , 3 , 4 , 5 });vector< int > v2 = { 1 , 2 , 3 , 4 , 5 };const vector< int >& v3 = { 1 , 2 , 3 , 4 , 5 };// 这⾥是 pair 对象的 {} 初始化和 map 的 initializer_list 构造结合到⼀起⽤了map<string, string> dict = { { "sort" , " 排序 " }, { "string" , " 字符串 " }};/ / initializer_list版本的赋值⽀持v1 = { 10 , 20 , 30 , 40 , 50 };return 0 ;}
右值引用和移动语义
C++98就有引用,在C++11新增了右值引用语法特性,C++11之后之前的学的就叫左值引用。无论左值还是右值引用,都是给对象取别名。
左值和右值
左值是一个表示数据的表达式,一般是有持久状态,存储在内存中,可以获取1它的地址,左值可以出现在赋值符号的左边,也可以出现在右边。定义时const修饰符后的左值,不能给它赋值,可以取地址。
右值也是一个表示数据的表达式,要么是字面值常量,要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现在左边,右值不能取地址。
左右值核心区别在于能不能取地址。
代码示例
#include<iostream>
using namespace std;
int main()
{
//以下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';
cout << &s << endl;
cout << (void*)&s[0] << endl;
//右值不能取地址
double x = 1.1, y = 2.2;
10;
x + y;
fmin(x, y);//参数为浮点数,返回较小的浮点数
string("111111");
return 0;
}
补充:用(void*)强制转换是为了打印出地址,没有就会打印出值
左值引用和右值引用
Type% r1=x;Type&& rr1=y; 第一个语句就是左值引用,左值引用就是给左值取别名,右值引用就是给右值取别名。
左值引用不能直接引用右值,但是const左值可以引用右值。
右值引用不能直接引用左值,但是右值可以引用move(左值)
template <class T> typename remove_reference<T>::type&& move (T&& arg);
move是库里面的一个函数模板,本质内部是进行强制类型转换。
需要注意变量表达式都是左值属性,也就说一个右值被右值引用绑定后,右值引用变量表达式的属性是左值。
代码示例
#include<iostream>
using namespace std;
int main()
{
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("11111");
s[0] = 'x';
double x = 1.1, y = 2.2;
//左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
//右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;//表达式返回值是临时变量
double&& rr3 = fmin(x, y);//返回值临时变量
string&& rr4 = string("11111");//匿名对象,活不过下一行
//左值引用不能直接引用右值,但是const左值可以引用右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
//右值引用不能直接用于左值,但是可以引用move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;//跟move(s)一样
//int&& r1=10,此时rr1的属性是左值,所以不能再被右值引用绑定,除非move
int& r6 = rr1;//因为rr1的属性是左值所以左值引用不用加const就可以引用rr1
//int&& rrx6=rr1 这里是不行的,右值不能直接引用左值
int&& rrx6 = move(rr1);
return 0;
}
引用延长生命周期
右值引用可以为临时变量延长生命周期,const的左值引用也可以延长临时对象生命周期,但是这些对象无法修改。
代码示例
int main()
{
std::string s1 = "Test";
const std::string& r2 = s1 + s1;//const的左值引用延长了生命周期,但是不能被修改
std::string&& r3 = s1 + s1;//右值引用延长生命周期
r3 += "Test";//能通过非const
return 0;
}
左值和右值的参数匹配
在C++98中,实现一个const左值引用作为参数的函数,则实参传递左值和右值都可以匹配。
C++11后,分别重载左值引用,const左值引用,右值引用作为形参的f函数,可以试验前面的结论
代码示例
#include<iostream>
using namespace std;
void f(int& x)
{
cout << "左值引用重载\n" << endl;
}
void f(const int& x)
{
cout << "const左值引用重载\n" << endl;
}
void f(int&& x)
{
cout << "右值引用重载\n" << endl;
}
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(move(i));//f(int&&)
int&& x = 1;
f(x);//右值引用在用于表达式时是左值
f(move(x));//f(int&&)
return 0;
}
在 C++ 中,右值引用可以延长生命周期的原因在于它们专门设计用来处理临时对象(右值),而左值引用则是用来引用可以持续存在于内存中的对象(左值)。以下是为什么右值引用可以延长生命周期,而左值引用不可以的详细解释:
### 右值引用延长生命周期的原因:
1. **临时对象的生命周期**:
- 临时对象(右值)通常在表达式求值结束后立即销毁。右值引用通过绑定到这些临时对象,可以延长它们的生命周期,使得它们可以被多次使用,直到右值引用本身被销毁或再次赋值。2. **移动语义**:
- 右值引用是实现移动语义的关键。移动语义允许资源(如动态内存、文件句柄等)从一个地方“移动”到另一个地方,而不是复制。这种转移是通过右值引用实现的,它允许临时对象的资源被转移给另一个对象,从而延长了资源的生命周期。3. **优化和性能**:
- 使用右值引用可以避免不必要的复制,提高程序性能。例如,在函数返回临时对象时,如果使用右值引用,可以直接将资源转移给调用者,而不是创建一个临时对象的副本。### 左值引用不延长生命周期的原因:
1. **持久性**:
- 左值引用是设计用来引用具有持久存储的对象。它们指向对象的身份,而不是值。左值引用的目的是提供对对象的直接访问,而不是延长其生命周期。2. **别名问题**:
- 如果左值引用可以延长临时对象的生命周期,那么它可能会引入别名问题(aliasing)。例如,如果两个左值引用指向同一个对象,对其中一个引用的修改可能会影响另一个引用,这在很多情况下是不希望发生的。3. **语义清晰**:
- 左值引用和右值引用有不同的语义。左值引用意味着对对象的持久引用,而右值引用意味着对临时对象的引用。这种区分有助于编译器优化和程序员理解代码。4. **避免悬挂引用**:
- 如果左值引用可以延长临时对象的生命周期,那么在对象被销毁后,引用仍然存在,这将导致悬挂引用(dangling reference),即引用指向一个已经不再有效的内存区域。总结来说,右值引用可以延长生命周期是因为它们专门设计用来处理临时对象,并且是实现移动语义的关键。而左值引用不延长生命周期是因为它们设计用来引用持久存储的对象,并且延长临时对象的生命周期可能会导致别名问题和悬挂引用等问题。这种设计使得左值引用和右值引用在语义上保持清晰,有助于编译器优化和代码理解。
右值引用和移动语义的使用场景
左值引用主要使用场景
左值引用主要使用场景是在函数中左值引用传参和左值引用返回值时减少拷贝,同时还可以修改实参和修改返回对象的值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如addStrings和generate函数。而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;
}
};
移动构造和移动赋值
移动构造函数是一种构造函数,类似于拷贝构造,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其它参数,额外参数必须有缺省值。
移动赋值是一个赋值运算符的重载,它跟拷贝赋值构成函数重载,类似拷贝函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用类型,本质是要抢夺引用的右值对象资源,而不是像拷贝构造和拷贝赋值去拷贝资源。
代码示例
在参数为string&& s的移动构造里,是通过swap函数交换俩个指向的内容,效率是比拷贝高的,同样移动赋值也是交换指向的内容。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<string.h>
#include<algorithm>
using namespace std;
namespace bit
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷⻉赋值" <<
endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity *
2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
int main()
{
bit::string s1("xxxxx");
// 拷⻉构造
bit::string s2 = s1;
// 构造+移动构造,优化后直接构造
bit::string s3 = bit::string("yyyyy");
// 移动构造
bit::string s4 = move(s1);
cout << "******************************" << endl;
return 0;
}
右值引用和移动语义解决传值返回问题
场景一是匿名对象,参数为匿名对象就会走移动拷贝,直接交换指向内容,场景二是先创建了一个对象,然后在用移动赋值。
namespace bit
{
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());
cout << "******************************" << endl;
return str;
}
}
// 场景1
int main()
{
bit::string ret = bit::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2
int main()
{
bit::string ret;
ret = bit::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
补充
临时变量和匿名对象区别在一个是自己创建的,一个是编译器生成的
对于一个函数内部变量的生命周期,如果给这个变量改变生命周期是不行的,因为函数结束后会销毁栈区,作为这个栈区的内部变量也会销毁,被跟着带走了
延长生命周期前提是存储空间都在,引用对象的存储空间不在就无法延长生命周期
把左值move后,就给这个左值赋予被抢夺资源的标签