1. 运算符的介绍
运算符的分类:
- 算术运算符(
+
、-
、*
、/
、%
) - 关系运算符(
==
、!=
、>
、<
、>=
、<=
) - 逻辑运算符(
&&
、||
、!
) - 赋值运算符(
=
)
2. 数据对象与左值和右值
数据对象:相当于一个盒子,可以把其他东西(数据/data)放进去
左值:Lvalue,变量名,相当于盒子的名字
右值:Rvalue,盒子里要放的东西(数据/data)
运算符:指挥盒子与数据,决定如何把东西放在盒子里
1 | uint32_t num1 = 2; |
使用
uint32_t
与PRIu32
打印输出需要引入头文件stdint.h
与inttypes.h
,具体可参考上一章第12与13节的内容。
3. 多重赋值
多重赋值也叫长链式赋值,C语言中,赋值为从右到左,例如:
1 | uint32_t num1, num2, num3; |
以上代码片断中的多重赋值num1 = num2 = num3 = 10;
相当于:
1 | num3 = 10; // 首先将10赋值给num3 |
但是一般编写代码时候,尽量避免三重赋值以上的多重赋值,语义可能会比较混乱且其他语言可能不支持。
4. 算术运算符的应用
1 | uint32_t num1 = 3; |
5. 一元与二元运算符
一元运算符:一次运算只需要操作一个数,例如负号(-
)。
1 | uint32_t num1 = 3; |
二元运算符:一次运算需要对两个数进行操作,例如减号(-
)。
1 | uint32_t num1 = 3; |
也有二者兼有的情况:
1 | uint32_t num1 = 3; |
6. 前缀后缀递增与递减
后缀递增:先赋值后递增,因此result = value++;
这一条语句就相当于下面的这两条语句:
1 | result = value; // 先将value的值赋给result |
因此:
1 | uint32_t value = 3; |
前缀递增:先递增后赋值,因此result = ++value;
这一条语句就相当于下面的这两条语句:
1 | value = value + 1; // value先加1 |
因此:
1 | uint32_t value = 3; |
后缀递减:先赋值后递减,因此result = value--;
这一条语句就相当于下面的这两条语句:
1 | result = value; // 先将value的值赋给result |
因此:
1 | uint32_t value = 3; |
前缀递减:先递减后赋值,因此result = --value;
这一条语句就相当于下面的这两条语句:
1 | value = value - 1; // value先减1 |
因此:
1 | uint32_t value = 3; |
是否选择前缀或后缀的计算方式取决于是否想要保留变量的旧值。
选择前缀和后缀中的哪种计算方式取决于需要的值是否是这个变量的旧值。
在企业中,除非在循环等固定场景,应尽量避免使用前后缀递增或递减,会导致代码逻辑不清晰,容易出错。
7. 按位移位运算符
左移运算符(<<
):每一位向左移动固定位数,低位补零(无符号型)
1 | uint8_t num = 22; // 0b00010110 |
左移会将数据扩大,左移n位相当于乘以2的n次方。
右移运算符(>>
):每一位向右移动固定位数,高位补零(无符号型)
1 | uint8_t num = 22; // 0b00010110 |
右移会将数据缩小,右移n位相当于除以2的n次方。
移位操作的速度非常快,例如22右移两位得到的值为5的操作,要比22减去17得到5的减法操作速度快得多,因此可以使用位移操作替代算术运算来优化性能。
在企业中使用按位移位运算符时,要保证目的清晰,要特别注意移位后的符号位以及可能出现的溢出问题。
8. 按位移位的另外问题
计算机中计算各种算术运算使用到的是CPU中的ALU单元(算术逻辑单元),其内部处理两个数乘法运算的逻辑如下,以25乘以1024为例:
- 将25和1024都转换为二进制:0b00011001与0b10000000000;
- 遍历0b10000000000中的每一位,找到为1的位;
- 对其中每一位为1的位,根据其所在的位数,对25(0b00011001)执行按位左移一定的位数的操作,此处1024只在的位置有一位为1,因此对25(0b00011001)做左移10位的操作;
- 移位完成后的二进制数,转换为十进制数后返回结果。
一通操作下来发现,计算机做乘法的本质也是移位,25乘以1024()等同于将25按位左移10位。
选修部分:
企业中的按位移位操作通常用在配置某个硬件设备的配置项,这些配置项在整数当中以不同的位来表示,此时位移操作符经常用来操作一些位掩码与一些快速运算。
举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 // 设置配置寄存器的初始状态(所有选项都未设置)
uint32_t device_config = 0;
// 定义每个选项的掩码
const uint32_t option1_mask = 1 << 0; // 0b0001
const uint32_t option2_mask = 1 << 1; // 0b0010
const uint32_t option3_mask = 1 << 2; // 0b0100
// 设置选项1和选项3
// 通过按位或操作设置选项1和选项3
// (0b0001 | 0b0100) | 0b0000 = 0b0101
device_config |= option1_mask | option3_mask; // 0b0101
// 获取选项2的状态
// 通过按位与操作获取选项2的状态
// (0b0101 & 0b0010) >> 1 = 0b0000
uint32_t option2_status = (device_config & option2_mask) >> 1; // 0b0000
// 清除选项1
// 通过按位与操作清除选项1
// 0b0101 & ~0b0001 = 0b0100
device_config &= ~option1_mask; // 0b0100写按位移位操作时候一定要写清楚注释,提高代码的可读性。
9. 逻辑的真与假、C关系运算符
在上一章中27节bool
类型中提到,逻辑中的“真”可以用1(true)来表示,“假”可以用0(false)来表示。
C语言中的关系运算符(==
、!=
、>
、<
、>=
、<=
)只用来做判断,不用来做计算,判断的结果有真(true)与假(false)两种情况。
例如5 > 3
的判断结果为“真/true
/0
“。
1 | int num1 = 10; |
使用
bool
类型需要引入头文件stdbool.h
,具体可参考上一章第27节的内容。
10. 条件表达式与运算符
C语言有一个三元运算符:conditional-expression运算符(? :
)。
它的结构是这样的:logical-OR-expression ? expression : conditional-expression
简单来说,它的结构为:A ? B : C
这个表达式会先判断A
的真假,当A
为真(true/1)的时候,就会将B
返回,当A
为假(false/0)的时候,就会将C
返回。
举个例子,可以将上一节中的代码输出的1
和0
自动转变为对应的true
和false
的字符串类型:
1 | int num1 = 10; |
或者还可以写得更简单,直接将关系运算符作为需要判断的A
部分,以“大于”举例:
1 | printf("a > b: %s\n", (num1 > num2) ? "true" : "false"); // 输出:false |
11. 按位“与”运算符(&
)
按位“与”运算符将其第一操作数的每个位与其第二操作数的相应位进行比较。如果两个位均为1,则对应的结果位将设置为1。否则,将对应的结果位设置为0。
真值表如下:
a | b | a & b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
例如对12(1100
)和25(11001
)执行按位与操作,就是先对位数比较少的做高位补0,变成01100
,然后对01100
和11001
的每一位做“与”运算,即只有比较的两位都为1的时候,结果才为1,其他情况结果均为0。因此得到结果为01000
即为1000
,则这两个数按位“与”运算的结果为8。
1 | // 按位与 |
对于实际应用,嵌入式中会使用按位与运算将寄存器中的某一位清0,利用了某一未知位与0做与运算,得到的结果必定为0的原理;按位与运算也可以用来检查某一位是否为1,利用的原理是某一未知位与1做与运算,得到的结果为1时,原未知位必定为1,否则为0。
12. 按位“与或”(|
)
按位“与或”也被称为按位“或”。
按位“与或”运算符将其第一操作数的每个位与第二操作数的相应位进行比较。如果其中一个位是1,则将对应的结果位设置为1。否则,将对应的结果位设置为0。
真值表如下:
a | b | a | b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
例如对12(1100
)和25(11001
)执行按位与或操作,就是先对位数比较少的做高位补0,变成01100
,然后对01100
和11001
的每一位做“与或”运算,即只要比较的两位中有一位是1,结果就是1,反之为0。因此得到结果为11101
,则这两个数按位“与或”运算的结果为29。
1 | // 按位与或 |
对于实际应用,与按位与类似,嵌入式中会使用按位或运算将寄存器中的某一位置为1,利用了某一未知位与1做或运算,得到的结果必定为1的原理;按位或运算同样也可以用来检查某一位是否为1,利用的原理是某一未知位与0做或运算,得到的结果为1时,原未知位必定为1,否则为0。
13. 按位“异或”(^
)
按位“异或”运算符将其第一操作数的每个位与其第二操作数的相应位进行比较。如果一个位是0,另一个位是1,则相应的结果位将设置为1。否则,将对应的结果位设置为0。
简单来说,就是做异或运算的两位不同时,结果为1,否则结果为0。
真值表如下:
a | b | a ^ b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
例如对12(1100
)和25(11001
)执行按位异或操作,就是先对位数比较少的做高位补0,变成01100
,然后对01100
和11001
的每一位做“异或”运算,即比较的两位不相同时,结果为1,反之为0。因此得到结果为10101
,则这两个数按位“异或”运算的结果为21。
1 | // 按位异或 |
按位“异或”在实际工程中一般有三个作用:
翻转特定位:嵌入式中翻转寄存器中某一位的值利用了按位“异或”的特性,当一个未知为与1做异或运算时,如果这一未知位为1,异或的结果为0;如果这一未知位为0,则异或的结果为1,这就实现了对某一位的翻转。
交换两个数:根据按位“异或”的运算特性,如果要交换a和b两个数的值,可以按照下面的逻辑来操作:
1 | a = a ^ b; |
对两个数进行交换还有另外两种方法
第一种方法就是常规的创建中间变量
temp
:
1
2
3 temp = a;
a = b;
b = temp;第二种方法是利用加减法运算:
1
2
3 a = a + b;
b = a - b;
a = a - b;一般来说,最快的方法还是创建中间变量,因为另外两种方法虽然不需要占用额外的内存空间,但是需要有计算时间上的开销,创建中间变量的方法算是利用占用一点内存来节省计算花费的时间。
检测两个数的不同:对两个数进行异或操作,输出结果中为1的位的位置,即对应了两个数中不同的位的位置。
14. 按位取反(~
)
对一个数进行按位取反运算,就是将这个数中的每一位都做取反操作,真值表如下:
a | ~a |
---|---|
0 | 1 |
1 | 0 |
例如对12(0b00001100
)进行按位取反操作,结果为0b11110011
,即为-13(作为有符号类型解释时)。
1 | uint8_t a = 0b00001100; |
15. C逻辑运算符
&&
:如果两个操作数具有非零值,则逻辑“与”运算符产生值1。如果其中一个操作数等于0,则结果为0。如果逻辑“与”运算的第一个操作数等于0,则不会计算第二个操作数。
1 | bool conditionA = true; |
||
:逻辑“或”运算符对其操作数执行“与或”运算。如果两个操作数的值均为0,则结果为0。如果其中一个操作数具有非零值,则结果为1。如果逻辑“或”运算的第一个操作数具有非零值,则不会计算第二个操作数。
1 | bool conditionA = true; |
16. 复合赋值
复合赋值运算符将简单赋值运算符与另一个二元运算符相结合。复合赋值运算符执行其他运算符指定的运算,然后将结果赋给左操作数。
例如,一个复合赋值表达式,如
1 | expression1 += expression2 |
可以理解为
1 | expression1 = expression1 + expression2 |
但是,复合赋值表达式不等于扩展版本,因为复合赋值表达式只计算expression1
一次,而扩展版本将计算 expression1
两次:在加法运算和赋值运算中。
也就是说,复合赋值是在直接对原先数据做运算等操作,被称为原地修改(In-place Modification),而扩展版本是先进行运算,得到运算的结果后再将结果赋值给原数据,这种方式在需要直接修改原数据时需要进行两步操作,显然没有复合赋值的操作直接。此外,在一些对大型结构体中的某一元素进行运算时,复合赋值能做到直接修改,而先运算再赋值的方式可能会创建一个结构体副本,大大拖慢程序的性能。
因此,需要直接对原数据修改的情况一般都会使用复合赋值处理,可以提升性能,并提升代码的可读性。
复合赋值运算符的操作数必须为整型或浮点型。
每个复合赋值运算符都将执行对应的二元运算符所执行的转换并相应地限制其操作数的类型。
加法赋值(+=
)和减法赋值(-=
)运算符还可以具有指针类型的左操作数,在此情况下,右操作数必须为整型类型。
复合赋值运算的结果具有左操作数的值和类型。
以下是一些复合赋值运算的例子:
1 | uint8_t base_number = 8; |
17. 逗号运算符(,
)
使用逗号运算符是为了把几个表达式放在一起。
整个逗号表达式的值为系列中最后一个表达式的值。
从本质上讲,逗号的作用是将一系列运算按顺序执行。
例如下面的示例程序:
1 | uint8_t num1 = 1; |
这个程序中,括号里由逗号隔开的三条语句都会执行,且为顺序执行。当括号整体作为右值赋值给左值时,只会将这些表达式中的最后一个值赋值给左值,其他的值则直接丢弃。
逗号运算符在企业中要谨慎使用或不用,因为它的可读性较差,有要求必须要使用它时,一定要写清楚注释,解释运算的逻辑。
18. 计算的优先级和顺序
参考:计算的优先级和顺序 | Microsoft Learn
C语言运算符的优先级和结合性将影响表达式中操作数的分组和计算。仅当存在优先级较高或较低的其他运算符时,运算符的优先级才有意义。首先计算带优先级较高的运算符的表达式。也可以通过“绑定”一词描述优先级。优先级较高的运算符被认为具有更严格的绑定。
下表总结了C运算符的优先级和结合性(计算操作数的顺序),并按照从最高优先级到最低优先级的顺序将其列出。如果几个运算符一起出现,则其具有相同的优先级并且将根据其结合性对其进行计算。
C 运算符的优先级和关联性
符号 | 操作类型 | 结合性 |
---|---|---|
[ ] ( ) . -> ++ -- (后缀) | 表达式 | 从左到右 |
sizeof & * + - ~ ! ++ -- (前缀) | 一元 | 从右到左 |
typecasts | 一元 | 从右到左 |
* / % | 乘法 | 从左到右 |
+ - | 加法 | 从左到右 |
<< >> | 按位移动 | 从左到右 |
< > <= >= | 关系 | 从左到右 |
== != | 相等 | 从左到右 |
& | 按位“与” | 从左到右 |
^ | 按位“异或” | 从左到右 |
&& | 逻辑“与” | 从左到右 |
|| | 逻辑“或” | 从左到右 |
? : | 条件表达式 | 从右到左 |
= *= /= %= += -= <<= >>= &= ^= |= | 简单和复合赋值 | 从右到左 |
, | 顺序计算 | 从左到右 |
运算符按优先级的降序顺序列出。如果多个运算符出现在同一行或一个组中,则它们具有相同的优先级。
所有简单的和复合的赋值运算符都有相同的优先级。
表达式可以包含优先级相同的多个运算符。当多个具有相同级别的这类运算符出现在表达式中时,计算将根据该运算符的结合性按从右到左或从左至右的顺序来执行。计算的方向不影响在相同级别包括多个乘法(*
)、加法(+
)或二进制按位(&
、|
或^
)运算符的表达式的结果。语言未定义运算的顺序。如果编译器可以保证一致的结果,则编译器可以按任意顺序随意计算此类表达式。
只有顺序计算(,
)、逻辑”与“(&&
)、逻辑”或“(||
)、条件表达式(? :
)和函数调用运算符构成序列点,因此,确保对其操作数的计算采用特定顺序。
函数调用运算符是一组紧跟函数标识符的圆括号。确保顺序计算运算符(,
)按从左到右的顺序计算其操作数。
逻辑运算符还确保按从左至右的顺序计算操作数。但是,它们会计算确定表达式结果所需的最小数目的操作数。这称作“短路”计算。因此,无法计算表达式的一些操作数。例如,在下面的表达式中
1 | x && y++ |
仅当y++
为true(非零)时,才计算第二操作数(x
)。因此,如果y
为false(0),则x
不增加。
注意到表格中优先级为2和优先级为5的地方都出现了
+
-
号,它们的区别在于高优先级的加减号是一元运算符,代表一个数的正负,因此其优先级高于加减运算的二元运算符。同样的,
*
符号在一元运算符中代表解引用(Dereference Operator),在乘法中是二元运算符。
&
符号在一元运算符中代表取地址,在按位运算中代表按位“与”。
示例
以下列表显示编译器如何自动绑定多个示例表达式:
表达式 | 自动绑定 |
---|---|
a & b || c | (a & b) || c |
a = b || c | a = (b || c) |
q && r || s– | (q && r) || s– |
在第一个表达式中,按位“与”运算符(&
)的优先级高于逻辑“或”运算符(||
)的优先级,因此,a & b
构成了逻辑“或”运算的第一操作数。
在第二个表达式中,逻辑“或”运算符(||
)的优先级高于简单赋值运算符(=
)的优先级,因此,b || c
在赋值中分组为右操作数。请注意,赋给a
的值为0或1。
第三个表达式显示可能会生成意外结果的格式正确的表达式。逻辑“与”运算符(&&
)的优先级高于逻辑“或”运算符(||
)的优先级,因此,将q && r
分组为操作数。由于逻辑运算符确保按从左到右的顺序计算操作数,因此q && r
先于s--
被计算。但是,如果q && r
计算的结果为非零值,则不计算s--
,并且s
不会减少。如果s
未减少会导致程序出现问题,则s--
应显示为表达式的第一操作数,或者在单独的运算中应减少s
。
以下表达式是非法的并会在编译时生成诊断消息:
非法表达式 | 默认分组 |
---|---|
p == 0 ? p += 1 : p += 2 | (p == 0 ? p += 1 : p) += 2 |
在此表达式中,相等运算符(==
)的优先级最高,因此,将p == 0
分组为操作数。条件表达式运算符(? :
)具有下一个最高级别的优先级。其第一操作数是p == 0
,第二操作数是p += 1
。但是,条件表达式运算符的最后一个操作数被视为p
而不是p += 2
,因为与复合赋值运算符相比,p
的匹配项将更紧密地绑定到条件表达式运算符。由于+= 2
没有左操作数,因此发生语法错误。
应使用括号以防止此类错误发生并生成可读性更高的代码。例如,可以按如下所示使用括号来更正和阐明前面的示例:
1 | (p == 0) ? (p += 1) : (p += 2) |
同样的,在企业开发时应尽量避免使用此类可读性较低的代码。
19. 第三章结束语
自己上手敲代码,思考,练习!
本文链接: https://hanqingjiang.com/2025/06/16/20250616_C_operators/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
