操作符和赋值

C 语言有许多操作符,可以方便地进行简单的数学运算。按优先级分组的操作符列表如下:

基本表达式

标识符是 C 中的名称,通常由字母或下划线(_)开头,后面可以跟字母、数字或下划线。标识符(或变量名)是一个基本表达式,前提是它已经声明为表示一个对象(此时它是一个 lvalue [可以作为赋值表达式左侧的值])或一个函数(此时它是一个函数设计符)。

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

字符串字面量是一个基本表达式。它由一串字符组成,字符被双引号(" ")包围。

带括号的表达式是一个基本表达式。它由圆括号(egg)包围的表达式组成。它的类型和值与括号内的非括号表达式相同。

在 C11 中,_Generic 后跟一个初始表达式,再跟一个类型:表达式的列表,其中类型可以是一个命名类型或 default 关键字,构成一个基本表达式。这个表达式的值是初始表达式类型对应的表达式的值,或者如果找不到则为默认值。

后缀操作符

首先,基本表达式也是后缀表达式。以下表达式也属于后缀表达式:

  • 后缀表达式后跟左中括号([)、一个表达式和右中括号(])依次组成数组下标操作符的调用。一个表达式应该是“指向对象类型的指针”,另一个表达式应该是整数类型;结果类型为该类型。连续的数组下标操作符指定多维数组的元素。

  • 后缀表达式后跟圆括号或可选的带括号的参数列表,表示调用函数操作符。函数调用操作符的值是带有提供参数的返回值。函数的参数是通过值拷贝到栈上的(或至少编译器像这样处理;如果程序员希望参数按引用传递,则可以传递待修改区域的地址,函数可以通过相应的指针访问该区域)。编译器通常是从右向左将参数压栈,但这不是普遍的做法。

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

  • 后缀表达式后跟递增或递减操作符(分别是 ++--),表示该变量作为副作用被递增或递减。该表达式的值是在递增或递减之前的后缀表达式的值。这些操作符仅适用于整数和指针。

一元表达式

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

  • 后缀递增或递减操作符后跟一个一元表达式,也是一个一元表达式。该表达式的值是在递增或递减之后的一元表达式的值。这些操作符仅适用于整数和指针。

  • 以下操作符后跟强制类型转换表达式,构成一元表达式:

操作符 含义
& 取地址;值是操作数的位置
* 解引用;值是位置上存储的内容
- 取反
+ 值操作符
! 逻辑取反((!E) 等价于 (0==E)
~ 按位取反

关键字 sizeof 后跟一个一元表达式,是一个一元表达式。其值是该表达式类型的字节大小。表达式本身不会被求值。

关键字 sizeof 后跟带括号的类型名也是一元表达式。其值是该类型的字节大小。

强制类型转换操作符

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

带括号的类型名后跟任何表达式(包括字面量)构成强制类型转换表达式。带括号的类型名会将该表达式强制转换为括号内类型名指定的类型。对于算术类型,这通常不改变表达式的值,或者当表达式是整数且新类型小于原类型时,会截断表达式的值。

例如,将 int 类型转换为 float 类型的例子:

int i = 5;
printf("%f\n", (float) i / 2); // 输出: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;

    /* 当 i 小于 5 且 j 小于 5 时,循环 */
    while( (i < 5) && (j < 5) )
    {
        /* 后缀递增,i++ 
         *     先读取 i 的值,然后递增
         */
        printf("i: %d\t", i++);

        /*
         * 前缀递增,++j
         *     先递增 j 的值,然后读取
         */
        printf("j: %d\n", ++j);
    }

    printf("最终它们的值相等:\ni: %d\tj: %d\n", i, j);

    getchar(); /* 暂停 */
    return 0;
}

执行该代码将输出:

i: 0    j: 1
i: 1    j: 2
i: 2    j: 3
i: 3    j: 4
i: 4    j: 5
最终它们的值相等:
i: 5    j: 5

位移操作符(可用于旋转位)

位移表达式也是加法表达式(意味着位移操作符的优先级略低于加法和减法)。

位移函数通常用于低级 I/O 硬件接口。位移和旋转函数在密码学和软件浮点仿真中被广泛使用。除此之外,位移可以用来代替乘除以 2 的幂。许多处理器有专门的功能模块来加速这些操作——见《微处理器设计/位移与旋转模块》。在具有这些模块的处理器上,大多数 C 编译器会将位移和旋转操作符编译成单条汇编语言指令——见《X86 汇编/位移与旋转》。

左移 << 操作符将二进制表示左移,丢弃最左边的位,并在右侧补上零位。其结果相当于将整数乘以 2 的幂。

无符号右移 无符号右移操作符,也有时称为逻辑右移操作符。它将二进制表示右移,丢弃最右边的位,并在左侧填充零。>> 操作符对于无符号整数来说相当于除以 2 的幂。

有符号右移 有符号右移操作符,也有时称为算术右移操作符。它将二进制表示右移,丢弃最右边的位,但在左侧填充原符号位的副本。对于有符号整数,>> 操作符并不等同于除法。

在 C 中,>> 操作符的行为取决于它作用的数据类型。因此,有符号和无符号右移在外观上是相同的,但在某些情况下会产生不同的结果。

右旋转 与常见的观点相反,编写 C 代码并将其编译为“旋转”汇编语言指令是可能的(对于有此指令的 CPU)。

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

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

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

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

左旋转 大多数编译器识别这个习惯用法:

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

并将其编译为单条 32 位旋转指令。

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

关系和等式操作符 关系表达式也是位移表达式;等式表达式也是关系表达式。

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

等式二元操作符 ==(等于)和 !=(不等于)与关系操作符类似,不同之处在于它们的优先级较低。它们也返回一个值,若操作结果为真则值为 1,若为假则值为 0。

关于浮点数和等式操作符的注意事项:由于浮点运算可能产生近似值(例如,0.1 在二进制中是一个无限循环的小数,所以 0.1 * 10.0 很少等于 1.0),因此不建议使用 == 操作符比较浮点数。相反,如果 ab 是要比较的数值,可以比较 fabs(a - b) 是否小于某个容差值。

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

被操作的值必须是整数,结果也为整数。

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

/* 这是一个位运算的示例 */
#define BITFLAG1    (1)
#define BITFLAG2    (2)
#define BITFLAG3    (4) /* 它们是 2 的幂 */

unsigned bitbucket = 0U;    /* 清除所有标志 */
bitbucket |= BITFLAG1;      /* 设置位标志 1 */
bitbucket &= ~BITFLAG2;     /* 清除位标志 2 */
bitbucket ^= BITFLAG3;      /* 翻转位标志 3 的状态,从关变开,或反之 */
if (bitbucket & BITFLAG3) {
  /* 位标志 3 被设置 */
} else {
  /* 位标志 3 没有被设置 */
}

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

&& 用于从左到右求值,如果两个条件都为真则返回 1,如果其中任一条件为假则返回 0。如果第一个表达式为假,则第二个表达式不再求值。

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

这里,&& 操作符先检查最左边的表达式,然后检查右边的表达式。如果有多个表达式串联(例如 x && y && z),操作符会依次检查 x,然后检查 y(如果 x 不为零),如果 xy 都不为零,则继续检查 z。由于两个条件都为真,&& 返回真,代码块被执行。

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

&& 操作符以同样的方式检查,发现第一个表达式为假,便停止评估并返回假。

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

/* 使用相同的变量 */
if(x == 2 || y == 5) { // || 操作符检查两个表达式,发现第二个为真并返回真
    ...
}

|| 操作符在这里检查最左边的表达式,发现它为假,但继续评估下一个表达式。它发现下一个表达式为真,便停止并返回 1。与 && 操作符相似,|| 在发现表达式为真时会停止评估。

值得注意的是,C 语言没有像其他语言那样的布尔值(真和假)。它将 0 视为假,任何非零值都视为真。

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

示例:

int x = 0;
int y;
y = (x ? 10 : 6); /* 括号实际上不是必须的,因为赋值运算符的优先级低于条件运算符,但为了清晰起见,括号是必要的。*/

表达式 x 评估为 0。三元运算符查找“如果为假”的值,在此例中为 6。它返回该值,因此 y 等于 6。如果 x 为非零值,则表达式将返回 10。

赋值运算符 赋值运算符包括 =, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, 和 |== 运算符将右操作数的值存储到左操作数指向的位置,左操作数必须是一个左值(具有地址的值,因此可以赋值给它

)。

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

  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; 会将零赋值给三个变量。

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

此运算符在包含多个操作的语句中很有用(例如在 for 循环的条件中)。

以下是逗号运算符的小示例:

int i, x;      /* 在一条声明语句中声明两个整数,i 和 x。
                  从技术上讲,这不是逗号运算符。*/

/* 此循环初始化 x 和 i 为 0,然后执行循环 */
for (x = 0, i = 0; i <= 6; i++) {
    printf("x = %d, and i = %d\n", x, i);
}

参考文献

  • GCC: "优化常见的旋转构造"
  • "ROTAL/ROTR DAG 合并代码的清理" 提到该代码支持 CellSPU 的 "rotate" 指令
  • "替换位旋转例程的私有副本"——建议包含 "bitops.h" 并使用其 rol32ror32 而非直接复制粘贴到新程序中。
Last modified: Sunday, 12 January 2025, 1:24 PM