| 目录 |

15 类型 III:转换

本章要讨论的,全是把一种类型转换成另一种类型。C 有各种各样的方式来做这件事,其中一些可能和你所熟悉的其他语言略有不同。

在讨论如何让转换发生之前,我们先聊聊转换真正发生时是如何运作的。

15.1 字符串转换

与许多语言不同,C 做字符串到数字(以及反过来)的转换时,并不像数值转换那样顺畅直接。

这类转换我们得调用函数来完成脏活。

15.1.1 数值转字符串

想把数字转换成字符串时,可以用 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。

15.1.2 字符串转数值

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() 风格的函数能让你更好地控制错误处理和输入的进制。

15.2 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 也就一直没去修它。

15.3 数值转换

15.3.1 布尔类型

如果把零转换成 bool,结果是 0。否则结果是 1。

15.3.2 整数之间的转换

如果把一种整数类型转换成无符号类型,而值又放不下,无符号结果就会像里程表一样回绕,直到能放进该无符号类型为止110。

如果把一种整数类型转换成有符号数,而值放不下,结果就是实现定义的行为!会发生某种有文档记载的事情,但你得自己去查111

15.3.3 整数与浮点数之间的转换

如果把浮点类型转换成整数类型,小数部分会被毫不留情地丢弃112。

但是——问题就在这里——如果数字太大,放不进整数,就会得到未定义行为。所以别那么干。

从整数或浮点数转换成浮点数时,C 会尽力找出所能找到的、最接近原值的浮点数。

不过同样,如果原值无法表示,那就是未定义行为。

15.4 隐式转换

这些是当你混用不同类型时,编译器自动为你完成的转换。

15.4.1 整型提升

在很多地方,如果一个 int 能够表示来自 char 或 short(无论有符号还是无符号)的值,这个值就会被提升为 int。如果它放不进 int,就会被提升为 unsigned int。

正因为如此,我们才能写出这样的代码:

char x = 10, y = 20;
int i = x + y;

在这种情况下,C 会在进行运算之前把 x 和 y 提升为 int。

整型提升会发生在寻常算术转换期间、调用可变参数函数113时、使用一元 + 和 - 运算符时,或者把值传给没有原型的函数114时。

15.4.2 寻常算术转换

这些是 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 吗?到底怎么运作?

下面把这些步骤改述一下,方便理解。

  1. 如果表达式中有一个东西是浮点类型,就把其他东西转换成那个浮点类型。

  2. 否则,如果两个类型都是整数类型,就对各自执行整型提升,然后把操作数类型扩大到足以容纳两者共同的最大值。有时这涉及把有符号改成无符号。

如果你想知道那些琐碎的细节,可以去看 C11 §6.3.1.8。不过你多半不想。

大致记住:只要表达式里任何地方出现了浮点类型,整数类型就会变成浮点类型;而编译器会努力确保混合的整数类型不会溢出。

最后,如果从一种浮点类型转换成另一种,编译器会尝试做精确转换。如果做不到,它就会给出所能做到的最佳近似。如果数字太大,放不进你要转换成的类型,砰:未定义行为!

15.4.3 void*

void* 类型很有意思,因为它可以从任何指针类型转换而来,也可以转换成任何指针类型。

int x = 10;

void *p = &x;  // &x 的类型是 int*,但我们把它存进了 void*

int *q = p;    // p 的类型是 void*,但我们把它存进了 int*

15.5 显式转换

这些是你必须主动要求的类型转换;编译器不会替你去做。

你可以用 = 把一种类型的值赋给另一种类型,从而完成转换。

你也可以用强制类型转换来显式转换。

15.5.1 强制类型转换

把新类型放在括号里,置于表达式前面,就可以显式改变表达式的类型。有些 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,以避免未定义行为。

再说一次,实践中很少需要强制转换。如果你发现自己正在做强制转换,也许还有别的方式能做同样的事,又或者这次转换本来就是多余的。

又或者它确实必要。就我个人而言,我会尽量避免,但真需要时也不怕用它。


| 目录 |