✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉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临时对象,然后用它拷贝初始化p2p3

但是在C++11之后,编译器会自动进行拷贝省略,也叫“返回值优化";

  • 实际上并不会真的先创建一个临时对象再拷贝给p2
  • 编译器会直接在p2的内存上构造函数,相当于只调用了一次构造函数,不会真的拷贝

但是还有一种情况,比如下面这种:

Point &p4 = {4, 4};

如果直接这样初始化的话,是会报错的

在这里插入图片描述

因为这里p4引用时,是要先创建一个临时对象的,但是临时对象具有常性,不能被非常量引用绑定,只能被const引用绑定,所以正确的写法应该是:

const Point &p4 = {4, 4};

想必这里应该就有个疑问了:前面不是还说编译器会优化省略,不会真的创建临时对象拷贝初始化吗?

这是因为变量p2p3在栈上都有自己独立的内存空间,所以编译器会进行优化,直接在它们两个的内存空间上调用构造函数,不会先创建一个临时对象再拷贝;

但是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中引入的一个非常重要的特性,它允许我们引用花括号初始化器列表{}来初始化对象。

  1. 基本特征

    • 它是一个轻量级的代理对象,表示一个数组的视图
    • 它是只读的,不能修改其中的元素
    • 它通常用于构造函数和函数参数中
  2. 主要用途

    • 允许容器类使用统一的初始化语法
    • 支持可变参数的构造函数
    • 使函数能够接受任意数量的同类型参数
  3. 重要特性

    • 它是一个模板类,可以包含任意类型的元素
    • 提供了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的主要特点

  1. 与auto的区别:

    • auto会忽略引用和cv限定符
    • decltype会保留完整的类型信息,包括引用和cv限定符
    • auto必须立即初始化
    • decltype可以只用于类型声明,不需要初始化
  2. 与typeid的区别:

    • typeid只能在运行时查看类型信息
    • decltype在编译时确定类型,可以用于变量声明
    • decltype保留了更多的类型细节(如引用、const等)
  3. 常见用途:

    • 泛型编程中推导复杂类型
    • 保留精确的类型信息(包括引用和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的使用,具体内容可以看我之前的文章(这里不再重复)。

【c++篇】:初识c+±-编程新手的快速入门之道(二)

四.智能指针

这个后面我会单独用一个篇章讲解,这里先跳过。

五.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 左值引用和右值引用比较

左值引用总结

  1. 左值引用只能引用左值,不能引用右值;
  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;
}

右值引用总结;

  1. 右值引用只能引用右值,不能引用左值;
  2. 但是右值引用可以引用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新特性第一部分的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐