【C++】C++11新特性 (上)
目录
- 一、C++11的发展历史
- 二、C++11
- 2.1 列表初始化
- 2.2 右值引用和移动语义
- 2.2.1 左值 和 右值
- 2.2.2 左值引用和右值引用
- 2.2.3 左值 和 右值的参数匹配
- 2.2.4 移动构造和移动赋值
- 2.3 右值引用和移动语义在传参中的提效
- 2.4 类型分类
- 2.5 引用折叠
- 2.5.1 万能引用

个人主页:矢望
专栏:C++、Linux、C语言、数据结构
一、C++11的发展历史
C++11 是 C++ 的第二个主要版本,并且是从 C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象机制和库组件。在它由 ISO 在 2011 年 8 月 12 日采纳前,人们曾使用名称C++0x称呼,因为它曾被期待在 2010 年之前发布。然而C++03 与 C++11 期间花了 8 年时间,故而这是迄今为止最长的版本间隔。从那时起,C++ 有规律地每 3 年更新一次。

二、C++11
2.1 列表初始化
C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
列表初始化内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化之后变成直接构造。
注意:列表初始化{},可以省略=。

这里我们分别写了一个简单的结构体和一个类:
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++98⽀持的int a1[] = { 1, 2, 3, 4, 5 };int a2[5] = { 0 };Point p = { 1, 2 };// C++11⽀持的int x = { 1 };int x1{ 3 };Date d = { 2025, 1, 1 }; // 没有调用拷贝构造Date d1{ 2025, 1, 2 }; // 没有调用拷贝构造// 这⾥d2引⽤的是{ 2024, 10, 31 }构造的临时对象const Date& d2 = { 2024, 10, 31 }; // 没有调用拷贝构造// 需要注意的是C++98⽀持单参数时类型转换,也可以不⽤{}Date d3 = { 2025 }; // 没有调用拷贝构造Date d4 = 2025; // 没有调用拷贝构造vector<Date> s;// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐s.push_back(Date{ 2025, 1, 1 });return 0;
}

注意:不要将列表初始化和initializer_list构造弄混淆了。

2.2 右值引用和移动语义
C++98的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
2.2.1 左值 和 右值
左值是可以取地址的表达式,有持久的状态,通常有名字;右值不能取地址的表达式,通常是临时对象或字面量。 这就是它们最简单直接的区分方法,它们的核心区别就是能不能取地址。
左值可以出现在赋值符号的左右两边,右值不能出现在赋值符号的左边。
左值示例:
void test_lvalues()
{// 变量int a = 10; // a是左值// 数组元素int arr[5];arr[0] = 1; // arr[0]是左值// 类对象成员struct Point { int x, y; };Point p;p.x = 10; // p.x是左值// 解引用int* ptr = &a;*ptr = 20; // *ptr是左值// 字符串字面量(特殊情况)const char* str = "hello"; // "hello"是左值// &"hello"; // ✓ 可以取地址
}
右值示例:
int getValue() { return 100; }
void test_rvalues()
{// 字面量int a = 42; // 42是右值double b = 3.14; // 3.14是右值bool flag = true; // true是右值// 算术表达式int x = 10, y = 20;int sum = x + y; // (x + y)是右值// 函数返回值(非引用)int val = getValue(); // getValue()是右值// 临时对象std::string s = std::string("temp"); // std::string("temp")是右值std::vector<int> v = std::vector<int>{ 1,2,3 }; // 临时vector是右值
}
2.2.2 左值引用和右值引用
Type& r1 = x; Type&& rr1 = 10;第一个语句就是左值引用,左值引用就是给左值取别
名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
左值引用不能直接引用右值,但是const左值引用可以引用右值;右值引用不能直接引用左值,但是右值引用可以引用move(左值)。move是库里面的一个函数模板,本质内部是进行强制类型转换,它并不移动任何东西,只是一个类型转换工具。
int main()
{int x = 5;// const左值引用可以引用右值const int& cr1 = 10;const int& cr2 = x + 5;int y = 10;// 没有move时:右值引用不能绑定左值// int&& r1 = y;// 使用move后:将左值转为右值引用int&& r2 = move(y);cout << "y = " << y << endl;cout << "r2 = " << r2 << endl;r2 = 20;cout << "y = " << y << endl;return 0;
}

细节1:右值引用可用于为临时对象延长生命周期。如上图改变r2,y也改变
int&& rr1 = 10; // 这个10的临时对象生命周期被延长了
字面量 10 原本是临时对象(右值),通过右值引用 rr1 绑定后,其生命周期被延长到 rr1 的作用域结束。
细节2:变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。
int&& rr1 = 10; // rr1本身是左值!// 验证:
int* p = &rr1; // 可以取地址 - 证明rr1是左值
// int* p2 = &10; // 不能取字面量的地址// 函数重载测试
void process(int& val) { cout << "lvalue\n"; }
void process(int&& val) { cout << "rvalue\n"; }process(rr1);
process(10);

根据细节2,也就是说右值引用不能进行引用的传递,左值引用是可以的。
int i = 0;
int& r1 = i; // r1 是 i 的别名(左值引用绑定左值)
int& r2 = r1; // r2 是 r1 的别名,也就是 i 的别名int&& rr1 = 10; // rr1 绑定到字面量10(右值引用绑定右值)// 错误:rr1 本身是左值,不能绑定到右值引用
// int&& rr2 = rr1;
2.2.3 左值 和 右值的参数匹配
C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
void f(const int& x)
{cout << "到 const 的左值引用重载 f(" << x << ")\n";
}int main()
{int i = 1;const int ci = 2;f(i);f(ci);f(3);f(std::move(i));return 0;
}

C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。
void f(int& x)
{cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{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;
}

2.2.4 移动构造和移动赋值
移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似用赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
我们通过下面的string类来讲清楚它们。
namespace STR
{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){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}string(const string& s){cout << "string(const string& s) -- 拷贝构造" << endl;_str = new char[s._capacity + 1];memcpy(_str, s._str, s._size + 1);_size = s._size;_capacity = s._capacity;}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(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){return _str[pos];}void reserve(size_t n){cout << "_capacity:" << _capacity << endl;if (n > _capacity){char* str = new char[n + 1];memcpy(str, _str, _size + 1);delete[] _str;_str = str;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;reserve(newcapacity);}_str[_size] = ch;_size++;_str[_size] = '\0';}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};
}
上面是string的实现代码。
int main()
{STR::string s1("xxxxx");// 拷⻉构造STR::string s2 = s1;// 构造 + 拷贝构造,编译器优化为直接构造STR::string s3 = STR::string("yyyyy");STR::string s4 = move(s1); // 将左值转换为右值引用return 0;
}
如果想要关闭编译器拷贝优化,在Linux下,可以在编译程序时添加指令-fno-elide-constructors,这样就把拷贝优化关闭了,这里不再进行演示。

没有移动构造时,这里的3,4句都会发生拷贝构造,尤其对于3而言,拷贝构造之后,原来的临时对象就销毁了,血亏,对于4,如果s1之后没有用了,我对它进行了拷贝构造,但之后s1就没有作用了,依旧是亏的。
加入移动构造:
// 移动构造
string(string&& s)
{cout << "string(const string&& s) -- 移动构造" << endl;swap(s);
}


没有移动赋值之前执行下面的代码:
int main()
{STR::string s5("yyyyyyyyyyyyyyyyy");STR::string s3 = s5;s5 = STR::string("yyyyy");return 0;
}
加入移动赋值:
// 移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;
}

移动构造和移动赋值的本质是去窃取引用的右值对象的资源。

对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,它的本质是要窃取引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。
关于右值引用为什么swap可以引用右值:void swap(string& s),我们在移动构造和移动赋值中都用到了swap,为什么右值可以被左值引用呢?因为右值引用变量的属性是左值。这个细节就是为这里服务的。
同时,在运行结果上来看,编译器的优化已经做的很好了,甚至在某些场景下,编译器的优化比没有优化时,使用移动构造、移动赋值运行的结果更超前,更加的效率。那么移动构造、移动赋值还有意义吗?
首先,主流编译器的优化通常走在C++标准前面,其次,所有上面产生的优化,都不是C++标准规定,具体取决于编译器,编译器可实现可不实现。但是C++有了移动构造、移动赋值之后,传值返回等场景的代价一定很低,不依赖编译器的优化。
2.3 右值引用和移动语义在传参中的提效
当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象;当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上。
以STL中的list为例:



库里面的容器,基本上插入,构造,赋值都会重载移动语义来提高效率。
当然对右值引用的使用需要小心,以下面为例:
string s1 = "hello world";
string s2(move(s1));
这段代码执行完后,s1中的数据就被窃取了,s1就失去它原来的数据了。当然这里注意一个盲区,不是move之后s1中的数据就没了,move做的就是数据的强转,(T&&)s1的工作,其中的数据是调用移动构造交换走的。
2.4 类型分类
C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
纯右值是指那些字面值常量或求值结果相当于字面值或者是一个不具有名称的临时对象。纯右值和将亡值是C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。
将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如move(x)、static_cast<X&&>(x)。
泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

官方文档:值类别 - cppreference.com
2.5 引用折叠
C++中不能直接定义引用的引用:
int& && r = i;
这样写会报错的,但是C++支持通过模板或 typedef中的类型操作可以构成引用的引用。
通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
typedef int& lref;
using rref = int&&; // 和上面等价int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
为了能够更好的理解,这里给出两个示例:
示例1:
//f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{ }int main()
{int n = 1;// 没有折叠->实例化为void f1(int& x)f1<int>(n);//f1<int>(0); // 左值引用,报错// 折叠->实例化为void f1(int& x)f1<int&>(n);//f1<int&>(0); // 左值引用,报错// 折叠->实例化为void f1(int& x)f1<int&&>(n);//f1<int&&>(0); // 左值引用,报错// 折叠->实例化为void f1(const int& x)f1<const int&>(n);f1<const int&>(0); // 可接收右值// 折叠->实例化为void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0); // 可接收右值return 0;
}
这里函数模板中的类型是T&,那么无论传递的是左值还是右值,f1最终都会是左值引用。
示例2:
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{ }int main()
{int n = 1;// 没有折叠->实例化为void f2(int&& x)//f2<int>(n); // 右值引用,报错f2<int>(0);// 折叠->实例化为void f2(int& x)f2<int&>(n);//f2<int&>(0); // 左值引用,报错// 折叠->实例化为void f2(int&& x)//f2<int&&>(n); // 右值引用,报错f2<int&&>(0);return 0;
}
f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,它传递左值时就是左值引用,传递右值时就是右值引用,所以也叫做万能引用。
2.5.1 万能引用
万能引用的使用:
template<class T>
void Function(T&& t)
{int a = 0;T x = a;cout << "&a = " << &a << endl;cout << "&x = " << &x << endl << endl;
}
上面的函数就是使用了万能引用,接下来,我们在不同场景调用一下它。
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值

这样的就是右值引用,T推导出来是int,所以两个的地址不同。
int a = 1;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值

这样的就是左值引用,T推导出来是int&,所以两个的地址相同。
当你传递的是const类型值时,推导出的T就是const类型的如const int / const int&。
关于万能引用的误区:
当你在模板类中使用万能引用时,很容易造成错误,如下:
template<class T>
class func
{// ...void Function(T&& t){int a = 0;T x = a;cout << "&a = " << &a << endl;cout << "&x = " << &x << endl << endl;}// ...
};
上面这种可不是万能引用,当 T 是类模板参数时,T&& 就是普通的右值引用,不是万能引用。因为T的类型是我们给定的,是固定的。
类型推导是万能引用的前提,当 T 需要根据函数调用实参进行推导时,T&& 才是万能引用;当 T 在类实例化时已经确定时,T&& 就是普通的右值引用。所以想在模板类中使用万能引用要像下面这样用:
template<class T>
class func
{// ...template<class Y>void Function(Y&& t){int a = 0;Y x = a;cout << "&a = " << &a << endl;cout << "&x = " << &x << endl << endl;}// ...
};
本期博客的分享就到这里,关注博主不迷路,我们下期见~
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~
