Lippman在《深度探索C++对象模型》的前言中写道:

I have heard a number of people over the years voice opinions similar to those of your colleagues. In every case, those opinions could be attributed to a lack of factual knowledge about the C++ language. Just last week I was chatting with an acquaintance who happens to work for an IC testing manufacturer, and he said they don't use C++ because "it does things behind your back." When I pressed him, he said that he understood that C++ calls malloc() and free() without the programmer knowing it. This is of course not true. It is this sort of "myth and legend" that leads to opinions such as those held by your colleagues….

Finding the right balance [between abstraction and pragmatism] requires knowledge, experience, and above all, thought. Using C++ well requires effort, but in my experience the returns on the invested effort can be quite high.

要想C++学得好、用得好,了解编译器在你背后做的事情是很有必要的。

在C++中有一类特殊的类型,聚合类型,可以被看作是纯正的数据类型;聚合类型有一个子集,POD类型,C++可以通过POD类型与其他语言交互。C++11定义了标准的内存模型,POD的概念被淡化,取而代之的是平凡性与标准布局。

在介绍这些类型分类之前,我们先来了解几种初始化。

零初始化

零初始化,顾名思义,就是初始化成零。零是指该类型的零,可能不是每一位都是0的表示。

在以下情形中,零初始化被执行:

  • static T object;

  • T();

  • T t = {};

  • T{};

  • CharT array[n] = "";

最后一种指的是字符串字面量长度不足数组长度,剩余的部分被零初始化。

零初始化和常量初始化与下面要讲的默认初始化和值初始化不在一个层次上,前者是静态存储期限对象的行为,后者是初始化器的行为。

默认初始化

当一个对象没有初始化器时,它被默认初始化,包括以下情形:

  • T object;

  • new T

  • 没有包含在构造函数初始化列表的成员对象。

默认初始化的效果是递归的:

  • 对于类类型,默认构造函数被调用;

  • 对于数组类型,每个元素被默认初始化;

  • 其他情况,什么都不做,对象被初始化为非确定值。

依据上面的规则,全局作用域下的int i;会被默认初始化,i会拥有非确定值,然而我们又知道,作为一个静态存储期限的对象,i的初始值是0。事实上,i先被零初始化,再被默认初始化,第二步中它的0值保持不变。

合成的默认构造函数对每个对象执行默认初始化,即使是显式= default也一样,类中的非类类型对象的初始值是非确定的,参见C++类成员默认初始值

访问一个非确定值的行为是未定义的,尽管那个对象可能只是一个普通整数,但如果较起真来,程序直接崩溃也是符合预期的。没有什么理由需要使用一个非确定值,如果需要随机数可以用专门的随机数设施

值初始化

当一个对象的初始化器为空时,它被值初始化,包括以下情形:

  • T()

  • new T()

  • Class::Class(...) : member() { ... }

  • 空的列表初始化

列表初始化包括前三种把()换成{},以及T object{};,但后者没有对应的圆括号版本,因为T object();是在声明一个函数。

值初始化的行为是:

  • 对于有用户提供的默认构造函数的类型,执行默认初始化;

  • 对于隐式声明或显式= default默认构造函数的类型,先执行零初始化,再执行默认初始化;

  • 如果是数组类型,对每个元素值初始化;

  • 否则,零初始化。

所谓用户提供的构造函数,是指有{...}定义的,= default= delete不在此列。

值初始化的意义在于它可以提供确定的对象。标准库容器在以容器大小为参数的构造函数中,执行的初始化就是元素类型的值初始化。

聚合类型

聚合类型的概念在C++的发展中有过许多细节调整,这里先根据C++11标准讲解。

聚合类型是数组类型,或满足以下条件的类类型:

  • 没有privateprotected非静态数据成员;

  • 没有用户提供的构造函数;

  • 没有基类;

  • 没有虚函数;

  • 没有类内初始化。

注意,这个定义不是递归的——聚合类型的成员完全可以是非聚合类型。

初始化一个聚合类型的方式有:

  • T object = {arg1, arg2, ...};

  • T object{arg1, arg2, ...};

C++14解禁了类内初始化;C++17允许基类,但不能是privateprotectedvirtual,相应地构造函数不能有继承的,还加了一条不能有explicit的;C++20又把构造函数的要求改回了没有用户声明的构造函数(= default也不行了)。虽然每次修改都有道理,但是频繁修改语言核心真心头疼。至于C99中的指定元素名称的初始化,也在C++20中才进入标准。

聚合初始化的行为是:

  • 所有基类、数组、非静态数据成员,按照在类中的出现顺序与数组下标的顺序,从初始化列表中的对应项拷贝初始化;

  • 除了列表初始化以外,隐式转换都是允许的;

  • 聚合初始化是递归的——如果初始化列表中有嵌套列表,对应项会被列表初始化;

  • 不指定长度的数组可以从初始化列表推导长度,非静态数据成员除外;

  • 静态数据成员与未命名的位域跳过;

  • 初始化列表的长度不能超过需要初始化的基类与成员的数量;

  • 如果长度不够,包括初始化列表为空:

    • 如果有类内初始化,用它;

    • 否则按照列表初始化的规则,对于非类类型和非聚合类类型,值初始化;

    • 对于聚合类型,递归使用该规则;

    • 没有对应初始化项的成员不能有引用类型的,否则报错;

  • 对于联合体,只有第一个成员被初始化。

学习聚合类型的规则,重在理解聚合初始化的行为——初始化要做的就是、只是拷贝构造每一个成员。这样就不难解释一些行为,比如,由于静态数据成员不是对象的一部分,因此在初始化时被跳过;虚函数和虚基类会引入vptr之类的东西,在初始化列表中没有体现,因而不被允许。这种思考方式在C++中是很实用的。

POD类型

在C++11以前,POD(Plain Old Data)类型定义为下列类型之一:

  • 标量类型,包括算术类型(整数与浮点数)、指针、成员指针、枚举类型、std::nullptr_t(C++11特性,只是为了给标量类型一个完整的定义);

  • POD类型的数组类型;

  • 类类型,满足:

    • 是聚合类型;

    • 没有非POD的非静态数据成员;

    • 没有引用类型的成员;

    • 没有用户提供的拷贝构造函数;

    • 没有用户提供的析构函数。

从C++11起,上述最后一大类修改为:

  • 是平凡类型(见下);

  • 是标准布局类型(见下);

  • 没有非POD的非静态数据成员、

那我们先来看这两个定义。

平凡

一个平凡(trivial)的类型是这样的类型:

  • 符合TriviallyCopyable要求;

  • 有一个或多个默认构造函数,每个都是平凡的(稍后解释)或删除的,至少有一个不是删除的。

对应类型特征(type trait)is_trivial

TriviallyCopyable的要求是指:

  • 每个拷贝构造函数都是平凡的或删除的;

  • 每个移动构造函数都是平凡的或删除的;

  • 每个拷贝赋值运算符都是平凡的或删除的;

  • 每个移动赋值运算符都是平凡的或删除的;

  • 析构函数是平凡的、非删除的;

  • TriviallyCopyable类型的数组仍然是TriviallyCopyable的。

这里有很多平凡,我不打算一一列出其要求,它们大致上讲了同一件事:

  • 不是用户提供的;

  • 所在类没有虚拟,包括虚基类和虚函数;

  • 对类型中的每个非静态成员,递归该要求。

平凡的构造函数还有一条:没有类内初始化。

标准布局

平凡规定了对象控制行为,标准布局(StandardLayoutType,is_standard_layout)则规定了对象模型:

  • 所有非静态数据成员都有相同的访问控制等级,即同为public、同为protected或同为private(这是因为,编译器有权把相同访问等级的成员安排在一起,那样会破坏布局);

  • 没有虚拟;

  • 没有非静态数据成员是引用类型;

  • 对基类和非静态数据成员类型递归要求StandardLayoutType;

  • 不能有同一个基类被继承两次,即所谓菱形继承(virtual早就已经否决了);

  • 继承链中只有一个类型有非静态数据成员;

  • 为了不与空基类优化冲突,基类不能有以下类型:

    • 对于非联合类型,第一个非静态数据成员的类型;

    • 对于联合类型,所有非静态成员类型;

    • 对于数组类型,其元素类型;

    • 以及这些类型递归调用这条规则产生的类型,有点计算LL(1)分析算法中FIRST集的味道。

现在可以回到POD类型了。POD是特殊的类型,它有许多非POD类型不具有的性质:

  • 完全与C兼容,但是仍然可以有成员函数;POD类型标准到甚至可以与其他语言兼容;

  • 可以用std::memcpy拷贝(对于非POD类型,即使满足TriviallyCopyable,用std::memcpy拷贝的行为也是未定义的);

  • 有更长的生命周期,从资源获取到资源释放,而非POD类型的是从构造函数结束到析构函数结束;

  • goto语句不能跳过变量的定义,但POD类型的是允许的;

  • POD类型对象的前部没有填充字节,即对象指针与第一个成员的指针是相等的。

聚合类型与POD类型的更多相关文章

  1. 关于POD和非POD类型中,list initialization和constructor initialization(未解决)

    如果你的成员是POD类型的,那么list initialization和constructor initialization没有任何区别 #include<iostream> using ...

  2. c++11 pod类型(了解)

    啥是POD类型? POD全称Plain Old Data.通俗的讲,一个类或结构体通过二进制拷贝后还能保持其数据不变,那么它就是一个POD类型. 平凡的定义 .有平凡的构造函数 .有平凡的拷贝构造函数 ...

  3. C++ trivial和non-trivial构造函数及POD类型(转)

    原博客地址http://blog.csdn.net/a627088424/article/details/48595525 最近正纠结这个问题就转过来了,做了点补充(参考<深度探索C++对象模型 ...

  4. C++11 POD类型

    POD,全称plain old data,plain代表它是一个普通类型,old代表它可以与c兼容,可以使用比如memcpy()这类c中最原始函数进行操作.C++11中把POD分为了两个基本概念的集合 ...

  5. POD类型

    POD类型 POD全称Plain Old Data.通俗的讲,一个类或结构体通过二进制拷贝后还能保持其数据不变,那么它就是一个POD类型. C++11将POD划分为两个基本概念的合集,即:平凡的和标准 ...

  6. 3. C++ POD类型

    POD全称Plain Old Data,通常用于说明1个类型的属性.通俗的讲,一个类或结构体通过二进制拷贝后还能保持其数据不变,那么它就是一个POD类型. C++11将POD划分为2个基本概念的合集, ...

  7. 关于C++ 中POD类型的解析

    转自: http://liuqifly.spaces.live.com/blog/cns!216ae3a149106df9!221.entry (C++-98:1.8;5)给出的定义:将对象的各字节拷 ...

  8. C++ POD类型

    POD( Plain Old Data)概念: Arithmetic types (3.9.1), enumeration types, pointer types, and pointer to m ...

  9. Kubernetes的pod控制器及ReplicaSet控制器类型的pod的定义

    为什么需要Pod Kubernetes项目之所以这么做的原因: 因为Kubernetes是谷歌公司基于Borg项目做出来的,谷歌工程师发现,他们部署的应用往往存在这进程与进程组的关系.具体说呢,就是这 ...

随机推荐

  1. 【python实现卷积神经网络】定义训练和测试过程

    代码来源:https://github.com/eriklindernoren/ML-From-Scratch 卷积神经网络中卷积层Conv2D(带stride.padding)的具体实现:https ...

  2. 008-进制-C语言笔记

    008-进制-C语言笔记 学习目标 1.[掌握]include预处理指令 2.[掌握]多文件开发 3.[了解]认识进制 4.[掌握]进制之间的互相转换 5.[掌握]原码,反码,补码 6.[掌握]位运算 ...

  3. python简易的大乐透数据获取及初步分析

    该项目从网上爬取并分析彩票数据,为用户查看和初步分析往期数据提供一种简易的工具. https://github.com/unknowcry/Lottery # -*- coding: utf-8 -* ...

  4. 【Java】FlowControl 流程控制

    FlowControl 流程控制 什么是流程控制? 控制流程(也称为流程控制)是计算机运算领域的用语,意指在程序运行时,个别的指令(或是陈述.子程序)运行或求值的顺序. 不论是在声明式编程语言或是函数 ...

  5. Thinking in Java,Fourth Edition(Java 编程思想,第四版)学习笔记(九)之Interfaces

    Interfaces and abstract classes provide more structured way to separate interface from implementatio ...

  6. Salesforce 开发 | Salesforce与微信集成实操指南

    配置前须知 Salesforce通过试点对特定客户提供Lightning WeChat Messaging,该试点需要同意特定的条款.除非Salesforce宣布WeChat Messaging全面可 ...

  7. R - Cow and Message CodeForces - 1307C

    思路对了,但是不会写. 等差数列长度不是1就是2,所以不是一个字母就是俩字母,一开始写的时候直接枚举两个字母,然后让次数相乘.这样是不对的,比如abaabb,字母ab的个数应该是3+2+2,因该是每一 ...

  8. 详解PHP反序列化中的字符逃逸

    首发先知社区,https://xz.aliyun.com/t/6718/ PHP 反序列化字符逃逸 下述所有测试均在 php 7.1.13 nts 下完成 先说几个特性,PHP 在反序列化时,对类中不 ...

  9. pytorch GPU训练好的模型使用CPU加载

    torch.load('tensors.pt') # 把所有的张量加载到CPU中 torch.load('tensors.pt', map_location=lambda storage, loc: ...

  10. C# 基础知识系列-13 常见类库(三)

    0. 前言 在<C# 基础知识系列- 13 常见类库(二)>中,我们介绍了一下DateTime和TimeSpan这两个结构体的内容,也就是C#中日期时间的简单操作.本篇将介绍Guid和Nu ...