【C++】C++11新特性

版权声明:本文为博主原创文章,未经博主允许不得转载。Copyright (c) 2018, code farmer from sust. All rights reserved. https://blog.csdn.net/sustzc/article/details/82734963

    统一初始化
        C++98支持 int array[] = {1,2,3,4,5};
            但是对于自定义类型无法使用这样的初始化  vector<int> v{1,2,3,4,5}; // 无法通过编译
            C++11扩大了用大括号括起的列表的使用范围,使得大括号括起来的列表用于所有的内置类型和用户自定义类型,
        使用初始化列表时,可添加等号,也可以不添加。
            注意:列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别。
            用于定义变量
                map<int, int> m{{1,2},{2,3},{3,4},{4,5}};
            创建对象
                Pointer p{ 1, 2 };
            函数参数
                初始化列表(形参)
                作为参数时,参数的类型必须是initializer_list<类型>的对象。
                initializer_list: 实际为带有两个迭代器_first, _last成员的模板类,
                    该类提供三个成员函数begin()、end()、size(),使用时必须包含initializer_list头文件。
            函数返回值
                return {1,2,3,4,5};        
                    注意:做函数的返回值时,列表构造什么类型最后是根据声明函数时返回值类型确定的。
        防止类型变窄
            类型变窄一般是指:一些可以使得数据变化或者精度丢失的隐式类型转换。比如:浮点类型转整形,高精度转低精度,整形转较低整形。
                const int x = 10;
                const int y = 1024;
                char c1{x}; // 可以通过编译
                char c2{y}; // 无法通过编译,y的高字节丢失
    
    声明之auto与decltype
        auto a;  // 必须进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型  
                 // auto相当于一个占位符,编译期间会将auto替换成变量实际的类型。
                 // auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
        auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
            int a = 10;
            auto b = a; // 根据a的类型推导出来b的类型
        在拥有初始化表达式的复杂类型的简化
            std::vector<int>::iterator it = v.begin();  ----->     auto it = v.begin();
        可以避免类型不明确的麻烦
            auto c = 2 * pi * radis;
        auto的自适应性能够在一定程度上支持泛型编程
            在模板函数中,每次对ret的结果使用auto进行推导,其“自适应性”可以加强C++泛型的能力,但遗憾的是该函数模板
        总是返回double类型的数据,使模板函数的使用范围受到限制,后面介绍追踪返回类型的函数声明来解决此问题。
        
        auto(直接定义变量)
            与指针引用结合起来使用
                声明其为auto和auto*没有任何区别,但是要声明auto为某一个变量的引用,则必须加&。
                    int x = 10;
                    auto a = &x;
                    auto* b = &x;
                    auto& c = x;
            与const和volatile一起使用
                声明为auto的变量并不能从其表达式中带走const和volatile的限制
                    const int a = 10;
                    auto b = a; // 此时b的类型是int,并不是const int型
                    
                    volatile int f = 20;
                    auto g = f; // g是volatile int型
            auto在同一行声明多个变量
                当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际
            只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
                    auto x = 1, y = 2;
                    auto a = 1, b = 1.0f;  //编译失败,因为a和b初始化表达式类型不同
            auto不能推导的场景
                1.auto不能作为函数的形参;
                        void FunTest(auto x)
                    函数的形参一旦不能被确定,就无法知道开辟栈帧时开辟栈帧的大小。即使带上缺省参数也不可以。    
                2.auto不能定义类的非静态成员变量;
                        struct A
                        {
                            auto _a;
                        };
                    无法确定变量大小
                3.auto不能用来定义数组;
                        auto b[3] = a;
                    无法确定数组元素类型
                4.实例化模板中也不能使用auto
                        std::vector<auto> v{ 1, 2, 3 };
                    无法确定具体类型
            typeid推导出来的类型只是一个字符串,不能定义变量。
                
        decltype(推演表达式类型)
            C++11中auto推导的出现,给程序的书写提供了许多方便,但auto使用的前提是:必须要对auto声明的类型进行初始化,
        否则编译器无法推导出auto的实际类型。但有时候可能需要使用表达式运行完成之后结果的类型。
            而此处最好的方式是能用加完之后结果的实际类型作为函数的返回值类型,但这需要程序运行完才能知道结果的实际类型,即RTTI。
            typeid只能查看类型不能用其结果类定义类型,而dynamic_cast只能应用于含有虚函数的继承体系中,
        而且运行时类型识别的缺陷是降低程序运行的效率。
                template<class T1, class T2>
                void Add(T1& left, T2& right, decltype(left + right)& ret)
                {
                    ret = left + right;
                }
            用来推演表达式 类型
                int a;
                decltype(a) b;  // 首先推导出a的类型然后定义b
                
            在decltype之前,假设我们程序中存在一些匿名对象,但想要使用这些匿名对象的类型时却手术无策。
                使用decltype
            推导函数返回值的类型(不会调用函数,而是推演出函数的返回值类型)
                cout << typeid(decltype(GetMemory)).name() << endl;  // 推导出函数的类型,类似于函数的声明
                cout << typeid(decltype(GetMemory(0))).name() << endl; // 推导出函数的返回值的类型
    追踪返回值类型
        decltype<left + right> Add(const T1& left, const T2& right)
        在编译器推导decltype(left+right)的类型时,由于left和right都未声明,虽然近在咫尺,却无法使用,因为按照C/C++编译器规
    则,变量必须先声明再使用。
        把函数的返回值移至参数声明之后,符合符号->decltype(left+right)被称为:追踪返回类型。
    原本函数返回值位置由auto占据,这样就可让编译器来推导函数的返回值类型了。
        auto Add(const T1& left, const T2& right)->decltype(left+right)
        在返回值前面加上auto类型,在形参中表达式后面加上delctype,那么根据其他形参列表可以推演出表达式的类型,并
    最终返回给返回值。(auto相当于是一个占位符)
    
        for_each算法
            #include <algorithm>
            
            int array[] = { 1, 2, 3, 4, 5 };
            for_each(array, array + sizeof(array) / sizeof(array[0]), action1);  // 给出起始和结束位置
            
            对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时
        候还会容易犯错误。因此C++11中引入了基于范围的for循环。  (STL中的区间是左闭右开的)
        
        基于范围的for循环
            vector<int> v{1, 2, 3, 4, 5};
            
            for (auto& e : v)
            {
                e *= 2;
            }
            
            for (auto e : v)
            {
                cout<<e<<" ";
            }
            
            for循环后的括号有冒号”:”分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
        它与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
            
            使用for循环的迭代范围是确定的,对于数组而言就是数组第一个元素和最后一个元素的范围;对于类来
        说,应该提供begin和end方法,begin和end就是for循环迭代的范围。
            注意:基于范围的for循环使用于标准库的容器时,如果使用auto来声明迭代的对象,那么这个对象不会是迭代器对象。
            
            统计字符串出现的次数
            void countstr(vector<string>& v)
            {
                unordered_map<string, int> countmap;
                for (auto& e : v)
                {
                    countmap[e]++;
                }
                
                for (auto& e : countmap)
                {
                    cout<<e.first<<":"<<e.second<<endl;
                }
            }
        
    lambda表达式
        lambda被用来表示一种匿名的函数,lambda函数跟普通函数相比不需要定义函数名,取而代之的多了一对方括号[],此外
    lambda函数还采用了追踪返回值类型的方式声明其返回值。
        lambda表达式书写格式:[capture] (parameters) mutable ->return-type{statement}
            mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
        auto sum = [](int x, int y)->int{return x + y;};    // ->表达式推演
            在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最为简单的lambda函数为:
                []{}; 该lambda函数不能做任何事情。
        
        捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式是传值还是传引用。
        
        捕捉列表有以下形式:
            [var]:表示值传递方式捕捉变量var    // 如果函数体实现中使用了var那么就需要在捕获列表中提供该变量
            [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
            [&var]:表示引用传递捕捉变量var
            [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
            [this]:表示值传递方式捕捉当前的this指针
        [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
        [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
            注意:捕捉列表不运行变量重复传递,否则就会导致编译错误。
        [=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
        
        在块作用域以外的lambda函数捕捉列表必须为空。在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任
    何非此作用域或者非局部变量都会导致编译报错。
    
        lambda与仿函数
            仿函数:又称函数对象,可以像函数一样使用的对象。简单来说就是在类中重载了operator()的一种自定义类型的对象。
        double operator()(double money, int year)
        {
            return money*_rate*year;
        }
        
        事实上,仿函数是编译器实现lambda的一种方式,在现阶段,通常编译器都会把lambda函数转换为一个仿函数,因此在C++11中,lambda可
    以视为仿函数的一种等价形式。
        有时候在编译时发现lambda函数出现了错误,编译器会提示一些构造函数的相关信息,这是由于lambda的这种实现方式造成的。
        
        lambda函数的使用场景比较特殊:比如打印一些内容状态,或者进行一些内部操作,这些功能不能与其他的代码共享,
    却要在一个函数中多次重用,比如:代码中可能要经常打印vector中内容,但该部分功能其他函数又不需要,就可使用lambda来封装内部函数。
        在lambda没有引入前,我们只能封装函数来实现,出于函数作用域及运行效率考虑,此函数通常还需加上static和inline关键字。
    但lambda的引入,其捕捉列表的功能,使我们不用考虑参数个数以及传递方式,而且主调函数结束函数,lambda函数也结束,
    不会影响命名空间中的其他东西,使代码的实现更加简答,可读性更高。
        
    默认函数控制
        构造函数有四种情况需要合成构造函数
            1.A类包含B类对象,A类构造函数没有给出,B类有默认构造函数,一定会给A类合成构造函数。
            2.B继承A,A有缺省的构造函数。
            3.虚拟继承,B继承A。
            4.类中包含析构函数。
            
        显示缺省函数 default
        
        在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,
    用=default修饰的函数称为显式缺省函数。
        A() = default; // 之前已经显示给出了构造函数,加上这句后,显示地指示编译器生成该函数的默认版本。
        优点
            可以对一个类实现多个版本,程序员可以选择所需要的版本进行编译。
            
        删除函数 delete
            程序员可能想要限制某些默认函数的生成,最典型的可能想要禁止生成默认的拷贝构造函数。
        在C++98中,是将拷贝构造函数设置成private的,只声明不给定义,这样只要其他人想要调用就会报错。
            在C++11中,有更简单的方法,即在函数定义或者声明时加上=delete,
                string(const string& s)=delete;
        该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
            删除函数也可以避免编译器做一些不必要的隐式类型转化。
            尽量避免删除函数和explicit一块使用。
        final
            当final修饰虚函数时,子类就不可以再重写这个虚函数。
            当final修饰类时,表明该类不能被继承。
        
        override重写
            帮助检查是否构成重写
            为了确保子类方法就是对基类某个方法的重写,C++11引入了override关键字。
                编译时会进行重写的检测
            override的出现,在重写时如果出现拼写错误,类型与基类不同,重写了基类的非虚函数等,override可以保证编译器辅助的做一些检查。
        注意:final/override也可以定义为正常的变量名,只有出现在类和函数之后才表示不能被继承或者是否正确重写。
            int final;
        
    委派构造函数
        其目的也是为了减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写更加容易。
        所谓委派构造函数:就是指委派函数将构造的任务委派给目标构造函数来完成的一种类构造的方式。
            在构造函数的初始化列表中调用构造函数。
            注意:如果有多个委派构造函数,不能形成环状委托。
    移动语义
        有两个引用,一个引用叫左值引用, Type& r = x;  右值引用:Type&& r = x;
            左值:存储结果的空间,在编译时可以知道其地址。    // int a = 10; a存储了10,a是个左值
            右值:存放空间的内容,运行时可知。                // int b = a;  a的值赋给b,a是个右值
            左值:放在赋值表达式的左边,可以被修改。
                int a = 10;
                int& b = a;
                b = 20;
            可以取地址的、有名字的就是左值,否则就是右值。右值不可被修改。
                eg: int a = 10;
                    int& b = 10;  //编译报错,10是个右值,而左边是左值引用。
                    const int& b = 10;  // 编译通过,这是因为此时b只能读不能去写了。
            左值引用要引用左值(想要引用右值左边加上const),右值引用要引用右值(想要引用左值右边加上move)。
                左值引用: int x = 10; int& rx = x;
                右值引用: int y = 20; int&& ry = 10;
                            int&& ry = y;  // 编译报错,C++11提供了move关键字可以让右值引用左值
                std::move():强制转化为右值                int&& ry = move(y);  // 把y的空间拿过来给ry去使用,交换的思想
                右值引用不支持引用的引用,而左值是支持的。
                
                C++11中,std::move()函数位于头文件中,这个函数名字具有迷惑性,它并不搬移任何东西,
            唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。
            实际上该函数就类似于一个类型转换:static_cast<T&&>(lvalue)。
                注意:被转化的左值,其声明周期并没有随着左右值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。
            
                string operator+(const string& s)
                    考虑到在赋值的时候会重新开辟空间,C++11使用右值引用的方式将之前的空间利用,减少了空间开辟的消耗。
                move更多的是用在生命周期即将结束的对象上。
        
        C++11有两种右值:纯右值(表达式赋值等等)和将亡值(返回值或者move过去的值)。
            String s2(move(s1));
        
        移动构造和移动赋值
            // String s1(s2);  对s进行了资源拥有权转移        传左值调用拷贝构造,传右值调用移动构造。
            String(String&& s)
            {
                _str = s._str;
                s._str = NULL;
            }
            
            String& operator=(String&& s)
            {
                swap(_str, s._str);
                return *this;
            }
                
            容器中的接口emplace的参数中包含右值引用的可变参数。
            
    线程支持
        要使用标准库中的线程,必须包含< thread >头文件,该头文件声明了std::thread 线程类及 std::swap (交换两个线程对象)
    辅助函数。另外命名空间 std::this_thread 也声明在 < thread > 头文件中。
        C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程运行时的上下文环境,比如:线程函数、线程栈、
    线程起始状态等以及线程ID等,所有操作全部封装在一起,最后在底层统一传递给_beginthreadex()(注意:_beginthreadex
    是windows中创建线程的底层c函数)创建线程函数来实现。std::thread()创建一个新的线程可以接受任意的可调用对象类型(带参数
    或者不带参数),包括lambda表达式(带变量捕获或者不带),函数,函数对象,以及函数指针。
        线程结束
            加入式:join()
            分离式:detach()        
            
        eg:
            thread t1(func, 10);
            t1.join();
            
            atomic<int> ai;
            ++ai; // 原子操作

猜你喜欢

转载自blog.csdn.net/sustzc/article/details/82734963