C编程
操作符和赋值
C 语言有许多操作符,可以方便地进行简单的数学运算。按优先级分组的操作符列表如下:
基本表达式
标识符是 C 中的名称,通常由字母或下划线(_)开头,后面可以跟字母、数字或下划线。标识符(或变量名)是一个基本表达式,前提是它已经声明为表示一个对象(此时它是一个 lvalue [可以作为赋值表达式左侧的值])或一个函数(此时它是一个函数设计符)。
常量也是一个基本表达式。其类型取决于它的形式和值。常量的类型包括字符常量(例如,' ' 是空格)、整数常量(例如,2)、浮点常量(例如,0.5)和通过 enum
之前定义的枚举常量。
字符串字面量是一个基本表达式。它由一串字符组成,字符被双引号(" ")包围。
带括号的表达式是一个基本表达式。它由圆括号()包围的表达式组成。它的类型和值与括号内的非括号表达式相同。
在 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),因此不建议使用 ==
操作符比较浮点数。相反,如果 a
和 b
是要比较的数值,可以比较 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
不为零),如果 x
和 y
都不为零,则继续检查 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= y
是 x = x op
的简写。因此,以下表达式是相同的:
x += y
-x = x + y
x -= y
-x = x - y
x *= y
-x = x * y
x /= y
-x = x / y
x %= y
-x = x % y
赋值表达式的值是左操作数赋值后的值。因此,可以链式赋值;例如,a = b = c = 0;
会将零赋值给三个变量。
逗号运算符 优先级最低的运算符是逗号运算符。表达式 x, y
会依次评估 x
和 y
,但返回 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" 并使用其
rol32
和ror32
而非直接复制粘贴到新程序中。