深入理解C++系列(19)——C++11(上)

⭐️博主: 此生决int-@CSDN博客
** 速胜派就是最大的投降派!!!**

**         🔥热门专栏🔥**

      深入理解 C++ 系列算法系列

      快速复习系列Java 速通系列


文章目录

  • C++11
    • [1. C++11 的发展历史](#1. C++11 的发展历史)
    • [2. 列表初始化](#2. 列表初始化)
      • [2.1 C++98 传统的 `{}`](#2.1 C++98 传统的 {})
      • [2.2 C++11 中的 `{}`](#2.2 C++11 中的 {})
      • [2.3 C++11 中的 `std::initializer_list`](#2.3 C++11 中的 std::initializer_list)
      • 总结
    • [3. 右值引用和移动语义(重难点)](#3. 右值引用和移动语义(重难点))
      • [3.1 左值和右值](#3.1 左值和右值)
      • [3.2 左值引用和右值引用](#3.2 左值引用和右值引用)
      • [3.3 引用延长生命周期](#3.3 引用延长生命周期)
      • [3.4 左值和右值的参数匹配](#3.4 左值和右值的参数匹配)
      • [3.5 右值引用和移动语义的使用场景](#3.5 右值引用和移动语义的使用场景)
        • [3.5.1 左值引用主要使用场景回顾](#3.5.1 左值引用主要使用场景回顾)
        • [3.5.2 移动构造和移动赋值](#3.5.2 移动构造和移动赋值)
        • [3.5.3 右值引用和移动语义解决传值返回问题](#3.5.3 右值引用和移动语义解决传值返回问题)
      • [3.6 类型分类](#3.6 类型分类)
      • [3.7 引用折叠](#3.7 引用折叠)
      • [3.8 完美转发(forward)](#3.8 完美转发(forward))
    • 下期预告
    • C++11下
    • 结语

上期回顾

上一篇我们主要学习了 (上期内容) ,重点掌握了 (知识点)。如果还有印象模糊的地方,建议先回顾上一篇内容,再继续阅读本文。

C++11

1. C++11 的发展历史

C++11 是 C++ 的第二个主要版本,并且是从 C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月 12 日采纳前,人们曾使用名称 "C++0x",因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故而这是迄今为止最长的版本间隔。从那时起,C++ 有规律地每 3 年更新一次

2. 列表初始化

2.1 C++98 传统的 {}

C++98 中一般数组和结构体可以用 {} 进行初始化。

cpp 复制代码
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;
}

2.2 C++11 中的 {}

  • C++11 以后想统一初始化方式,试图实现一切对象皆可用 {} 初始化{} 初始化也叫做列表初始化
  • 内置类型支持,自定义类型也支持 。自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
  • {} 初始化的过程中,可以省略掉 =
  • C++11 列表初始化的本意是想实现一个大统一的初始化方式,其次它在有些场景下带来了不少便利,如容器 push / insert 多参数构造的对象时,{} 初始化会很方便。
cpp 复制代码
	// C++98支持的
	int a1[] = { 1, 2, 3, 4, 5 };
	int a2[5] = { 0 };
	Point p = { 1, 2 };

	// C++11支持的
	// 内置类型支持
	int x1 = { 2 };
	int x2 = 2;

	// 自定义类型支持
	// 这里本质是用{ 2025, 1, 1}构造一个Date临时对象
	// 临时对象再去拷贝构造d1,编译器优化后合二为一变成{ 2025, 1, 1}直接构造初始化d1
	// 运行一下,我们可以验证上面的理论,发现是没调用拷贝构造的
	Date d1 = { 2025, 1, 1 };
	Date d20(2025, 1, 1);

	// 这里d2引用的是{ 2024, 7, 25 }构造的临时对象,所以,不能不加const
	const Date& d2 = { 2024, 7, 25 };

	// 需要注意的是C++98支持单参数时类型转换,也可以不用{}
	Date d3 = { 2025 }; // C++11
	Date d4 = 2025;     // C++98
	string str = "1111";

	// 可以省略掉=
	Point p1{ 1, 2 };
	int x3{ 2 };
	Date d6{ 2024, 7, 25 };
	const Date& d7{ 2024, 7, 25 };

	vector<Date> v;
	v.push_back(d1);
	v.push_back(Date(2025, 1, 1));
	// 比起有名对象和匿名对象传参,这里{}更有性价比
	v.push_back({ 2025, 1, 1 });
	map<string, string> dict;
	dict.insert({ "xxx", "yyyy" });

2.3 C++11 中的 std::initializer_list

  • 上面的对单个对象的初始化已经很方便,但是对象容器初始化还是不太方便。

  • C++11 库中提出了一个 std::initializer_list 的类,auto il = { 10, 20, 30 }; --- il 的类型是 initializer_list。这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束。

  • 参考文档:initializer_list - cppreference.com

  • std::initializer_list 支持迭代器遍历。

  • 之前学的容器都支持std::initializer_list 初始化

总结

内容
C++11 是现代 C++ 非常重要的版本
{} 是一种初始化语法 ,一般是构造的临时对象
列表初始化 C++11 对初始化方式的重要扩展
initializer_list 一种表示一组同类型初始化元素的类型
vector<int> v(10,5) 10 个 5
vector<int> v{10,5} 两个元素:10、5
窄化转换 {} 初始化限制更严格

3. 右值引用和移动语义(重难点)

C++98 的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,C++11 之后,我们之前学习的引用就叫做左值引用无论左值引用还是右值引用,都是给对象取别名。

财哥:

关于后面要学习的左值右值,左值引用,右值引用,移动构造等等,他们引入这些的一个目的就是------避免复制资源而是直接"偷"资源,就是直接抢别人的资源! ,因为有些对象里面可能存了很多资源,比如文件啊,什么其他的,复制的话非常消耗时间和空间,不如直接"抢", ,但是,也不是谁的都可以抢,肯定是你刚好"不要了",即要销毁了,那我直接拿来自己用,,那一般是谁的资源我们可以确定是不要的呢?一般就是临时对象等等,那这种就被称为"右值",这就是下面要讲的内容!

3.1 左值和右值

  1. 左值 是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址。左值可以出现在赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰符后的左值,不能给它赋值,但是可以取它的地址。
  2. 右值 也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等。右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。
  3. 值得一提的是,左值的英文简写为 lvalue ,右值的英文简写为 rvalue 。传统认为它们分别是 left value、right value 的缩写。现代 C++ 中,lvalue 被解释为 locator value 的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象;而 rvalue 被解释为 read value ,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量、字面量常量、存储于寄存器中的变量等。也就是说左值和右值的核心区别就是能否取地址
    简单总结:
    左值:通常是变量,可以取地址
    右值:通常是常量,临时对象,匿名对象,不能取地址
cpp 复制代码
 // 左值:可以取地址
    // 以下的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 << &c << endl;
    cout << (void*)&s[0] << endl;

    // 右值:不能取地址
    double x = 1.1, y = 2.2;
    // 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
    10;
    x + y;
    fmin(x, y);//产生的临时变量
    string("11111");//匿名对象

3.2 左值引用和右值引用

  1. Type& r1 = x;左值引用,左值引用就是给左值取别名。
  2. Type&& rr1 = y;右值引用,右值引用就是给右值取别名.
  3. 左值引用不能直接引用右值,但是 const 左值引用可以引用右值
  4. 右值引用不能直接引用左值,但是 右值引用可以引用 move(左值)
  5. 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值。
  6. 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要混到一起理解。
cpp 复制代码
    // 左值:可以取地址
    int* p = new int(0);
    int b = 1;
    const int c = b;
    *p = 10;
    string s("111111");
    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");

    // 3,左值引用不能直接引用右值,但是const左值引用可以引用右值
    const int& rx1 = 10;
    const double& rx2 = x + y;
    const double& rx3 = fmin(x, y);
    const string& rx4 = string("11111");

    //4 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
    int&& rrx1 = move(b);
    int*&& rrx2 = move(p);
    int&& rrx3 = move(*p);
    string&& rrx4 = move(s);
    string&& rrx5 = (string&&)s;

    // b、r1、rr1都是变量表达式,都是左值
    cout << &b << endl;
    cout << &r1 << endl;
    cout << &rr1 << endl;
//   int&& rrx1 = move(b);
    // 这里要注意的是,rr1的属性是左值,所以不能再被右值引用绑定,除非move一下,也就是说,rr1这里用的是又值引用,但是本身自己的性质还是左值!
    //即变量一旦有了名字,它作为表达式使用时通常就是左值,即使它的类型是右值引用 T&&。
    int& r6 = r1;
    // int&& rrx6 = rr1;
    int&& rrx6 = move(rr1);

财哥理解:

左值和右值的根本区别,
左值就是在计算机里面有地址的,有身份的,例如,我们int a;就是在计算机里面申请了空间,有了身份,
右值就是一些计算结果(临时变量,10这类常量),没有给它分配空间,所以,没有地址,然后,右值又分为纯右值,就是10这类的常量,和将亡值(例如临时变量这类)

3.3 引用延长生命周期

右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。

cpp 复制代码
	std::string s1 = "Test";
    // std::string&& r1 = s1; // 错误:不能绑定到左值

    const std::string& r2 = s1 + s1; // OK:到 const 的左值引用延长生存期
    // r2 += "Test"; // 错误:不能通过到 const 的引用修改
    std::string&& r3 = s1 + s1; // OK:右值引用延长生存期
    r3 += "Test"; // OK:能通过到非 const 的引用修改⭐️

3.4 左值和右值的参数匹配

  • C++98 中,我们实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
  • C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 f 函数,那么实参是左值会匹配 f(左值引用),实参是 const 左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)
  • 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉很怪,下一小节我们讲右值引用的使用场景时,就能体会这样设计的价值了。
    引入了右值以后,我们在构造函数的时候就可以重载一个右值为参数的函数,让参数更加匹配!
cpp 复制代码
 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&&)

    // 右值引用变量在用于表达式时是左值
    int&& x = 1;
    f(x);            // 调用 f(int& x)
    f(std::move(x)); // 调用 f(int&& x)

3.5 右值引用和移动语义的使用场景

3.5.1 左值引用主要使用场景回顾

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如 addStringsgenerate 函数,(就是返回值是在函数里面创建的,函数结束后要销毁,无法传出来)C++98 中的解决方案只能是被迫使用输出型参数 解决。那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法改变对象已经析构销毁的事实。

addStringsgenerate 函数

cpp 复制代码
```cpp
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;
    }
};
3.5.2 移动构造和移动赋值
  • 移动构造函数 是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
  • 移动赋值 是一个赋值运算符的重载,它跟拷贝赋值构成函数重载。类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用
  • 对于像 string / vector 这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义。因为移动构造和移动赋值的第一个参数都是右值引用的类型,它的本质是要**"窃取"**引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率。

下面的 bit::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。

财哥说:

本质上就是,在原来的基础上增加了移动构造和移动赋值,就是可以更好的匹配传入的参数是右值的情况。

cpp 复制代码
        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;
        }
........................................
bit::string s1("xxxxx");
    // 走拷贝构造
    bit::string s2 = s1;
    // 走构造+移动构造,优化后直接构造
    bit::string s3 = bit::string("yyyyy");
    //走 移动构造
    bit::string s4 = move(s1);
3.5.3 右值引用和移动语义解决传值返回问题

返回值什么的都没变,只是加入了移动语义,即移动构造和移动赋值

先来看看没有的时候:

有了移动语义之后:

3.6 类型分类

C++11 以后,进一步对类型进行了划分,右值被划分为纯右值 (pure value,简称 prvalue)和将亡值(expiring value,简称 xvalue)。

  • 纯右值 :指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如:42truenullptrstr.substr(1, 2)str1 + str2,或者类似传值返回函数调用,或者整形 a ba++a + b 等。纯右值和将亡值是 C++11 中提出的,C++11 中的纯右值概念划分等价于 C++98 中的右值。

  • 将亡值 :指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达式,如 move(x)static_cast<X&&>(x)

  • 泛左值(generalized value,简称 glvalue):包含将亡值和左值。

  • 参考文档:值类别 - cppreference.com

3.7 引用折叠

  • C++ 中不能直接定义引用的引用如 int& && r = i;,这样写会直接报错,但通过模板或 typedef 中的类型操作可以构成引用的引用。
  • 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时 C++11 给出了一个引用折叠的规则:
  • 右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用 。即,只有T&& && 是右值,其他都是左值!
    那么,一般什么情况下会出现引用折叠呢?来看,如果模版
cpp 复制代码
template<class  T>
void f1(T&&,x)//这里一般就用右值引用,

注意:

这里的 T&& 不能简单理解成"右值引用"

这样的话,你实例化左值它就是左值,你实例化右值,它就是右值!

具体来讲就是,

因为如果 T 参与模板推导,它可能发生引用折叠。

  1. 1,传左值

即T是左值引用(比如int&),那么就变成int& && ,发生引用折叠,表现出来就是左值!

  1. 传右值
    即T是右值引用(比如int&&),那么就变成了int&& && 表现出来就是右值!
    所以,就可以实现你传左值就是实例化出左值版本,你传右值就是实例化出右值版本,所以,&&也被称为"万能引用"
    但是我们依旧无法实现一种情况,就是,假如
cpp 复制代码
template<class  T>
void f1(T&&,x)//这里一般就用右值引用,
{
//里面又调用了一个函数
f2(x);
//但是我想让我x是左值还是右值的这一特性传下去,怎么办
}

这就要用到我们下面要讲的:完美转发!

3.8 完美转发(forward)

完美转发,顾名思义,就是完美的把该值的属性完美的传递下去(一般就是左值还是右值)

  • Function(T&& t) 函数模板程序中,传左值实例化以后是左值引用的 Function 函数,传右值实例化以后是右值引用的 Function 函数。
  • 但是结合 3.2 章节的讲解,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值。也就是说 Function 函数中 t 的属性是左值,那么我们把 t 传递给下一层函数 Fun,那么匹配的都是左值引用版本的 Fun 函数。这里我们想要保持 t 对象的属性,就需要使用完美转发实现。
cpp 复制代码
template<class T>
void Wrapper(T&& x)
{
//如果没有完美转发
Func(x);
//那么,无论你外面调用的时候传T是什么类型,传给下面的func只能是左值,(x不管怎么样都是左值,前面讲过int&& x虽然是右值引用,但是x本身的属性是左值!)
   Func(std::forward<T>(x));
   //所以,要用完美转发,完美的传递x的属性
}

OK,那关于左值右值等等的内容就暂时告一段落!

下期预告

C++11下

结语

**  本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**

**  也欢迎订阅我的**

深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++

算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线

快速复习系列:知识梳理、查漏补缺,考前冲刺必备

Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试


**  愿每一次敲下键盘,都比昨天更进一步!** **  愿每一行代码落下,都让未来多一种可能!**

相关推荐
郝学胜-神的一滴1 小时前
C++20模板元编程 05:吃透变参模板,解锁编译期万能参数能力
服务器·开发语言·c++·windows·vscode
C++ 老炮儿的技术栈11 小时前
我们在设计tcp协议时,要传一个字符串过去,报文:头十长度十内容,是否要把‘\0‘也填入,长度是否包含‘\0‘
开发语言·数据结构·c++·mfc·c
传奇开心果编程11 小时前
【Rust入门练中学】第2课:变量、可变性与常量
开发语言·学习·rust
zr想努力11 小时前
从0开始配置Perforce4环境
开发语言
幸运小圣13 小时前
PointerEvent 常用属性与事件整理【JavaScript】
开发语言·javascript·ecmascript
长谷深风11113 小时前
评测AI Agent:三种裁判各司其职
java·大数据·开发语言·人工智能·ai agent
边境悍匪13 小时前
蜗牛学苑 Java 智能体学习 Day42|项目周开发技术汇总 1 思维导图复盘
java·开发语言·spring boot·学习·spring
君顾114 小时前
本地托管机构管理源码实战:需求拆解、数据建模与多端部署
java·开发语言·托管
传奇开心果编程15 小时前
【Rust入门练中学】 第3课:数据类型
开发语言·学习·rust