一、数据类型的意义
数据类型是编程语言的最基础的部分,用来向上层语言的应用提供支撑和底层的内存布局标准。当这些数据类型的内存处理决定后,就实现了编程语言与计算机的操作的格式。C++语言从开始到现在最新的标准的演进过程中,数据类型也因为应用场景和设计思想的不断的发展,也在不断的进行发展。所以在C++中,可以看到非常多的种类的数据类型。其实这也是C++语言看上去让开发者觉得复杂的一个重要原因。
二、常见数据类型
在开发者的开发经历中,一定是遇到过各种数据类型,应该或多或少也遇到过不同角度的数据类型的划分。当然,这并不是说数据类型有多么的复杂。其实,在学习其它学科,如数学、物理等时,也遇到过一些类似的概念,比如常见的标量。
在C++语言中,经常遇到的数据类型的名词包括:
1、标量
ScalarType,这种数据类型在标准中并没有严格的定义,但一般来说,C++中基本的类型(Primitive Type)中的整数类型和浮点类型以及枚举类型及指针都是标量。
2、复合类型
Compound Type,复合类型则一般是指数组、函数、类、结构体和联合、引用和指针类型都是复合类型即由基础类型组合而成的类型。
3、POD数据类型
POD(Plain Old Data),主要是为了和C语言的类型进行兼容,它可以直接使用字节或二进制进行复制并保持数据结构的安全性。这也意味着这种数据类型是纯数据内容,不包含各种填充的字节。这个类型目前已经被下面的标准布局类型和平凡类型取代了,更严格的说是C++20标准中弃用了。
4、标准布局类型
StandardLayoutType,这是C++11引入的一个新的类型,其目的也是为与C兼容并可以与C语言进行安全的数据对象的交互,它需要满足:所有非静态数据成员具有相同的访问控制;没有虚函数或虚基类;没有引用类型的非静态数据成员;类类型的所有非静态成员和基类本身都是标准布局类型;没有与第一个非静态数据成员类型相同的基类;另外满足最底层派生类中没有非静态数据成员,并且具有非静态数据成员的基类不超过一个,或者没有含非静态数据成员的基类
5、平凡类型
TrivialType,要求必须是平凡可复制的;如果类型是类类型或其数组,则该类具有一个或多个符合条件的默认构造函数,所有这些构造函数都是平凡的。而平凡可复制类型可参见前面的相关文章或cppreference,其一个显著的特征是:一个平凡可复制的类没有虚函数或虚基类。
平凡类型从C++11引入但在C++26中被弃用了。
6、聚合类型
AggregateType,聚合类型是一种特殊的数据类型,它可以是类类型或数组类型。对于类类型,它必须满足以下条件:它没有用户声明的构造函数(在C++11之前);它没有私有或保护的非静态数据成员;它没有虚函数和虚基类。
上面的很多定义和说明其实都是参看cppreference中的内容的,如果有些内容不清楚或者觉得不妥,可能是理解或者翻译的有问题,大家可自行查看相关的英文文档资料即可。
三、应用
很多开发者可能会感觉到,这些基础的知识有什么用呢?举一个例子,前面刚刚分析过的平凡迁移,其实就和这些基础知识有关系。对于C++为什么会有这么多数据类型,其实就是在语言发展过程中的一个必然经历的过程,就如人从幼时到成年过程中,即使同一个器官也会有很大的变化。
标量类型和复合类型是基础的数据类型,C++的应用是必须要使用的;而为了保持与C语言的兼容,才会有POD和平凡类型以及标准布局类型,这样的话,在两种语言的数据通信中,可以勿需考虑一些特别的情况,如果一些数据需要序列化或反序列化,这种数据类型也具有一定的优势;而在前面的C++新标准的学习过程中,不同的数据类型的初始化可以直接赋值,也可以使用初始化列表,也可以使用花括号。让数据对象的初始化越来越直白简单,而聚合类型就可以满足这种直接使用大括号进行初始化。
当大家都明白这种标准演进的过程中的底层支持的情况后,就明白了标准为什么是语言本身和库一起推进。需要注意的是,从C++11开始,数据类型的演进还是比较快的,有引进,有丢弃,这个非常重要。只要大家明白标准演进的方向,就会明白这种变化的过程是如何推进的。
四、例程
其实这方面的例子非常多,这里只举几个简单的例子:
//平凡类型
struct A { int m; };
static_assert(std::is_trivial_v<A> == true);
struct B { B() {} };
static_assert(std::is_trivial_v<B> == false);
// The following class shows why std::is_trivial(_v) may be a pitfall.
class C
{
private://注意私有
C() = default;
};
static_assert(std::is_trivial_v<C> == true);
static_assert(std::is_trivially_default_constructible_v<C> == false);
//标准布局-POD
struct A { int m; };
static_assert(std::is_standard_layout_v<A> == true);
class B: public A { int m; };
static_assert(std::is_standard_layout_v<B> == false);
struct C { virtual void foo(); };
static_assert(std::is_standard_layout_v<C> == false);
//聚合类型及初始化:Aggregate initialization
struct S1 { long a, b; };
struct S2 { S1 s, t; };
// Each subaggregate of “x” is appertained to an initializer clause starting with {
S2 x[2] =
{
// appertains to “x[0]”
{
{1L, 2L}, // appertains to “x[0].s”
{3L, 4L} // appertains to “x[0].t”
},
// appertains to “x[1]”
{
{5L, 6L}, // appertains to “x[1].s”
{7L, 8L} // appertains to “x[1].t”
}
};
这里的代码其实并没有完全体现出概念中的定义,不过大家可以照着定义进行分析即可。
五、总结
基础知识的特点是非常枯燥,容易理解学习,但却往往不知道有什么作用。更进一步说可能是不知道怎么灵活的应用,这才是根本的问题。其实这种认知也是学习编程的一个必须经过的历程。想把基础知识灵活应用,最容易实现的方法就是对一些复杂的知识进行拆解并不断的分析其应用到了哪种基础知识。重点是要弄明白别人是怎么用的,为什么这么用?而不要囫囵吞枣,看上去学得不少,但实际没有真正掌握几个。
在学会拆解别人的应用方式后,自己模拟着实现几次,慢慢的就会对应用到的基础知识有一个恍然大悟的应用掌握过程,如此迭代,基础知识自然就可以灵活的掌握和应用了。