• C++ POD类型


    什么是POD类型?

    POD是Plain Old Data,是C++定义的一类数据结构,如char,int,float,double等都是POD类型。Plain顾名思义,表明POD是个普通类型,Old代表是旧的,与C语言兼容,意味着可以用古老的memcpy()进行复制,memset()进行初始化等。也就是说,POD特征的类或结构体,通过二进制拷贝后,仍然能保持数据结构(内存布局)不必

    C++11中,将POD划分为2个基本概念的合集:平凡的(trivial)和标准布局的(standard layout)。
    一个POD类型,就是指平凡的类型

    先看平凡的定义,一个平凡的类或结构体应该符合以下定义

    1)拥有平凡的默认构造函数(trivial constructor)和析构函数(trivial destructor)
    通常,不定义类的构造函数,编译器就会生成一个平凡的默认构造函数。而一旦定义了构造函数,即使构造函数不含参数,函数体也没有任何代码,那么该构造函数也不再是“平凡”的。

    比如:

    struct NoTrivial {
        NoTrivial();
    };
    

    这里NoTrivial构造函数就是非平凡的,而下面的构造函数就是平凡的,因为是编译器默认合成的。

    struct NoTrivial {
        NoTrivial() = default;
    };
    

    2)拥有平凡的拷贝构造函数(trivial copy constructor)和移动构造函数(trivial move constructor)。
    平凡的copy构造函数等同于用memcpy进行类型的构造。同平凡的默认构造函数一样,不声明copy构造函数的话,编译器会帮程序员自动地生成平凡的默认copy构造函数。

    平凡的move构造函数与平凡的copy构造函数类似。

    3)拥有平凡的拷贝赋值运算符(trivial assignment operator)和移动赋值运算符(trivial move operator)。
    与平凡的copy构造函数和平凡的move构造函数类似。

    4)不能包含虚函数和虚基类。
    因为编译器会为包含虚函数/虚基类的类生成一个隐含的指向虚函数表的指针vptr,而要构造。

    如何判断一个类型是否为平凡的类型?

    除了看类和结构是否包含上面提到的4类定义,最简单方式,当然是看 类和数据结构的内置数据是否都是内置的基本类型(int、float等平凡类型),数组的元素是否为平凡类型。

    C++11中,还可以通过辅助的类模板帮助判断:

    template<typename T> struct std::is_trivial;
    

    is_trivial的成员value可以用于判断T类型是否是一个平凡的类型。除了类和结构体,is_trivial也可以对内置的标量数据类型(int、float等)以及数组类型(元素类型是平凡的数组总是平凡的)进行判断。

    示例:is_trivial判断指定类型是否为平凡的。

    // 用支持C++11标准的编译器编译
    #include <iostream>
    #include <type_traits>
    
    using namespace std;
    
    struct Trivial1 { };
    struct Trivial2 {
    public:
        int a;
    private:
        int b;
    };
    
    struct Trivial3 {
        Trivial1 a;
        Trivial2 b;
    };
    
    struct Trivial4 {
        Trivial2 a[23];
    };
    
    struct Trivial5 {
        int x;
        static int y;
    };
    
    struct NonTrivial1 {
        NonTrivial1() : z(42) { }
        int z;
    };
    
    struct NonTrivial2 {
        NonTrivial2() = default;
        int y;
    };
    
    struct NonTrivial3 {
        NonTrivial3();
        int w;
    };
    
    NonTrivial3::NonTrivial3() = default;
    
    struct NonTrivial4 {
        Trivial5 c;
        virtual void f();
    };
    
    int main()
    {
        cout << is_trivial<Trivial1>::value << endl;  // 1
        cout << is_trivial<Trivial2>::value << endl;  // 1
        cout << is_trivial<Trivial3>::value << endl;  // 1
        cout << is_trivial<Trivial4>::value << endl;  // 1
        cout << is_trivial<Trivial5>::value << endl;  // 1
        cout << is_trivial<NonTrivial1>::value << endl;  // 0
        cout << is_trivial<NonTrivial2>::value << endl;  // 1
        cout << is_trivial<NonTrivial3>::value << endl;  // 0
        cout << is_trivial<NonTrivial4>::value << endl;  // 0
        return 0;
    }
    

    注意:NonTrivial2和NonTrivial3的构造函数,虽然都是用的编译器生成的默认构造函数,但后者是相当于自定义了构造函数,不过用编译器生成的版本。

    什么是标准布局?

    POD包含另外一个概念,即标准布局。标准布局的类或结构体应该符合以下定义:
    1)所有非静态成员有相同的访问权限(public,private,protected)。

    // 非标准布局
    struct {
    public:
       int a;
    private:
       int b;
    };
    
    // 标准布局
    struct {
    public:
        int a;
        int b;
    };
    

    2)在类或结构体继承时,满足下面两种情况之一:

    • 派生类中有非静态成员,且只有一个仅包含静态成员的基类。
    • 基类有非静态成员,而派生类没有非静态成员。
    // 派生类有非静态成员d, 且只有一个仅包含静态成员的基类B1
    struct B1 { static int a; };
    struct D1 : B1 { int d; }; 
    
    // 基类B2有非静态成员a, 派生类D2没有非静态成员
    struct B2 { int a; };
    struct D2 : B2 { static int d; }; 
    
    struct D3 : B2, B1 { static int d; }; // D3有1个基类B1包含静态成员, 1个基类B2包含非静态成员
    struct D4 : B2 { int d; }; // D4的基类B2包含非静态成员, 派生类D4也包含非静态成员
    struct D5 : B2, D1 { };    // D5有2个基类B2, D1, 都包含了非静态成员, 但多重继承会导致类型布局的变化
    

    D1, D2, D3都是标准布局的,D4, D5则不属于标准布局。实际上,非静态成员只要同时出现在派生类和基类间,就不属于标准布局。而多重继承也会导致类型布局的一些变化,所以一旦非静态成员出现在多个基类中,派生类也不属于标准布局。

    3)类中第一个非静态成员的类型与其基类不同。

    形如:

    struct A : B { B b; };
    

    这里A类型不是一个标准布局的类型,因为第一个非静态成员变量b的类型跟A所继承的类型B相同。而形如:

    struct C : B { 
        int a;
        B b;
    };
    

    这里C是一个标准布局的类型。

    struct B1 {};
    struct B2 {};
    
    struct D1 : B1 {
        B1 b; // 第一个成员仍然是基类
        int i;
    };
    
    struct D2 : B1 {
        B2 b; // 第一个成员并非基类
        int i;
    };
    
    int main()
    {
        D1 d1;
        D2 d2;
        cout << hex;
        // 7ffcdf314d00
        cout << reinterpret_cast<long long>(&d1) << endl;
        // 7ffcdf314d01
        cout << reinterpret_cast<long long>(&d1.b) << endl;
        // 7ffcdf314d04
        cout << reinterpret_cast<long long>(&d1.i) << endl;
    
        // 7ffcdf314d10
        cout << reinterpret_cast<long long>(&d2) << endl;
        // 7ffcdf314d10
        cout << reinterpret_cast<long long>(&d2.b) << endl;
        // 7ffcdf314d14
        cout << reinterpret_cast<long long>(&d2.i) << endl;
    
        return 0;
    }
    

    上面代码声明了4个类,其中2个没有成员的基类B1和B2,以及2个派生于B1的派生类D1和D2。D1,D2唯一区别是第一个非静态成员的类型。在D1中,第一个非静态成员的类型是B1,跟它的基类相同;而在D2中,第一个非静态成员的类型则是B2。

    直观地看,D1和D2应该是“布局相同”的,程序员应该可以用memcpy这样的函数,在这2种类型间进行拷贝,但实际上并不是这样。

    可以看到main函数中,打印出的D1类型变量d1和D2类型变量d2的地址,以及其成员的地址,是不一样的。

    d1.b地址 = d1地址 +1,而d2.b地址 = d2地址。

    在C++标准中,如果基类没有成员,则允许派生类的第一个成员与基类共享地址。因为派生类的地址总是“堆叠”在基类之上的,所以这样的地址共享,表明了基类没有占据任何的实际空间(节省一点数据空间)。但如果派生类第一个成员仍然是基类(如D1第一个成员B1 b),编译器仍会为该成员分配1byte空间。分配1byte空间,是因为C++标准要求类型相同的对象必须地址不同,即基类地址与派生类的成员b地址必须不同。

    也就是说,在标准布局中,C++11标准强制要求派生类的第一个非静态成员的类型必须不同于基类。

    下图表示代码示例中,2个基类地址与派生类地址第一个非静态成员地址关系:

    可以看到D1类型对象d1中,从基类B1继承而来的数据地址跟d1相同,占1byte,跟D1自定义的B1类型对象b地址d1.b并不相同。

    而在D2类型对象d2中,从基类B1继承而来的数据地址,跟D2自定义的B2类型对象b地址d2.b重叠,地址完全相同。

    如何判断类型是一个标准布局的类型?

    类似于用std::is_trivial判断一个类型是否为平凡的,C++11中,可以用std::is_standard_layout模板来判断类型是否是一个标准布局的类型。

    例,用 std::is_standard_layout::value 打印出类型的标准布局属性。

    #include <iostream>
    #include <type_traits>
    using namespace std;
    
    struct SLayout1 { }; // 空类是标准布局
    
    struct SLayout2 { // 所有非静态成员有相同访问属性, 成员类型是基本类型(不存在基类), 是标准布局
    private:
        int x;
        int y;
    };
    
    struct SLayout3 : SLayout1 { // 第一个非静态成员x, 并非基类成员, 是标准布局
        int x;
        int y;
        void f();
    };
    
    struct SLayout4 : SLayout1 { // 第一个非静态成员x, 并非基类成员, 是标准布局
        int x;
        SLayout1 y;
    };
    
    struct SLayout5 : /*SLayout1,*/ SLayout3 { }; // 第一个非静态成员(不存在), 并非基类成员, 是标准布局
    
    struct SLayout6 { static int y; }; // 没有非静态成员, 是标准布局
    
    struct SLayout7 : SLayout6 { int x; }; // 第一个非静态成员x, 并非基类成员, 是标准布局
    
    struct NonSLayout1 : SLayout1 { // // 第一个非静态成员x, 是基类成员, 不是标准布局
        SLayout1 x;
        int i;
    };
    
    struct NonSLayout2 : SLayout2 { int z; }; // 非静态成员同时出现在派生类和基类中, 不是标准布局
    
    struct NonSLayout3 : NonSLayout2 {}; // 非标准布局类型的派生类也不是标准布局
    
    struct NonSLayout4 { // 非静态数据成员访问属性不同, 不是标准布局
    public:
        int x;
    private:
        int y;
    };
    
    int main()
    {
        cout << is_standard_layout<SLayout1>::value << endl; // 1
        cout << is_standard_layout<SLayout2>::value << endl; // 1
        cout << is_standard_layout<SLayout3>::value << endl; // 1
        cout << is_standard_layout<SLayout4>::value << endl; // 1
        cout << is_standard_layout<SLayout5>::value << endl; // 1
        cout << is_standard_layout<SLayout6>::value << endl; // 1
        cout << is_standard_layout<SLayout7>::value << endl; // 1
    
        cout << is_standard_layout<NonSLayout1>::value << endl; // 0
        cout << is_standard_layout<NonSLayout2>::value << endl; // 0
        cout << is_standard_layout<NonSLayout3>::value << endl; // 0
        cout << is_standard_layout<NonSLayout4>::value << endl; // 0
        return 0;
    }
    

    参考

    [1]MichaelWon, IBM XL编译器中国开发团队. 深入理解C++11:C++11新特性解析与应用[M]. 机械工业出版社, 2013.

  • 相关阅读:
    Java自学-数字与字符串 字符串
    Java自学-数字与字符串 格式化输出
    Java自学-数字与字符串 数学方法
    Java自学-数字与字符串 字符串转换
    Java自学-数字与字符串 装箱和拆箱
    Java自学-接口与继承 UML图
    Mysql优化系列之查询优化干货1
    Mysql优化系列之查询性能优化前篇3(必须知道的几个事实)
    Mysql优化系列之查询性能优化前篇2
    第二十二篇:Spring简单定时任务
  • 原文地址:https://www.cnblogs.com/fortunely/p/16069895.html
Copyright © 2020-2023  润新知