跳转到内容

C 编程/运算符和类型转换

来自维基教科书,开放的世界中的开放书籍

运算符和赋值

[编辑 | 编辑源代码]

C 语言具有广泛的运算符,可以轻松处理简单的数学运算。运算符按优先级分组的列表如下

主表达式

[编辑 | 编辑源代码]

标识符是 C 语言中事物的名称,由字母或下划线(_)组成,后面可以是字母、数字或下划线。标识符(或变量名)是主表达式,前提是它已被声明为指定一个对象(在这种情况下它是一个左值 [一个可以被用作赋值表达式的左侧的值])或一个函数(在这种情况下它是一个函数指示符)。

常量是主表达式。它的类型取决于它的形式和值。常量的类型包括字符常量(例如' ' 是一个空格)、整数常量(例如2)、浮点常量(例如0.5)以及之前通过enum定义的枚举常量。

字符串字面量是主表达式。它由双引号内的字符串组成(" ).

带括号的表达式是主表达式。它由括号内的表达式组成(( ))。它的类型和值与括号内非带括号表达式的类型和值相同。

在 C11 中,以_Generic开头的表达式,后面是一个初始表达式,以及一列形如type: expression的值,其中 type 既可以是命名类型,也可以是关键字 default,构成一个主表达式。该值是紧随初始表达式类型之后的表达式,如果未找到,则为 default。

后缀运算符

[编辑 | 编辑源代码]

首先,主表达式也是后缀表达式。以下表达式也是后缀表达式

后缀表达式后跟左方括号 ([)、表达式和右方括号 (]) 按顺序构成对数组下标运算符的调用。其中一个表达式应具有“指向对象类型的指针”类型,另一个表达式应具有整数类型;结果类型为类型。连续的数组下标运算符指定多维数组的元素。

后缀表达式后跟括号或可选的带括号参数列表表示对函数调用运算符的调用。函数调用运算符的值是使用提供的参数调用的函数的返回值。函数的参数在堆栈上按复制(或者至少编译器表现得好像是这样;如果程序员希望参数按引用复制,那么将要修改的区域的地址按值传递会更容易,然后被调用的函数可以通过相应的指针访问该区域)。编译器传递参数的趋势是从右到左压入堆栈,但这并非普遍情况。

后缀表达式后跟一个点 (.) 后跟一个标识符,将从结构或联合体中选择一个成员;后缀表达式后跟一个箭头 (->) 后跟一个标识符,将从结构或联合体中选择一个成员,该成员由表达式左侧的指针指向。

后缀表达式后跟增量或减量运算符(分别为++--)表示变量将作为副作用进行增量或减量。表达式的值为后缀表达式增量或减量之前的值。这些运算符仅适用于整数和指针。

一元表达式

[编辑 | 编辑源代码]

首先,后缀表达式是一元表达式。以下表达式都是一元表达式

增量或减量运算符后跟一元表达式是一元表达式。表达式的值为一元表达式增量或减量之后的值。这些运算符仅适用于整数和指针。

以下运算符后跟强制转换表达式是一元表达式

Operator     Meaning
========     =======
   &         Address-of; value is the location of the operand
   *         Contents-of; value is what is stored at the location
   -         Negation
   +         Value-of operator
   !         Logical negation ( (!E) is equivalent to (0==E) )
   ~         Bit-wise complement

关键字sizeof 后跟一元表达式是一元表达式。该值为表达式的类型以字节为单位的大小。表达式不会被求值。

关键字sizeof 后跟带括号的类型名称是一元表达式。该值为该类型以字节为单位的大小。

强制转换运算符

[编辑 | 编辑源代码]

一元表达式也是强制转换表达式。

带括号的类型名称后跟任何表达式(包括字面量)都是强制转换表达式。带括号的类型名称具有将强制转换表达式强制转换为括号中类型名称指定的类型的效果。对于算术类型,这要么不改变表达式的值,要么截断表达式的值(如果表达式是整数,而新类型小于之前的类型)。

将 int 强制转换为 float 的示例

int i = 5;
printf("%f\n", (float) i / 2); // Will print out:  2.500000

乘法和加法运算符

[编辑 | 编辑源代码]

首先,乘法表达式也是强制转换表达式,加法表达式也是乘法表达式。这遵循乘法优先于加法的优先级。

在 C 语言中,简单的数学运算非常容易处理。存在以下运算符:+(加法)、-(减法)、*(乘法)、/(除法)和%(模数);您可能从数学课上了解所有这些运算符 - 除了可能只有模数。它返回除法的余数(例如 5 % 2 = 1)。(模数没有为浮点数定义,但math.h 库有一个fmod 函数。)

必须注意模数,因为它不等于数学模数:(-5) % 2 不是 1,而是 -1。整数的除法将返回一个整数,负整数除以正整数将朝零取整而不是向下取整(例如 (-5) / 3 = -1 而不是 -2)。然而,对于所有整数 a 和非零整数 b,始终为真((a / b) * b) + (a % b) == a.

没有内联运算符可以执行幂运算(例如 5 ^ 2 不是25 [它为 7;^ 是异或运算符],而 5 ** 2 是一个错误),但有一个幂函数

数学运算顺序确实适用。例如 (2 + 3) * 2 = 10 而 2 + 3 * 2 = 8。乘法运算符优先于加法运算符。

#include <stdio.h>

int main(void)
{
int i = 0, j = 0;

    /* while i is less than 5 AND j is less than 5, loop */
    while( (i < 5) && (j < 5) )
    {
        /* postfix increment, i++
         *     the value of i is read and then incremented
         */
        printf("i: %d\t", i++);

        /*
         * prefix increment, ++j 
         *     the value of j is incremented and then read
         */
        printf("j: %d\n", ++j);
    }

    printf("At the end they have both equal values:\ni: %d\tj: %d\n", i, j);

    getchar(); /* pause */
    return 0;
}

将显示以下内容

i: 0    j: 1
i: 1    j: 2
i: 2    j: 3
i: 3    j: 4
i: 4    j: 5
At the end they have both equal values:
i: 5    j: 5

移位运算符(可用于旋转位)

[edit | edit source]

移位表达式也是加法表达式(这意味着移位运算符的优先级仅次于加法和减法)。

移位函数通常用于低级 I/O 硬件接口。移位和旋转函数在密码学和软件浮点仿真中被大量使用。除此之外,移位可以用来代替除以或乘以二的幂。许多处理器都有专门的功能块来使这些操作快速完成 -- 请参阅 微处理器设计/移位和旋转块。在具有这些功能块的处理器上,大多数 C 编译器将移位和旋转运算符编译成单个汇编语言指令 -- 请参阅 X86 汇编/移位和旋转

左移

[edit | edit source]

<< 运算符将二进制表示向左移动,丢弃最高有效位并用零位追加。结果等效于将整数乘以二的幂。

无符号右移

[edit | edit source]

无符号右移运算符,有时也称为逻辑右移运算符。它将二进制表示向右移动,丢弃最低有效位,并在前面追加零。对于无符号整数,>> 运算符等效于除以二的幂。

有符号右移

[edit | edit source]

有符号右移运算符,有时也称为算术右移运算符。它将二进制表示向右移动,丢弃最低有效位,但用原始符号位的副本追加。对于有符号整数,>> 运算符不等效于除法。

在 C 中,>> 运算符的行为取决于它所作用的数据类型。因此,有符号和无符号右移看起来完全一样,但在某些情况下会产生不同的结果。

右旋转

[edit | edit source]

与普遍看法相反,可以编写 C 代码编译成“旋转”汇编语言指令(在具有此类指令的 CPU 上)。

大多数编译器会识别这个惯用法

  unsigned int x;
  unsigned int y;
  /* ... */
  y = (x >> shift) | (x << (32 - shift));

并将其编译成单个 32 位旋转指令。 [1] [2]

在某些系统上,这可能被 "#define" 作为宏,或定义为一个内联函数,在标准头文件(如 "bitops.h")中称为 "rightrotate32" 或 "rotr32" 或 "ror32"。 [3]

左旋转

[edit | edit source]

大多数编译器会识别这个惯用法

  unsigned int x;
  unsigned int y;
  /* ... */
  y = (x << shift) | (x >> (32 - shift));

并将其编译成单个 32 位旋转指令。

在某些系统上,这可能被 "#define" 作为宏,或定义为一个内联函数,在头文件(如 "bitops.h")中称为 "leftrotate32" 或 "rotl32"。

关系运算符和相等运算符

[edit | edit source]

关系表达式也是移位表达式;相等表达式也是关系表达式。

关系二元运算符 <(小于)、>(大于)、<=(小于或等于)和 >=(大于或等于)运算符如果操作结果为真,则返回 1,如果为假,则返回 0。这些运算符的结果类型为 int

相等二元运算符 ==(等于)和 !=(不等于)运算符类似于关系运算符,只是它们的优先级较低。它们也返回 1 如果操作的结果为真,返回 0 如果为假。

浮点数和相等运算符的一个问题:由于浮点运算可能产生近似值(例如,0.1 在二进制中是循环小数,所以 0.1 * 10.0 几乎永远不会是 1.0),所以不建议对浮点数使用 == 运算符。相反,如果 a 和 b 是要比较的数字,将 fabs (a - b) 与一个误差因子进行比较。

位运算符

[edit | edit source]

位运算符是 &(与)、^(异或)和 |(或)。& 运算符的优先级高于 ^^ 的优先级高于 |

被操作的值必须是整型;结果是整型。

位运算符的一种用途是模拟位标志。这些标志可以用 OR 设置,用 AND 测试,用 XOR 翻转,用 AND NOT 清除。例如

/* This code is a sample for bitwise operations.  */
#define BITFLAG1    (1)
#define BITFLAG2    (2)
#define BITFLAG3    (4) /* They are powers of 2 */

unsigned bitbucket = 0U;    /* Clear all */
bitbucket |= BITFLAG1;      /* Set bit flag 1 */
bitbucket &= ~BITFLAG2;     /* Clear bit flag 2 */
bitbucket ^= BITFLAG3;      /* Flip the state of bit flag 3 from off to on or
                               vice versa */
if (bitbucket & BITFLAG3) {
  /* bit flag 3 is set */
} else {
  /* bit flag 3 is not set */
}

逻辑运算符

[edit | edit source]

逻辑运算符是 &&(与)和 ||(或)。这两个运算符如果关系为真则产生 1,如果为假则产生 0。这两个运算符都短路;如果可以从第一个操作数确定表达式的结果,则忽略第二个操作数。&& 运算符的优先级高于 || 运算符。

&& 用于从左到右评估表达式,如果两个语句都为真,则返回 1,如果其中任何一个为假,则返回 0。如果第一个表达式为假,则不评估第二个表达式。

  
  int x = 7;
  int y = 5;
  if(x == 7 && y == 5) {
      ...
  }

这里,&& 运算符检查最左边的表达式,然后检查其右侧的表达式。如果有更多表达式链接(例如 x && y && z),运算符将检查x首先,然后是 y(如果x不为零),然后如果 x 或 y 均不为零,则继续向右到 z。由于两个语句都返回真,&& 运算符返回真,并执行代码块。

  
    if(x == 5 && y == 5) {
        ...
    }

&& 运算符以与之前相同的方式检查,发现第一个表达式为假。&& 运算符一旦发现语句为假,就会停止评估并返回一个假。


|| 用于从左到右评估表达式,如果任何一个表达式为真,则返回 1,如果两个都为假,则返回 0。如果第一个表达式为真,则不评估第二个表达式。

      
    /* Use the same variables as before. */
    if(x == 2 || y == 5) { // the || statement checks both expressions, finds that the latter is true, and returns true
        ...
    }

这里的 || 运算符检查最左边的表达式,发现它为假,但继续评估下一个表达式。它发现下一个表达式返回真,停止并返回 1。与 && 运算符一旦发现返回假的表达式就会停止评估一样,|| 运算符一旦发现返回真的表达式就会停止评估。

值得注意的是,C 没有其他语言中常见的布尔值(真和假)。它而是将 0 解释为假,将任何非零值解释为真。

条件运算符

[edit | edit source]

三元 ?: 运算符是条件运算符。表达式 (x ? y : z) 如果 x 不为零,则值为 y,否则为 z

示例

  
int x = 0;
int y;
y = (x ? 10 : 6); /* The parentheses are technically not necessary as assignment
                     has a lower precedence than the conditional operator, but
                     it's there for clarity.  */

表达式 x 评估为 0。然后三元运算符查找“if-false”值,在本例中为 6。它返回该值,所以 y 等于 6。如果 x 为非零,则表达式将返回 10。

赋值运算符

[edit | edit source]

赋值运算符包括:=*=/=%=+=-=<<=>>=&=^=|== 运算符将右操作数的值存储到由左操作数确定的位置,左操作数必须是左值(具有地址的值,因此可以被赋值)。

对于其他运算符,x op= yx = x op (y) 的简写形式。因此,以下表达式是相同的:

    1. x += y     -     x = x+y
    2. x -= y     -     x = x-y
    3. x *= y     -     x = x*y
    4. x /= y     -     x = x/y
    5. x %= y     -     x = x%y

赋值表达式的值为赋值后左操作数的值。因此,赋值可以连锁;例如,表达式 a = b = c = 0; 会将值零赋值给所有三个变量。

逗号运算符

[edit | edit source]

优先级最低的运算符是逗号运算符。表达式 x, y 的值将评估 xy,但返回 y 的值。

此运算符可用于在一个语句中包含多个操作(例如,在 for 循环条件中)。

以下是一个逗号运算符的小示例。

int i, x;      /* Declares two ints, i and x, in one declaration.
                  Technically, this is not the comma operator. */

/* this loop initializes x and i to 0, then runs the loop */
for (x = 0, i = 0; i <= 6; i++) {
    printf("x = %d, and i = %d\n", x, i);
}

参考文献

[edit | edit source]
  1. GCC: "Optimize common rotate constructs"
  2. "Cleanups in ROTL/ROTR DAG combiner code" mentions that this code supports the "rotate" instruction in the CellSPU
  3. "replace private copy of bit rotation routines" -- recommends including "bitops.h" and using its rol32 and ror32 rather than copy-and-paste into a new program.


华夏公益教科书