C++11

慈云数据 2024-05-09 技术支持 37 0

c++11

  • 统一使用列表进行初始化
  • 可变参数模板
  • 右值引用
    • 右值引用和移动语义
    • 万能引用和完美转发
    • 新增类功能
      • 默认成员函数
      • 类成员变量初始化
      • 新增关键字
        • default关键字
        • delete关键字
        • final关键字
        • override关键字
        • lambda表达式
        • 包装器
          • function包装器
          • bind包装器
          • 新增加容器
          • 增加变量声明方式
          • 范围for循环
          • 指针空值
          • 线程库
          • 智能指针

            在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(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能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员开发效率,因此在实际项目开发中也用得比较多,由于C++11增加的语法特性非常篇幅非常多,这里主要介绍在实际中比较实用的语法。

            需要查看C++11的文档介绍可点击: C++11,同时在这里也可以查看支持该特性的编译器或编译器版本。

            统一使用列表进行初始化

            c++11扩大了使用大括号{}(初始化列表)进行初始化的范围,使其可以用于所用的内置类型和用户自定义类型,从而支持一切皆可以使用{}进行初始化,同时使用初始化列表时,赋值运算符‘=’可以省略,但不建议。

            内置类型:

            int i=1;
            int i={1};
            int i{1};//不建议这样写
            //以上语句等价
            int *p=new int[3]{2,1};//只给了2个初始化值,剩余的调用其默认构造
            

            自定义类型:

            Date d1(2024,5,1);//调用Date类的构造函数
            Date d1={2024,5,1};//调用Date类的构造函数+拷贝构造函数,编译器优化成构造
            Date d1{2024,5,1};//调用Date类的构造函数+拷贝构造函数,编译器优化成构造
            //以上语句功能等价
            Date*p1=new Date[2]{d1,d1};//调用Date拷贝构造
            Date*p1=new Date[2]{{2024,5,1},{2024,5,5}};//调用Date类的构造函数+拷贝构造函数,编译器优化成构造
            Date* p2=new Date{2024,5,1};//调用Date类的构造函数+拷贝构造函数,编译器优化成构造
            Date* p2=new Date(2024,5,1);//调用Date类的构造函数
            

            c++也支持了一些容器使用{}进行初始化,但其底层原理与前面支持的{}初始化的不同。这主要是c++11增加了一个容器initializer_list,其可以接受多个参数,将initializer_list作为某个容器的构造函数的参数,那么该容器就支持使用{}进行多参数初始化了。initializer_list

            vector、list、string、map、set等常见容器都支持了{}初始化。

            可变参数模板

            可变参数模板即函数模板和类模板可以接受的参数个数和类型是可变的,样式如下:

            template 
            void ShowList(Args... args)
            {}
            

            可以通过sizeof…(Args);查看参数个数。

            由于模板是在编译阶段进行推演的,因此必须在编译时将模板参数包解析出来,常用方法有两个:

            1.递归函数方式展开参数包

            // 递归终止函数
            template 
            void ShowList(const T& t)
            {
             	//...
            }
            // 展开函数
            template 
            void ShowList(T value, Args... args)
            {
            	//...
             	ShowList(args...);
            }
            

            2.逗号表达式展开参数包

            template 
            void printArg(T t)
            {
             	//...
            }
            //展开函数
            template 
            void ShowList(Args... args)
            {
             	int arr[] = { (printArg(args), 0)... };
            }
            

            (printarg(args), 0),先执行printarg(args),再得到逗号表达式的结果0。同时通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)执行与参数匹配函数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

            右值引用

            右值引用和移动语义

            c++的左值引用解决了传参时需要拷贝的问题,但函数返回时返回值拷贝的问题没有完全解决,例如函数的返回值是局部对象时,依旧避免不了需要进行拷贝,为此c++提出了右值引用的概念。

            左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址并对它赋值,左值可以出现赋值符号的左边,定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

            右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

            无论是左值引用还是右值引用,都是给对象取别名,最终目的都是为了减少拷贝的次数

            需要注意:

            1.左值引用只能引用左值,不能引用右值,但是const左值引用既可引用左值,也可引用右值

            2.右值引用只能右值,不能引用左值,但是右值引用可以引用move以后的左值(move只是将一个对象的属性暂时标记为右值,其不能改变该对象原本的属性)

            右值引用不能直接减少对象的拷贝次数,我们需要在类中增加右值引用版本的构造函数,称为移动构造,该函数直接将右值的资源窃取过来,这样就不必进行资源的拷贝了,从而达到减少拷贝次数的目的。

            以下以模拟的string类进行说明:

            class string
            {
            pubilc:
                string(const string& s)//拷贝构造
                    :_str(new char[s.capacity])
                    ,_capacity(s._capacity)
                    ,_size(s.size())
                {
                    if (NULL == _str)
                    {
                        perror("string");
                        return;
                    }
                    int i = 0;
                    while (i  
            

            因此随意使用move将一个左值标记为右值是极其危险的事,该左值的资源很有可能被其他对象掠夺。

            由上我们也可以知道只有进行深拷贝的类才需要移动构造,移动构造对于浅拷贝来说并无意义。

            编译器会默认对作为函数返回值的局部对象进行move处理,将其标记为右值,这样当局部对象作返回值时就会调用移动构造。但这些努力依旧不够,此时还需要配合编译器的优化处理,该问题就得到了完美解决。

            在这里插入图片描述

            除了移动构造函数,还有赋值构造函数,用于将一个右值对象的资源转移给一个已经存在的对象。

            万能引用和完美转发

            为了符合实际需求,语法上规定右值被右值引用后,右值引用的属性是左值。

            int&& r=10;\\r的属性为左值
            

            因为只有当r为左值时才可以将资源转移到r身上,才可以对r的资源进行更改,否则r为右值的话其没有地址,资源自然就无法转移,但这种属性的更改,会引发一些问题:

            void fun1(string& s)
            {}
            void fun1(string&& s)
            {}
            void fun2(string&& s)
            {
            	fun1(s);//s属性为左值
            }
            void fun2(string& s)
            {
            	fun1(s);
            }
            int main()
            {
            	string s;
            	fun2(move(s));
            }
            

            我们本意是想让右值引用版本的fun2函数调用右值引用版本的fun1函数,但由于其属性被改为左值,就会导致右值引用版本的fun2函数调用左值引用版本的fun1函数。为了保持值的属性在引用后不被更改,c++提出了万能引用和完美转发的概念。

            template
            void fun(T&& s)
            {
            	fun1(s);
            }
            

            在模板中&&不代表右值引用,而是万能引用,其既可以接收左值、const左值也可以接收右值、const右值,但接受过来的s依旧会被转为左值\const左值,我们需要完美转发std::forward保持接收过来的对象的原生属性。

            template
            void fun(T&& s)
            {
            	fun1(std::forward(s));
            }
            

            这样我们既可以保持住s对象的原生属性,也可以不必写多个版本的fun2函数了。

            我们可以看到C++的各种STL容器也增加了移动构造和移动赋值,同时一些容器的接口也增加了右值引用的版本,如emplace系列接口,其支持模板的可变参数和万能引用:

            template 
              void emplace_back (Args&&... args);
            template 
            iterator emplace (const_iterator position, Args&&... args);
            

            emplace系列接口是直接在容器中构造对象,从而减少不必要的拷贝和移动操作。

            vector vs;
            vs.emplace_back("12345");
            //直接在vs尾部用"12345"构造string类对象
            vs.push_back("12345");
            //先用“12345”构造一个临时的string类对象,再将该对象移动构造到vs尾部
            

            其实效率差不了多少,当他们都是传有名对象或匿名对象时没有区别:

            vector vs;
            //传匿名对象,效率上没有区别
            vs.emplace_back(string("12345"));
            vs.push_back(string("12345"));
            string s("12345");
            //传有名对象,效率上没有区别
            vs.emplace_back(s);
            vs.push_back(s);
            

            新增类功能

            默认成员函数

            c++11新增加了2个默认的类成员函数:移动构造函数和移动运算符重载。

            如果用户没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

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

            只有当用户没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个时编译器才会生成默认的移动构造和移动赋值函数,这样做是很合理的,因为一般是当用户进行了深拷贝才会自己去实现这3个函数,此时如果用户需要就必须自己去写这2个默认成员函数。

            类成员变量初始化

            在类定义时可以给成员变量缺省值是c++11才开始允许的,默认生成的构造函数会使用这些缺省值进行初始化。

            新增关键字

            default关键字

            由于一些原因可能会导致某些默认成员函数没有自动生成,如果用户有需求可以使用default关键字强制生成某个默认成员函数。

            class Date
            {
            	Date(Date& d)=default;//强制生成默认构造函数
            }
            

            delete关键字

            如果用户不想生成某些默认成员函数,可以使用delete关键字禁止它的生成。

            class Date
            {
            	Date(Date& d)=delete;//强制生成默认构造函数
            }
            

            final关键字

            修饰虚函数,表示该虚函数不能被重写,修饰类时表示该类不能被继承。

            override关键字

            检查派生类是否重写了某个虚函数,如果没有重写,则编译报错。

            lambda表达式

            例如在使用仿函数作为比较函数进行排序时,由于我们每次想比较数据的类型可能会不一样,就需要实现多个不同比较规则的仿函数,这种代码在实现和阅读上都会给编程者带来不便,由此c++11也实现了lambda表达式,其本质是一个匿名函数。

            其语法如下:

            //lambda表达式书写格式:
            [capture-list] (parameters) mutable -> return_type { statement };
            [&](int c)mutable->int{int a=2*c; return a;};
            

            lambda表达式各部分说明:

            1.[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

            捕捉列表说明:

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

            [var]:表示值传递方式捕捉变量var

            [=]:表示值传递方式捕获所有父作用域中的变量(包括this)

            [&var]:表示引用传递捕捉变量var

            [&]:表示引用传递捕捉所有父作用域中的变量(包括this)

            [this]:表示值传递方式捕捉当前的this指针

            需要注意:

            ①.父作用域是指包含lambda函数的语句块,块作用域是指lambda的函数体包含的作用域。

            ②. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割,比如:

            [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量

            [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

            ③. 捕捉列表不允许变量重复传递,否则就会导致编译错误。

            比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

            ④. 在全局作用域或类的作用域中的lambda函数捕捉列表必须为空。

            ⑤. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。

            ⑥. lambda表达式之间不能相互赋值,即使看起来类型相同

            2.(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。

            3.mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

            4.->return_type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

            5.{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

            总之关于省略部分就是除了捕捉列表和函数不能省略外,其余的都可以省略,因此[]{}是最简单的lambda表达式,表示不做任何事情。在底层,lambda表达式就是仿函数,捕捉列表就是仿函数的成员变量,参数列表就是传给仿函数的参数,其默认构造被编译器禁止了,类名由编译器自动生成,用户无法知道,因此如果用户需要得到该lambda表达式就只能使用auto去获取他。

            int a=10;
            //返回值为整型
            auto fun1=[&](int c)->int{int a=2*c; return a;};
            //无返回值
            auto fun2=[&](int c){int a=2*c;};
            

            包装器

            function包装器

            funtion包装器也叫适配器,本质是一个类模板,包含在头文件中。

            template  function;     // undefined
            template  
            class function;
            //Ret: 被调用函数的返回类型
            //Args…:被调用函数的形参
            

            由于函数指针使用起来复杂,不同的仿函数类型又是不一样的(同时意味着会被实例化多份,影响效率),而lambda表达式则没有类型,因此他们在使用上都有自己的缺点,而funtion包装器就解决了这些问题,其可以包装函数指针、仿函数对象、lambda表达式,这样就可以这些函数包装成统一的类型,方便使用。

            funtion包装器的使用方法如下:

            1.包装普通函数

            int f(int a, int b)
            {
            	return a + b;
            }
            function func = f;
            int i = func(1, 2);
            

            2.包装类成员函数

            class Plus
            {
            public:
            	 static int plusi(int a, int b)
            	 {
            	 	return a + b;
            	 }
            	 double plusd(double a, double b)
            	 {
            	 	return a + b;
            	 }
            };
            

            ①包装普通成员函数

            function func = &Plus::plusi;//&也可以不加
            int i =func(1, 2);
            

            ②包装静态成员函数

            function func5 = &Plus::plusd;//必须有一个参数类型为类类型
            int i =func(Plus(), 1.1, 2.2);
            

            对于普通成员函数来说,&可加可不加,对于静态成员函数来说,&必须加上,由于静态成员函数没有隐含的this指针,因此需要一个参数类型为类类型,同时在使用时也需要将对象传过去。

            3.包装仿函数对象

            struct Functor
            {
            public:
             int operator() (int a, int b)
             {
             	return a + b;
             }
            };
            function func2 = Functor();
            int i =func(1, 2);
            

            4.包装lambda表达式

            function func = [](const int a, const int b) {return a + b; };
            int i = func(1, 2);
            

            举个funtion包装器的简单使用的例子:

             map opFuncMap =
             {
            	 { "+", [](int i, int j){return i + j; } },
            	 { "-", [](int i, int j){return i - j; } },
            	 { "*", [](int i, int j){return i * j; } },
            	 { "/", [](int i, int j){return i / j; } }
             };
             //这样我们只要找到对应的运算符号,就可以调用对应的函数
            

            bind包装器

            template 
              /* unspecified */ bind (Fn&& fn, Args&&... args);
            template 
              /* unspecified */ bind (Fn&& fn, Args&&... args);
            

            bind本质上是函数模板,用于接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用bind函数还可以实现参数顺序调整等操作。

            其使用形式为:

            auto newCallable = bind(callable,arg_list);
            

            其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

            arg_list中的参数可能包含形如placeholder::_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:placeholder::_1为newCallable的第一个参数,placeholder::_2为第二个参数,以此类推。例如:

            1.绑定普通函数

            int Plus(int a, int b)
            {
             	return a + b;
            }
            //绑定普通函数
            auto  func1 = std::bind(Plus, 2 ,placeholder::_1);   
            int i = func1(1);//i=3,这样就达到了减少参数的目的,其中2传给了b,1传给了a
            

            2.绑定成员函数

            class Sub
            {
            public:
            	int sub(int a, int b)
            	{
             		return a - b;
            	}
            };
            //需要传类对象过去
            auto func2 = std::bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
            //也可以function func2 = std::bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
            int i =func2(1, 2);//i=1,因为1传给了b,2传给了a
            //这样就达到了调整参数顺序的目的
            

            新增加容器

            c++11增加了4个新容器:

            1.:数组

            2.:单链表

            3.:无序集合

            4.:无序映射

            增加变量声明方式

            1.auto

            在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

            2.使用关键字decltype

            关键字decltype将变量的类型声明为表达式指定的类型

            decltype(result_type) ret;//根据括号里面表达式的结果的类型推断ret的类型
            

            如:

            const int x = 1;
            double y = 2.2;
            decltype(x * y) ret; // ret的类型是double
            decltype(&x) p;      // p的类型是int*
            

            范围for循环

            当for循环迭代范围确定时,可以使用以下方法遍历数组

            int main()
            {
            	int a[3]={1,2,3};
            	for (auto& b:a)//将a数组的每个元素乘2
            	{
            		b*=2;
            	}
            }
            void fun(int a[])//出错,迭代范围不确定
            {
            	for (auto& b:a)//将a数组的每个元素乘2
            	{
            		b*=2;
            	}
            }
            

            指针空值

            C语言NULL指针的定义为0或无类型((void*)0)的指针,这就会存在一些问题,c++使用nullptr表示指针空值,为无类型(void*)0。

            线程库

            以后补上

            智能指针

            以后补上

微信扫一扫加客服

微信扫一扫加客服

点击启动AI问答
Draggable Icon