• C++11


    ●🧑个人主页:你帅你先说.
    ●📃欢迎点赞👍关注💡收藏💖
    ●📖既选择了远方,便只顾风雨兼程。
    ●🤟欢迎大家有问题随时私信我!
    ●🧐版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。

    1.C++11简介

    在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

    2.列表初始化

    在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:

    int arr1[] = {1,2,3,4,5};
    int arr2[5] = {0};
    
    • 1
    • 2

    但对于自定义类型,这种语法就不支持了。

    所以C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

    #include 
    using namespace std;
    class A
    {
    public:
    	A(int a, int b)
    	{
    		_a = a;
    		_b = b;
    	}
    private:
    	int _a;
    	int _b;
    };
    int main()
    {
    	int a = 2;
    	int a{ 2 };
    	
    	int arr1 = {1,2,3,4,5};
    	int arr1[]{ 1,2,3,4,5 };
    	//前面两种就省略了一个赋值符号,没有看出来新语法有什么优势,接下来这三种则是C++98所不能支持的写法
    	A a = {1,2};
    	int* ptr1 = new int[5]{ 1,2,3 };
    	A* ptr2 = new A[2]{ {1,2},{3,4} };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在C++11里提供了一个容器initializer_list,用于解决自定义类型初始化的问题。
    例如有了这个容器,标准库里的其它容器就能这样初始化了

    vector<int> v = {1,2,3,4,5,6,7,8};
    list<int> lt = {1,2,3,4,5};
    map<string,int> = {{"苹果",5},{"西瓜",10}};
    
    • 1
    • 2
    • 3

    除了初始化列表,initializer_list还提供了赋值(即operator=),例如:

    vector<int> v = {1, 2, 3, 4, 5};
    v = {6, 7, 8, 9, 10};
    //此时容器里的内容被修改
    
    • 1
    • 2
    • 3

    3.变量类型推导

    3.1auto的使用

    我们前面说过,auto可以自动推导变量的类型,这一特点可能对于普通类型没有很大的优势,而对于迭代器类型有这很大的便利,迭代器类型一般前缀名字很长,有了auto就可以这样:

    //正常写法
    list<int>::iterator it = lt.begin();
    //使用auto
    auto it = lt.begin();
    
    • 1
    • 2
    • 3
    • 4

    3.2decltype类型推导

    decltype是根据表达式的实际类型推演出定义变量时所用的类型。

    为什么需要decltype?
    auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
    在这里插入图片描述
    decltype的使用

    int x;
    decltype(x) z = 2;
    
    • 1
    • 2

    4.STL新增内容

    在C++11里STL新增了以下几个内容。
    在这里插入图片描述
    除了array,其余的在前面我们都讲过了。
    在这里插入图片描述
    我们发现array其实就是一个静态数组,但静态数组我们本身就可以直接定义了,为什么还要再封装成一个容器使用?
    有两个原因:
    1.支持迭代器,更好兼容STL容器玩法
    2.对于越界的检查

    除了增加容器,还新增了
    1.移动构造、移动赋值
    2.右值引用版本的插入接口

    5.右值引用

    在说右值之前,我们先来了解一下什么是左值。

    左值是一个表示数据的表达式(变量名或解引用的指针)。我们可以获取它的地址也可以对它赋值,左值既可以出现在赋值符号的左边也可以出现在右边,右值只能出现在赋值符号的右边。

    int main()
    {
    	int a = 10;
    	int& r1 = a;
    	int* p = &a;
    	int& r2 = *p; 
    	//a、p、*p都是左值 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    思考一下,接下来这个是左值吗?

    const int b = 10;
    
    • 1

    这种情况我们也认为是左值,所以换言之,可以取地址的对象,就是左值。
    铺垫完了左值,我们再来看看右值。

    右值是一个表示数据的表达式(字面常量、表达式返回值、传值返回函数的返回值)。右值引用就是对右值进行引用,给右值取别名,右值不能取地址。

    例如:

    int main()
    {
    	int x = 1;
    	int y = 2;
    	
    	10;//字面常量
    	x + y;//表达式返回值
    	int ret = fmin(x,y);//传值返回函数的返回值
    	//右值引用
    	int&& r1 = 10;
    	int&& r2 = x + y;
    	int&& r3 = fmin(x,y);//假设fmin返回两者中最小值
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    大家思考一下:
    左值引用能否引用右值?
    在这里插入图片描述
    很明显,无法直接引用,但我们可以通过加const对右值进行引用。

    右值引用能否引用左值?
    这个问题比较复杂了,在这里直接说答案,后面会再介绍。
    不能直接引用,但是右值引用可以引用move以后的左值。
    这里还有一点说明:
    右值引用后的变量可以进行赋值、取地址,相当于右值引用后又变成了一个左值,因为右值引用后会开一块空间来存储这个值。

    右值引用是用来解决左值引用的不足。

    string operator+(char ch)
    {
    	string tmp(*this);
    	push_back(ch);
    	
    	return tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这段代码会导致返回时有一次深拷贝,影响程序效率。
    左值引用针对于下面这种场景完美解决了拷贝的问题。

    string& operator+=(char ch)
    {
    	push_back(ch);
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但对于+的情况没办法很好的解决。
    所以就利用右值引用来解决这个问题,通过增加一个移动构造函数。

    //在这里补充一个概念,C++将右值分为两种:纯右值、将亡值(即自定义类型的返回值)
    string(string&& s):_str(nullptr), _size(0), _capacity(0)
    {
    	this->swap(s);//本质就是资源转移,传过来的右值即将被销毁不如把资源让出来。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    有了移动构造函数,就能解释前面我们所说的编译器对传值返回的优化问题,我们先来回顾一下。

    string sayHello(string& str)
    {
    	string ret("hello ");
    	ret += str;
    	return ret;
    }
    int main()
    {
    	string s("world");
    	string ret = sayHello(s);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在编译器没有优化的情况下
    在这里插入图片描述
    此时发生了两次拷贝构造。
    编译器优化了之后
    在这里插入图片描述
    此时只发生了一次拷贝构造。
    有了移动构造函数,还能再进行优化。
    相信有了上面的铺垫你也能猜到,直接在函数结束前拿sayHello里的ret直接去与main函数里的ret交换资源,一次拷贝构造都不用调用,这对性能的影响是非常重大的。
    💡:这里编译器会进行优化是因为构造了一个新对象,如果是下面这种场景,编译器无法进行优化。

    string sayHello(string& str)
    {
    	string ret("hello ");
    	ret += str;
    	return ret;
    }
    int main()
    {
    	string s("world");
    	cout << sayHello(s) << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    既然有移动构造,相应的就会有移动赋值。

    string& operator=(string&& s)
    {
    	swap(s);
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    string sayHello(string& str)
    {
    	string ret("hello ");
    	ret += str;
    	return ret;
    }
    int main()
    {
    	string s("world");
    	string ret;
    	ret = sayHello(s);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这个过程发生了一次移动构造,一次移动赋值。
    调用sayHello函数,在函数结束前ret与临时对象交换资源,然后临时对象再与main函数里的ret交换资源。
    与右值引用相关联的还有万能引用

    模板中的&&不代表右值引用,而是万能引用,既能接收左值又能接收右值。

    但前面我们说过,右值引用后会开一块空间来存储,相当于又变成了左值引用,所以在后续使用中都退化成了左值。
    为了解决这个问题C++给出了完美转发的概念,即在传参过程中保留对象原生类型属性。

    void Fun(T& t)
    {
    	cout << "左值引用" << endl;
    }
    void Fun(T&& t)
    {
    	cout << "右值引用" << endl;
    }
    void PerfectForward(T&& t)
    {
    	Fun(forward(t));//保证t的属性不被改变
    }
    int main()
    {
    	PerfectForward(10);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    6.lambda表达式

    6.1语法

    书写格式:[capture-list] (parameters) mutable -> return-type { statement }

    • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
    • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
    • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。默认情况下捕获的值不能修改,有了mutable就可以修改捕获的值。
    • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
    • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

    💡:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
    因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

    6.2捕捉列表说明

    捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

    • [var]:表示值传递方式捕捉变量var
    • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
    • [&var]:表示引用传递捕捉变量var
    • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
    • [this]:表示值传递方式捕捉当前的this指针

    还有几点需要注意的:

    a. 父作用域指包含lambda函数的语句块。
    b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
    c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a导致重复。
    d. 在块作用域以外的lambda函数捕捉列表必须为空。
    e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。即在全局作用域中捕捉列表必须为空
    f. lambda表达式之间不能相互赋值,即使看起来类型相同

    6.3lambda表达式的使用

    int main()
    {
    	 // 最简单的lambda表达式, 该lambda表达式没有任何意义
    	 []{}; 
    	 
    	 // 省略参数列表和返回值类型,返回值类型由编译器推导为int
    	 int a = 3, b = 4;
    	 [=]{return a + 3; }; 
    	 
    	 // 省略了返回值类型,无返回值类型
    	 auto fun1 = [&](int c){b = a + c; }; 
    	 fun1(10)
    	 cout<<a<<" "<<b<<endl;
    	 
    	 // 各部分都很完善的lambda函数
    	 auto fun2 = [=, &b](int c)->int{return b += a+ c; }; 
    	 cout<<fun2(10)<<endl;
    	 
    	 // 赋值捕捉x
    	 int x = 10;
    	 auto add_x = [x](int a) mutable { x *= 2; return a + x; }; 
    	 cout << add_x(10) << endl; 
    	 return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    7.类的新功能

    7.1default关键字

    在类里面,我们知道,如果我们不写构造函数,编译器会自动生成一个默认构造函数,若写了构造函数,编译器则不会生成。
    但经常会有这样一个场景,写了有参构造函数,没写无参构造函数,在定义对象时需要无参的情况,这个时候再自己去写无参构造函数显的太麻烦了。default关键字可以强制编译器生成默认构造函数。

    class A
    {
    public:
    	A() = default;
    	A(int a,int b)
    	{
    		_a = a;
    		_b = b;
    	}
    private:
    	int _a;
    	int _b;
    };
    int main()
    {
    	A a;
    	A aa(1,2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    7.2delete关键字

    default是强制生成构造函数,对应的delete是强制不生成构造函数。
    使用场景是有的时候我们不想要发生拷贝,而编译器是会自动生成默认拷贝构造函数的,所以用delete关键字可以防止拷贝。

    7.3新增的默认构造函数

    注意了,前面我们说过一个类有6个默认成员函数,那是在C++98里,到了C++11里多了移动构造和移动赋值构造两个,即C++11里有8个默认成员函数。
    与之前6个不同,它们的生成规则更复杂了。

    • 如果你没有自己实现移动构造函数,且析构函数 、拷贝构造、拷贝赋值重载都没有
      实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型 成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如
      果实现了就调用移动构造,没有实现就调用拷贝构造。
    • 如果你没有自己实现移动赋值重载函数,且析构函数 、拷贝构造、拷贝赋值重载都
      没有实现,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置 类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
      完全类似)
    • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

    7.4类成员变量初始化

    我们知道,类成员如果是自定义类型,它会去调用它的构造函数,而针对于内置类型,则不会处理,C++11则增加了给缺省值来给内置成员变量初始化。

    class A
    {
    private:
    	int a = 10;
    	string s;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    8.可变参数模板

    8.1Args…

    C++11的新特性可变参数模板能够接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

    // Args是一个模板参数包,args是一个函数形参 参数包
    // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
    template <class ...Args>
    void ShowList(Args... args)
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

    // 递归终止函数
    template <class T>
    void ShowList(const T& t) 
    {
    	  cout << t << endl; 
    }
    // 展开函数
    template <class T, class ...Args>
    void ShowList(T value, Args... args) 
    {
    	 cout << value <<" ";
    	 ShowList(args...);
    }
    int main()
    {
    	 ShowList(1);
    	 ShowList(1, 'A');
    	 ShowList(1, 'A', std::string("sort"));
    	 return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    可能有的人没看懂这个过程,我们以ShowList(1, 'A', std::string("sort"))为例,
    第一次调用函数
    value = 1 args包含'A'"sort"
    第二次调用函数(即进入递归)
    value = ‘A’ args = “sort”
    第三次调用函数
    此时调用的是重载版本的ShowList(const T& t)
    其实这就一次次拆包的过程,把参数包里的参数一个个拿出来。

    8.2emplace_back

    可变参数模板的应用

    template <class... Args>
    void emplace_back(Args&&... args);
    
    • 1
    • 2

    emplace系列的接口,支持模板的可变参数,并且万能引用。

    int main()
    {
    	 std::list< std::pair<int, char> > mylist;
    	 // emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
    	 // 除了用法上,和push_back没什么太大的区别
    	 mylist.emplace_back(10, 'a');
    	 mylist.emplace_back(20, 'b');
    	 mylist.emplace_back(make_pair(30, 'c'));
    	 mylist.push_back(make_pair(40, 'd'));
    	 mylist.push_back({ 50, 'e' });
    
    	 return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    喜欢这篇文章的可以给个一键三连点赞👍关注💡收藏💖

  • 相关阅读:
    go 包的引入
    2022.8.9-----leetcode.1413
    AI伦理与机器道德:人工智能的道德挑战
    【408数据结构与算法】—希尔排序 Donald Shell(十七)
    Ubuntu 安装 CUDA 与 CUDNN GPU加速引擎
    graphviz 绘制红黑树
    Nacos 使用
    业务出海,灵感乍现前要先「把手弄脏」
    pytorch代码实现之CoordConv卷积
    费曼学习法(一)
  • 原文地址:https://blog.csdn.net/qq_52363432/article/details/126064152