C Programming Language

Basics #03: Types

# Basic Tutorial,  # Programming Language,  # C Programming Language

Last Updated: 2020-01-01


前置知识

在阅读本文档前,请确保你已经掌握指针的基础知识,详见 Basics #01: Pointer


前言

类型系统

本文会详细介绍 C 语言的类型系统。一方面,确实有些初学者对一些有意义的细节问题的认识有误,例如有些人可能并不知道 char 类型也属于整数类型;另一方面,初学者很难了解到类型系统中较为复杂深入的一面,本文也将对此进行介绍,但不会介绍对于初学者而言没有必要去了解的部分。

C 语言的 类型 (types) 总体上可以分为:

  • 对象类型;

  • 函数类型;

  • 不完全类型。

对于初学者而言,对象类型是需要被重点关注的,但函数类型和不完全类型也是 C 语言的必备知识。本文会对上述三种类型依次进行介绍,其中对象类型的介绍最为详细。

这部分内容涉及到一些条条框框的定义,对初学者而言可能有些晦涩,读者可以选择直接跳过,挑选其中你认为现阶段需要的部分来阅读。但是这些条条框框的定义仍然是重要的,虽然它们看起来似乎缺乏实用价值,但是很多英文资料中会使用这些定义,如果你对这些定义不了解,将会在阅读时受到阻碍。

标准类型

C 语言标准所规定的基本类型被称为 标准类型 (standard types) 。但是在各个编译环境中往往存在一些实现定义的基本类型,它们被称为 扩展类型 (extended types)

本文会详细介绍 C 标准头文件 stddef.hstdint.h 中为我们提供的一组非常实用的标准类型,包括其意义和用法。

在本文对类型系统的介绍中,我们只会讨论标准类型。为便于描述,下文中将省略“标准”描述。例如标准整数类型会被简写为整数类型。而事实上,整数类型包括标准整数类型和扩展整数类型,其它类型类同。


对象类型

对象类型 (object types) 指的是完整描述对象的类型。

对象类型和不完全类型是相对的,在学习了不完全类型的概念以后,你才能理解对象类型的概念性描述的含义。因而我不会在此处对上面的概念性描述进行解释。

事实上,初学者平时见到的类型声明多数属于对象类型,例如 int x , char ch , int * p 中的类型说明符都属于对象类型。除了函数类型和不完全类型以外的所有类型都是对象类型。


深入理解类型

引言

不知道读者是否曾经思考过这一问题:类型存在的意义是什么?为什么 C 语言(以及其它很多语言)都规定了各种各样的基本数据类型,并对其运算规则加以限制?这经常会带来一些麻烦,例如:

double x = 1 / 2;

上述代码中 xx 的值是 0 而不是 0.5 即便 xx 是 double 类型,因为参与运算的 12 都是整型,整型的除法运算结果会被向下取整,然后这个结果才被赋值给 xx 。我们必须使用浮点类型参与运算,或是其中一个操作数的类型为浮点类型以便于触发隐式类型转换,或是使用强制类型转换:

double x = 1.0 / 2.0;
double y = 1.0 / 2;
double z = (double)1 / 2;

为什么我们不能用一个统一的 number 类型来囊括所有的实数运算?这样写起程序来岂不是非常方便?

类型的意义

正如 C 语言标准原文所述:存储在对象中的值的含义取决于用于访问它的表达式的类型。

6.2.5 Types 1 The meaning of a value stored in an object or returned by a function is determined by the type of the expression used to access it. ...

类型存在的意义一方面是让计算机明白应该如何处理内存中的数据,另一方面是为计算机合理地节约空间消耗。

我们知道,计算机中的数据都是以二进制位 0/10/1 串的形式存储的,将若干个位组合在一起视为一个二进制数,就可以用来表示一个整数值。越多的二进制位就可以表示越大的数据范围,例如一个字节 88 位至多能表示不超过 28=2562^8 = 256 种不同的值,一般是 [27, 271][-2^7,\ 2^7 - 1][0, 281][0,\ 2^8 - 1]

一方面,浮点数值的表示并没有整数值这么简单。如果没有学习过计算机组成原理的相关知识,你很可能完全无法想象出人们是如何用二进制位来处理浮点类型的,要知道 double 类型的长度仅为 88 个字节 6464 位,却能表示高达 1030810^{308} 这个数量级的数值,远远超过了 2642^{64} ——虽然事实上数值越大其表示精度就越低,它仍然只能表示不超过 2642^{64} 种不同的浮点数值,中间有许多数值是无法被 double 类型所精确表示的。

本文不会对浮点数的表示原理进行介绍,但是读者肯定已经明白,计算机对整数值和浮点数值的解析方法肯定有很大的不同,即便它们都是用二进制位来表示的。这也就是数据类型的作用——告诉计算机应该如何解析并处理数据。同样是 nn 位二进制数,将其当作整型或是浮点类型看待,亦或是当作某种结构体类型看待,其结果都是截然不同的。

另一方面,越多的二进制位就可以表示越大的数据范围,例如对于整型而言,一个字节 88 位至多能表示不超过 28=2562^8 = 256 种不同的值,而 1616 位就能表示 216=655362^{16} = 65536 种不同的值,数据范围足足翻了 256256 倍,但相应地,空间消耗也翻了一倍。

那么问题来了:如果我们要用一个统一的 number 类型来囊括所有的实数运算,那么我们应该为每个 number 类型的对象分配多少内存呢?

如果出手阔绰(例如,一个 number 类型占据 128128 位),在进行小数据的运算时显然是对空间的极大浪费,我们根本不需要占用那么多的空间。即便是在计算机硬盘空间动辄几千 G 甚至几百 T 的今天,在存放海量数据的大型程序或数据库中,这种浪费根本是无法容忍的。不止于此,这还会影响到程序的运行速度,处理 3232 个二进制位显然比处理 128128 个要快得多。

这也就是为什么 C 语言把整型划分成了 char , short int , int , long int 等多个类型的原因——在上个世纪,计算机的容量远远没有如今这么大,甚至一块硬盘可能只有 16M 的空间,因此节约空间就显得更加重要。显而易见,用不同长度的数据类型来处理不同的计算需求,可以在很大程度上减少空间消耗。即便是如今,在确定只需要进行小数值的运算时,使用 short int 代替 int 也可以略微提高程序的运行速度。


基础类型

基本类型

基本类型 (basic types) 包括且仅包括:

  • 有符号整数类型;

  • 无符号整数类型;

  • char 类型;

  • 浮点类型。

整数类型

整数类型 (integer types) 包括且仅包括:

  • 有符号整数类型;

  • 无符号整数类型;

  • char 类型;

  • 枚举类型。

有符号整数类型 (signed integer types) 包括且仅包括 signed char 类型、 short int 类型、 int 类型、 long int 类型和 long long int 类型,其中:

  • int 类型的长度在不同编译环境中可能不同,要想了解你所在编译环境的情况,可以查看头文件 <limits.h> 中的宏定义 INT_MININT_MAX

  • long long int 类型在 C99 标准中首次引入。

对于每一种有符号整数类型,都有一种对应的 无符号整数类型 (unsigned integer types) ,占用相同的长度,并用 unsigned 关键字来标识。

除此以外,在 C99 标准中首次引入的 _Bool 类型也属于无符号整数类型,人们将其俗称为 布尔类型 (boolean type)_Bool 类型的长度在不同编译环境中可能不同(大多数情况下是 11 个字节),且 _Bool 类型的表达式的值只能是 01

关于 char 类型的细节详见后文。

枚举类型 (enumerated type) 指的是由关键字 enum 定义的类型,注意枚举类型属于整数类型,但是不属于基本类型。

字符类型

字符类型 (character types) 包括且仅包括 char 类型、 signed char 类型和 unsigned char 类型,其中后两者同时也分别属于有符号整数类型和无符号整数类型,而 char 类型则有些特殊。

char 类型的定义在不同编译环境中可能不同,但是标准规定 char 类型的特征(包括数据范围、表示形式、行为等)必须和 signed char 类型与 unsigned char 类型的其中之一等价。但是即便如此, char 类型都是一个不同的、独立的类型。

标准规定三种字符类型的长度相同。

字符类型和整数类型的定义是重叠的,而非互相独立的。所有的字符类型都是某种整数类型,很多初学者对此并不清楚。

节选 C99 标准对 char 类型的定义的部分原文如下:

6.2.5 Types 3 An object declared as type char is large enough to store any member of the basic execution character set. If a member of the basic execution character set is stored in a char object, its value is guaranteed to be nonnegative. If any other character is stored in a char object, the resulting value is implementation-defined but shall be within the range of values that can be represented in that type. ... 16 The three types char , signed char , and unsigned char are collectively called the character types. The implementation shall define char to have the same range, representation, and behavior as either signed char or unsigned char .

浮点类型

浮点类型 (floating types) 包括且仅包括:

  • 实浮点类型;

  • 复数类型。

实浮点类型 (real floating types) 包括且仅包括 float 类型、 double 类型和 long double 类型,其中 long double 类型在 C99 标准中首次引入。

标准规定三种实浮点类型的表示范围是依次为子集的,即 float 类型的表示范围是 double 类型的子集,且 double 类型的表示范围是 long double 类型的子集。

复数类型 (complex types) 包括且仅包括 float _Complex 类型、 double _Complex 类型和 long double _Complex 类型。

实数类型

实数类型 (real types) 包括且仅包括:

  • 整数类型;

  • 实浮点类型。

算术类型和类型域

算术类型 (arithmetic types) 包括且仅包括:

  • 整数类型;

  • 浮点类型。

每个算术类型都属于一个 类型域 (type domain)实型域 (real type domain) 包含实数类型, 复型域 (complex type domain) 包含复数类型。


函数类型

函数类型 (function types) 指的是专门用于描述函数的类型。

我把这部分内容放在函数的文章中具体介绍,详见 Basics #02: Function


不完全类型

不完全类型 (incomplete types) 指的是描述对象但缺乏足以确定其占用空间大小的信息的类型,包括且仅包括:

  • void 类型;

  • 大小未知的数组类型;

  • 内容未知的结构体或联合体类型。

void 类型用于表示空值,它是永远无法被完成的不完全类型。除 void 类型以外的不完全类型都可以在程序的其它翻译单元中被完成。

大小未知的数组类型常见于 extern 声明,例如:

extern int a[]; // 不完全
int a[10];      // 完全

不能使用不完全类型来声明或定义普通变量、数组、结构体等等,但可以用来声明或定义指针,因为指针的大小只和编译环境的地址空间规则有关,和被指向类型的大小无关。例如:

struct Test1 {
	struct Test1 t;
};
struct Test2 {
	struct Test2 * t;
};

Test1 的定义是不合法的,因为在第 33 行之前的 Test1 的内容还未完全已知,所以 Test1 暂为不完全类型,第 22 行的成员定义是不合法的。而对 Test2 的定义就是合法的,因为我们只是定义了一个指向不完全类型的指针。


派生类型

可以根据对象类型、函数类型和不完全类型构造任意数量的 派生类型 (derived types) ,包括且仅包括:

  • 数组类型 (array type)

  • 结构体类型 (structure type)

  • 联合体类型 (union type)

  • 函数类型 (function type)

  • 指针类型 (pointer type)

数组类型和结构体类型统称为 聚合类型 (aggregate types) ,算术类型和指针类型统称为 标量类型 (scalar types)


类型系统总结

C99 标准中规定的 C 语言标准类型可以汇总为:

  • 基本类型

    • 有符号整数类型

      • signed char 类型, short int 类型, int 类型, long int 类型, long long int 类型
    • 无符号整数类型

      • unsigned char 类型, unsigned short int 类型, unsigned int 类型, unsigned long int 类型, unsigned long long int 类型

      • 布尔类型,即 _Bool 类型

    • char 类型

    • 浮点类型

      • 实浮点类型

        • float 类型, double 类型, long double 类型
      • 复数类型

        • float _Complex 类型, double _Complex 类型, long double _Complex 类型
  • 枚举类型

  • void 类型

  • 派生类型

    • 数组类型

    • 结构体类型

    • 共用体类型

    • 函数类型

    • 指针类型

其它存在重叠的类型分类可以汇总为:

  • 对象类型

    • 除了函数类型和不完全类型以外的所有类型
  • 整数类型

    • 有符号整数类型,无符号整数类型, char 类型,枚举类型
  • 字符类型

    • char 类型, signed char 类型, unsigned char 类型
  • 实数类型

    • 整数类型,实浮点类型
  • 算术类型

    • 整数类型,浮点类型
  • 标量类型

    • 算术类型,指针类型
  • 聚合类型

    • 数组类型,结构体类型

标准库定义的类型

C 标准头文件 stddef.hstdint.h 为我们定义了一组非常实用的标准类型,对于开发兼容性强的程序而言具有重要价值。

头文件 stddef.h 定义了 ptrdiff_t , size_twchar_t 三种类型,以及 NULLoffsetof 两个宏。

头文件 stdint.h 定义了一组 width-specified 的整数类型以及一组对应的宏。具体地讲,类型包括:

  • 具有某些精确宽度的整数类型;

  • 至少具有某些指定宽度的整数类型;

  • 至少具有某些指定宽度的、最快的整数类型;

  • 足够宽的整数类型可以容纳指向对象的指针;

  • 具有最大宽度的整数类型。

本节将分类详细介绍这些由 C 标准提供的类型和宏定义。在本节中,符号 NN 表示一个无符号的十进制整数(不含前导零),例如 8, 16, 32, 648,\ 16,\ 32,\ 64 等。

width-specified 整数类型

头文件 stdint.h 定义了如下四组整数类型:

  • exact-width 整数类型:intN_tint\bold{N}\_tuintN_tuint\bold{N}\_t 分别表示长度为 NN 的有符号和无符号整数类型,例如 int8_t 类型和 uint8_t 类型。

  • minimum-width 整数类型:int_leastN_tint\_least\bold{N}\_tuint_leastN_tuint\_least\bold{N}\_t 分别表示宽度至少为 NN 的有符号和无符号整数类型,例如 int_least32_N 类型和 uint_least32_N 类型。

  • fast minimum-width 整数类型:int_fastN_tint\_fast\bold{N}\_tuint_fastN_tuint\_fast\bold{N}\_t 分别表示宽度至少为 NN 的、最快的有符号和无符号整数类型,例如 int_fast32_N 类型和 uint_fast32_N 类型。

  • greatest-width 整数类型:intmax_t 类型和 uintmax_t 类型分别表示长度最大的有符号和无符号整数类型。也就是说,其长度足够表示任何(对应有或无符号)整数类型的任何值,将任何(对应有或无符号)类型转换到这两个类型都不会发生溢出。

对于上述类型,常用的编译器都提供了 N=8, 16, 32, 64N = 8,\ 16,\ 32,\ 64 的类型,而 NN 的其它取值很少被支持。

在许多情况下,我们希望整数变量的长度是确定且可识别的,此时 exact-width 整数类型就非常实用。

  • 一方面,前文已经提到,int 类型的长度在不同编译环境中可能不同,这在程序对可移植性的要求较高时是不可接受的。例如,当我们需要长度在任何运行环境下都为 3232 的整数时,就可以使用 int32_t 类型。

  • 另一方面,在一些情景下,我们仅关心整数变量的长度而不是语义。例如对于以 11 字节和 22 字节为单位的两个缓冲区数组,我们当然可以使用 unsigned charunsigned short 类型,但是使用 uint8_tuint16_t 类型可能是一种更好的选择,长度作为关键的信息直接显现于类型上。

以我目前的认知,暂且无法理解 minimum-width 整数类型和 fastest minimum-width 整数类型有什么实用价值,因此我们暂时不讨论它们。

至于 greatest-width 整数类型可以发挥作用的情况,我想读者不需要任何解释也能想到一些,就不再赘述了。

头文件 stdint.h 还定义了如下宏,用于便捷且兼容性强地实现从其它整数类型到 minimum-width 整数类型和 greatest-width 整数类型的转换:

  • INTN_C(value)INT\bold{N}\_C(value)UINTN_C(value)UINT\bold{N}\_C(value) :分别展开为对应于 int_leastN_tint\_least\bold{N}\_t 类型和 uint_leastN_tuint\_least\bold{N}\_t 类型的、值为 valuevalue 的常量表达式,例如 INT64_C(123) 可能会展开为 123LL

  • INTMAX_C(value)UINTMAX_C(value) :分别展开为对应于 intmax_t 类型和 uintmax_t 类型的、值为 valuevalue 的常量表达式。

兼容指针的整数类型

头文件 stdint.h 定义了 intptr_t 类型和 uintptr_t 类型,分别表示可以与指针类型相兼容的有符号和无符号整数类型,也就是可以和 T * 类型之间无损地互相转换。通常被实现为和当前环境的地址位数相同的长度。

当我们需要存取具体的地址值时,兼容指针的整数类型就非常实用,这在接近底层的系统中是再常见不过的需求。直接将地址值赋给 int 类型是一种可移植性差的做法,例如下面的代码在 6464 位环境下就不能通过编译:

int x;
unsigned int addr = (unsigned int)&x; // get address value

可兼容的实现如下所示:

uintptr_t addr = (uintptr_t)&x;

头文件 stddef.h 定义了 ptrdiff_t 类型,这是表示两个指针相减的结果的有符号整数类型。该类型也是用于对可移植性有需求的情况,可在进行指针算术运算时使用。

int * p = &arr[i];
int * q = &arr[j];
ptrdiff_t offset = p - q; // equals to (i - j) here

最值宏定义

头文件 stdint.h 为前文所述的整数类型定义了如下宏,分别表示每种类型的最大值和最小值:

  • INTN_MAXINT\bold{N}\_MAXINTN_MININT\bold{N}\_MIN 表示长度为 NN 的 exact-width 有符号整数类型的最大值和最小值,即 2N112^{N - 1} - 12N1-2^{N - 1} ,例如 INT32_MAXINT32_MIN

  • UINTN_MAXUINT\bold{N}\_MAX 表示长度为 NN 的 exact-width 无符号整数类型的最大值,即 2N12^N - 1 ,例如 UINT32_MAX

  • INT_LEASTN_MAXINT\_LEAST\bold{N}\_MAXINT_LEASTN_MININT\_LEAST\bold{N}\_MIN 表示长度为 NN 的 minimum-width 有符号整数类型的最大值和最小值。

  • UINT_LEASTN_MAXUINT\_LEAST\bold{N}\_MAX 表示长度为 NN 的 minimum-width 无符号整数类型的最大值。

  • INT_FASTN_MAXINT\_FAST\bold{N}\_MAXINT_FASTN_MININT\_FAST\bold{N}\_MIN 表示长度为 NN 的 fast minimum-width 有符号整数类型的最大值和最小值。

  • UINT_FASTN_MAXUINT\_FAST\bold{N}\_MAX 表示长度为 NN 的 fast minimum-width 无符号整数类型的最大值。

  • INTMAX_MAXINTMAX_MIN 表示 intmax_t 类型最大值和最小值。

  • UINTMAX_MAX 表示 uintmax_t 类型的最大值。

  • INTPTR_MAXINTPTR_MIN 表示 intptr_t 类型最大值和最小值。

  • UINTPTR_MAX 表示 uintptr_t 类型的最大值。

  • PTRDIFF_MAXPTRDIFF_MIN 表示 ptrdiff_t 类型最大值和最小值。

其它整数类型和宏定义

ptrdiff_t 类型外,头文件 stddef.h 还定义了如下类型和宏:

  • size_t 类型:表示 sizeof 运算符结果的无符号整数类型。

  • SIZE_MAX 宏定义:表示 size_t 类型的最大值。

  • wchar_t 类型:初学者基本用不到,暂时无需在意。

  • NULL 宏定义:展开为空指针常量。其具体实现方式是实现定义的,例如可能是 #define (void *)0 NULL

  • offsetof 宏定义:用于获取结构体成员在结构体内部的相对地址,详见 C 语言的 06 - More about the Structure Type 文档。


文章更新记录

更新时间更新内容
2021.11.30追加章节【标准库定义的类型】