操作符详解
C语言操作符详解
1. 算数操作符
算数操作符是用于执行基本算术运算的符号,包括+(加法)、-(减法)、*(乘法)、/(除法)、%(取模)。
(1)加法与减法
int a = 10 + 5; // 加法:15
int b = 10 - 5; // 减法:5
float c = 3.14f - 1.14f; // 浮点数减法:2.0
注意:
操作数类型需兼容(如int与float混合运算会触发类型转换)
int result = 10 + 3.5; // 错误:类型不匹配(需强制类型转换)
(2)乘法与除法
- 整数除法:当参与运算的两个操作数都是整数时,结果会向下取整,只保留整数部分,舍弃小数部分。
int mul = 5 * 3; // 乘法:15
int div_int = 10 / 3; // 整数除法:3(截断小数部分)
float div_float = 10.0f / 3.0f; // 浮点数除法:3.333...
- 浮点数除法:当参与运算的操作数中至少有一个是浮点数时,运算结果为浮点数,会保留小数部分。
-float c = 7.0 / 3; // 7.0是浮点数,运算结果为2.333...,c的值约为2.333
double d = 7 / 3.0; // 3.0是浮点数,运算结果为2.333...,d的值约为2.333
特殊情况:
int zero_div = 5 / 0; // 错误:除以零(运行时错误)
float zero_div_f = 5.0f / 0.0f; // 结果:inf(无穷大)
(3)取模运算(%)
取模操作符(%):其功能是计算两个整数相除后的余数,操作数必须都是整数,余数的符号与被除数的符号保持一致。
int mod = 10 % 3; // 取模:1(10除以3的余数)
int mod_neg = -10 % 3; // 结果:-1(取决于编译器,C标准未明确规定)
int e = 7 % 3; // 7除以3商2余1,e的值为1
int f = -7 % 3; // -7除以3商-2余-1,f的值为-1
int g = 7 % -3; // 7除以-3商-2余1,g的值为1
int h = -7 % -3; // -7除以-3商2余-1,h的值为-1
限制:
操作数必须为整数类型(如int、long)
float mod_float = 3.14 % 1.0; // 错误:不支持浮点数取模
2. 移位操作符
移位操作符专门针对整数的二进制位进行操作,操作数必须是整数,包括左移操作符(<<)和右移操作符(>>)。
在 C 语言中,算术右移操作是直接对补码形式的二进制数进行的
(1)整数的二进制表示
原码:直接将整数转换为二进制,最高位为符号位,0表示正数,1表示负数。例如,+5的原码是00000101(假设为8位二进制),-5的原码是10000101。
反码:正数的反码与原码相同;负数的反码是在原码的基础上,符号位不变,其余各位按位取反。例如,-5的原码是10000101,反码是11111010。
补码:正数的补码与原码相同;负数的补码是其反码加1。例如,-5的反码是11111010,补码是11111011。
整数在计算机内存中存储的是补码
,而当进行打印等操作时,会将补码转换为原码后再显示。
示例:以8位整数为例
数值:5
原码:0000 0101
反码:0000 0101
补码:0000 0101
数值:-5
原码:1000 0101
反码:1111 1010(原码除符号位取反)
补码:1111 1011(反码加 1)
(2)左移操作符(<<)
- 操作规则:将整数的二进制补码向左移动指定的位数,左边超出的最高位被丢弃,右边空缺的位用0补齐。
- 特点:左移操作不会改变原变量的值,其结果是一个新的值。
- 示例:
char a = 5; // 原码:00000101,反码:00000101,补码:00000101
char b = a << 1; // 将补码向左移动1位,得到1010,高位补0后为00001010(补码),转换为原码也是00001010,即b = 10
char c = -5; // 原码:10000101,反码:11111010,补码:11111011
char d = c << 1; // 补码左移1位后为11110110,转换为原码:先减1得反码11110101,再取反(符号位不变)得10001010,即d = -10
(3)右移操作符(>>)
int a = 20; // 二进制:0000 0000 0000 0000 0000 0000 0001 0100
int b = a >> 2; // 右移2位 → 0000 0000 0000 0000 0000 0000 0000 0101(十进制:5)
int c = -20; // 补码:1111 1111 1111 1111 1111 1111 1110 1100
int d = c >> 2; // 算数右移 → 1111 1111 1111 1111 1111 1111 1111 1011(补码)
// 补码转原码:1111 1011 → 减1得1111 1010 → 取反得1000 0101(原码)
// 原码对应的十进制值为 -5(错误!实际应为-6)
两种右移方式:
算数右移:右边丢弃,左边补原符号位(常见于编译器)
见编译器多采用这种方式,将整数的二进制补码向右移动指定的位数,右边超出的位被丢弃,左边空缺的位用原符号位补齐。
int e = -5; // 补码:1111 1111 1111 1111 1111 1111 1111 1011
int f = e >> 1; // 补码右移1位,左边补符号位1,得到补码:1111 1111 1111 1111 1111 1111 1111 1101
// 补码1111 1101转换为原码:取反加1(符号位不变)-> 1000 0011(原码)-> 对应十进制值-3
逻辑右移:右边丢弃,左边补 0
右边超出的位被丢弃,左边空缺的位用 0 补齐,这种移位方式相对少用。
注意事项:移位操作符不能移动负数位,因为这在 C 语言标准中是未定义的行为,不同编译器可能会有不同的处理结果。
警告:
int e = 10 >> -2; // 错误:移动负数位(标准未定义行为)
3. 位操作符
(1)按位与(&)
int a = 5; // 二进制:0000 0000 0000 0000 0000 0000 0000 0101
int b = 3; // 二进制:0000 0000 0000 0000 0000 0000 0000 0011
int c = a & b; // 结果:0000 0000 0000 0000 0000 0000 0000 0001(十进制:1)
规则:
对应位都为 1 时结果为 1,否则为 0
0000 0000 0000 0000 0000 0000 0000 0101
0000 0000 0000 0000 0000 0000 0000 0011
0000 0000 0000 0000 0000 0000 0000 0001
(2)按位或(|)
int a = 5; // 二进制:0000 0000 0000 0000 0000 0000 0000 0101
int b = 3; // 二进制:0000 0000 0000 0000 0000 0000 0000 0011
int c = a | b; // 结果:0000 0000 0000 0000 0000 0000 0000 0111(十进制:7)
规则:
对应位只要有一个为 1 结果为 1,否则为 0
0000 0000 0000 0000 0000 0000 0000 0101
0000 0000 0000 0000 0000 0000 0000 0011
0000 0000 0000 0000 0000 0000 0000 0111
(3)按位异或(^)
int a = 5; // 二进制:0000 0000 0000 0000 0000 0000 0000 0101
int b = 3; // 二进制:0000 0000 0000 0000 0000 0000 0000 0011
int c = a ^ b; // 结果:0000 0000 0000 0000 0000 0000 0000 0110(十进制:6)
规则:
对应位不同时结果为 1,相同为 0
0000 0000 0000 0000 0000 0000 0000 0101
0000 0000 0000 0000 0000 0000 0000 0011
0000 0000 0000 0000 0000 0000 0000 0110
特性:
a ^ a = 0
a ^ 0 = a
满足交换律:a ^ b = b ^ a
4. 赋值操作符
赋值操作符用于给变量赋值,需要注意初始化和赋值的区别。
- 初始化与赋值的差异:初始化是在变量创建的同时给其赋予初始值,而赋值是给已经存在的变量重新赋予新的值。
(1)简单赋值(=)
int a = 10; // 初始化
a = 20; // 赋值(覆盖原有值)
注意:
初始化是创建变量时赋值,赋值是修改变量值
int b; // 声明变量
b = 30; // 赋值(非初始化)
(2)复合赋值符
为了简化赋值操作,C 语言提供了多种复合赋值符,它们将算术运算和赋值操作结合在一起。常见的复合赋值符包括 +=、-=、*=、/=、%=、<<=、>>=、&=、|=、^= 等,其功能是先进行相应的算术运算,再将结果赋给左边的变量。
int a = 10;
a += 5; // 等价于:a = a + 5(结果:15)
a -= 3; // 等价于:a = a - 3(结果:12)
a *= 2; // 等价于:a = a * 2(结果:24)
a /= 4; // 等价于:a = a / 4(结果:6)
a %= 5; // 等价于:a = a % 5(结果:1)
a <<= 2; // 等价于:a = a << 2(结果:4)
a >>= 1; // 等价于:a = a >> 1(结果:2)
a &= 3; // 等价于:a = a & 3(结果:2)
a |= 1; // 等价于:a = a | 1(结果:3)
a ^= 2; // 等价于:a = a ^ 2(结果:1)
(3)连续赋值
int a, b, c;
a = b = c = 10; // 连续赋值(从右向左)
// 等价于:c = 10; b = c; a = b;
建议:避免复杂的连续赋值,提高代码可读性
// 不推荐
x = (y = 5) + (z = 10); // 难以理解
// 推荐
y = 5;
z = 10;
x = y + z;
5. 单目操作符
单目操作符只需要一个操作数,下面对各单目操作符进行详细介绍:
(1)sizeof操作符
sizeof:用于计算变量或数据类型所占用的内存空间大小,单位是字节。需要注意的是,sizeof 是操作符而不是函数,当计算类型的大小时,类型名需要用括号括起来;计算变量大小时,变量名可以加括号也可以不加。另外,类型在没有创建变量时不占用内存空间。
int a = 10;
printf("%lu\n", sizeof(a)); // 输出:4(int类型大小)
printf("%lu\n", sizeof(int)); // 输出:4(类型大小)
printf("%lu\n", sizeof a); // 合法:省略括号
// printf("%lu\n", sizeof int); // 错误:必须有括号
注意:
sizeof是操作符,不是函数
对数组使用sizeof返回整个数组大小
运行
int arr[5];
printf("%lu\n", sizeof(arr)); // 输出:20(5×4字节)
(2)取地址操作符(&)
int a = 10;
int *ptr = &a; // 获取变量a的地址
printf("%p\n", ptr); // 输出a的内存地址
注意:
不能对寄存器变量取地址(如register int x; &x;错误)
数组名在多数情况下隐式转换为首元素地址,但&数组名是整个数组的地址
int arr[5];
printf("%p\n", arr); // 首元素地址
printf("%p\n", &arr); // 整个数组地址(数值相同,但类型不同)
(3)解引用操作符(*)
解引用操作符,也称为间接访问操作符,用于通过指针访问其指向的变量。
int a = 10;
int *ptr = &a;
printf("%d\n", *ptr); // 解引用:10(访问ptr指向的值)
*ptr = 20; // 修改ptr指向的值(a变为20)
(4)自增自减操作符
int a = 10;
int b = ++a; // 前置自增:先加1再赋值(a=11,b=11)
int c = a++; // 后置自增:先赋值再加1(a=12,c=11)
int d = 5;
int e = --d; // 前置自减:先减1再赋值(d=4,e=4)
int f = d--; // 后置自减:先赋值再减1(d=3,f=4)
性能提示:
对于迭代器(如std::vector的迭代器),前置++效率更高(避免临时对象)
// 推荐(尤其对自定义类型)
for (auto it = vec.begin(); it != vec.end(); ++it) {
// ...
}
(5)正负号操作符
int a = 10;
int b = -a; // 负号:-10
int c = +a; // 正号:10(通常省略)
(6)逻辑非(!)
- !:逻辑非操作符,用于对操作数的逻辑值进行取反。如果操作数为非0(逻辑真),结果为0(逻辑假);如果操作数为0(逻辑假),结果为1(逻辑真)。
int a = 10; // 非零值表示真
printf("%d\n", !a); // 输出:0(逻辑非,真变假)
int b = 0; // 零值表示假
printf("%d\n", !b); // 输出:1(逻辑非,假变真)
(7)按位取反(~)
int a = 5; // 二进制:0000 0101
int b = ~a; // 二进制:1111 1010(补码,十进制:-6)
(8)强制类型转换((type))
float f = 3.14f;
int i = (int)f; // 强制转换为int(截断小数部分,结果:3)
int a = 10;
double d = (double)a / 3; // 转换为double以避免整数除法(结果:3.333...)
警告:
强制类型转换可能导致数据丢失(如浮点数转整数)
错误的类型转换可能导致未定义行为
int *ptr = (int*)0x12345678; // 危险:将任意地址转换为指针
6. 关系操作符
关系操作符用于比较两个操作数之间的关系,其运算结果是逻辑值,即1(表示真)或0(表示假)。常见的关系操作符包括
1.>:大于,判断左边的操作数是否大于右边的操作数。
2. >=:大于等于,判断左边的操作数是否大于或等于右边的操作数。
3. <:小于,判断左边的操作数是否小于右边的操作数。
4. <=:小于等于,判断左边的操作数是否小于或等于右边的操作数。
5. ==:等于,判断两个操作数是否相等。
6. !=:不等于,判断两个操作数是否不相等。
示例:
int a = 5, b = 3;
int c = a > b; // a大于b,结果为真,c = 1
int d = a >= 5; // a等于5,满足大于等于,d = 1
int e = a < b; // a不小于b,结果为假,e = 0
int f = b <= 3; // b等于3,满足小于等于,f = 1
int g = a == b; // a不等于b,g = 0
int h = a != b; // a不等于b,h = 1
(1)基本比较
int a = 10, b = 5;
printf("%d\n", a > b); // 大于:1(真)
printf("%d\n", a < b); // 小于:0(假)
printf("%d\n", a >= b); // 大于等于:1(真)
printf("%d\n", a <= b); // 小于等于:0(假)
printf("%d\n", a == b); // 等于:0(假)
printf("%d\n", a != b); // 不等于:1(真)
(2)浮点比较陷阱
float x = 0.1f * 3; // 理论值:0.3
float y = 0.3f;
printf("%d\n", x == y); // 可能输出0(假),因为浮点数精度问题
// 正确比较方式
printf("%d\n", fabs(x - y) < 1e-6); // 使用误差范围比较
(3)指针比较
int a = 10, b = 20;
int *p1 = &a, *p2 = &b;
printf("%d\n", p1 > p2); // 比较指针地址(结果取决于内存布局)
需要注意的是,在判断两个变量是否相等时,要使用 == 操作符,而不能使用 =(赋值操作符),否则会导致逻辑错误。
7. 逻辑操作符
(1)逻辑与(&&)
int a = 5, b = 0;
printf("%d\n", a && b); // 输出:0(假,因为b为0)
// 短路特性
int x = 0, y = 10;
if (x == 0 && ++y) { // 左边为假,右边不执行
printf("不会执行\n");
}
printf("y=%d\n", y); // 输出:y=10(y未自增)
(2)逻辑或(||)
int a = 5, b = 0;
printf("%d\n", a || b); // 输出:1(真,因为a非0)
// 短路特性
int x = 1, y = 10;
if (x == 1 || ++y) { // 左边为真,右边不执行
printf("会执行\n");
}
printf("y=%d\n", y); // 输出:y=10(y未自增)
(3)逻辑非(!)
int a = 10;
printf("%d\n", !a); // 输出:0(假,因为a非0)
int b = 0;
printf("%d\n", !b); // 输出:1(真,因为b为0)
(4)逻辑与 vs 按位与
int a = 5; // 二进制:0000 0000 0000 0000 0000 0000 0000 0101
int b = 3; // 二进制:0000 0000 0000 0000 0000 0000 0000 0011
printf("%d\n", a && b); // 逻辑与:1(真,因为a和b都非0)
printf("%d\n", a & b); // 按位与:1(二进制:0000 0000 0000 0000 0000 0000 0000 0001)
(5)逻辑或 vs 按位或
int a = 5; // 二进制:0000 0000 0000 0000 0000 0000 0000 0101
int b = 0; // 二进制:0000 0000 0000 0000 0000 0000 0000 0000
printf("%d\n", a || b); // 逻辑或:1(真,因为a非0)
printf("%d\n", a | b); // 按位或:5(二进制:0000 0000 0000 0000 0000 0000 0000 0101)
8. 三目操作符(条件表达式)
三目操作符也称为条件操作符,其语法形式为:表达式1 ? 表达式2 : 表达式3。
- 运算规则:首先计算表达式1的值,如果表达式1的值为真(非0),则整个三目表达式的结果为表达式2的值;如果表达式1的值为假(0),则结果为表达式3的值。
- 特点:三目操作符可以简化简单的条件判断语句,使代码更简洁,但如果表达式2和表达式3过于复杂,会降低代码可读性。
int a = 10, b = 5;
int max = (a > b) ? a : b; // 如果a>b为真,max=a;否则max=b
printf("max=%d\n", max); // 输出:max=10
// 嵌套使用
int x = 10, y = 20, z = 30;
int largest = (x > y) ? ((x > z) ? x : z) : ((y > z) ? y : z);
printf("largest=%d\n", largest); // 输出:largest=30
注意:
三目表达式会返回一个值,可以用于赋值或函数参数
int result = (a > b) ? a * 2 : b * 3; // 赋值
printf("%d\n", (a > b) ? a : b); // 作为函数参数
9. 逗号表达式
逗号表达式是由逗号分隔的多个表达式组成的表达式,其语法形式为:表达式1, 表达式2, …, 表达式n。
- 运算规则:逗号表达式会从左到右依次执行每个表达式,整个表达式的最终结果是最后一个表达式的值。
- 应用场景:
- 在for循环的初始化部分和更新部分,可以使用逗号表达式同时处理多个变量的初始化和更新操作。
- 在需要执行多个表达式并以最后一个表达式的结果作为返回值的场景中使用。
int a = 1, b = 2, c;
c = (a++, b++, a + b); // 逗号表达式:先执行a++,再执行b++,最后计算a+b
printf("a=%d, b=%d, c=%d\n", a, b, c); // 输出:a=2, b=3, c=5
// 常见用途:在for循环中初始化多个变量
for (int i = 0, j = 10; i < j; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}
特性:
从左到右依次执行每个表达式
整个表达式的值是最后一个表达式的结果
int x = (printf("A"), printf("B"), 100); // 输出:AB,x=100
10. 下标引用与成员访问操作符
下标引用操作符[]是一个双目操作符,它有两个操作数,分别是数组名和下标值,并且支持交换律。
- 等价关系:数组元素的访问形式arr[7]与*(arr + 7)是等价的。其中,arr表示数组的首地址,arr + 7表示将首地址向后移动7个元素的位置,指向数组的第8个元素(数组下标从0开始),然后通过解引用操作符*获取该元素的值。
- 交换律体现:由于加法具有交换律,所以arr[7]也可以写成7[arr],两者是完全等价的。
(1)下标引用([])
int arr[5] = {1, 2, 3, 4, 5};
printf("%d\n", arr[2]); // 输出:3(访问第3个元素)
// 等价于
printf("%d\n", *(arr + 2)); // 指针偏移访问
// 支持交换律(但不推荐)
printf("%d\n", 2[arr]); // 等价于arr[2],因为2[arr] → *(2 + arr)
(2)结构体成员访问(.)
struct Point {
int x;
int y;
};
struct Point p = {10, 20};
printf("x=%d, y=%d\n", p.x, p.y); // 输出:x=10, y=20
plaintext
(3)结构体指针成员访问(->)
struct Point p = {10, 20};
struct Point *ptr = &p;
printf("x=%d, y=%d\n", ptr->x, ptr->y); // 输出:x=10, y=20
// 等价于
printf("x=%d, y=%d\n", (*ptr).x, (*ptr).y); // 解引用后用.访问
11. 表达式求值
表达式求值的顺序受到多种因素影响,其中操作符的优先级和结合性是重要的因素,同时还涉及到隐式类型转换等内容。
(1)隐式类型转换
① 整形提升
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
规则:小于
int
的类型(如char
、short
)在运算时会自动提升为int
目的:提高运算精度,避免截断错误
整型提升的意义:表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。
示例:
char a = 3; // 8位:0000 0011
char b = 127; // 8位:0111 1111
char c = a + b; // 运算前a和b提升为int:0000 0000 0000 0011 和 0000 0000 0111 1111
// 相加结果:0000 0000 1000 0010(130)
// 赋值给c时截断为8位:1000 0010(补码,十进制:-126)
printf("%d\n", c); // 输出:-126
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
整形提升的例子:
//实例1
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if(a==0xb6)
printf("a");
if(b==0xb600)
printf("b");
if(c==0xb6000000)
printf("c");
return 0;
}
实例1中的a,b要进行整形提升,但是c不需要整形提升
a,b整形提升之后,变成了负数,所以表达式 a == 0xb6 , b == 0xb600 的结果是假,但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真.
所程序输出的结果是:
输出 c
//实例2
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
实例2中的,c只要参与表达式运算,就会发生整形提升,表达式+c
,就会发生提升,所以 sizeof(+c)
是4个字
节.
表达式 -c 也会发生整形提升,所以 sizeof(-c)
是4个字节,但是sizeof(c)
,就是1个字节.
② 算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
- 规则:不同类型运算时,低精度类型自动转换为高精度类型
- 转换顺序:
char
→short
→int
→unsigned int
→long
→unsigned long
→float
→double
→long double
示例:
int a = 10;
double b = 3.14;
double result = a + b; // a先转换为double(10.0),再与b相加
- 为什么需要算数转换:不同类型的整数或浮点数在内存中的表示方式和精度不同,进行运算时需要统一类型,以避免精度损失或运算错误。
- 算数转换的意义: 确保了不同类型数据之间运算的准确性,使得运算结果能够尽可能地保留原始数据的信息。
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
警告:
但是算术转换要合理,要不然会有一些潜在的问题
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
12. 操作符的属性
复杂表达式的求值结果受到三个因素的影响,
- 分别是操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
- 操作符的优先级:优先级决定了在表达式中多个操作符同时存在时,哪个操作符先进行运算。优先级高的操作符先执行。
- 示例:
int a = 3 + 5 * 2; // *的优先级高于+,先计算5 * 2 = 10,再计算3 + 10 = 13,所以a = 13
int b = (3 + 5) * 2; // 括号的优先级最高,先计算3 + 5 = 8,再计算8 * 2 = 16,所以b = 16
- 是否控制求值顺序:有些操作符有明确的求值顺序,而有些操作符则没有。例如,逻辑与(&&)、逻辑或(||)、逗号表达式等有明确的求值顺序,而像 +、-、*、/ 等操作符则没有明确规定操作数的求值顺序。
- 示例:
int h = 0, i = 5;
int j = h++ && ++i; // 逻辑与有明确求值顺序,先算h++(h变为1,值为0),左边为假,右边++i不执行,j = 0
int k = (h = 3, i = 4, h + i); // 逗号表达式有明确求值顺序,从左到右执行,k = 7
int m = f() + g(); // +没有明确规定f()和g()的求值顺序,可能先调用f()再调用g(),也可能先调用g()再调用f()
两个相邻的操作符先执行哪个,取决于它们的优先级。如果两者的优先级相同,那么就取决于它们的结合性。例如,在表达式 a + b * c 中,的优先级高于 +,所以先执行 b * c;在表达式 a /b * c 中,/ 和优先级相同,且都是左结合,所以先执行 a /b,再执行与 c 的乘法。
① 优先级表
操作符 | 描述 | 用法示例 | 结果类型 | 结合性(L-R就是从左往右) | 是否控制求值顺序 |
---|---|---|---|---|---|
() | 聚组 | (表达式) | 与表达式同 | N/A | 否 |
() | 函数调用 | rexp(rexp,…,rexp) | rexp | L-R | 否 |
[ ] | 下标引用 | rexp[rexp] | lexp | L-R | 否 |
. | 访问结构成员 | lexp.member_name | lexp | L-R | 否 |
-> | 访问结构指针成员 | rexp->member_name | lexp | L-R | 否 |
++ | 后缀自增 | lexp ++ | rexp | L-R | 否 |
– | 后缀自减 | lexp – | rexp | L-R | 否 |
! | 逻辑反 | ! rexp | rexp | R-L | 否 |
~ | 按位取反 | ~ rexp | rexp | R-L | 否 |
+ | 单目,表示正值 | + rexp | rexp | R-L | 否 |
- | 单目,表示负值 | - rexp | rexp | R-L | 否 |
++ | 前缀自增 | ++ lexp | rexp | R-L | 否 |
– | 前缀自减 | – lexp | rexp | R-L | 否 |
* | 间接访问 | * rexp | lexp | R-L | 否 |
& | 取地址 | & lexp | rexp | R-L | 否 |
sizeof | 取其长度,以字节表示 | sizeof rexp、sizeof(类型) | rexp | R-L | 否 |
(类型) | 类型转换 | (类型) rexp | rexp | R-L | 否 |
* | 乘法 | rexp * rexp | rexp | L-R | 否 |
/ | 除法 | rexp / rexp | rexp | L-R | 否 |
% | 整数取余 | rexp % rexp | rexp | L-R | 否 |
+ | 加法 | rexp + rexp | rexp | L-R | 否 |
- | 减法 | rexp - rexp | rexp | L-R | 否 |
<< | 左移位 | rexp << rexp | rexp | L-R | 否 |
>> | 右移位 | rexp >> rexp | rexp | L-R | 否 |
> | 大于 | rexp > rexp | rexp | L-R | 否 |
>= | 大于等于 | rexp >= rexp | rexp | L-R | 否 |
< | 小于 | rexp < rexp | rexp | L-R | 否 |
<= | 小于等于 | rexp <= rexp | rexp | L-R | 否 |
== | 等于 | rexp == rexp | rexp | L-R | 否 |
!= | 不等于 | rexp != rexp | rexp | L-R | 否 |
& | 位与 | rexp & rexp | rexp | L-R | 否 |
^ | 位异或 | rexp ^ rexp | rexp | L-R | 否 |
位或 | rexp | rexp | rexp | ||
&& | 逻辑与 | rexp && rexp | rexp | L-R | 是 |
逻辑或 | rexp | ||||
? : | 条件操作符 | rexp ? rexp : rexp | rexp | N/A | 是 |
= | 赋值 | lexp = rexp | rexp | R-L | 否 |
+= | 以…加 | lexp += rexp | rexp | R-L | 否 |
-= | 以…减 | lexp -= rexp | rexp | R-L | 否 |
*= | 以…乘 | lexp *= rexp | rexp | R-L | 否 |
/= | 以…除 | lexp /= rexp | rexp | R-L | 否 |
%= | 以…取模 | lexp %= rexp | rexp | R-L | 否 |
<<= | 以…左移 | lexp <<= rexp | rexp | R-L | 否 |
>>= | 以…右移 | lexp >>= rexp | rexp | R-L | 否 |
&= | 以…与 | lexp &= rexp | rexp | R-L | 否 |
^= | 以…异或 | lexp ^= rexp | rexp | R-L | 否 |
= | 以…或 | lexp | = rexp | rexp | |
, | 逗号 | rexp,rexp | rexp | L-R | 是 |
② 示例分析
例子1:
int a = 3, b = 5, c = 7;
int result = a + b * c; // 先乘后加:3 + (5*7) = 38
int x = a * b / c; // 乘除优先级相同,从左到右:(3*5)/7 = 2
int y = a << b + c; // 先加后移位:3 << (5+7) = 3 << 12 = 12288
(3)副作用与求值顺序
int a = 1;
int b = (a++) + (a++); // 未定义行为!不同编译器结果可能不同
// 可能的计算方式:
// 1. 先计算第一个a++(值为1),再计算第二个a++(值为2),结果:1+2=3
// 2. 先计算两个a++(值都为1),再相加,结果:1+1=2
注意:
避免在同一表达式中多次修改同一变量
使用明确的顺序(如拆分成多个语句)
// 正确写法
int a = 1;
int x = a++; // x=1, a=2
int y = a++; // y=2, a=3
int result = x + y; // result=3
例子2:
//表达式的求值部分由操作符的优先级决定。
a*b + c*d + e*f
在计算的时候,由于*比+的优先级高,只能保证,的计算是比+早,但是优先级并不
能决定第三个比第一个+早执行。
所以表达式的计算机顺序就可能是:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
或者:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
例子3:
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
这个代码有没有实际的问题?
有问题!
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,
再算减法。
函数的调用先后顺序无法通过操作符的优先级确定。
13. 一句话总结
操作符类型 | 关键点 | 注意事项/陷阱 |
---|---|---|
算数操作符 | 整数除法截断小数,取模操作数必须为整数 | 避免除以零,注意负数取模的结果 |
移位操作符 | 左移补0,右移分算数/逻辑,操作的是补码 | 不要移动负数位(未定义行为) |
位操作符 | 按二进制位运算(&|^),异或满足交换律和自反性 | 注意与逻辑操作符的区别 |
赋值操作符 | 支持复合赋值(+=、-=等),连续赋值从右向左 | 避免复杂的连续赋值,降低可读性 |
单目操作符 | sizeof返回类型或变量大小,++/–分前置后置 | 前置++返回自增后的值,后置返回原值 |
关系操作符 | 比较结果为0或1,浮点比较需考虑精度问题 | 避免直接用==比较浮点数 |
逻辑操作符 | &&和||有短路特性,注意与位操作符的区别 | 利用短路特性避免无效计算 |
三目操作符 | 简洁的条件表达式,返回值可直接使用 | 避免嵌套过深,影响可读性 |
逗号表达式 | 从左到右执行,结果为最后一个表达式的值 | 常用于for循环初始化多个变量 |
下标与成员访问 | arr[i]等价于*(arr+i),->是指针访问成员的简写 | 结构体指针必须用->访问成员 |
表达式求值 | 受优先级、结合性和类型转换影响,注意整形提升和算数转换 | 避免在同一表达式中多次修改同一变量 |