跳转到内容

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

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

[编辑 | 编辑源代码]

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

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

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

无符号右移

[编辑 | 编辑源代码]

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

有符号右移

[编辑 | 编辑源代码]

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

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

右旋转

[编辑 | 编辑源代码]

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

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

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

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

在某些系统上,这可能被“#define”定义为宏,或在像“bitops.h”这样的标准头文件中定义为名为“rightrotate32”或“rotr32”或“ror32”的内联函数。 [3]

左旋转

[编辑 | 编辑源代码]

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

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

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

在某些系统上,这可能被“#define”定义为宏,或在像“bitops.h”这样的头文件中定义为名为“leftrotate32”或“rotl32”的内联函数。

关系运算符和相等运算符

[编辑 | 编辑源代码]

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

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

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

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

位运算符

[编辑 | 编辑源代码]

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

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

位运算符的一种用途是模拟位标志。这些标志可以用 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 */
}

逻辑运算符

[编辑 | 编辑源代码]

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

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

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

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

  
    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 解释为假,并将任何非零值解释为真。

条件运算符

[编辑 | 编辑源代码]

三元 ?: 运算符是条件运算符。表达式 (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。三元运算符然后查找“如果为假”的值,在本例中是 6。它返回该值,因此 y 等于 6。如果 x 为非零,则表达式将返回 10。

赋值运算符

[编辑 | 编辑源代码]

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

对于其他运算符,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.


华夏公益教科书