引言

在C++中,POD是一个极其重要的概念。要理解POD类型,我们首先需要理解Aggregate类型。下文结合stackoverflow上的高票回答将对Aggregate类型做一个全面的解读。

对于Aggragates的定义

C++标准(C++ 03 8.5.1 §1)中的正式定义如下:

An aggregate is an array or a class (clause 9) with no user-declared constructors (12.1), no private or protected non-static data members (clause 11), no base classes (clause 10), and no virtual functions (10.3).

译文:一个Aggregate是一个数组或者一个没有用户声明构造函数,没有私有或保护类型的非静态数据成员,没有父类和虚函数的类型

从定义中我们首先可以解读出以下两点信息:

1. 数组一定是Aggregate。

2. 满足以下定义的class也可以是Aggregate(注意:class包括classes、structs和unions):

-> Aggregate不能拥有用户自定义的构造函数。事实上,它可以拥有一个默认构造函数或者一个默认复制构造函数,只要它们是被编译器声明,而非用户自定义的即可。

-> Aggregate不能拥有private或者protected的非静态数据成员。事实上,你可以定义其他private和protected成员方法(不包括构造函数,对于Aggregate,不能由用户自定义构造函数,参见上条),也可以定义private和protected静态类型的数据成员和方法,这都不会违背aggregate类型的规则。

-> Aggregate没有父类和虚函数。

-> Aggregate类型可以由用户自定义赋值操作符和析构函数。

注意:数组一定是Aggregate类型,即便数组中存放的是非Aggregate类型的元素。

例子

class NotAggregate1
{
virtual void f(){} //remember? no virtual functions
}; class NotAggregate2
{
int x; //x is private by default and non-static
}; class NotAggregate3
{
public:
NotAggregate3(int) {} //oops, user-defined constructor
}; class Aggregate1
{
public:
NotAggregate1 member1; //ok, public member
Aggregate1& operator = (Aggregate1 const & rhs) {/* */} //ok, copy-assignment
private:
void f() {} // ok, just a private function
};

Why Aggregates are special?

与非Aggregate类型不同的是,Aggregate类型可以使用“{}”来进行初始化。这种初始化语法,实际上在数组中是很常见的。别忘了我们的数组类型也属于Aggregate,来看以下例子:

Type array_name[n] = {a1, a2, ..., am};

1. 如果m与n相等:很自然,数组的第i个元素被初始化为ai。

2. 如果m小于n   :数组的前m个元素被依次初始化为a1,a2,…,am,剩余的n-m个元素将进行值初始化(前提是可以进行值初始化)。

3. 如果m大于n   :产生编译错误。

注意,类似于以下形式也是正确的:

int a[] = {1,2,3};) // 等价于 int a[3] = {1,2,3};

数组的长度将由编译器推测为3,因此以上两种形式是等价的。

值初始化(value-initialization

先来看以下解释,摘自stackoverflow:

When an object of scalar type (bool, int, char, double, pointers, etc.) is value-initialized it means it is initialized with 0 for that type (false for bool, 0.0 for double, etc.). When an object of class type with a user-declared default constructor is value-initialized its default constructor is called. If the default constructor is implicitly defined then all nonstatic members are recursively value-initialized. This definition is imprecise and a bit incorrect but it should give you the basic idea. A reference cannot be value-initialized. Value-initialization for a non-aggregate class can fail if, for example, the class has no appropriate default constructor.

译文如下:

对于标量类型(如:bool、int、char、double、指针)的对象如果按值初始化,是指其将被初始化为0(如:bool类型将被初始化为false,double类型将被初始化为0.0)。

默认构造函数由用户自定义的类类型对象如果按值初始化,那么其默认构造函数将会被调用;如果默认构造函数为隐式定义,那么所有的非静态数据成员将会递归地按值初始化。

虽然以上定义并不精确,也不完全,但是可以让我们有个基本的认识。注意,引用不能按值初始化。对于非Aggregate类型的class进行值初始化,可能会失败,例如没有合适的默认构函数。

1. 来看以下数组初始化的例子:

class A()
{
A(int){} // 用户自定义了构造函数,因此是非Aggrerate类型。注意,类A没有默认构造函数。定义对象时,无法进行值初始化
};
class B()
{
B() {} // 用户自定义了构造函数,因此是非Aggrerate类型。注意,类B拥有可用的默认构造函数。定义对象时,可以进行值初始化。
}; int main()
{
A a1[3] = {A(2), A(1), A(14)}; // OK n == m
A a2[3] = {A(2)}; // ERROR A没有默认构造函数. 不能按值初始化a2[1] 和 a2[2]
B b1[3] = {B()}; // OK b1[1]和b1[2]使用默认构造函数按值初始化 int Array1[1000] = {0}; // 所有元素被初始化为0
int Array2[1000] = {1}; // 注意: 只有第一个元素被初始化为1,其他为0;
bool Array3[1000] = {}; // 大括号里可以为空,所有元素被初始化为false; int Array4[1000]; // 没有被初始化. 这和空{}初始化不同;这种情形下的元素没有按值初始化,他们的值是未知的,不确定的; (除非Array4是全局数据) int array[2] = {1,2,3,4}; // ERROR, 太多初始值,编译出错。
}

2. 现在我们来看Aggregates类类型是如何使用{ }进行初始化的。和对数组进行初始化非常类似,按照在类内部声明的顺序(按照定义都必须是public类型)初始化非静态类型的成员变量。如果初始值比成员少,那么其他的成员将按值初始化。如果有一个成员无法进行按值初始化,我们将会得到一个编译期错误。如果初始值比成员多,我们同样得到一个编译期错误。

struct X{
int i1;
int i2;
};
struct Y{
char c;
X x;
int i[2];
float f;
protected:
static double d;
private:
void g(){}
}; Y y = {'a', {10,20}, {20,30}};

上面的例子中,y.c被初始化为’a’,y.x.i1被初始化为10,y.x.i2被初始化为20,y.i[0]为20,y.i[1]为30,y.f被按值初始化为0.0。protected类型的static数据成员d不会被初始化,因为它是静态类型的。 

值初始化与默认初始化的区别

对于内置类型变量而言,值初始化就是zero-initialization,默认初始化将根据变量所在位置而有所区别。以下讨论针对类类型。

在C++03标准中,针对类类型,只要用户没有自定义构造函数,那么就认为默认构造函数为隐式定义(即使其中有const类型变量,有引用类型)。

1. 如果用户自定义(显式定义)默认构造函数,那么无论值初始化还是默认初始化,都将会调用默认构造函数。

2. 如果默认构造函数为隐式定义,那么:

1)对于默认初始化,其采取的策略是分别对类成员递归地进行默认初始化,那么类中不能出现const内置类型成员、没有显式定义默认构造函数的const类类型对象、引用类型。

2)对于值初始化,其采取的策略是分别对类成员递归地进行值初始化,那么类中可以出现const类型成员,但是仍然不能出现引用类型成员(这是很显然的,因为引用不能按值初始化)。

注意:针对c++03标准,对于非静态数据成员,是不能给类内初始值的。c++11可以。

总结如下:

1. 默认初始化:内置类型变量在函数体内值未定义,在函数体外值为0。类类型变量调用默认构造函数进行默认初始化,显式默认构造函数直接调用,隐式默认构造函数则递归对其成员进行默认初始化。

2. 值初始化:内置类型变量为zero-initialization。类类型变量调用默认构造函数进行默认初始化,显式默认构造函数直接调用,隐式默认构造函数则递归对其成员进行值初始化。

ps:所有const成员都需要用户显式初始化。因此const内置类型成员无法默认初始化以及没有显式定义默认构造函数的const类类型对象无法默认初始化,而由用户显式定义了默认构造函数的const类类型对象可以完成默认初始化。而对于值初始化,实际上属于显式初始化,因为值初始化本质上是用零去初始化(针对内置类型变量)和用用户自定义的默认构造函数去初始化(针对类类型对象)。默认初始化,本质上是用一个未定义值去初始化(针对内置类型变量,属于隐式初始化)和用用户自定义的默认构造函数去初始化(针对类类型对象,属于显式初始化)。

pps:对于引用类型,既不能默认初始化,也不能按值初始化。

请看以下例子:

#include <iostream>
using namespace std; class A
{
public:
const int ival;
}; int main()
{
A object = {}; // 如果写成A object;进行默认初始化,由于A没有显式定义默认构造函数,因此对其成员递归进行默认初始化,而const内置类型变量必须用户显式初始化,程序报错
         cout << object.ival << endl;
}

调用该程序时,对象object 进行值初始化,由于该类默认构造函数为隐式定义,因此其成员a会递归地进行值初始化,为0。

再看以下例子:

#include <iostream>
#include <string>
#include <vector>
using namespace std; class A
{
public:
int ival;
//A():ival(0){}
void print() const
{
cout << ival << endl;
}
}; class B
{
public:
const A object_;
void print()
{
cout << "right" << endl;
}
}; int main(int argc, const char *argv[])
{
B object;
// object.object_.print();
object.print();
return 0;
}

main函数中对象object进行默认初始化。由于B没有显示定义默认构造函数,因此递归对其成员A(常量类类型对象)进行默认初始化,而A也没有显式定义默认构造函数,因此对常量对象A 默认初始化失败,程序报错。

总结

我们知道了Aggregates的特别之处,现在让我们来尝试理解一下它对类型的限制,也就是说为什么会有这些限制。来看以下解释,同样摘自stackoverflow:

We should understand that memberwise initialization with braces implies that the class is nothing more than the sum of its members. If a user-defined constructor is present, it means that the user needs to do some extra work to initialize the members therefore brace initialization would be incorrect. If virtual functions are present, it means that the objects of this class have (on most implementations) a pointer to the so-called vtable of the class, which is set in the constructor, so brace-initialization would be insufficient. You could figure out the rest of the restrictions in a similar manner as an exercise :)

我们应当理解使用{ }对成员进行逐一初始化意味着这一类型仅仅是成员的集合。

如果有一个用户定义的构造函数,那意味着用户需要做一些额外的工作来初始化成员,因此使用{ }初始化是不正确的。如果出现了虚函数,那意味着这个类型(大多数实现)有一个指向vtable的指针,需要在构造函数内设置,所以使用{ }初始化是不够的。我们可以按照这种方式理解其他限制的含义。

综上,Aggregare类型可以使用{ }进行初始化,且Aggregare类类型一定可以进行值初始化(前提是类中没有引用类型成员)。

之后的博文,我将会通过Aggregate类型来介绍POD(原生数据类型)。

参考文献

1. What are Aggregates and PODs and how/why are they special?

Aggregate类型以及值初始化的更多相关文章

  1. golang之类型零值初始化及比较

    综述 变量声明时未赋初值,则变量被自动赋值为该类型的零值(固定值) func new(Type) *Type new()返回一个指针,指向新分配的该类型的零值,不是空指针(nil).the value ...

  2. javascript 核心语言笔记- 3 - 类型、值和变量

    JavaScript 中的数据类型分为两类:原始类型(primitive type)和对象类型(object type).原始类型包括数字.字符串和布尔值 JavaScript 中有两个特殊的原始值: ...

  3. C#中异常:“The type initializer to throw an exception(类型初始值设定项引发异常)”的简单分析与解决方法

    对于C#中异常:“The type initializer to throw an exception(类型初始值设定项引发异常)”的简单分析,目前本人分析两种情况,如下: 情况一: 借鉴麒麟.NET ...

  4. Memcached Memcached.ClientLibrary.SockIOPool”的类型初始值设定项引发异常

    又一次遭遇"xxx类型初始值设定项引发异常" 下了个c#实现的轻量级IoC开源项目,可是在本地使用时发现一运行就捕捉到"类型初始值设定项引发异常"的异常信息,调 ...

  5. 零值初始化&字符串常数作为函数模板参数

    1.在定义一个局部变量时,并希望该局部变量的初始化一个值,可以显示调用其默认构造函数,使其值为0(bool类型默认值为false). template <typename T> void ...

  6. Lua 学习笔记(二)语法、类型、值

    首先Lua执行的每一段代码都称之为“程序块”,一个程序块也就是一连串的语句或命令,例如一个源码文件或一行代码.Lua语句之间并不需要分隔符,如代码中的换行就不起任何作用,当然为了养成编码习惯当两条或者 ...

  7. 第三章:Javascript类型、值和变量。

    计算机程序的运行需要对值(value)比如数字3.14或者文本"hello world"进行操作,在编程语言中,能够表示并操作的值的类型叫做数据类型(type),编程语言最基本的特 ...

  8. “System.Transactions.Diagnostics.DiagnosticTrace”的类型初始值设定项引发异常[WCF]

    未处理System.TypeInitializationException  HResult=-2146233036  Message=“System.ServiceModel.Diagnostics ...

  9. 脚踏实地学C#2-引用类型和值类型

    引用类型和值类型介绍 CLR支持两种类型,引用类型和值类型两种基本的类型: 值类型下有int.double.枚举等类型同时也可以称为结构,如int结构类型.double结构类型,所有的值类型都是隐式密 ...

随机推荐

  1. Spring Environment(三)生命周期

    Spring Environment(三)生命周期 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring Envi ...

  2. SSH无法连上CentOS7的问题

    今天安装完带GNOME的CentOS后发现XShell无法连接上Linux. 原因是sshd服务没有开启.下面是解决办法: 1 ip addr 发现网卡名称为ens33 2 在/etc/sysconf ...

  3. mysql 设置用户并授权

    一, 创建用户: 命令:CREATE USER 'username'@'host' IDENTIFIED BY 'password'; 说明:username - 你将创建的用户名, host - 指 ...

  4. ceres入门学习

    转载自https://www.jianshu.com/p/e5b03cf22c80 Ceres solver 是谷歌开发的一款用于非线性优化的库,在谷歌的开源激光雷达slam项目cartographe ...

  5. 【搜索】 Prime Path

    #include<cstdio> #include<cstring> #include<cmath> #include<queue> #include& ...

  6. [C#.net]处理UTF-8文件乱码

    今天帮同事处理一个2M左右的文件的格式,发现使用Encoding.default & Encoding.UTF8 & Encoding.GetEncoding("GB2312 ...

  7. usart下位机输出使用printf的格式化技巧

    输出使用printf("0x%3X ", I2c_Buf_Write[i]);时,上位机接收过程,使用文本格式,显示结果如下 虽然是保留了三位的宽度,但是并不美观. 相比于使用pr ...

  8. JavaScript函数和内置对象

    一.函数 function f1(){ console.log("666"); } f1(); //调用函数 1.普通函数定义 function f1(a,b){ console. ...

  9. 2019.01.26 codeforces 528D. Fuzzy Search(fft)

    传送门 fftfftfft好题. 题意简述:给两个字符串s,ts,ts,t,问ttt在sss中出现了几次,字符串只由A,T,C,GA,T,C,GA,T,C,G构成. 两个字符匹配的定义: 当si−k, ...

  10. 2018.12.15 hdu4641 K-string(后缀自动机)

    传送门 后缀自动机基础题. 题意简述:支持动态在串尾插入字符,查询在串中出现超过kkk次的子串的个数. 动态修改samsamsam,每次增量构造好了之后在parentparentparent树上从新建 ...