本章要讨论的,全是把一种类型转换成另一种类型。C 有各种各样的方式来做这件事,其中一些可能和你所熟悉的其他语言略有不同。
在讨论如何让转换发生之前,我们先聊聊转换真正发生时是如何运作的。
与许多语言不同,C 做字符串到数字(以及反过来)的转换时,并不像数值转换那样顺畅直接。
这类转换我们得调用函数来完成脏活。
想把数字转换成字符串时,可以用 sprintf()(读作 SPRINT-f)或 snprintf()(s-n-print-f)106
它们基本上和 printf() 一样,只不过输出到字符串里,之后你可以再把这个字符串打印出来,或者干点别的。
例如,把 π 值的一部分转换成字符串:
#include <stdio.h>
int main(void)
{
char s[10];
float f = 3.14159;
// 把 "f" 转换成字符串,存入 "s",最多写 10 个字符
// 包括 NUL 终止符
snprintf(s, 10, "%f", f);
printf("String value: %s\n", s); // 字符串值:3.141590
}所以你可以像平常处理整数那样使用 %d 或 %u。
C 里有好几类函数可以做这件事。我们分别把它们叫作 atoi 系列(读作 a-to-i)和 strtol 系列(stir-to-long)。
如果只是做基本的字符串到数字转换,可以试试 <stdlib.h> 里的 atoi 系列函数。它们的错误处理特性很差(传入坏字符串甚至会导致未定义行为),所以要小心使用。
| 函数 | 说明 |
|---|---|
atoi |
字符串转 int |
atof |
字符串转 float |
atol |
字符串转 long int |
atoll |
字符串转 long long int |
虽然规范并不承认,但函数开头的 a 代表的是 ASCII107,所以 atoi() 其实是“ASCII-to-integer”,不过今天还这么说就有点 ASCII 中心主义了。
下面是一个把字符串转换成 float 的例子:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *pi = "3.14159";
float f;
f = atof(pi);
printf("%f\n", f);
}但是,就像我说的,像这样的怪东西会导致未定义行为:
int x = atoi("what"); // "What" 可不是我听说过的数字(我运行时得到的是 0,但你绝对不该指望这一点。你完全可能得到别的结果。)
为了获得更好的错误处理特性,我们来看看那些 strtol 系列函数,它们也在 <stdlib.h> 里。不仅如此,它们还能转换成更多类型、支持更多进制!
| 函数 | 说明 |
|---|---|
strtol |
字符串转 long int |
strtoll |
字符串转 long long int |
strtoul |
字符串转 unsigned long int |
strtoull |
字符串转 unsigned long long int |
strtof |
字符串转 float |
strtod |
字符串转 double |
strtold |
字符串转 long double |
这些函数的使用方式都差不多,也是很多人第一次接触指向指针的指针的地方!不过别担心——它没有看上去那么难。
我们来看一个例子:把字符串转换成 unsigned long,并丢弃错误信息(也就是关于输入字符串中坏字符的信息):
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *s = "3490";
// 把字符串 s(基 10 的数)转换成 unsigned long int。
// NULL 表示我们不想了解任何错误信息。
unsigned long int x = strtoul(s, NULL, 10);
printf("%lu\n", x); // 3490
}注意几点。尽管我们根本不屑于捕获字符串中错误字符的任何信息,strtoul() 也不会让我们陷入未定义行为;它只会返回 0。
另外,我们指定了这是一个十进制(基 10)数。
这是不是意味着我们能转换不同进制的数?当然!来试试二进制!
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *s = "101010"; // 这个数字是什么意思?
// 把字符串 s(基 2 的数)转换成 unsigned long int。
unsigned long int x = strtoul(s, NULL, 2);
printf("%lu\n", x); // 42
}好吧,这些都挺好玩,但里面那个 NULL 是怎么回事?它是干什么用的?
它帮我们判断处理字符串时是否发生了错误。它是一个指向 char 的指针的指针,听起来吓人,但一旦你把它想明白就没什么了。
我们来看一个例子:故意喂给它一个坏数字,看看 strtol() 如何告诉我们第一个非法数字位在哪里。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *s = "34x90"; // "x" 不是基 10 下的有效数字!
char *badchar;
// 把字符串 s(基 10 的数)转换成 unsigned long int。
unsigned long int x = strtoul(s, &badchar, 10);
// 它会尽可能多地转换,所以进行到这里:
printf("%lu\n", x); // 34
// 但我们能看到那个肇事的坏字符,因为 badchar
// 就指向它!
printf("Invalid character: %c\n", *badchar); // 输出 "x"
}于是,strtoul() 通过修改 badchar 所指向的内容,来告诉我们哪里出了问题108。
但要是没出错呢?这种情况下,badchar 会指向字符串末尾的 NUL 终止符。所以我们可以这样检测:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *s = "3490"; // "x" 不是基 10 下的有效数字!
char *badchar;
// 把字符串 s(基 10 的数)转换成 unsigned long int。
unsigned long int x = strtoul(s, &badchar, 10);
// 检查是否一切顺利
if (*badchar == '\0') {
printf("Success! %lu\n", x);
} else {
printf("Partial conversion: %lu\n", x);
printf("Invalid character: %c\n", *badchar);
}
}就是这样。atoi() 风格的函数在可控的应急场合还不错,但 strtol() 风格的函数能让你更好地控制错误处理和输入的进制。
char 转换如果你有一个包含数字的单个字符,比如 '5'……它和数值 5 是一回事吗?
我们来试试看。
printf("%d %d\n", 5, '5');在我的 UTF-8 系统上,它打印出:
5 53所以……不是。那 53 又是什么?它是字符符号 '5' 的 UTF-8(也是 ASCII)码点109
那么,怎样把字符 '5'(显然它的值是 53)转换成数值 5 呢?
用一个巧妙的技巧就行!
C 标准保证这些字符的码点是连续的,并且按如下顺序排列:
0 1 2 3 4 5 6 7 8 9想一想——我们该怎么利用这一点?下面要剧透了……
我们来看看这些字符及其在 UTF-8 中的码点:
0 1 2 3 4 5 6 7 8 9
48 49 50 51 52 53 54 55 56 57可以看到,'5' 就是 53,和我们刚才得到的一样。而 '0' 是 48。
所以,用任意数字字符减去 '0',就能得到它的数值:
char c = '6';
int x = c; // x 的值是 54,即 '6' 的码点
int y = c - '0'; // y 的值是 6,正是我们想要的反过来转换也可以,只要把数值加上去就行。
int x = 6;
char c = x + '0'; // c 的值是 54
printf("%d\n", c); // 打印出 54
printf("%c\n", c); // 用 %c 打印出 6你也许觉得这种转换方式很奇怪,按今天的标准来看确实如此。但在当年计算机真是用木头做的那个古老年代,这就是做这种转换的方法。既然它没坏,C 也就一直没去修它。
如果把零转换成 bool,结果是 0。否则结果是 1。
如果把一种整数类型转换成无符号类型,而值又放不下,无符号结果就会像里程表一样回绕,直到能放进该无符号类型为止110。
如果把一种整数类型转换成有符号数,而值放不下,结果就是实现定义的行为!会发生某种有文档记载的事情,但你得自己去查111
如果把浮点类型转换成整数类型,小数部分会被毫不留情地丢弃112。
但是——问题就在这里——如果数字太大,放不进整数,就会得到未定义行为。所以别那么干。
从整数或浮点数转换成浮点数时,C 会尽力找出所能找到的、最接近原值的浮点数。
不过同样,如果原值无法表示,那就是未定义行为。
这些是当你混用不同类型时,编译器自动为你完成的转换。
在很多地方,如果一个 int 能够表示来自 char 或 short(无论有符号还是无符号)的值,这个值就会被提升为 int。如果它放不进 int,就会被提升为 unsigned int。
正因为如此,我们才能写出这样的代码:
char x = 10, y = 20;
int i = x + y;在这种情况下,C 会在进行运算之前把 x 和 y 提升为 int。
整型提升会发生在寻常算术转换期间、调用可变参数函数113时、使用一元 + 和 - 运算符时,或者把值传给没有原型的函数114时。
这些是 C 在你请求的数值运算周围自动进行的转换。(顺便说一句,C11 §6.3.1.8 里确实就是这么叫的。)注意,本节我们只讨论数值类型——字符串以后再说。
这些转换回答的是混用类型时会发生什么的问题,比如这样:
int x = 3 + 1.2; // 混用 int 和 double
// 4.2 被转换成 int
// 4 被存入 x
float y = 12 * 2; // 混用 float 和 int
// 24 被转换成 float
// 24.0 被存入 y它们会变成 int 吗?会变成 float 吗?到底怎么运作?
下面把这些步骤改述一下,方便理解。
如果表达式中有一个东西是浮点类型,就把其他东西转换成那个浮点类型。
否则,如果两个类型都是整数类型,就对各自执行整型提升,然后把操作数类型扩大到足以容纳两者共同的最大值。有时这涉及把有符号改成无符号。
如果你想知道那些琐碎的细节,可以去看 C11 §6.3.1.8。不过你多半不想。
大致记住:只要表达式里任何地方出现了浮点类型,整数类型就会变成浮点类型;而编译器会努力确保混合的整数类型不会溢出。
最后,如果从一种浮点类型转换成另一种,编译器会尝试做精确转换。如果做不到,它就会给出所能做到的最佳近似。如果数字太大,放不进你要转换成的类型,砰:未定义行为!
void*void* 类型很有意思,因为它可以从任何指针类型转换而来,也可以转换成任何指针类型。
int x = 10;
void *p = &x; // &x 的类型是 int*,但我们把它存进了 void*
int *q = p; // p 的类型是 void*,但我们把它存进了 int*这些是你必须主动要求的类型转换;编译器不会替你去做。
你可以用 = 把一种类型的值赋给另一种类型,从而完成转换。
你也可以用强制类型转换来显式转换。
把新类型放在括号里,置于表达式前面,就可以显式改变表达式的类型。有些 C 开发者除非绝对必要,否则不赞成这种做法,但你很可能会遇到一些包含强制转换的 C 代码。
我们来看一个例子:想把 int 转换成 long,以便存进一个 long。
注意:这个例子是人为编造的,此处的强制转换完全没有必要,因为 x + 12 表达式会自动变成 long int,以匹配 y 更宽的类型。
int x = 10;
long int y = (long int)x + 12;在那个例子里,尽管 x 原本是 int 类型,表达式 (long int)x 却是 long int 类型。我们说:“我们把 x 强制转换成了 long int。”
更常见的情况是,你可能会看到用强制转换把 void* 转成特定的指针类型,以便对它解引用。
内置 qsort() 函数的回调可能会展示这种行为,因为传给它的参数是 void*:
int compar(const void *elem1, const void *elem2)
{
if (*((const int*)elem2) > *((const int*)elem1)) return 1;
if (*((const int*)elem2) < *((const int*)elem1)) return -1;
return 0;
}但你也可以明确地用赋值来写:
int compar(const void *elem1, const void *elem2)
{
const int *e1 = elem1;
const int *e2 = elem2;
return *e2 - *e1;
}更常看到强制转换的地方之一,是在用少见的 %p 打印指针值时避免警告——它对 void* 以外的任何类型都很挑剔:
int x = 3490;
int *p = &x;
printf("%p\n", p);会生成这样的警告:
warning: format ‘%p’ expects argument of type ‘void *’, but argument
2 has type ‘int *’可以用强制转换修好:
printf("%p\n", (void *)p);另一个地方是显式的指针转换——如果你不想借助中间的 void* 的话,不过这种用法也相当少见:
long x = 3490;
long *p = &x;
unsigned char *c = (unsigned char *)p;第三个经常需要它的地方,是 <ctype.h>115 里的字符转换函数——你应该把符号性存疑的值强制转换成 unsigned char,以避免未定义行为。
再说一次,实践中很少需要强制转换。如果你发现自己正在做强制转换,也许还有别的方式能做同样的事,又或者这次转换本来就是多余的。
又或者它确实必要。就我个人而言,我会尽量避免,但真需要时也不怕用它。