2.8 整数常量、浮点常量和字符常量
常量的写法决定了它的类型,也就决定了它参与运算时会不会溢出、会怎样转换。
1. 整数常量
整数常量支持十进制、八进制、十六进制和(C23 起)二进制形式。前缀分别是无前缀、0、0x/0X、0b/0B。后缀 u、l、ll 及其组合用于指定无符号或更宽类型。
- 十进制常量包含
0~9的数字,且不以0开头:1,123,50000000000000; - 八进制常量包含
0~7的数字,以0开头:017,0777777777; - 十六进制常量包含
0~9中的数字和a~f中的字母(或者大写A~F,可以混用,但写代码时别混用),以0x开头:0x23,0x7E7,0xFf,0x0d000721。 - 二进制常量包含
0和1,以0b开头:0b101,0b11110000。
int a = 42; /* 十进制 */
int b = 052; /* 八进制,值为 42 */
int c = 0x2A; /* 十六进制 */
int d = 0b101010; /* 二进制,C23 */
unsigned long long e = 42ull;2
3
4
5
标准会根据常量形式和后缀选择“能容纳该值的首个类型”。如果希望类型明确可读,直接写后缀或使用 <stdint.h> 的宏常量;关于 <stdint.h>,见 19.1。
若你的编译器尚未切到 C23,二进制前缀 0b 可能不可用,这时可用十六进制常量做下位替代。
1.1 整数常量类型的推导规则
1.1.1 十进制常量
从小到大尝试:int → long int → long long int。
1.1.2 二进制、八进制和十六进制常量
从小到大尝试:int → unsigned int → long int → unsigned long int → long long int → unsigned long long int。
1.1.3 如果更大?
可能会尝试寻找扩展类型(比如 __int128 等);如果超出所有类型的表示范围,可能报错。这类行为由编译器实现决定,不在此讨论。
1.1.4 为什么两种常量的规则不同?
人类写十进制数的时候,会更关注数的大小,自然认为它是有符号的(比如 2 大于 -1)。
如果十进制整数常量被按照 int → unsigned int 的次序推导(假设 int 最大值为 2147483647),那么以下代码就会发生严重错误:
if (3000000000 > -1) {
// 三十亿大于 -1,所以这块代码应该被执行
}2
3
按这种次序,3000000000 会被推导为 unsigned int,-1 会被隐式提升为无符号数 4294967295,导致左边小于右边。
程序员写下十六进制(如 0x80000000)或八进制(如 020000000000)时,通常用它们表示位的模式。如果强制推导成有符号类型,会扰乱位运算。
2. 浮点常量
浮点常量可写成十进制科学计数法,也可写成十六进制浮点形式。后缀 f/F 指定 float,l/L 指定 long double,无后缀默认 double。
double x = 3.1415926;
double y = 1.0e-3;
double z = 0x1.8p+2; /* 十六进制浮点,值为 6.0 */
float w = 6.02e23f;2
3
4
十六进制浮点在描述二进制可精确表示值时很有优势,适合写底层测试和数值边界用例。
3. 字符常量
普通字符常量使用单引号包围,例如 'A'。其类型通常是 int,值为执行字符集中对应字符编码。转义序列如 '\n'、'\0'、'\x41' 可表达不可见字符或指定码位。
#include <stdio.h>
int main(void) {
int a = 'A';
int b = '\n';
int c = '\x41';
printf("%d %d %d\n", a, b, c);
return 0;
}2
3
4
5
6
7
8
9
10
在执行字符集采用 ASCII、换行字符编码值为 10 的常见实现中,输出为:
65 10 65
其中 'A' 和 '\n' 的值由执行字符集决定,'\x41' 的值为十六进制 41,即十进制 65。
多字符常量(如 'AB')是实现定义行为,不建议用于可移植代码。若要表达文本,请优先使用字符串字面量。
4. 习题
4.1 巩固
先读下面的程序,写出输出,再回答后面的问题。不要先编译。
#include <stdio.h>
int main(void) {
printf("%d %d %d %d\n", 0x10, 010, 0b10, 42);
printf("%c%c%c\n", 'A', 'A' + 1, '0' + 5);
printf("%d %d\n", '9' - '0', '\101' == 'A');
printf("%d %d\n", '\0', '0');
return 0;
}2
3
4
5
6
7
8
9
- 第一行的四个数都是 16、8、2、42 里的哪一个?哪个常量用了八进制、哪个用了十六进制、哪个用了二进制?把它们改写成十进制,说明
010与10相差多少。 - 第二、三行说明字符常量参与算术运算时被当成了什么?
'A' + 1的类型是char还是int? '\0'与'0'的值分别是多少?为什么这两个值不能混用?'\101'与'\x41'写法不同,值与'A'的关系分别是什么?\101里的101是十进制还是八进制?把这两个常量分别赋给char和int,输出有区别吗?0b10需要哪个标准版本?用-std=c17 -pedantic编译会看到什么提示?
验收:第一行 16 8 2 42,第二行 AB5,第三行 9 1,第四行 0 48。第 5 问要能说出 0b 前缀在 C23 才进入标准。
后缀的作用是把类型写明确。下面几行都从同一个数值出发,类型却各不相同。
42
42u
42L
42LL
42ULL
-2147483648
-2147483647 - 12
3
4
5
6
7
要求:
- 用
sizeof打印每个写法的类型大小,并写出各自的类型。-2147483648与-2147483647 - 1的数值相同,类型却不同,说明为什么:负号是运算符,不是常量的一部分,2147483648本身已经超出int。 - 写程序用
printf("%d\n", …)分别打印-2147483648、-2147483647 - 1、-2147483648LL,用-Wall -Wextra编译,抄下警告。三行输出相同,为什么只有中间一行不触发格式警告?据此说明三个写法里常量的类型分别是什么。 - 下面是一张掩码表,作者希望所有常量都是无符号的 32 位值:
unsigned int masks[] = {0xFFFFFFFF, 0x80000000, 0x7FFFFFFF, 1 << 31};用 -Wconversion -Wsign-conversion 编译,指出哪些项有警告,给每项补上合适的后缀或改写写法,使警告消失且数值不变。1 << 31 为什么要单独处理? 4. 为什么工程上更推荐用后缀把类型写死,而不是依赖「能容纳该值的首个类型」这条推导规则?结合第 3 问说明:同一份掩码表换到 int 为 16 位、long 为 32 位的实现上,哪些常量的类型会变化。
验收:第 1 问在 long 为 4 字节的实现上,42 到 42ULL 的 sizeof 依次为 4、4、4、8、8(long 为 8 字节时 42L 随之变成 8),-2147483648 为 8,-2147483647 - 1 为 4;第 2 问第一、三两行报警,中间一行不报警;第 3 问中 1 << 31 触发有符号转换警告,其余三项不报警,改写成 1u << 31 后消失。
4.2 编程
写一个十六进制浮点与字符编码的对照工具。程序只用本页讲过的常量写法,不调用 strtod 或 ctype。
要求:
- 实现三个函数:
/* 字符 '0'~'9'、'a'~'f'、'A'~'F' 返回对应的数值,其他字符返回 -1。 */
int mdr_hex_digit(char c);
/* 两位十六进制文本转字节,例如 "4C" 得 76;格式不对返回 -1。 */
int mdr_parse_hex_byte(const char *s);
/* 小写字母转大写,其余字符原样返回。 */
char mdr_to_upper(char c);2
3
4
5
6
7
8
- 两个函数的转换都要用字符常量做算术,不许写 48、97、65 这类魔数。
mdr_to_upper只在'a'到'z'之间做偏移。 - 用下面的输入测试并打印:
4C -> 76
ff -> 255
0x -> -1
G1 -> -1
7 -> -1
(空串) -> -12
3
4
5
6
- 再打印
mdr_to_upper作用于'q'、'Z'、'5'的结果。 - 用十六进制浮点常量写一组断言,验证
0x1.8p+2等于 6.0、0x1p-1等于 0.5、0x1.99999ap-4f与0.1f相等,全部用==比较并打印pass或fail。 - 打印
0.5、0.5f、0.5L、6.02e23f、0x1p0的sizeof,说明浮点常量的后缀如何决定类型。再打印0.1f == 0.1与0.1f == 0x1.99999ap-4f的结果,解释为什么前者为假、后者为真。
验收:第 3 步输出 4C 76、ff 255、0x -1、G1 -1、7 -1、空串 -1;第 4 步 Q Z 5;第 5 步三行全是 pass;第 6 步的 sizeof 依次为 8、4、16(long double 为 80 位扩展精度时是 16,其他实现按实际结果记录),后两行结果为 0 和 1。-Wall -Wextra 无警告。
4.3 综合与挑战
下面的程序有五处写错,其中一处直接编译失败。用 -std=c17 -Wall -Wextra 编译,逐条定位、说明依据,再改写为正确版本。
#include <stdio.h>
int main(void) {
int permissions = 0755;
char code = 'AB';
int capacity = 4 * 1024 * 1024 * 1024;
unsigned long long product = 0xFFFFFFFF * 0xFFFFFFFF;
double exact = 0x1.8;
printf("%d %d %d %llu %f\n", permissions, code, capacity, product, exact);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
要求:
- 抄下全部诊断,按行号归类。编译失败的那一处是什么原因?其余四处为什么只是警告或静默出错?
- 逐条说明每一处「作者想写什么」与「实际写成什么」:
0755的十进制值是多少,作者如果想写十进制 755 该怎么写?code最终保存的是哪个字符? 4 * 1024 * 1024 * 1024溢出后得到 0,作者本意是 4 GiB。分别用int、long、long long、unsigned int承接到该值,写出哪些能成功、哪些不能,并解释与long宽度有关的部分。0xFFFFFFFF * 0xFFFFFFFF的类型是unsigned int,乘积按 2^32 取模。要拿到精确值应给哪个操作数加后缀?- 重写整个程序,要求输出
755 66 4294967296 18446744065119617025 6.000000,-Wall -Wextra零警告。permissions这里按作者本意写成十进制,code取'B'。
验收:第 5 步的输出与上面逐字相同,编译无警告;第 3 问要指出在这台 long 为 4 字节的机器上 long 只能扛到 2 GiB 减 1。