C++11 POD类型

POD(Plain Old Data,普通旧数据)类型是从C++11开始引入的概念,Plain代表它是一个普通类型,Old代表它可以与C兼容。通俗的讲,一个类、结构、共用体对象或非构造类型对象能通过二进制拷贝(如memcpy())后还能保持其数据不变正常使用的就是POD类型的对象。严格来讲,一个对象既是普通类型(Trivial Type)又是标准布局类型(Standard-layout Type)那么这个对象就是POD类型。

不同类型的对象意味着对象的成员在内存中的布局是不同的。在某些情况下,布局是有规范明确的定义,但如果类或结构包含某些 C++ 语言功能,如虚拟基类、 虚函数、 具有不同的访问控制的成员,则不同编译器会有不同的布局实现,具体取决于编译器对代码的优化方式,比如实现内存对齐,减少访存指令周期。例如,如果类具有虚函数,该类的所有实例都会包含一个指向虚拟函数表的指针,那么这个对象就不能直接通过二进制拷贝的方式传到其它语言编程的程序中使用。

C++给定对象类型取决于其特定的内存布局方式,一个对象是普通、标准布局还是POD类型,可以根据标准库函数模板来判断is_trivial<T>is_standard_layout<T>is_pod<T>,使用时需要包含头文件<type_traits>

1.普通类型

当类或结构体满足如下几个条件时则是普通类型: (1)没有虚函数或虚拟基类; (2)由C++编译器提默认的特殊成员函数(默认的构造函数、拷贝构造函数、移动构造函数、赋值运算符、移动赋值运算符和析构函数); (3)数据成员同样需要满足条件(1)和(2)。

注意,普通类型可以具有不同的访问说明说明符。下面我们使用模版类std::is_trivial<T>::value来判断数据类型是否为普通类型。

#include <iostream>
#include <string>

class A { A() {} };
class B { B(B&) {} };
class C { C(C&&) {} };
class D { D operator=(D&) {} };
class E { E operator=(E&&) {} };
class F { ~F() {} };
class G { virtual void foo() = 0; };
class H : virtual F{};
class I {};

int main()
{
    std::cout << std::is_trivial<A>::value ;  //有自定义构造函数
    std::cout << std::is_trivial<B>::value;  //有自定义的拷贝构造函数
    std::cout << std::is_trivial<C>::value;  //有自定义的移动构造运算符
    std::cout << std::is_trivial<D>::value;  //有自定义的赋值运算符
    std::cout << std::is_trivial<E>::value;  //有自定义的移动赋值运算符
    std::cout << std::is_trivial<F>::value;  //有自定义的析构函数
    std::cout << std::is_trivial<G>::value;  //有虚函数
    std::cout << std::is_trivial<H>::value;  //有虚基类
    std::cout << std::is_trivial<I>::value ;   //普通的类return 0;
}

程序输出结果如下:

000000001

2.标准布局类型

当类或结构体满足如下几个条件时则是标准布局类型: (1)没有虚函数或虚拟基类; (2)所有非静态数据成员都具有相同的访问说明符; (3)在继承体系中最多只有一个类中有非静态数据成员; (4)子类中的第一个非静态成员的类型与其基类不同;此规则是因为C++允许优化不包含成员基类而产生的。在C++标准中,如果基类没有任何数据成员,基类应不占用空间,为了体现这一点,C++标准允许派生类的第一个成员与基类共享同一地址空间。但是如果派生类的第一个非静态成员的类型和基类相同,由于C++标准要求相同类型的对象的地址必须不相同,编译器就会为基类分派一个字节的地址空间。比如下面的代码:

class B1{};
class B2{};

class D1: public B1
{
    B1 b;
    int i ;
};
class D2: public B1
{
    B2 b ;
int i ;
}

D1和D2类型的对象内存布局应该是相同,但实际上是不同的,因为D1中基类B1和对象b都占用了1个字节,D2中基类B1为空,并不占用内存空间。D1和D2的内容布局从左至右如下图所示:

注意,这条规定GNU C++遵守,Visual C++并不遵守。 (5)所有非静态数据成员同样需要满足条件(1)、(2)、(3)和(4),即符合标准布局类型。 考察如下程序:

#include <iostream>
using namespace std;

class A { virtual void foo() = 0; };

class B
{
private:
    int a;
public:
    int b;
};

class C1
{
    int x1;
};

class C:C1
{
    int x;
};

class D1 {};
class D : D1
{
    D1 d1;
};

class E : virtual C1 {};
class F { B x; };
class G :C1, D1 {};

int main()
{
    std::cout << std::is_standard_layout<A>::value ;  // 有虚函数  
    std::cout << std::is_standard_layout<B>::value ;  // 成员a和b具有不同的访问权限  
    std::cout << std::is_standard_layout<C>::value ;  // 继承树有非静态数据成员的类超过1个  
    std::cout << std::is_standard_layout<D>::value ;  // 第一个非静态成员是基类类型
    std::cout << std::is_standard_layout<E>::value ;  // 有虚基类
    std::cout << std::is_standard_layout<F>::value ;  // 非静态成员x不符合标准布局类型
    std::cout << std::is_standard_layout<G>::value ;  // return 1
    return 0;
}

程序运行结果:

00000001

3.POD类型

一个对象既是普通类型(Trivial Type)又是标准布局类型(Standard-layout Type)那么这个对象就是POD类型。为什么我们需要POD类型满足这些条件呢,POD类型在源代码兼容于ANSI C。POD对象与C语言的对应对象具有共同的一些特性,包括初始化、复制、内存布局、寻址: (1)可以使用字节赋值,比如用memset、memcpy对POD类型进行赋值操作; (2)对C内存布局兼容,POD类型的数据可以使用C函数进行操作且总是安全的; (3)保证了静态初始化的安全有效,静态初始化可以提高性能,如将POD类型对象放入BSS段默认初始化为0。

下面看一下POD类型的二进制拷贝示例:

#include <iostream> 
using namespace std;

class A
{
public:
    int x;
    double y;
};

int main()
{
    if (std::is_pod<A>::value)
    {
        std::cout << "before" << std::endl;
        A a;
        a.x = 8;
        a.y = 10.5;
        std::cout << a.x << std::endl;
        std::cout << a.y << std::endl;

        size_t size = sizeof(a);
        char *p = new char[size];
        memcpy(p, &a, size);
        A *pA = (A*)p;

        std::cout << "after" << std::endl;
        std::cout << pA->x << std::endl;
        std::cout << pA->y << std::endl;

        delete p;
    }
    return 0;
}

程序运行结果如下:

before
8
10.5
after
8
10.5

可见,POD类型使用字节拷贝赋值可以正常使用。事实上,如果对象是普通类型,不是标准布局,例如类有public与private的非静态数据成员,可以使用memcpy进行二进制赋值。如果对象是标准布局类型,不是普通类型,例如类有复杂的move与copy构造函数,能使用C函数进行操作。


参考文献

[1]Trivial、standard-layout 和 POD 类型 [2]C++11之POD类型 [3]C++11新特性之POD类型 [4]POD (程序设计).wikipedia [5]深入理解C++11[M].3.8POD类型

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • C++之IO格式控制

    C语言中,我们可以通过函数printf和scanf进行格式化控制,而在C++中仍然包含了前者,但还提供了以下两种格式控制的方法: (1)使用流成员函数进行格...

    Dabelv
  • C++ IO格式控制

    C语言中,我们可以通过函数printf和scanf进行格式化控制,而在C++中仍然包含了前者,但还提供了以下两种格式控制的方法: (1)使用流成员函数进行格式...

    Dabelv
  • Go语言——sync.Once分析

    sync.Once表示只执行一次函数。要做到这点,就需要两点: (1)计数器,统计函数执行次数; (2)线程安全,保障在多G情况下,函数仍然只执行一次,比如...

    Dabelv
  • c++11模板:容器(map,set,list,vector)中元素类型转换

    版权声明:本文为博主原创文章,转载请注明源地址。 https://blog.csdn.net...

    用户1148648
  • 现代C++教程:高速上手(四)-容器

    std::array与std::vector不同的是,array对象的大小是固定的,如果容器大小是固定的,那么可以优先考虑使用std::array容器。

    程序员小涛
  • C++ IO格式控制

    C语言中,我们可以通过函数printf和scanf进行格式化控制,而在C++中仍然包含了前者,但还提供了以下两种格式控制的方法: (1)使用流成员函数进行格式...

    Dabelv
  • [译]C++17,optional, any, 和 variant 的更多细节

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tkokof1/article/details/826...

    用户2615200
  • C++之IO格式控制

    C语言中,我们可以通过函数printf和scanf进行格式化控制,而在C++中仍然包含了前者,但还提供了以下两种格式控制的方法: (1)使用流成员函数进行格...

    Dabelv
  • OpenCV3.3深度学习模块(DNN)应用-图像分类

    DNN模块介绍 在OpenCV3.3版本发布中把DNN模块从扩展模块移到了OpenCV正式发布模块中,当前DNN模块最早来自Tiny-dnn,可以加载预先训练好...

    OpenCV学堂
  • 计算机模拟定积分的定义

    黎曼(Riemann)对定积分的定义是:积分区间划分为无数子区间,子区间内任意一点的函数值乘以子区间的长度得到一个矩形面积,然后将这些矩形面积累加起来可以得到积...

    fem178

扫码关注云+社区

领取腾讯云代金券