【C++篇】:C++11新特性入门指南(一)——掌握现代C++的基础工具箱
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:c++篇–CSDN博客

文章目录
C++11
一.统一的列表初始化
1.{}初始化
C++11引入了一种新的初始化语法,叫做“列表初始化”,也叫“统一初始化”。它使用大括号{}来初始化变量或对象。
在C++98中,标准允许使用花括号{}对数组或结构体元素进行统一的列表初始化:
struct Point{
int _x;
int _y;
};
int main(){
int arr1[] = {1, 2, 3, 4};
int arr2[5] = {0};
Point p = {2, 2};
return 0;
}
在C++11中扩大了列表初始化的适用范围,使其可以用于所有的内置类型和用户自定义类型,使用列表初始化时,可添加=,也可以不添加。
(一般最好还是加上,方便阅读)
struct Point{
Point(int x, int y)
:_x(x), _y(y)
{
cout<<_x<<" "<<_y<<endl;
}
Point(const Point& other) {
cout << "拷贝构造" << endl;
}
int _x;
int _y;
};
int main(){
int a=1;
int b={2};
int c{3};
cout<<(a + b + c)<<endl;
// C++11支持的列表初始化, 这里会调用构造函数初始化
Point p1(1, 1);
Point p2={2, 2};
Point p3{3, 3};
return 0;
}
创建对象时也可以使用列表初始化方式,会自动调用构造函数:

这里的p2={2, 2}和{3, 3}其是拷贝列表初始化,相当于用{2, 2}和{3, 3}分别创建了一个Point临时对象,然后用它拷贝初始化p2和p3。
但是在C++11之后,编译器会自动进行拷贝省略,也叫“返回值优化";
- 实际上并不会真的先创建一个临时对象再拷贝给
p2; - 编译器会直接在
p2的内存上构造函数,相当于只调用了一次构造函数,不会真的拷贝
但是还有一种情况,比如下面这种:
Point &p4 = {4, 4};
如果直接这样初始化的话,是会报错的

因为这里p4引用时,是要先创建一个临时对象的,但是临时对象具有常性,不能被非常量引用绑定,只能被const引用绑定,所以正确的写法应该是:
const Point &p4 = {4, 4};
想必这里应该就有个疑问了:前面不是还说编译器会优化省略,不会真的创建临时对象拷贝初始化吗?
这是因为变量
p2和p3在栈上都有自己独立的内存空间,所以编译器会进行优化,直接在它们两个的内存空间上调用构造函数,不会先创建一个临时对象再拷贝;但是
p4就不同了,引用本质上只是一个别名(指针),他本身没有独立的内存空间,必须指向一个已经存在的对象。所以这里会先创建一个临时对象,这个临时对象有独立的内存空间,然后这个内存空间再被p4引用;因为引用只是指向,必须有一个实际的对象让它指向,所以这里编译器不会省略。
C++11列表初始化也可以适用于new表达式中:
int *pa=new int[4]{0};
Point *p5=new Point{5, 5};
2.std::initializer_list
std::initializer_list是C++11中引入的一个非常重要的特性,它允许我们引用花括号初始化器列表{}来初始化对象。
-
基本特征:
- 它是一个轻量级的代理对象,表示一个数组的视图
- 它是只读的,不能修改其中的元素
- 它通常用于构造函数和函数参数中
-
主要用途:
- 允许容器类使用统一的初始化语法
- 支持可变参数的构造函数
- 使函数能够接受任意数量的同类型参数
-
重要特性:
- 它是一个模板类,可以包含任意类型的元素
- 提供了begin()、end()迭代器方法
- 提供了size()方法获取元素个数
二.声明
C++11提供了多种简化声明的方式,尤其是在使用模板时。
2.1 auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃了auto原来的用法,将其用于实现自动类型推断。
但是这样要求就必须进行显示初始化,让编译器将定义变量的类型设置为初始化值的类型,由值的类型推导出变量的类型
auto不能用来定义未初始化的变量,不能先定义一个变量出现
// 正确用法
auto a = 10; // a被推导为int
auto b = 3.14; // b被推导为double
auto c = "hello"; // c被推导为const char*
auto d = std::string(); // d被推导为std::string
// 错误用法
auto x; // 错误:auto必须初始化
auto y = {1}; // 特殊情况:y被推导为std::initializer_list<int>
auto是类型推导的工具,必须通过初始化表达式来确定类型;- 它简化了复杂类型的书写,特别是在模板和泛型编程中;
- 使用
auto可以避免类型不匹配的问题; - 但过度使用
auto可能会降低代码的可读性,因为类型信息不再显示可见; - 在需要明确类型的场合,最好显示出类型而不是使用
auto;
除此之外typeid().name也可以推导出类型,但是推出的类型只是一个字符串,只能用来看看,不能使用 。
typeid().name主要用于调试和类型信息查看;- 不能用于类型声明或变量定义;
- 输出的类型名可能难以理解(因为经过了名字改编);
#include <typeinfo> // 使用typeid需要包含这个头文件
// 错误用法
typeid(42).name() x = 42; // 这样是无法编译的
// 正确用法(仅用于查看类型)
auto x = 42;
cout << typeid(x).name() << endl; // 输出类型信息
2.2 decltype
decltype是C++11引入的另一个类型推导工具,与auto不同,它有一些特殊的用法和规则。
1.基本用法-获取表达式的类型:
int x = 42;
decltype(x) y = 100; // y的类型是int
decltype(42) z = 200; // z的类型是int
double d = 3.14;
decltype(d) d2 = 2.718; // d2的类型是double
2.可以用于声明变量(这是和typeid的重要区别):
vector<int> vec;
decltype(vec) vec2; // vec2是vector<int>类型
decltype(vec.size()) len; // len的类型是vector<int>::size_type
3.保留cv限定符和引用:
vector<int> vec;
decltype(vec) vec2; // vec2是vector<int>类型
decltype(vec.size()) len; // len的类型是vector<int>::size_type
4.获取类成员的类型:
struct Point {
int x;
double y;
};
decltype(Point::x) a; // a的类型是int
decltype(Point::y) b; // b的类型是double
decltype的主要特点:
-
与auto的区别:
- auto会忽略引用和cv限定符
- decltype会保留完整的类型信息,包括引用和cv限定符
- auto必须立即初始化
- decltype可以只用于类型声明,不需要初始化
-
与typeid的区别:
- typeid只能在运行时查看类型信息
- decltype在编译时确定类型,可以用于变量声明
- decltype保留了更多的类型细节(如引用、const等)
-
常见用途:
- 泛型编程中推导复杂类型
- 保留精确的类型信息(包括引用和cv限定符)
- 声明与表达式类型相同的变量
2.3 nullptr
由于C++中NULL被定义为字面量0,这样就可能会带来一些问题,因为0既能表示指针常量,又能表示整型常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
小测试:
void func(int x){
cout<<"func(int x)"<<endl;
}
void func(int *y){
cout<<"func(int *y)"<<endl;
}
int main(){
func(NULL); //预处理 宏替换后变为func(0)
func(nullptr);
reutrn 0;
}

注释掉后,单独执行func(nullptr):

三.范围for
这个我在之前C++入门那里讲解过关于范围for的使用,具体内容可以看我之前的文章(这里不再重复)。
四.智能指针
这个后面我会单独用一个篇章讲解,这里先跳过。
五.STL中一些变化
5.1 增加了几个新容器


下面这四个就不过多讲解了,都是基于哈希表实现的,我在之前的文章中讲解过,感兴趣的可以看我之前的文章。
这里主要介绍上面两个,但是这两个说实话,也没啥多大用处,比较鸡肋,就简单了解一下吧。
array:
array静态数组和C语言中的静态数组差不多
#include <array>
int main(){
int a1[10];
array<int, 10> a2; //大小10在编译时确定
cout<<sizeof(a1)<<endl;
cout<<sizeof(a2)<<endl;
return 0;
}
- 不能动态改变大小;
- 大小必须是编译时常量;
- 如果需要不同大小的数组,必须重新定义新的类型;
物理上没啥区别
a1[15]=1;
a2[15]=1;
唯一的区别就是对越界访问更”敏感“,因为C语言的静态数组是对指针的解引用,而array是**operator[]函数调用,内部监查**。
其他的语法使用上与原生数组差异不大。
forward_list:
#include <forward_list>
forward_list<int> fl = {1, 2, 3, 4, 5};
// 没有size()方法
// auto size = fl.size(); // 错误:不支持
// 不能反向遍历
// auto rit = fl.rbegin(); // 错误:不支持
- 单向链表实现;
- 比
list更节省内存; - 只能前向遍历;
- 没有
size()成员函数; - 不能从尾部添加元素;
- 不支持反向迭代器;
实际应用场景有限
// 大多数情况下,list更实用
list<int> l = {1, 2, 3, 4, 5};
l.push_back(6); // forward_list做不到
l.size(); // forward_list做不到
- 为了节省那点内存,牺牲了太多功能;
- 大多数情况下,
lsit更加实用;
5.2 容器中增加了几个新接口
1.所有容器均增加了const迭代器

比如vector中的:
vector<int> v = {1, 2, 3};
for (auto it = v.cbegin(); it != v.cend(); ++it) {
// 保证迭代器是const的
}
- 返回const迭代器;
- 保证不会修改容器内容;
2.所有容器均支持{}初始化的构造函数


// 所有容器都支持列表初始化
vector<int> v = {1, 2, 3, 4, 5};
map<string, int> m = {{"one", 1}, {"two", 2}};
3.所有容器均新增了emplack系列接口
emplace_back(),emplace_front()和emplace()三个。


这个后面讲模板的可变参数时再讲解,先了解即可。
4.所有容器均增加了移动构造和移动赋值接口


这个后面将右值引用和移动语义时具体讲解,先了解即可。
六.右值引用和移动语义
6.1 左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了右值引用语法特性,所以我们之前学习过的引用就叫做左值引用。无论是左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),最简单的理解就是左值可以取地址,并且可以对它赋值,左值可以出现在赋值符号的左边,也可以出现在右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main(){
// 以下的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;
return 0;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式(如字面常量、表达式返回值,函数返回值),最简单的理解就是右值不能取地址,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边(所以一般出现在
=左边的一定是左值,右边的可能是左值,也可能是右值)。右值引用就是对右值的引用,给右值取别名。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
在C++之后,右值分为两类:
- 纯右值
// 1.字面量:
42; // 数字字面量
true; // 布尔字面量
"hello"; // 字符串字面量(特殊情况,实际上是左值)
// 2.表达式结果:
int a = 10, b = 20;
a + b; // 算术表达式的结果
a > b; // 关系表达式的结果
// 3.返回非引用的函数调用:
int getValue() {
return 42;
}
getValue(); // 函数返回值是纯右值
// 4.后置自增/自减:
int i = 0;
i++; // 后置自增的结果是纯右值
- 将亡值
// 1.std::move的结果:
string str = "hello";
std::move(str); // 将亡值
// 2.返回右值引用的函数调用
vector<int>&& getVector() {
return std::move(vector<int>{1, 2, 3});
}
getVector(); // 将亡值
一个注意点:
右值虽然不能取地址,但是右值引用,给右值取别名后,会导致右值被存储在一个特定的内存空间,且可以取到该位置的地址,也就是说:字面量10不能取地址,但是
rr1右值引用后,可以取rr1的地址,并且也可以修改(实际上rr1就是一个左值);如果不想被修改,可以用const int&& rr1去引用,这样只能取地址,但是不能修改。
int main(){
double x=1,1, y=2,2;
int&& rr1 = 10;
const double&& rr2 = x+y;
rr1 = 20; //可以修改
rr2 = 5.5 //出错,不可以修改
return 0;
}
6.2 左值引用和右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值;
- 但是const左值引用既可以引用左值,也可以引用右值;
int main(){
int a = 10; // 10是右值, a是左值
int& ra1 = a; // 左值引用 引用左值 正确
int& ra2 = 10; // 左值引用 引用右值 错误
const int& ra3 = a; //const左值引用 引用左值
const int& ra4 = 10;//const左值引用 引用右值
return 0;
}
右值引用总结;
- 右值引用只能引用右值,不能引用左值;
- 但是右值引用可以引用move以后的左值;
int main(){
int a = 10; // 10是右值, a是左值
int&& ra1 = 10; // 右值引用 引用右值 正确
int&& ra2 = a; // 右值引用 引用左值 错误
int&& ra3 = std::move(a); //右值引用 引用move以后的左值 正确
return 0;
}
6.3 右值引用使用场景和意义
前面可以看到左值引用既可以引用左值又可以引用右值,那为什么C++11这里还要提出右值引用呢?是不是有点多余了,实际上并不是多余,因为右值引用的出现是为了弥补左值引用的短板。
这是一个封装好的string类,后面的测试都是使用这个。
string.hpp:
#include <iostream>
#include <string.h>
#include <string>
#include <assert.h>
using namespace std;
namespace Mystring
{
class string
{
public:
typedef char *iterator;
iterator begin()
{
return _str;
}
iterator end()
{
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);
}
// s1.swap(s2)
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;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string &operator=(const string &s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
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];
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)
string &operator+=(char ch)
{
push_back(ch);
return *this;
}
const char *c_str() const
{
return _str;
}
private:
char *_str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
先来看看左值引用的场景:
- 1.做参数时:
void func1(Mystring::string s)
{
cout << "func1(Mystring::string s)" << endl;
}
void func2(const Mystring::string &s)
{
cout << "func2(const Mystring::string &s)" << endl;
}
int main()
{
Mystring::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
cout << endl;
func2(s1);
return 0;
}
结果:

调用func1值传递时存在一次深拷贝,调用过程:
- 需要将实参s1拷贝构造一个新的string对象作为形参s
- 会调用string的拷贝构造函数进行深拷贝
- 函数结束时,形参s会被销毁
调用func2常量左值引用传递时没有发生深拷贝,调用过程:
- 形参s直接引用实参s1
- 不会发生任何拷贝
- 函数结束时,引用结束,但不会销毁原对象
根据上面的对比,可以发现:
- 值传递会导致对象的深拷贝,有额外的性能开销;
- 常量左值引用避免了拷贝,提高了效率;
- 在处理对象参数时,优先考虑使用常量左值引用;
- 除非特殊需求,否则避免值传递大对象;
- 2.做返回值时:
// 1.传值返回 有深拷贝
string operator+=(char ch) {
push_back(ch);
return *this; // 会创建一个新的string临时对象(深拷贝)
}
int main()
{
Mystring::string s1("hello world");
s1 += '!'; // 每次+=都会产生一个新的临时对象
return 0;
}
// 2.左值引用返回 没有深拷贝
string& operator+=(char ch) {
push_back(ch);
return *this; // 直接返回对象的引用 没有深拷贝
}
int main()
{
Mystring::string s1("hello world");
s1 += '!'; // 直接在s1上修改,没有临时对象
return 0;
}
结果:

只有第一次有深拷贝,因为返回时是先创建一个临时对象,涉及到构造开销;而第二次左值引用返回,是直接返回对象的引用,所以没有拷贝。
左值引用的短板:
如果函数的返回值是一个局部变量,当函数调用结束,栈帧销毁,返回值也会跟着销毁,此时就不能使用左值引用返回了,只能使用传值返回。
- 测试代码
Mystring::string getString() {
Mystring::string ret("hello"); // 局部对象
return ret;
}
int main(){
// 测试局部变量返回
Mystring::string s1 = getString();
cout << "测试完成" << endl;
return 0;
}
- 自动化编译
makefile文件:
test:test.cc
g++ -fno-elide-constructors test.cc -o test -std=c++11
.PHONY:clean
clean:
rm -f test
在调用getString函数时,返回值是一个局部对象,如果使用左值引用返回(Mystring::string& getString),编译器就会直接报错:

对于这种情况,就只能先暂时使用传值返回,具体过程就是:
- 传值返回时会先创建一个临时对象,将ret拷贝给临时对象,这是第一次深拷贝;
- 然后临时对象会再拷贝给s1,这是第二次深拷贝;
- 现代编译器都很智能,一般都有返回值优化,所以可能不同的编译器下,可能会看到不同的深拷贝次数,没有优化就是两次,有的可能只有一次,也有的甚至一次都没有,两次都被优化掉了;
为了防止编译器过度优化,导致无法详细的看到具体结果,这里在使用g++编译时,可以带上-fno-elide-constructors选项,这样就能看到两次深拷贝的结果了。

如果这里的传值返回时,是一个非常非常大的返回值,并且还要涉及到两次深拷贝,那么就会大大降低效率,所以这就是左值引用的一个短板,为了解决这个问题,C++11中引入了右值引用和移动语义。
右值引用和移动语义解决上述问题:
在
Mystring::string中增加移动构造,移动构造的本质就是用自己的资源和参数右值的资源进行交换(相当于资源转换),这样就不用再做深拷贝了,所以叫做移动构造,用别人的资源来构造自己。
//在Mystring::string中增加移动构造
string(string &&s)
: _str(nullptr), _size(0), _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
还是继续用上面的测试代码测试:

因为Mystring::string中即存在拷贝构造有存在了移动构造,但是编译器会自动选择最匹配的参数调用,因为这里局部返回对象是一个将亡值,所以是右值,自然就会调用移动构造,具体过程:
ret是一个局部变量,在return时会成为将亡值,编译器会用移动构造创建临时对象,将ret的资源转移给这个临时对象;然后ret就会变为空,被销毁,这是第一次移动构造;- 因为临时对象本身也是一个将亡值,属于右值,所以用这个临时对象初始化s1时,又触发了移动构造,临时对象的资源转移给
s1,然后临时对象变为空,被销毁,这是第二次移动构造;
不仅仅有移动构造,还有移动赋值:
在
Mystring::string类中增加移动赋值函数,测试时先创建一个对象s2,然后调用getString函数,将返回值赋值给s2。
//在Mystring::string中增加移动赋值
string &operator=(string &&s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
- 测试代码:
Mystring::string getString() {
Mystring::string ret("hello"); // 局部对象
return ret;
}
int main(){
// 测试局部变量返回
// Mystring::string s1 = getString();
Mystring::string s2; // 先构造
s2 = getString(); // 再赋值
cout << "测试完成" << endl;
return 0;
}
结果:

具体过程:
ret是将亡值,用移动构造先创建临时对象,将资源转移给临时对象,这里是一次移动构造;- 然后因为
s2是一个已经创建好的对象,并且临时对象也是一个右值,所以临时对象是调用移动赋值,将资源转移给s2,这里是一次移动赋值;
C++11对STL的容器都增加了移动构造和移动赋值,这就是我们前面提到的一个关于STL的变化的一个点。
6.4 右值引用更深入的使用场景分析
1.右值引用move(左值)
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?
因为有些场景下,可能真的需要右值去引用左值实现移动语义。当需要右值引用一个左值时,可以通过
move函数将左值转换为右值。C++11中,std::move()位于头文件中,这个函数名字有点迷惑性,它并不搬移任何东西,只是单纯的将一个左值强制转换为右值,然后实现移动语义。
- 测试代码:
int main(){
Mystring::string s1("helloworld");
Mystring::string s2(s1); // s1是左值 调用拷贝构造
Mystring::string s3(move(s1)); // move(s1)是右值 调用移动构造
return 0;
}
结果:

注意点:在将s1 move处理后,会被当成右值,调用移动构造,但是一般不建议这样用,因为移动构造是转移资源,构造s3后,s1的资源就会被置为空,之后不能在使用了。
2.STL容器插入相关接口函数也增加了右值引用版本
以
list链表为例,在插入时,需要先创建一个新的节点然后再插入,在这个过程中一定会涉及到拷贝问题,如果增加了右值引用版本就会减少拷贝次数,通过下面的测试来证明:
- 测试代码:
int main(){
list<Mystring::string> lt;
Mystring::string s1("hellowolrd");
lt.push_back(s1); // 第一次:深拷贝构造
lt.push_back("1111111"); // 第二次:移动构造
lt.push_back(move(s1)); // 第三次:移动构造
return 0;
}
结果:

具体过程:
- 第一次的拷贝构造:
s1是一个左值;- 直接调用拷贝构造函数创建出一个新节点;
- 第二次的移动构造:
"1111111"是一个字符串常量,会先调用构造函数先创建一个临时对象;- 这个临时对象是一个将亡值,右值;
- 所以使用移动构造把临时对象的资源转移到新节点;
- 第三次的移动构造:
- 因为
s1是一个已经创建好的对象,并且move之后变成了右值;- 所以直接使用移动构造把
s1的资源转移给新节点;- 之后
s1置为空;
STL容器增加了右值引用版本的插入接口,不仅可以避免不必要的拷贝,提高效率;还可以支持不可拷贝但可移动的类型(比如直接插入右值/临时对象)。
6.5 完美转发
- 模板中的
&&万能引用
先来测试一个例子看看效果
测试代码:
void Fun(int &x){
cout << "左值引用" << endl;
}
void Fun(const int &x){
cout << "const 左值引用" << endl;
}
void Fun(int &&x){
cout << "右值引用" << endl;
}
void Fun(const int &&x){
cout << "const 右值引用" << endl;
}
template<typename T>
void PerfectForward(T&& t){
Fun(t);
}
int main(){
PerfectForward(10); // 右值
int a = 1;
PerfectForward(a); // 左值
PerfectForward(move(a)); // 右值
const int b = 2;
PerfectForward(b); // const左值
PerfectForward(move(b)); // const右值
return 0;
}
结果:

观察上面的结果,我们可以发现,右值最后调用的是左值引用的Fun函数,而const右值最后调用的则是const左值引用的Fun函数,为什么会是这样的结果?这就是接下来要讲解的万能引用。
- 模板中的
&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值(如果t接收的是一个左值,编译器就会处理为T& t;如果t接收的是一个右值,编译器就会处理为T&& t);- 模板的万能引用提供了能够同时接收左值引用和右值引用的能力;
- 尽管t接收的可以是右值,也可以是左值,但是接收完之后在函数内部,这个t是有具体的名字的,可以取地址,所以t是一个左值;
- 因此在
PerfectForward函数中不管后续调用任何版本的Fun函数,最后t都是最为左值或者const左值被传过去;
既然如此,那可不可以在传递的过程中保持它的左值或者右值属性呢?(比如t接收的是一个右值,那就是右值引用,之后t还是作为右值去调用Fun函数)
有的有的,这就是接下来要讲的完美转发的功能。
- std::forward 完美转发在传参的过程中保留对象原生类型属性
void Fun(int &x){
cout << "左值引用" << endl;
}
void Fun(const int &x){
cout << "const 左值引用" << endl;
}
void Fun(int &&x){
cout << "右值引用" << endl;
}
void Fun(const int &&x){
cout << "const 右值引用" << endl;
}
template<typename T>
void PerfectForward(T&& t){
Fun(forward<T>(t)); // 使用forward保持值类型
}
int main(){
PerfectForward(10); // 右值
int a = 1;
PerfectForward(a); // 左值
PerfectForward(move(a)); // 右值
const int b = 2;
PerfectForward(b); // const左值
PerfectForward(move(b)); // const右值
return 0;
}
这样修改后:
如果传入右值,forward会将t转发为右值;
如果传入左值,forward会将t转发为左值;
如果传入const左值,forward会将t转发为const左值;
如果传入const右值,forward会将t转发为const右值;
这就是为什么它叫做"完美转发" ,它能够完整地保持参数的值类型和cv限定符。
- 完美转发实际中的应用场景
用很早之前自己封装的list进行插入测试(关于我自己封装的list可以看我之前的文章,代码太多这里就不展示了)
【c++篇】:掌握list–实用的基本使用方法与模拟实现思路
测试代码:
#include <iostream>
#include <vector>
#include <list>
#include "String.hpp"
#include "list.h"
int main(){
Mylist::list<Mystring::string> lt;
Mystring::string s1("helloworld");
lt.push_back(s1);
lt.push_back("11111111");
lt.push_back(move(s1));
return 0;
}
因为之前封装好的list并没有移动语义,所以在插入节点时,都是调用string的拷贝构造

为了让自己封装的list也可以像库里的一样,实现移动语义,就需要对之前的代码进行修改
首先调用尾插函数时,这里接收参数时,使用的是const T& x,尽管既能接收右值,又能接收左值,但是最后在函数内部x始终保持左值属性,所以这里需要进行修改
//尾插
void push_back(const T& x){
insert(end(),x);
}
// 模板万能引用+完美转发版本的尾插
template<class Ty>
void push_back(Ty&& x){
insert(end(), forward<Ty>(x));
}
其次调用insert函数时,和尾插也是同样的问题,在函数内部new node(x)时x始终保持左值属性,所以这里也需要修改
//插入
iterator insert(iterator pos ,const T& x){
node* cur=pos._node;
node* prev=cur->_prev;
node* newnode=new node(x);
prev->_next=newnode;
newnode->_prev=prev;
newnode->_next=cur;
cur->_prev=newnode;
_size++;
return pos;
}
// 模板万能引用+完美转发 版本插入
template<class Ty>
iterator insert(iterator pos ,Ty&& x){
node* cur=pos._node;
node* prev=cur->_prev;
node* newnode=new node(forward<Ty>(x));
prev->_next=newnode;
newnode->_prev=prev;
newnode->_next=cur;
cur->_prev=newnode;
_size++;
return pos;
}
最后就是在节点的构造函数这里,也是同样的问题,在函数内部,因为val是自定义类型的string,所以从val到_val涉及到string拷贝的问题,因此这里的val也要保持对应的属性
//构造函数 创造节点
list_node(const T& val)
:_next(nullptr)
,_prev(nullptr)
,_val(val)
{}
// 模板万能引用+完美转发 版本创造节点
template<class Ty>
list_node(Ty &&val)
: _next(nullptr)
, _prev(nullptr)
, _val(forward<Ty>(val))
{}
修改完上面这三个点,最后的插入效果就能和库里的一样实现移动语义了

以上就是关于C++11新特性第一部分的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
更多推荐
所有评论(0)