zoukankan      html  css  js  c++  java
  • C11简洁之道:初始化改进

    1、  C++98/03初始化

      我们先来总结一下C++98/03的各种不同的初始化情况:

    //普通数组
    int i_arr[3] = {1, 2, 3};
    
    //POD(plain old data)
    struct A
    {
        int x;
    
        struct B
        {
            int i;
            int j;
        }b;
    }a = {1, {2, 3}};
    
    //拷贝初始化
    int i = 0;
    
    class Foo
    {
    public:
        Foo(int){};
    }Foo = 123;
    
    //直接初始化
    int j(0);

      这些不同的初始化方法都有各自的适用范围和方法,但是种类繁多的方法却没有一种可以通用的。所以C++11提出了初始化列表的方法来解决通用问题。

    2、  统一初始化方法

      其实C++98/03中已经存在初始化列表的方法,只是范围比较窄,只适用于常规POD类型。

    int i_arr[3] = {1, 2, 3};
    int i_arr2[] = {1, 2, 3, 4};
    
    struct B
    {
         int i;
         int j;
    }b = {1, 2};

      而C++11将这种初始化方法适用于所有类型的初始化。我们先来看一组例子:

    class Foo
    {
    public:
        Foo(int){};
    private:
        Foo(const Foo &){};
    };
     
    void testFunc(void)
    {
        Foo val1(123);
        //Foo val2 = 123; // error:Foo::Foo(const Foo &) is private.
        Foo val3 = {123};
        Foo val4{123};
        int a5 = {2};
        int a6{3};
    }

      val3、val4使用了初始化列表来初始化对象,a3虽然使用等号,但是并不影响到私有拷贝,仍然是初始化列表的方式,等统一val1的直接初始化,而val2则调用私有拷贝函数会报错。a5、a6则是一般类型的初始化,val4和a6都是C++11特有的,而C++98/03并不支持。

      新的初始化方法是变量名后面加{}来进行初始化,{}内则是初始化的内容,等号是否存在并不影响。

    type val {};

      C++11的新方式同样支持new操作符:

    int *a = new int{5};
    double b = double {12.34};
    int *arr = new int[3]{1,2,3};

      a指向了new操作符分配的一块内存,通过初始化列表将内存的初始值指定为了5;

      b是对匿名对象进行初始化之后然后进行拷贝初始化;

      arr则是通过new动态申请一个数组,并通过初始化列表进行初始化。

      初始化列表还有一个特殊的地方,就是作为函数的返回值。

    struct Foo
    {
        Foo(int, double){};
    };
    
    Foo testFunc(void)
    {
        return {12,  12.3};
    }

      在C++11中,初始化列表是非常方便的,不仅统一了对象的初始化方式,还使代码更加简洁清晰。

    3、  使用细节

    3.1 自定义类型初始化

      当我们在C++11中使用初始化列表时,可能有以下情况:

    struct A
    {
        int x;
        int y;
    }a = {123,123};   //a.x = 123, a.y = 123
    
    struct B
    {
        int x;
        int y;
        B(int, int) : x(0), y(0) {};
    }b = {123,123};   //b.x = 0, b.y = 0

      这个例子说明什么问题呢,a是以C++98/03的聚合类型来初始化的,用拷贝的方式初始化a中的成员,而b呢,由于自定义了构造函数,所以初始化是以构造函数来初始化的。所以有以下结论:

      当使用初始化列表时,如果是聚合类型,则以拷贝的方式来初始化成员,如果是非聚合类型,则是以构造函数来初始化成员。

    3.2 聚合类型

      提了这么多的聚合类型,那么到底什么是聚合类型呢?我们来看聚合类型的定义:

      1)  类型是普通数组(int[10],char[],long[2][3]等)。

      2)  类型是一个类,且:

    • 无用户自定义构造函数;
    • 无私有或者保护的非静态成员;
    • 无基类;
    • 无虚函数;
    • 无{}和=直接初始化的非静态数据成员。

    3.2.1 数组

      对于数组而言,就很简单了,只要该类型是一个普通的数组,如果数组的元素并不是聚合类型,那么这个数组也是一个聚合类型:

    int [] = {1,2,3};
    std::string s_arr[3] = {“hello”,  “C++”,  “11”};

    3.2.2 存在自定义构造函数

    struct A
    {
        int x;
        int y;
        int z;
        A(int, int){};
    }; 
    
    A a = {123, 123, 12};

      当一个自定义类拥有自己的构造函数使,无法将该类看作一个聚合类型,必须通过自定义的构造函数才能构造对象。

    3.2.3 存在私有或者非静态成员

    struct A
    {
        int x;
        int y;
    protected:
        int z;
    }; 
    
    A a = {123, 123, 12}; //error
    
    struct B
    {
        int x;
        int y;
    protected:
        static int z;
    }; 
    
    B b = {123, 123}; //ok

      例子中,A的实例化是失败的,因为z是一个受保护的非静态成员。而b是成功的,因为z是一个受保护的静态数据成员,所以,类成员里面的静态数据成员是不能通过初始化列表来初始化的,静态数据成员的初始化遵循静态成员的初始化方式。

    3.2.4 有基类或者虚函数

      有基类或者虚函数同样不适用于使用初始化列表。

    struct A
    {
        int x;
        int y;
        virtual void fun(){};
    }; 
    
    A a = {123, 123}; //error
    
    class B {};
    
    struct C : public B
    {
        int x;
        int y;
    }; 
    
    B b = {123, 123}; //error

    3.2.5 {}和=初始化的非静态数据成员

    struct A
    {
        int x;
        int y = 2;
    }; 
    
    A a = {123, 123}; //error

      在类型A中,y在声明时即被=初始化为2,所以A不再是一个聚合类型。

      这个例子中需要注意的是,C++11中放宽了类型申明的初始化操作,即在非静态数据成员的声明时调用{}或者=来对成员进行初始化,但是造成的影响是该类型不再是聚合类型,所以不能直接使用初始化列表。所以,如果要使用初始化列表就必须自己定义一个构造函数。

    3.2.6 聚合类型并非递归

    struct A
    {
        int x;
        int y;
    private:
        int z;
    }; 
    
    A a{1, 2, 3}; // error
    
    A a1{}; //ok
    
    struct B
    {
        A a;
        int x;
        double y;
    };
    
    B b{{}, 1, 2.5};

      A有一个私有化的非静态成员,所以使用A a{1, 2, 3}是错误的,但是可以调用他的无参构造函数,所以在B中,即使成员a是一个非聚合类型,但是B仍然是一个聚合类型,可以直接使用初始化列表。

    3.2.6 小结

      根据这么多例子,我们得到以下结论:

      对于一个聚合类型,使用初始化列表相当于对其中每个元素分别赋值;而对于非集合类型,则需要先定义一个合适的构造函数,此时使用初始化列表将调用它对应的构造函数。

    4、  初始化列表

    4.1 任意长度的初始化列表

      在c++中,对于stl容器和未显示数组长度的数组可以进行任意长度的初始化,在初始化的时候可以书写任意长度的内容。

    int i_arr[] = {1,2,3,4};
    
    std::vector<int> veci_t = {1,2,3,4};
    
    std::map<std::string, int> mapsi_t = {{"1", 1}, {"2", 2}, {"3", 3}};

      但是对于自定义类型不具备这种能力,但是C++11解决了这个问题,C++11中可以通过轻量级模板std::initalizer_list来解决这个问题。我们只需要添加一个std::initializer_list的构造函数,这个自定义类型即可拥有这种任意长度初始化列表来初始化的能力。

    class Foo
    {
    public:
        Foo( std::initializer_list<int> list ) {};
    };
    
    Foo foo = {1,2,3,4,5};

      std::initializer_list负责接收初始化列表,可以通过for循环来读取其中的元素,并将元素做操作。不仅可以作为类型的初始化,同样的,可以作为函数参数传递同类型的数据集合。在任何需要的时候,都可以使用std::initializer_list来一次性传递多个参数。

    // code1
    class FooVector
    {
    public:
        FooVector(std::initializer_list<int> list)
        {
            for(auto it = list.begin(); it != list.end(); ++it)
            {
                mveci_content.push_back(*it);
            }
        }
    private:
        std::vector<int> mveci_content;
    };
    
    FooVector foo1 = {1,2,3,4,5};
    
    //code2
    using pair_t = std::map<int, int>::value_type;
    class FooMap
    {
    public:
        FooMap(std::initializer_list<pair_t> list)
        {
            for(auto it = list.begin(); it != list.end(); ++it)
            {
                mmapii_content.insert(*it);
            }
        }
    private:
        std::map<int, int> mmapii_content;
    };
    
    FooMap foo2 = {{1,2}, {3,4}, {5,6}};
     
    //code3
    void vFunc(std::initializer_list<int> list)
    {
        for(auto it = list.begin(); it != list.end(); ++it)
        {
            std::cout << *it << std::endl;
        }
    }
    
    void vCallFunc(void)
    {
        vFunc({});
        vFunc({1,2,3,4});
    }

    4.2 std::initializer_list使用细节

      std::initializer_list的特点如下:

    • 它是一个轻量级的容器类型,内部定义了iterator等容器等必须的概念;
    • 可以接收任意长度的初始化列表,但是要求元素必须都是同种类型;
    • 有三个成员接口,size(),begin(),end();
    • 只能被整体初始化或者赋值。

      //获取长度

    std::initializer_list<int> list = {1,2,3};  //初始化
    size_t len = list.size();       //len = 3

      std::initializer_list的访问只能通过begin()和end()来进行循环遍历,遍历取得的迭代器是只读的,所以无法修改其中元素的值,但是可以整体赋值来修改其中的元素。

    std::initializer_list<int> list;
    size_t len = list.size();  //len = 0
    list = {1,2,3,4,5};
    len = list.size();   //len = 5
    list = {1,2};
    len = list.size();    //len = 2

      在研究了std::initializer_list的用法之后,我们来看std::initializer_list的效率。很多时候,如果容器内部是自定义类型或者数量较大,那么是不是就像vector之类的容器一样,把每个元素都赋值一遍呢?答案是不是!std::initializer_list是非常高效的,它的内部并不保存初始化列表元素中的拷贝,仅仅保存初始化列表中的引用。

      如果我们按照下面的代码来使用std::initializer_list是错误的,虽然可以正常通过编译,但是可能无法得到我们希望的结果,因为a,b在函数结束时生存周期也结束了,返回的是不确定的内容。

    std::initializer_list<int> func1(void)
    {
        int a = 1, b = 2;
        return {a, b};  //a,b在返回时并没有拷贝
    }

      正确的用法应该是这样,通过真正的容器或者具有转移拷贝语意的物件来替代std::initializer_list返回的结果。

    std::vector<int> func2(void)
    {
        int a = 1, b = 2;
    
        return {a, b};  //ok
    }

      我们应该将std::initializer_list看作保存对象的引用来使用,在它持有的对象的生命周期结束之前来完成传递。

    5、  防止类型收窄

    5.1 类型收窄的情况

      我们先来看一段代码:

    struct Foo
    {
        Foo(int i) { std::cout << i << std::endl;}
    };
    
    Foo foo(1.2);

      这个例子就是类型收窄的情况,虽然说能够正常通过编译,但是在传递i之后不能完整的保存浮点数的数据。

      我们来看C++中有哪些情况会有类型收窄的情况:

    • 从一个浮点数隐式转换为一个整数,如int I = 2.2;
    • 从高精度浮点数隐式转换为低精度浮点数,如long doule隐式转换为double或者float;
    • 从一个整数隐式转换为一个浮点数,并且超出了浮点数的范围,如float f = (unsigned long long ) – 1;
    • 从一个整型隐式转换为一个长度较短的整型数,并且超出了长度较短的整型数范围,如char x = 65536;

      这些类型收窄的情况,在编译器并不会报错,但是可能存在潜在的错误。

    5.2 C++11的改善

      C++11中可以通过初始化列表来检查,防止类型的收窄。我们来看一组例子:

    int a = 1.1;            //ok
    int b = {1.1};          //error
    
    float fa = 1e40;          //ok
    float fb = {1e40};        //error
    
    float fc = (unsigned long long) -1;         //ok
    float fd = { (unsigned long long) -1 };     //error
     
    float fe = (unsigned long long)1;           //ok
    float ff = {(unsigned long long)1};         //ok
    
    const int x = 1024, y = 1;
    char c = x;                       //ok
    char d = {x};                     //error
    char e = y;                       //ok
    char f = {y};                     //ok

      在C++11中,遇到各种类型收窄的情况,初始化列表是不允许这种转换的,上述例子中,如果x,y去掉const限定符,最后的f也会因为类型收窄而报错。

  • 相关阅读:
    bzoj2004(矩阵快速幂,状压DP)
    bzoj1242(弦图判定)
    uva1659(最大费用循环流)
    bzoj1009
    bzoj2893(费用流)
    bzoj4873(最大权闭合子图)
    bzoj2879(动态加边费用流)
    51nod 1239 欧拉筛模板
    poj2774 sa模版
    洛谷3391文艺平衡树
  • 原文地址:https://www.cnblogs.com/ChinaHook/p/7648330.html
Copyright © 2011-2022 走看看