『C Programming』 Type Conversion

『C程序设计』 类型转换

Posted by Coekjan on May 8, 2021

本文参考自 C in a Nutshell: The Definitive Reference.

算术类型的转换

两个算术类型之间的类型转换总是可行的, 只要有必要, 编译器就会执行这种隐式类型转换. 只要新值能够表示原表达式的值, 转换结果就会保留原来的值.

但情况并非总是如此. 例如, 当一个负值转换为无符号类型, 或者将 double 类型的浮点小数转换为 int 类型, 新的类型就无法表示原来的值. 这种情况下, 编译器通常会产生编译警告.

类型转换的等级

当算术操作数具有不同类型时, 隐式的类型转换会遵循类型转换等级. 如下所示:

  • 任意两个无符号整数类型均具有不同的转换等级. 如果其中一个位长大于另一个, 则其拥有更高的转换等级.
  • 每个带符号整数类型与其对应的无符号整数类型具有相同的等级. 如: char 型与 signed char 型, unsigned char 型具有相同的等级.
  • 标准整数类型的等级次序: _Bool < char < short < int < long < long long .
  • 任何标准整数类型都比等宽度的扩展整数类型等级更高.
  • 每个枚举类型都与其对应的整数类型具有相同的等级.
  • 浮点类型的等级次序: float < double < long double .
  • float 是最低等级的浮点类型, 但它比任何整数类型的转换等级都高.
  • 每个复数浮点类型, 其等级与其实部和虚部的类型等级一样.

整数提升

在任何表达式中, 总可以使用类型等级比 int 类型低的值, 代替 int 类型或 unsigned int 类型的操作数. 也可以使用位字段作为整数操作数.

这些情况下, 编译器会采用整数提升: 任何操作数, 当 int 类型的取值范围足以表示该操作数原始类型的所有值, 且操作数类型的转换类型比 int 低, 则会自动转换为 int 类型. 如果 int 范围难以满足要求, 操作数会被转换为 unsigned int 类型.

寻常算术转换

寻常算术转换是一种隐式类型转换操作, 对大多数运算符来说, 寻常算术转换会被自动应用到不同算术类型的操作数上. 寻常算术转换的目的是找出所有操作数和操作结果之间的通用实数类型.

对于下面的运算符来说, 寻常算术转换会隐式进行:

  • 具有两个操作数的算术运算符: * , / , % , + , -
  • 关系和相等运算符: < , <= , > , >= , == , !=
  • 位运算符: & , | , ^
  • 条件运算符: ?: (第2, 3个操作数)

使用方法如下:

  1. 如果两个操作数中的一个是浮点类型, 那么转换等级较低的操作数会被转换成与另一个操作数同样高的等级. 然而实数类型只能被转换成实数类型, 复数类型只能被转换成复数类型.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    #include <complex.h>
    // ...
    short n = -10;
    double x = 0.5, y = 0.0;
    float _Complex f_z = 2.0F + 3.0F * I;
    double _Complex d_z = 0.0;
    
    y = n * x;                   // n    (short          ->          double)
    d_z = f_z + x;               // f_z  (float _Complex -> double _Complex)
    f_z = f_z / 3;               // 3    (int            ->           float)
    d_z = d_z - f_z;             // f_z  (float _Complex -> double _Complex)
    
  2. 若两个操作数都是整数, 会先对两个操作数进行整数提升(到 intunsigned int 级). 如果在整数提升后, 操作数仍然类型不一致, 则进行如下的转换:
    • 若其中一个操作数是无符号类型 T , 其转换等级不低于另一个操作数, 则另一个操作数就会被转换为 T 类型. (可能有数学意义上损失)
    • 否则, 一个操作树具有一个带符号类型 T , 其转换等级比另一个操作数的等级高.
      • 只要 T 类型范围足以表示另一个操作数类型的所有值, 则另一个操作数会被转换为 T 类型. (无数学意义上损失)
      • 否则, 两个操作数都会被转换成与 T 相应的无符号类型. (可能有数学意义上损失)

举例如下:

1
2
3
4
5
6
int i = -1;
unsigned int m = 200U;
long n = -1L;

i < m;                      // [1]
m * n;                      // [2]
  • [1] : i 的值将被转换为 unsigned int 型, 32位机器中即: -1 -> 0xffffffff , 因此 [1] 处将为 false (0)
  • [2] : C标准中, longint 具有相同的最低位宽限制, 这意味着 long 有可能覆盖 unsigned int 的范围, 也可能不覆盖 unsigned int 的范围:
    • long 覆盖 unsigned int 的范围: m 被转换为 long 型.
    • long 不覆盖 unsigned int 的范围: mn 都被转换为 unsigned long 类型.

其他隐式类型转换

在如下的情况中, 编译器也会自动进行类型转换:

  • 在赋值与初始化过程中, 右操作数总是被转换成左操作数的类型.
  • 在调用函数时, 实际参数会被转换成函数对应的形式参数类型. 如果形式参数没有被声明(如可变长参数表), 那么就会采用默认的实参类型提升: 整数类型作整数提升, float 类型转换为 double 类型.
  • return 语句中, return 表达式的值会被转换成函数的返回类型.

复合赋值语句中, 如 x += 2.5 , 两个操作数的值都会先通过寻常算术转换, 然后算术结果被转换成左操作数的类型, 如同单纯的赋值转换一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <math.h>               // double sqrt(double);
int i = 7;
float x = 0.5;                  // double -> float
i = x;                          // float  -> int
x += 2.5;                       // 执行加法时, x 被转换为 double 类型
                                // 求和结果被转换为 float 类型存入 x
x = sqrt(i);                    // 实参 i 从 int 类型转换为 double 类型
                                // 返回值从 double 类型转换为 float 类型存入 x

long func() {
    // ...
    return 0;                   // 常数 0 被转换为 long 类型, 以匹配返回值类型
}

算术类型转换的结果

转换到 _Bool

任何标量类型的任何值都可以被转换为 _Bool 类型:

  • 若该标量值为 0 , 则转换结果为 false
  • 否则, 转换结果为 true

转换到 _Bool 以外的无符号整数类型

整数类型到无符号整数类型的转换规则很简单, 就是尽可能截取多的二进制位数.

数学意义上, 若类型 T 的值 x 要转换到无符号类型 U , 则只需求 x % (U_MAX + 1) 即可.

实数浮点类型到无符号整数类型的转换规则也很简单, 就是直接丢弃小数部分, 如果剩下的整数部分超出了新类型范围, 则转换结果未定义.

复数类型到无符号整数类型的转换规则也很简单, 就是直接丢弃虚数部分, 然后余下的浮点值按前述的规则转换.

转换到带符号的整数类型

当一个带符号或无符号整数类型值, 被转换成另一种带符号整数类型值时, 超出目标类型取值范围的问题可能发生. 例如, 当一个值从 longunsigned int 转换成 int 类型时. 这样的一处行为, 将由实现版本自行决定.

转换到实数浮点类型

整数类型到实数浮点类型: 并非所有整数值都可以精确使用浮点类型表示. 这是与浮点数的存储方式(IEEE 754)决定的. 例如有一些很长的值无法被精确地存储为 float 型:

1
2
3
long l_var = 123456789L;
float f_var = l_var;
printf("%d\n", (double)f_var - l_var); // 3.000000

实数浮点类型到实数浮点类型: 浮点类型的任何值都可以使用更高精度的浮点类型精确无误地表示出来. 因此, 当 double 值转换为 long double 时, 或者 float 值被转换为 doublelong double 时, 其值都可以被精确保留.

然而, 从高精度类型转向低精度类型时, 其值若超出目标类型取值范围, 则结果将未定义; 其值若在目标类型取值范围内, 但精度超出目标类型的精度范围, 则被表示为近似值.

复数类型到实数浮点类型的转换规则很简单, 就是直接丢弃虚部, 再对实部进行上述转换.

转换到复数浮点类型

整数, 实数浮点数转换到复数类型时, 实部等于将值转换为对应的实数浮点类型, 虚部等于 0

复数到其他复数类型的转化规则很简单, 实部虚部根据各自的实数浮点类型转换规则进行转换.

非算术类型的转换

指针, 数组, 函数名称也涉及某些隐式或显式的类型转换. 结构和联合无法被转换(虽然他们的指针可以来自于或者被转换成其他指针类型).

数组和函数指示符

数组或函数的指示符是任何拥有数组或函数类型的表达式.

多数情况下, 编译器隐式地将一个具有数组类型的表达式(如数组名), 转换成指向数组第一个元素的指针. 只有如下的几种情况, 数组表达式才不会被转换成指针:

  • 当数组是 sizeof 运算符的操作数时
  • 当数组的地址运算符 & 的操作数时
  • 当字符串字面量用于初始化一个 char , wchar_t , char16_t , char32_t 数组时

类似地, 任何指定一个函数的表达式(如函数名), 都可以隐式地被转换成指向该函数的指针. 当表达式是地址运算符 & 的操作数时, 不会进行这样的转换.

显式的指针转换

把某一类型的指针转换为另一类型的指针, 必须进行显示转换. 某些情况下, 编译器会提供隐式转换. 指针也可以显式地转换成整数, 反之亦然.

对象指针

可以显式地将一个对象指针转换到任何其他对象指针类型. 在程序中, 必须确定被转换指针方式是有意义的.

若转换后的对象指针不符合目标类型的要求, 则结果是不可确定的. 在所有其他情况下, 将指针值重新转换为原始指针类型, 将确保可以获得原来的指针值.

如果将任何类型的对象指针转换为指向任何 char 类型的指针, 则结果为指向对象第一个字节的指针. 无论系统的字节次序如何(大小端), 这里所指的第一个字节就是地址最低的字节. 下面的例子借助这个特点输出了结构变量的十六进制值:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
struct Data {
    short id;
    double val;
};
struct Data myData = {0x123, 77.7};
unsigned char *cp = (unsigned char *)(&myData);
int i;
printf("%p: ", cp);
for (i = 0; i < sizeof(myData); ++i) {
    printf("%02X ", *(cp + i));
}
putchar('\n');

笔者本地输出结果如下:

1
000000000062FE00: 23 01 00 00 00 00 00 00 CD CC CC CC CC 6C 53 40

观察到前两个字节 0x23 , 0x01 的次序, 可见笔者本地是小端存储系统.

函数指针

一个函数的类型, 总是包含了其返回值类型与形参类型. 可以显式地将一个指向函数的指针转换成指向不同类型的函数指针. 如果指向了类型不对应的函数, 则程序的行为是无法确定的.

隐式的指针转换

编译器会隐式地转换某些指针的类型. 赋值操作, 使用 ==== 的条件表达式, 以及函数调用, 都涉及三种隐式的指针转换:

  • 任何对象指针类型可以被隐式转换成 void* 类型, 反之亦然.
  • 任何指向一个确定类型的指针, 可以被隐式地转换为该类型更高限制的指针, 也就是可以转换为指向一个具有额外类型限定符的类型.
  • 空指针常量可以被隐式转换成任何类型.

通用指针 void*

void* 型的指针, 可以表示任何对象的地址. 在 bsearch , qsort , malloc 等库函数中发挥着巨大用处.

指向有限定符对象类型的指针

C语言的类型限定符为 const , volatile , restrict . 若有必要, 编译器将隐式地将任何 T* 型指针转换为 const T* 型指针. 如果想去掉限定符, 则必须使用显式类型转换.

空指针常量

下面是空指针常量的定义:

1
#define NULL ((void*)0)

当一个空指针常量转换为另一指针类型, 结果就称为空指针.

指针与整数类型之间的转换

可以显式地将指针转换为整数类型, 反之亦然. 这一转换对于系统编程来说很有用, 特别是当程序需要访问特定物理地址时.

如果将一个指针转换为一个整数类型, 而该整数类型的取值范围不够大, 不足以表示指针的值, 则转换结果是不确定的. 相反, 将一个整数转换为指针类型, 不一定生成一个有效的指针.

stdint.h 标准头可选择定义整数类型 intptr_tuintptr_t . 任何有效指针可以被转换成上述两种类型之一时, 对应的逆向转换可确保得到原来的指针.

当 stdint.h 定义了这两种类型的前提下, 在需要进行指针与整数类型间的转换时, 应该使用它们.