2.2 类型
1. 什么是类型
数据类型是一组性质相同的值集合以及定义在这个值集合上的一组操作的总称:
- “一组性质相同的值的集合”: 这定义了这种类型能表示什么(性质)。
int类型的值集合是整数,比如-100,0,42。它不能是3.14,也不能是"Hello"。char类型的值集合是字符,比如'A','!','\n'(换行符)。 - “定义在这个值集合上的一组操作”: 这定义了我们能对它做什么(约束)。 对于
int类型,我们可以进行+(加)、-(减)、*(乘)、/(除) 等数学运算。 对于字符串 (在 C 中是char数组),我们不能用 + 来连接它们,需要使用库函数,比如strcat()(拼接)、strcpy()(拷贝)。
类型最重要的作用是规定“如何解释值”。存入对象中的值、函数返回的值,以及表达式求值产生的值,都要依赖对应的类型来确定含义。
2. 标准中的类型地图
C 标准先把类型分成两大类:
- 对象类型:描述对象的类型,例如
int、double、数组、结构体、指针。 - 函数类型:描述函数的类型,由返回类型、形参数量和形参类型共同表征。
对象类型还会进一步区分为完整类型和不完整类型。完整类型已经有足够信息确定该类型对象的大小;不完整类型暂时缺少这些信息,例如只声明了标记但还没有给出成员列表的 struct node。不完整类型常用于接口分层:头文件可以只暴露“有这样一种类型”,实现文件再给出具体布局。
/* 头文件中只声明类型名,隐藏布局细节 */
struct parser;
struct parser *parser_create(void);
void parser_destroy(struct parser *p);2
3
4
5
标准还会按用途把类型分组:算术类型和指针类型等属于标量类型;数组类型和结构体类型属于聚合类型;结构体、联合体、数组、函数、指针和原子类型都属于从已有类型继续构造出来的派生类型。
3. C 的基本类型 (Basic Type)
3.1 字符类型 (Character Type)
charsigned char,unsigned char
char、signed char、unsigned char 是三种不同的字符类型。普通 char 的表示和行为由实现选择为接近 signed char 或 unsigned char 之一,但它仍然是独立类型。
wchar_t、char16_t、char32_t 等名字来自标准库头文件中的类型定义,用于宽字符或固定宽度字符相关接口。
3.2 整数类型 (Integer Type)
charsigned char,short,int,long,long long__int128等实现定义的类型- 2 和 3 统称有符号整数类型
boolunsigned char,unsigned short,unsigned,unsigned long,unsigned long longunsigned __int128等实现定义的类型- 5,6 和 7 统称无符号整数类型
- 类型
size_t是实现定义的类型
bool 属于标准无符号整数类型。无符号整数运算按模 2^N 进行,不会以“溢出”形式触发未定义行为;有符号整数溢出则不受标准保证。
3.3 浮点类型 (Floating-Point Type)
- 实浮点类型(表示一个实数):
float,double,long double - 十进制实浮点类型:
_Decimal32,_Decimal64,_Decimal128 - 复数类型(表示一个复数):
float _Complex,double _Complex,long double _Complex
十进制浮点类型和复数类型都可能受实现支持情况影响。虚数类型只在标准的附录中规定,可移植代码不应假定所有实现都支持它。
4. C 语言中的其他类型
4.1 类型 void
void 是一种不完整类型,表示“无可取值集合”。它通常出现在三种位置:函数返回类型、函数形参列表中的 void、以及 void * 通用指针。第一种表示函数不返回值;第二种表示函数不接受参数;第三种表示“可指向任意对象类型”,但在解引用前必须先转换为具体对象类型指针。
#include <stdlib.h>
void log_ready(void) {
/* ... */
}
int main(void) {
void *p = malloc(sizeof(int));
if (p == NULL) {
return 1;
}
*(int *)p = 42;
free(p);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void * 适合用在需要接收任意类型指针的通用接口上。它本身不记录所指对象的类型,所以真正使用对象的代码应尽快把它转换回具体类型的指针。
4.2 枚举类型
枚举类型由一组具名整数常量组成,适合表示有限个状态或选项。C23 开始可以为枚举声明固定底层类型;在早期标准中,底层类型由实现决定,只保证能够容纳全部枚举项。
enum state {
STATE_INIT = 0,
STATE_READY = 1,
STATE_STOP = 2
};2
3
4
5
当一组值是“封闭集合”时,优先使用枚举,避免让宏常量散落在代码各处。
4.3 派生类型
- 数组类型
- 结构体类型
- 联合体类型
- 函数类型
- 指针类型
- 原子类型
5. 类型的大小和表示范围
5.1 位和字节
- 位:执行环境中的存储单元。它可以容纳一个“可以拥有两种状态”的对象。
- 字节:执行环境中的存储单元。它有“地址”,可以容纳基础字符集里的字符。
类型大小以“字节”为单位描述,不直接以“位”为单位描述。标准保证 sizeof(char) == 1,但一个字节包含多少位由 CHAR_BIT 决定,常见实现是 8。因此“1 字节 = 8 位”不是无条件成立的真理,必要时使用 <limits.h> 中的宏进行约束与检查。
5.2 sizeof 运算符的一种用法
sizeof ( 类型名 )- 返回值类型为
size_t,值为此类型的大小,单位是字节。
sizeof 在编译期可求值(对变长数组除外),因此常用于计算对象布局与数组长度。工程里建议使用 sizeof *ptr 或 sizeof arr / sizeof arr[0] 这类“随类型变化自动更新”的写法,避免手写类型名导致维护时不一致。
int arr[10];
size_t n = sizeof arr / sizeof arr[0];2
5.3 整型占据空间的大小和能表示的范围
signed char/unsigned char:至少 8 位;(unsigned) short:至少 16 位;(unsigned) int:至少 16 位,常见实现为 32 位;(unsigned) long:至少 32 位;(unsigned) long long:至少 64 位。- 8 位的有符号整数,取值范围为 -128~127;8 位的无符号整数,取值范围为 0~255;
- 16 位的有符号整数,取值范围为 -32768~32767;16 位的无符号整数,取值范围为 0~65535;
- 32 位的有符号整数,取值范围为 -2147483648~2147483647;32 位的无符号整数,取值范围为 0~4294967295;
- 64 位的有符号整数,取值范围为 -9,223,372,036,854,775,808~9,223,372,036,854,775,807;64 位的无符号整数,取值范围为 0~18,446,744,073,709,551,615。
1==sizeof(char)≤sizeof(short)≤sizeof(int)≤sizeof(long)≤sizeof(long long)。size_t是一种无符号整数类型,至少 16 位。
5.4 浮点型占据空间的大小,能表示的范围和精度
- IEEE-754 详情参见 IEEE754.md
float:32 位,通常匹配 IEEE-754 binary32 格式。double:64 位,通常匹配 IEEE-754 binary64 格式。long double:实现定义,常见的实现为 128 位或 80 位。_Decimal32:32 位,匹配 IEEE-754 decimal32 格式。_Decimal64:64 位,匹配 IEEE-754 decimal64 格式。_Decimal128:128 位,匹配 IEEE-754 decimal128 格式。
本章内容对应 cppref 链接如下:
6. 习题
6.1 巩固
下面六段代码各自单独编译,没有一段能原样通过。先逐段预测编译器拒绝的理由(要落到本页的“类型的值集合与操作”上),再逐段编译核对。
/* a.c */
int main(void) {
double x = 7.0;
double y = x % 2.0;
return (int)y;
}2
3
4
5
6
/* b.c */
int main(void) {
int a[3] = {1, 2, 3};
int b[3];
b = a;
return b[0];
}2
3
4
5
6
7
/* c.c */
struct point {
int x;
int y;
};
int main(void) {
struct point p = {1, 2};
struct point q = {3, 4};
return p == q;
}2
3
4
5
6
7
8
9
10
11
/* d.c */
struct node;
int main(void) {
return (int)sizeof(struct node);
}2
3
4
5
6
/* e.c */
int f(void) {
return 1;
}
int main(void) {
return (int)sizeof f;
}2
3
4
5
6
7
8
/* f.c */
#include <stdlib.h>
int main(void) {
void *p = malloc(4);
if (p == NULL) {
return 1;
}
*p = 1;
free(p);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
要求:
- 用
gcc -std=c17 -Wall -Wextra -pedantic-errors 文件.c -o 文件编译,抄下每段的第一条 error。 - 把
b.c改成复制数组元素(逐个赋值或使用循环),把c.c改成逐个比较成员,把f.c改成先转换成int *再解引用。各写出正确版本并运行。 a.c和e.c的共同点是什么?%、sizeof各自要求操作数属于哪一类类型?f.c里void *能接收malloc的返回值、又能传给free,为什么不能直接解引用?d.c与f.c中的struct node、void都属于不完整类型,两者作为指针的目标类型时有何不同?
编译并运行下面的程序,先预测再核对。
#include <stdio.h>
int main(void) {
char plain = (char)200;
signed char signed_value = (signed char)200;
unsigned char unsigned_value = 200;
printf("%d %d %d\n", plain, signed_value, unsigned_value);
printf("%d %d %d\n", (int)sizeof(char), (int)sizeof(signed char),
(int)sizeof(unsigned char));
unsigned char byte = 255;
byte = byte + 1;
unsigned int word = 0;
word = word - 1;
printf("%u %u\n", (unsigned)byte, word);
unsigned short half = 65535;
half = half + 1;
printf("%u\n", (unsigned)half);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 三种字符类型的大小相同,为什么值却不同?普通
char在这里表现得更接近哪一种? word从 0 减 1 得到 4294967295,half从 65535 加 1 得到 0。这两个结果说明无符号整数的运算按什么规则进行?有符号整数是否也有同样的保证?- 在原程序的四个
printf之后追加byte = 300;、signed char s = 127; s = s + 1;,再打印这两个对象。用-Wall -Wextra编译,记录出现的唯一一条警告、指出它来自哪一行;说明为什么byte + 1与s + 1这两处溢出都没有被警告。
验收:原有输出依次为 -56 -56 200、1 1 1、0 4294967295、0;追加的两行打印 44 与 -128,唯一的警告来自 byte = 300;。
sizeof 是本页讲得最简略、实际用得最多的一节。编译并运行下面的程序。
#include <limits.h>
#include <stdio.h>
int main(void) {
int samples[25];
double readings[8];
int i = 0;
printf("samples: %d bytes, %d elements\n", (int)sizeof samples,
(int)(sizeof samples / sizeof samples[0]));
printf("readings: %d bytes, %d elements\n", (int)sizeof readings,
(int)(sizeof readings / sizeof readings[0]));
printf("type-name: %d\n", (int)sizeof(int[4]));
printf("CHAR_BIT: %d, int: %d bits\n", CHAR_BIT, CHAR_BIT * (int)sizeof(int));
printf("before: %d, sizeof(i++): %d, after: %d\n", i, (int)sizeof(i++), i);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 写出输出。
sizeof的结果类型是什么?为什么打印时要转换成int? - 最后一行
i在前后都是 0。sizeof(i++)为什么没有让i加一?换成长度可变的数组(例如int a[n])后情况是否相同? - 在
main前面加一个函数void show(int arr[10]),函数体里打印sizeof arr与sizeof arr[0],在main里传samples进去。写出结果,并解释为什么和在main里算出来的不同;编译器给了什么提示? - 把
samples换成double samples[25],元素个数应改用哪种写法才能自动跟着类型走?给出改后的两行代码和新的字节数。
验收:第 1 问输出 samples: 100 bytes, 25 elements、readings: 64 bytes, 8 elements、type-name: 16、CHAR_BIT: 8, int: 32 bits、before: 0, sizeof(i++): 4, after: 0;第 3 问函数里的两个值是 8 和 4,编译器给出 -Wsizeof-array-argument 警告。
浮点类型的大小和精度不能靠“多少位”硬背,用头文件里的宏量一次。写一个程序:
#include <float.h>
#include <stdio.h>2
要求:
- 打印
float、double、long double的sizeof,以及FLT_MANT_DIG、DBL_MANT_DIG、LDBL_MANT_DIG和FLT_DIG、DBL_DIG、LDBL_DIG。 - 用
%.20f分别打印0.1和(double)0.1f,写出两者的差别,说明为什么同一个十进制小数在两种类型里表示成不同的值。 - 打印
0.1 + 0.2 == 0.3与0.1f + 0.2f == 0.3f的结果,解释两个结果为什么不同,并给出浮点数比较的写法(例如与一个充分小的差比较)。 float price = 19.99f; int cents = price * 100;与double price = 19.99; int cents = price * 100;分别得到 1999 和 1998。预测并验证这两个值,说明金额为什么应当用整数分存储、并在转换前加上四舍五入。
本机(MinGW-w64,GCC 13.2)的部分输出供对照:
sizeof: float 4, double 8, long double 16
mantissa: 24 53 64
digits: 6 15 18
0.10000000000000000555
0.100000001490116119382
3
4
5
long double 的宽度与 LDBL_MANT_DIG 随实现而变,取别的值也算通过,只要解释与本机 float.h 的取值一致。
6.2 编程
不完整类型常用于接口分层。按下面的结构建立三个文件。
parser/
├── parser.h # 只暴露类型名和函数声明
├── parser.c # 给出 struct parser 的成员定义
└── main.c # 使用接口2
3
4
parser.h 的内容:
#ifndef PARSER_H
#define PARSER_H
struct parser;
struct parser *parser_create(void);
int parser_value(const struct parser *p);
void parser_destroy(struct parser *p);
#endif2
3
4
5
6
7
8
9
10
要求:
- 补全
parser.c:struct parser只含一个int value成员,parser_create用malloc分配、把value设为 42、失败返回NULL;parser_destroy调用free。用sizeof *p而不是sizeof(struct parser)计算分配大小。 - 补全
main.c:创建对象、用parser_value读取值、用sizeof p打印句柄本身的大小、销毁对象。用gcc -std=c17 -Wall -Wextra main.c parser.c -o app构建,运行应输出42与句柄大小(本机为 8)。 - 分别写两个错误客户端并编译,抄下 error:一个在
main.c里访问p->value,一个写sizeof(struct parser)。这两条错误说明“只暴露类型名”限制了调用方做什么? - 第三个错误客户端写
struct parser p;(直接定义对象)。编译器的 error 是storage size of 'p' isn't known。为什么指针可以,对象不行? - 把
struct parser { int value; };的定义整个搬进parser.h,保留parser.c里的同一份定义,重新编译。抄下 error(redefinition of 'struct parser'),说明它为什么必然出现。现在调用方也能写sizeof(struct parser)了,代价是什么?据此说明本页说的“不完整类型用于接口分层”具体分掉了什么。
6.3 综合与挑战
下面的程序是一份传感器记录的统计代码,至少存在五处类型选择缺陷,其中有的能被 -Wall -Wextra 发现,有的不会。
#include <stdio.h>
int main(void) {
int population = 8000000000;
double price = 19.99;
int cents = price * 100;
signed char score = 100;
unsigned int stock = 0;
score = score + 100;
stock = stock - 1;
printf("population = %d\n", population);
printf("cents = %d\n", cents);
printf("score = %d\n", (int)score);
printf("stock = %u\n", stock);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(1) [3.3] 用 gcc -std=c17 -Wall -Wextra 编译,抄下全部警告;再加上 -Wconversion 重新编译,抄下新增的警告。逐条说明警告对应哪一处缺陷。
(2) [4.4] 运行程序,写出四行输出。score 和 stock 的结果分别说明有符号、无符号整数在超出范围时的行为有什么不同?哪一处属于标准保证的行为,哪一处没有保证?
(3) [4.4] 逐条修正:population 改用能容纳它的类型并写清后缀;cents 改用四舍五入;score 改用不会溢出的类型;stock 改成先判断再减、不能减时打印 out of stock。修正后要求 -Wall -Wextra -Wconversion 下没有任何警告。
(4) [4.3*] 给记录加一个状态字段:
enum state {
STATE_INIT = 0,
STATE_READY = 1,
STATE_STOP = 2,
STATE_COUNT
};2
3
4
5
6
打印各枚举项的值和 sizeof(enum state);再把 (enum state)9 赋给一个 enum state 对象并打印。最后用 C23 的写法 enum state : unsigned char { ... } 改写并测量大小,说明这一写法要求的最低标准版本,以及在 C17 下编译会得到什么诊断。
(5) [5.4*] 写一个 types.c,用 sizeof 与 <limits.h>、<float.h> 的宏打印一张表,每一行是一种类型、它的大小和取值范围,至少覆盖 int、long long、unsigned int、double、金额场景用的整数分类型,以及一个枚举类型。表后附三行结论:存数量、存金额、做状态标记各选什么类型,依据是本页的哪几条结论。
验收:第 2 问输出依次为 population = -589934592、cents = 1998、score = -56、stock = 4294967295;第 3 问修正后分别输出 8000000000、1999、200 和 out of stock;第 4 问 C17 下枚举大小为 4,C23 写法下为 1。