• C++11的一些常见特性


    因为面试被问到了,C++的新特性,但从未归纳过,故将整理c++11,c++17,c++20的常见特性,并用例子实现一遍。加油!!!

    1.nullptr

    C++用nullptr代替NULL,原因NULL在C++中会被定义为0或(void*)0,取决于编译器。

    C++ 不允许直接将 void * 隐式转换到其他类型,但如果 NULL 被定义为 ((void*)0),那么当编译char *ch = NULL;时,NULL 只好被定义为 0。

    从而会引发重载的一些问题,例如

    void foo(char *a);
    void foo(int a);

    为了避免这块从而引入nullptr,nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。

    const class nullptr_t
    {
    public:
        template<class T>
        inline operator T*() const
            { return 0; }
    
        template<class C, class T>
        inline operator T C::*() const
            { return 0; }
     
    private:
        void operator&() const;
    } nullptr = {};

    2.类型推导

    C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。

    auto

    auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用,对 auto 的语义变更也就非常自然了。(register以前就是将变量放到寄存器中,不能直接用地址取,对register使用取地址操作会让register失效,反之默认是auto,现已废弃)

    注意:auto只能推导出数据的不加引用(&)的数据类型

    但是实测可以推导出const char *和char *,推导不出const char和char,说明顶层const的const会被忽略

    此外,auto 还不能用于推导数组类型

    auto 的推导规则

    1. 在不声明为引用或指针时,auto会忽略等号右边的引用类型和const、volatile限定
    2. 在声明为引用或者指针时,auto会保留等号右边的引用和const、volatile属性
    auto i; // error: declaration of variable 't' with deduced type 'auto' requires an initializer
    //因此我们在使用auto时,必须对该变量进行初始化。
    
    auto i= 0; //0为int类型,auto自动推导出int类型
    auto j = 2.0; //auto 自动推导出类型为float
    
    int a = 0;
    auto b = a; //a 为int类型
    auto &c = a; //c为a的引用
    auto *d = &a; //d为a的指针
    auto i = 1, b = "hello World"; //error: 'auto' deduced as 'int' in declaration of 'i' and deduced as 'const char *' in declaration of 'b'
    
    /* auto 作为成员变量的使用*/
    class test_A
    {
    public:
        test_A() {}
        auto a = 0; //error: 'auto' not allowed in non-static class member
        static auto b = 0; //error: non-const static data member must be initialized out of line
        static const auto c = 0;
    };
    
    /*c11 中的使用*/
    auto func = [&] {
        cout << "xxx";
    }; 
    // 不关心lambda表达式究竟是什么类型
    auto asyncfunc = std::async(std::launch::async, func);

    decltype

     decltype用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算

    注意:decltype不会像auto一样忽略引用和const、volatile属性,decltype会保留表达式的引用和const、volatile属性

    decltype 的推导规则

    对于decltype(exp)有:

    1. exp是表达式,decltype(exp)和exp类型相同
    2. exp是函数调用,decltype(exp)和函数返回值类型相同
    3. 其它情况,若exp是左值,decltype(exp)是exp类型的左值引用
    int a = 0, b = 0;
    decltype(a + b) c = 0; // c是int,因为(a+b)返回一个右值
    decltype(a += b) d = c;// d是int&,因为(a+=b)返回一个左值
    
    d = 20;
    cout << "c " << c << endl; // 输出c 20

    auto 与 decltype 配合

    decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将
    “=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例子:
    int e = 4;
    const int* f = &e; // f是底层const
    decltype(auto) j = f;//j的类型是const int* 并且指向的是e

    基于范围的 for 循环

    C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句。

    // & 启用了引用
    for(auto &i : arr) {    
        std::cout << i << std::endl;
    }

    初始化列表

    C++11 提供了统一的语法来初始化任意的对象,例如:

    struct A {
        int a;
        float b;
    };
    struct B {
    
        B(int _a, float _b): a(_a), b(_b) {}
    private:
        int a;
        float b;
    };
    
    A a {1, 1.1};    // 统一的初始化语法
    B b {2, 2.2};
    对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
    列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成
    员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员
    赋值,就是说初始化这个数据成员此时函数体还未执行。

    默认模板参数

    //这里用到了auto和decltype的结合推导
    template<typename T = int, typename U = int> auto add(T x, U y) -> decltype(x+y) { return x+y; }

    Lambda 表达式

    Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。

    Lambda 表达式的基本语法如下:

    [ caputrue ] ( params ) opt -> ret { body; };

    1) capture是捕获列表;
    2) params是参数表;(选填)
    3) opt是函数选项;可以填mutable,exception,attribute(选填)
    mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
    exception说明lambda表达式是否抛出异常以及何种异常。
    attribute用来声明属性。
    4) ret是返回值类型(拖尾返回类型)。(选填)
    5) body是函数体。

    捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。

    1) []不捕获任何变量。
    2) [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
    3) [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。

    int a = 0;
    auto f = [=] { return a; };
    
    a+=1;
    
    cout << f() << endl;       //输出0
    
    int a = 0;
    auto f = [&a] { return a; };
    
    a+=1;
    
    cout << f() <<endl;       //输出1

    4) [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
    5) [bar]按值捕获bar变量,同时不捕获其他变量。
    6) [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。

    class A
    {
     public:
         int i_ = 0;
    
         void func(int x,int y){
             auto x1 = [] { return i_; };                   //error,没有捕获外部变量
             auto x2 = [=] { return i_ + x + y; };          //OK
             auto x3 = [&] { return i_ + x + y; };        //OK
             auto x4 = [this] { return i_; };               //OK
             auto x5 = [this] { return i_ + x + y; };       //error,没有捕获x,y
             auto x6 = [this, x, y] { return i_ + x + y; };     //OK
             auto x7 = [this] { return i_++; };             //OK
    };
    
    int a=0 , b=1;
    auto f1 = [] { return a; };                         //error,没有捕获外部变量    
    auto f2 = [&] { return a++ };                      //OK
    auto f3 = [=] { return a; };                        //OK
    auto f4 = [=] {return a++; };                       //error,a是以复制方式捕获的,无法修改
    auto f5 = [a] { return a+b; };                      //error,没有捕获变量b
    auto f6 = [a, &b] { return a + (b++); };                //OK
    auto f7 = [=, &b] { return a + (b++); };                //OK

    注意f4,虽然按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。

    原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。

    lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。

    lambda表达式是不能被赋值的:

    auto a = [] { cout << "A" << endl; };
    auto b = [] { cout << "B" << endl; };
    
    a = b;   // 非法,lambda无法赋值
    auto c = a;   // 合法,生成一个副本

    lambda表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。

    最常用的是在STL算法中,比如你要统计一个数组中满足特定条件的元素数量,通过lambda表达式给出条件,传递给count_if函数:

    int value = 3;
    vector<int> v {1, 3, 5, 2, 6, 10};
    int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });

    再比如你想生成斐波那契数列,然后保存在数组中,此时你可以使用generate函数,并辅助lambda表达式:

    vector<int> v(10);
    int a = 0;
    int b = 1;
    std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; });
    // 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}

    当需要遍历容器并对每个元素进行操作时:

    std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
    int even_count = 0;
    for_each(v.begin(), v.end(), [&even_count](int val){
        if(!(val & 1)){
            ++ even_count;
        }
    });
    std::cout << "The number of even is " << even_count << std::endl;

    新增容器

    std::array栈上的数组

    std::forward_list 单向链表(不提供size()函数)

    std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。

    无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)。

    std::tuple元组,不可变的字典

    右值引用和move语义

    为什么要有右值引用。

    1.效率性,如果一个变量不用了,我们用移动构造可以复用之前的内存,从而只需要实现指针的转移,而不是重新去申请一块内存进行赋值,可能会有阻塞(即使很小),效率慢。

    2.安全性,当我们用左值可能调用类的成员函数,这会导致不可预知的行为,右值却是非常安全的,因为复制构造函数之后,我们不能再使用这个临时对象了,因为这个转移后的临时对象会在下一行之前销毁掉。

    td::move仅仅是简单地将左值转换为右值,它本身并没有转移任何东西。它仅仅是让对象可以转移。

    当然,如果你在使用了mova(a)之后,还继续使用a,那无疑是搬起石头砸自己的脚,还是会导致严重的运行错误。

    总之,std::move(some_lvalue)将左值转换为右值(可以理解为一种类型转换),使接下来的转移成为可能。

  • 相关阅读:
    大数据离线分析平台 JavaSDK数据收集引擎编写
    大数据离线分析平台 需求分析(三)
    大数据离线分析平台 需求分析(二)
    大数据离线分析平台 需求分析(一)
    Hive 严格模式与非严格模式
    MapReduce案例:统计共同好友+订单表多表合并+求每个订单中最贵的商品
    Hadoop 目录分析及存储机制
    zookeeper 启动显示started,jps查看进程却没有,解决方法
    Azkaban介绍+安装部署+实战案例
    Flume 高可用配置案例+load balance负载均衡+ 案例:日志的采集及汇总
  • 原文地址:https://www.cnblogs.com/2462478392Lee/p/15240832.html
Copyright © 2020-2023  润新知