章节大纲

  •  

    算术操作数(Arithmetic Operands)

    布尔值、字符和整数(Boolean, Characters, and Integers)
    • char 是有符号(signed)还是无符号(unsigned)是实现定义的。

    在 C89 制定期间,存在两套常见的整数算术转换规则:保留无符号(UP, Unsigned Preserving)和保留数值(VP, Value Preserving)

    • UP 规则:如果表达式中包含两个较小的无符号类型(例如 unsigned charunsigned short),它们被提升为 unsigned int

    • VP 规则:若值可以放入 int,则提升为 int,否则提升为 unsigned int

    尽管多数情况下两者结果一致,但在某些特定场景下会产生差异。例如,在一个 16 位二进制补码系统上:

    #include <stdio.h>
    
    int main() {
        unsigned char uc = 10;
        int i = 32767;
        int j;
        unsigned int uj;
    
        j = uc + i;
        uj = uc + i;
    
        printf("j = %d (%x), uj = %u (%x)\n", j, j, uj, uj);
        printf("expr shifted right = %x\n", (uc + i) >> 4);
        return 0;
    }
    
    • UP 规则结果

      j = -32759 (8009), uj = 32777 (8009)
      expr shifted right = 800
      
    • VP 规则结果

      j = -32759 (8009), uj = 32777 (8009)
      expr shifted right = f800
      

    因为类型不同,右移时可能出现符号扩展(VP)或逻辑移位(UP),导致结果差异。

    再如:

    uc = 10;
    i = 30000;
    

    则不论规则如何,右移结果都一致。

    结论:不同规则在高位无符号位被设置的情况下才会产生差异。

    为避免这种差异,可使用强制类型转换(cast):

    #include <stdio.h>
    
    int main() {
        unsigned char uc = 10;
        int i = 32767;
        int expr1, expr2, expr3;
    
        expr1 = ((int) uc + i) >> 4;
        expr2 = (uc + (unsigned) i) >> 4;
        expr3 = (uc + i) >> 4;
    
        printf("expr1 = %x\n", expr1);
        printf("expr2 = %x\n", expr2);
        printf("expr3 = %x\n", expr3);
        return 0;
    }
    
    • UP 规则结果

      expr1 = f800
      expr2 = 800
      expr3 = 800
      
    • VP 规则结果

      expr1 = f800
      expr2 = 800
      expr3 = f800
      

    由此可见,使用类型转换可以消除因规则差异带来的影响。

    虽然标准 C 使用 VP 规则,但在 C89 之前,许多编译器采用 UP 规则,因此依赖旧规则的代码在标准 C 中可能表现不同。

    • C99 引入了 _Bool 类型,并支持扩展整型类型。

    • 整数扩展规则同样适用于位字段(bit fields),它们可以是有符号或无符号的。


    浮点与整数转换(Floating and Integer)

    浮点类型(Floating Types)
    • 将有限的浮点类型值转换为整型时,小数部分将被截断(向零舍入)。

      若整数部分无法在目标整型中表示,行为未定义。

    • 将整数类型值转换为浮点类型时:

      • 可精确表示时,值不变;

      • 若不能精确表示,则结果为最接近的高值或低值,选择方式为实现定义;

      • 若超出可表示范围,行为未定义。

    • double 转为 float,或将 long double 转为 double/float

      若值不能表示,行为未定义;若可表示但不精确,则结果为两个最邻近值之一(实现定义)。

    注意:在有函数原型的情况下,float 可按值传递而不必提升为 double;但这是允许行为,不是强制要求。


    复数类型(Complex Types)

    • C99 引入了 _Complex 类型及其转换规则,定义于 <complex.h>


    常规算术转换(Usual Arithmetic Conversions)

    • 标准 C 调整了转换规则以适应 VP 模式。

    • 表达式可在“更宽”模式下求值(以提高硬件效率),也可在“更窄”类型下求值,只要结果相同。

    • 二元运算符的两个操作数若类型不同,将触发一种或两种操作数的转换。

    • 这些规则大致继承自 K&R,但作出了三点扩展:

      1. 兼容 VP 规则;

      2. 新增类型;

      3. 允许在不扩展的前提下使用“窄”类型进行运算。


    其他操作数类型


    指针类型(Pointers)

    • C89 引入了 void * 概念。它可以无需强制类型转换转换为任何对象类型的指针。

    • 对象指针也可转换为 void * 并可还原,且不会丢失信息。

    关于 C++ 的考虑:标准 C++ 要求将 void * 指针赋值给对象指针时必须使用显式强转。

    • 不同类型的对象指针不一定具有相同大小,例如 char *int * 大小可能不同。

    • 将一个类型的对象指针转换为另一类型的指针,如果转换后未正确对齐,则行为未定义。

    • 尽管 int 和指针在某些平台上大小相同,但两者属于不同的类型,它们之间的转换并不具有可移植性。

    空指针(null pointer)的值不必是“全为零”的位模式,但常实现为全零。标准 C 仅要求 (void *)0 表示一个永不与任何对象或函数地址相等的地址。

    • 表达式 p == 0 中,0 会提升为指针类型再与 p 比较。

    • 整数转为指针或指针转为整数的行为是实现定义的。

      若结果无法用目标整数类型表示,则行为未定义。

    • 函数指针 与数据指针是截然不同的类型:

      • 它们的大小与格式都可能不同;

      • 不能假设它们可互换。

    标准 C 要求,当一个返回某种类型的函数指针被赋值为另一种返回类型的函数指针时,必须进行强制类型转换。

    • 标准 C 对函数指针的类型检查更严格:

      • 函数指针不仅包括返回类型,还包括参数类型;

      • 若函数指针类型不兼容却被用于调用函数,行为未定义。


    总之,标准 C 在类型转换方面进行了细致规范,尤其在涉及不同整型、浮点型、指针、函数指针之间转换时,需要特别关注:

    ✅ 使用显式类型转换;
    ✅ 避免依赖实现定义行为;
    ✅ 遵守标准规定的转换语义;
    ✅ 编写可移植的跨平台代码。