引言
操作符是C语言的灵魂,它们决定了数据的计算方式、逻辑判断和内存操作。理解操作符的优先级、结合性和使用规则,是写出正确、高效代码的基础。
C语言拥有丰富的操作符,包括算术操作符、移位操作符、位操作符、赋值操作符、逻辑操作符、条件操作符等。今天,我将从底层视角,全面讲解各类操作符的用法、注意事项以及常见陷阱。
第一部分:算术操作符
一、基本算术操作符
| 操作符 | 名称 | 示例 | 说明 |
|---|---|---|---|
+ |
加法 | a + b |
两数相加 |
- |
减法 | a - b |
两数相减 |
* |
乘法 | a * b |
两数相乘 |
/ |
除法 | a / b |
两数相除(整数除法截断) |
% |
取模 | a % b |
取余数,操作数必须为整数 |
二、注意事项
cpp
#include <stdio.h>
int main() {
// 整数除法:结果截断,不四舍五入
int a = 10 / 3; // 3,不是3.333
int b = 10 / 4; // 2
// 浮点数除法:至少有一个操作数为浮点数
double c = 10.0 / 3; // 3.333333
double d = 10 / 3.0; // 3.333333
// 取模操作符:操作数必须为整数
int e = 10 % 3; // 1
// double f = 10.0 % 3; // 错误!取模不能用于浮点数
// 负数取模:结果符号与被除数相同
int g = -10 % 3; // -1
int h = 10 % -3; // 1
int i = -10 % -3; // -1
printf("10 / 3 = %d\n", a);
printf("10.0 / 3 = %lf\n", c);
printf("-10 %% 3 = %d\n", g);
return 0;
}
第二部分:移位操作符
一、左移(<<)和右移(>>)
移位操作符只针对整数类型(整型家族)。
| 操作符 | 名称 | 示例 | 说明 |
|---|---|---|---|
<< |
左移 | a << n |
高位丢弃,低位补0 |
>> |
右移 | a >> n |
低位丢弃,高位补符号位(算术右移)或0(逻辑右移) |
cpp
#include <stdio.h>
int main() {
// 左移:相当于乘以2的n次方
int a = 15; // 00000000 00000000 00000000 00001111
a <<= 7; // 00000000 00000000 00000111 10000000 = 1920
printf("15 << 7 = %d\n", a); // 1920
// 右移:相当于除以2的n次方(向下取整)
int b = 320; // 00000000 00000000 00000001 01000000
b >>= 4; // 00000000 00000000 00000000 00010100 = 20
printf("320 >> 4 = %d\n", b); // 20
// 负数右移(算术右移,高位补1)
int c = -10; // 11111111 11111111 11111111 11110110
c >>= 2; // 11111111 11111111 11111111 11111101 = -3
printf("-10 >> 2 = %d\n", c); // -3
return 0;
}
二、移位操作的注意事项
cpp
int main() {
int a = 10;
// 1. 移位位数不能大于等于类型位数(未定义行为)
// a << 32; // 在32位系统中是未定义行为
// 2. 不能移位负数位
// a << -1; // 未定义行为
// 3. 移位不会改变原变量的值(除非使用赋值操作符)
int b = 10;
int c = b << 2; // b不变,c=40
b <<= 2; // b变为40
return 0;
}
第三部分:位操作符
一、位操作符分类
| 操作符 | 名称 | 示例 | 说明 |
|---|---|---|---|
& |
按位与 | a & b |
两个都为1,结果为1 |
| ` | ` | 按位或 | `a |
^ |
按位异或 | a ^ b |
相同为0,不同为1 |
~ |
按位取反 | ~a |
0变1,1变0(单目操作符) |
二、按位与(&)
cpp
#include <stdio.h>
int main() {
// 判断奇偶数:与1按位与,结果为1是奇数,0是偶数
int a = 10; // 1010
int b = 7; // 0111
if (a & 1) {
printf("%d 是奇数\n", a); // 不执行
} else {
printf("%d 是偶数\n", a); // 执行
}
// 将某一位清零(与0按位与)
int c = 0xFF; // 11111111
int d = c & 0xFE; // 11111110,最低位清零
printf("0xFF & 0xFE = 0x%X\n", d); // 0xFE
return 0;
}
三、按位或(|)
cpp
int main() {
int a = 0b00001000; // 第3位为1
int b = 0b00000100; // 第2位为1
// 将某位置为1(与1按位或)
int c = a | b; // 0b00001100,第2位和第3位都为1
// 将第4位置为1
int d = 0; // 00000000
d |= (1 << 4); // 00010000
printf("d = %d\n", d); // 16
return 0;
}
四、按位异或(^)
cpp
int main() {
// 异或的特性:
// a ^ a = 0
// a ^ 0 = a
// a ^ b ^ b = a
int a = 5; // 0101
int b = 3; // 0011
int c = a ^ b; // 0110 = 6
// 加密解密(相同密钥异或两次恢复原值)
int plain = 123;
int key = 456;
int cipher = plain ^ key; // 加密
int decrypted = cipher ^ key; // 解密
printf("加密后:%d,解密后:%d\n", cipher, decrypted);
return 0;
}
五、按位取反(~)
cpp
int main() {
int a = 0; // 00000000 00000000 00000000 00000000
int b = ~a; // 11111111 11111111 11111111 11111111 = -1
int c = -1; // 11111111 11111111 11111111 11111111
int d = ~c; // 00000000 00000000 00000000 00000000 = 0
printf("~0 = %d\n", b); // -1
printf("~(-1) = %d\n", d); // 0
return 0;
}
六、异或实现交换(无临时变量)
cpp
#include <stdio.h>
// 方法1:使用临时变量(推荐,易懂)
void swap1(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 方法2:使用加减法(可能溢出)
void swap2(int* a, int* b) {
*a = *a + *b;
*b = *a - *b;
*a = *a - *b;
}
// 方法3:使用异或(无溢出风险,但只适用于整数)
void swap3(int* a, int* b) {
// a=5(101), b=3(011)
*a = *a ^ *b; // a = 101 ^ 011 = 110 (6)
*b = *a ^ *b; // b = 110 ^ 011 = 101 (5)
*a = *a ^ *b; // a = 110 ^ 101 = 011 (3)
}
int main() {
int x = 5, y = 3;
printf("交换前:x=%d, y=%d\n", x, y);
swap3(&x, &y);
printf("交换后:x=%d, y=%d\n", x, y);
return 0;
}
第四部分:逻辑操作符
一、逻辑与(&&)和逻辑或(||)
| 操作符 | 名称 | 示例 | 说明 |
|---|---|---|---|
&& |
逻辑与 | a && b |
两边都为真,结果为真 |
| ` | ` | 逻辑或 | |
! |
逻辑非 | !a |
真变假,假变真 |
二、短路求值(重要!)
cpp
#include <stdio.h>
int main() {
int a = 0, b = 2, c = 3, d = 4;
int i;
// 逻辑与短路:左边为假,右边不执行
i = a++ && ++b && d++;
// a++ = 0(假),整个表达式为假,++b和d++不执行
printf("i=%d, a=%d, b=%d, d=%d\n", i, a, b, d);
// 输出:i=0, a=1, b=2, d=4
// 重置变量
a = 0, b = 2, c = 3, d = 4;
// 逻辑或短路:左边为真,右边不执行
i = a++ || ++b || d++;
// a++ = 0(假),继续执行++b = 3(真),整个表达式为真,d++不执行
printf("i=%d, a=%d, b=%d, d=%d\n", i, a, b, d);
// 输出:i=1, a=1, b=3, d=4
printf("0 && 9 = %d\n", 0 && 9); // 0
printf("0 || 9 = %d\n", 0 || 9); // 1
return 0;
}
第五部分:赋值操作符
一、复合赋值操作符
| 操作符 | 示例 | 等价于 |
|---|---|---|
+= |
a += b |
a = a + b |
-= |
a -= b |
a = a - b |
*= |
a *= b |
a = a * b |
/= |
a /= b |
a = a / b |
%= |
a %= b |
a = a % b |
<<= |
a <<= n |
a = a << n |
>>= |
a >>= n |
a = a >> n |
&= |
a &= b |
a = a & b |
| ` | =` | `a |
^= |
a ^= b |
a = a ^ b |
二、自增自减操作符
cpp
#include <stdio.h>
int main() {
int a = 10;
int b, c;
// 后置++:先使用,后自增
b = a++; // b = 10, a = 11
printf("a=%d, b=%d\n", a, b); // a=11, b=10
// 前置++:先自增,后使用
a = 10;
c = ++a; // a = 11, c = 11
printf("a=%d, c=%d\n", a, c); // a=11, c=11
// 表达式中的副作用
int p = 0;
int l = p++; // l = 0, p = 1
printf("l=%d, p=%d\n", l, p); // l=0, p=1
return 0;
}
第六部分:关系操作符
| 操作符 | 名称 | 示例 |
|---|---|---|
> |
大于 | a > b |
>= |
大于等于 | a >= b |
< |
小于 | a < b |
<= |
小于等于 | a <= b |
== |
等于 | a == b |
!= |
不等于 | a != b |
cpp
int main() {
int a = 10, b = 20;
// 关系表达式的结果是整数:真为1,假为0
int r1 = a > b; // 0
int r2 = a < b; // 1
int r3 = a == b; // 0
printf("10 > 20 = %d\n", r1);
printf("10 < 20 = %d\n", r2);
return 0;
}
第七部分:条件操作符(三目运算符)
一、语法与使用
cpp
// 语法:表达式1 ? 表达式2 : 表达式3
// 如果表达式1为真,返回表达式2,否则返回表达式3
#include <stdio.h>
int main() {
int a = 10, b = 20;
// 求最大值
int max = (a > b) ? a : b;
printf("max = %d\n", max); // 20
// 求绝对值
int x = -5;
int abs_x = (x >= 0) ? x : -x;
printf("|%d| = %d\n", x, abs_x); // 5
return 0;
}
二、嵌套使用
cpp
int main() {
int score = 85;
// 成绩等级判断
char grade = (score >= 90) ? 'A' :
(score >= 80) ? 'B' :
(score >= 70) ? 'C' :
(score >= 60) ? 'D' : 'F';
printf("成绩:%d,等级:%c\n", score, grade); // B
return 0;
}
第八部分:逗号操作符
一、语法与使用
逗号操作符从左到右依次执行所有表达式,返回最后一个表达式的值。
cpp
#include <stdio.h>
int main() {
int a, b, c;
// 逗号操作符示例
a = (1, 2, 3, 4, 5); // a = 5
printf("a = %d\n", a);
// 在循环中使用
for (int i = 0, j = 10; i < j; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}
return 0;
}
第九部分:sizeof 操作符
一、sizeof 的特点
sizeof 既是操作符也是关键字,在编译时计算类型或变量占用的字节数。
cpp
#include <stdio.h>
int main() {
int a = 10;
int arr[10];
// sizeof 的三种用法
printf("sizeof(a) = %zu\n", sizeof(a)); // 4(使用变量)
printf("sizeof(int) = %zu\n", sizeof(int)); // 4(使用类型)
printf("sizeof a = %zu\n", sizeof a); // 4(省略括号)
// 错误写法
// printf("%zu\n", sizeof int); // 错误!类型不能省略括号
// 计算数组元素个数
int len = sizeof(arr) / sizeof(arr[0]);
printf("数组长度:%d\n", len);
return 0;
}
第十部分:类型转换
一、隐式类型转换(算术转换)
cpp
int main() {
// 算术转换顺序(下转上)
// long double ← double ← float ← unsigned long ← long ← unsigned int ← int
int a = 10;
double b = 3.14;
double c = a + b; // a被自动转换为double
// 截断:赋值时大类型转小类型
int d = 3.14; // d = 3,小数部分被截断
return 0;
}
二、整型提升
cpp
int main() {
char a = 0b10111111; // 0xBF
char b = 0b10111111; // 0xBF
// 运算时会发生整型提升:char → int
int c = a + b; // 0xBF + 0xBF = 0x17E
printf("%d\n", c); // 382
return 0;
}
三、强制类型转换
cpp
int main() {
int a = 10, b = 3;
// 整数除法
double c = a / b; // 3.0
double d = (double)a / b; // 3.33333
double e = a / (double)b; // 3.33333
printf("c = %lf\n", c);
printf("d = %lf\n", d);
return 0;
}
第十一部分:操作符优先级与结合性
一、优先级表(从高到低)
| 优先级 | 操作符 | 结合性 |
|---|---|---|
| 1 | () [] . -> |
从左到右 |
| 2 | ++ -- + - ! ~ * & sizeof (单目) |
从右到左 |
| 3 | * / % |
从左到右 |
| 4 | + - |
从左到右 |
| 5 | << >> |
从左到右 |
| 6 | < <= > >= |
从左到右 |
| 7 | == != |
从左到右 |
| 8 | & (按位与) |
从左到右 |
| 9 | ^ (按位异或) |
从左到右 |
| 10 | ` | ` (按位或) |
| 11 | && (逻辑与) |
从左到右 |
| 12 | ` | |
| 13 | ?: (条件) |
从右到左 |
| 14 | = += -= 等 (赋值) |
从右到左 |
| 15 | , (逗号) |
从左到右 |
二、常见优先级陷阱
cpp
#include <stdio.h>
int main() {
int a = 10, b = 20;
// 陷阱1:== 优先级高于 & 和 |
if (a & b == 0) { // 实际:a & (b == 0),不是 (a & b) == 0
// ...
}
// 正确写法
if ((a & b) == 0) {
// ...
}
// 陷阱2:移位优先级低于加减
int c = a + b << 2; // 实际:(a + b) << 2
printf("c = %d\n", c);
// 陷阱3:逻辑操作符优先级低于关系操作符
if (a > 0 && b > 0) { // 正确:实际上就是 (a > 0) && (b > 0)
printf("a和b都为正数\n");
}
return 0;
}
第十二部分:常见关键字说明
一、auto
cpp
// auto 用于自动类型推导(C++11),C语言中很少使用
auto int a = 10; // 等价于 int a = 10(默认就是auto)
二、static
cpp
#include <stdio.h>
// 1. 修饰局部变量:延长生命周期至整个程序
void test_static() {
static int a = 1; // 只初始化一次
a++;
printf("%d\n", a);
}
// 2. 修饰全局变量:限制作用域为本文件
// static int global_var = 100; // 其他文件无法访问
// 3. 修饰函数:限制作用域为本文件
static void helper() {
// 只能在本文件内调用
}
int main() {
test_static(); // 2
test_static(); // 3
test_static(); // 4
return 0;
}
三、const
cpp
int main() {
const int a = 10; // a是只读变量,不能修改
// a = 20; // 错误!
// const修饰指针
int x = 10, y = 20;
const int* p1 = &x; // 指向常量的指针:不能通过p1修改指向的值
int* const p2 = &x; // 常量指针:p2的指向不能改变
const int* const p3 = &x; // 既不能修改值,也不能修改指向
p1 = &y; // 可以修改指向
*p2 = 30; // 可以修改值
// p2 = &y; // 错误!
return 0;
}
四、extern
cpp
// file1.c
int global_var = 100;
// file2.c
extern int global_var; // 声明外部变量
int main() {
printf("%d\n", global_var); // 100
return 0;
}
五、volatile
cpp
// volatile 告诉编译器不要优化这个变量,每次直接从内存中读取
volatile int flag = 1;
// 常用于多线程编程或硬件寄存器访问
六、register
cpp
// register 建议编译器将变量存储在寄存器中(现代编译器自动优化,很少使用)
register int counter = 0;
// 不能对register变量取地址
// int* p = &counter; // 错误!
第十三部分:枚举常量(enum)
cpp
#include <stdio.h>
enum Color {
RED, // 默认0
GREEN, // 1
BLUE // 2
};
enum Weekday {
MON = 1,
TUE, // 2
WED, // 3
THU, // 4
FRI, // 5
SAT, // 6
SUN // 7
};
int main() {
enum Color c = RED;
printf("RED = %d\n", RED); // 0
printf("GREEN = %d\n", GREEN); // 1
printf("BLUE = %d\n", BLUE); // 2
printf("MON = %d\n", MON); // 1
printf("SUN = %d\n", SUN); // 7
return 0;
}
第十四部分:union(共用体)
一、基本用法
cpp
#include <stdio.h>
union Data {
int i;
char c;
float f;
};
int main() {
union Data d;
printf("union大小:%zu\n", sizeof(d)); // 4(最大成员的大小)
d.i = 65;
printf("d.i = %d\n", d.i); // 65
printf("d.c = %c\n", d.c); // 'A'
d.f = 3.14;
printf("d.f = %f\n", d.f); // 3.14
return 0;
}
二、判断大小端(经典面试题)
cpp
#include <stdio.h>
// 方法1:使用union
union Endian {
int a;
char b;
};
int isLittleEndian1() {
union Endian e;
e.a = 1;
return e.b == 1; // 小端返回1,大端返回0
}
// 方法2:使用指针
int isLittleEndian2() {
int a = 1;
char* p = (char*)&a;
return *p == 1; // 小端返回1,大端返回0
}
int main() {
if (isLittleEndian1()) {
printf("小端模式\n");
} else {
printf("大端模式\n");
}
return 0;
}
总结
一、操作符优先级速记
-
括号 :
() -
单目 :
++--!~*&sizeof -
算术 :
*/%→+- -
移位 :
<<>> -
关系 :
<<=>>=→==!= -
位运算 :
&→^→| -
逻辑 :
&&→|| -
条件 :
?: -
赋值 :
=+=-=等 -
逗号 :
,
二、常见陷阱总结
| 陷阱 | 说明 |
|---|---|
= 和 == 混淆 |
if (a = 10) 是赋值,永远为真 |
| 短路求值 | && 和 ` |
| 整数除法截断 | 10 / 3 = 3,不是 3.33 |
| 移位位数过大 | 移位位数 ≥ 类型位数是未定义行为 |
| 优先级错误 | a & b == 0 实际是 a & (b == 0) |
操作符是C语言的基本构件,掌握它们的使用规则和优先级是写出正确代码的基础。
学习建议:
-
不确定优先级时使用括号明确顺序
-
理解短路求值对代码执行的影响
-
注意整数除法、取模的运算规则
-
熟悉位操作在嵌入式、加密、标志位等场景的应用