2.2运算符与表达式

82 阅读5分钟

运算符与表达式
——代码世界的数学法则与电子魔术


一、运算符的机器语言面孔

每个运算符都对应底层特定的机器指令,理解其本质能写出更高效的代码。

1. 算术运算符的二进制实现

int a = 10, b = 3;  
// 加法 → ADD指令  
int sum = a + b;    // 0x0A + 0x03 = 0x0D  

// 减法 → SUB指令  
int diff = a - b;   // 0x0A - 0x03 = 0x07  

// 乘法 → MUL指令(消耗较多时钟周期)  
int product = a * b; // 0x0A * 0x03 = 0x1E  

// 除法 → DIV指令(注意符号处理)  
int quotient = a / b;   // 10/3=3(向零取整)  
unsigned u_quot = 10 / 3; // 10/3=3(无符号除法)  

底层特性

  • 整数除法不保留小数(直接截断)
  • 有符号数使用补码运算,最高位为符号位

2. 位运算符的电子开关逻辑

unsigned char x = 0b10101010; // 170  
unsigned char y = 0b11001100; // 204  

// 按位与 → AND指令(筛选特定位)  
x & y = 0b10001000   // 136  

// 按位或 → OR指令(合并位标记)  
x | y = 0b11101110   // 238  

// 异或 → XOR指令(找不同)  
x ^ y = 0b01100110   // 102  

// 左移 → SHL指令(快速乘2^n)  
x << 2 = 0b1010101000 // 高位溢出后实际为0b10101000 (168)  

// 右移 → SHR/SAR指令(无符号/有符号)  
y >> 3 = 0b00011001   // 25(无符号右移)  

3. 逻辑运算符的短路特性

int *ptr = NULL;  
// 逻辑与(&&)短路:左操作数为假时跳过右操作数  
if (ptr != NULL && *ptr > 0) {  
    // 安全访问:不会触发段错误  
}  

// 逻辑或(||)短路:左操作数为真时跳过右操作数  
int flag = 1;  
if (flag == 1 || printf("test")) {  
    // "test"永远不会被打印  
}  

二、优先级陷阱:代码的隐形杀手

1. 常见优先级排序(从高到低)

运算符描述
() []函数调用、数组访问
++ --自增/自减(后缀)
! ~逻辑非、按位取反
* / %乘、除、取模
+ -加、减
<< >>位移
< <=关系运算符
== !=相等判断
&按位与
^按位异或
|按位或
&&逻辑与
||逻辑或
?:三元条件运算符
=赋值

2. 经典坑点案例

// 案例1:位移 vs 加法  
int a = 5 << 1 + 1;   // 等价于5 << (1+1)=20,而非(5<<1)+1=11  

// 案例2:位与 vs 相等判断  
if (x & 0x0F == 0x0A) // 实际解析为x & (0x0F == 0x0A) → 永远为假  
// 正确写法:if ((x & 0x0F) == 0x0A)  

// 案例3:自增与解引用  
int arr[] = {10,20};  
int *p = arr;  
int val = *p++;       // val=10,p指向arr[1](等价于*(p++))  

黄金法则:当优先级不明确时,多用括号


三、类型转换:数据形态的变形记

1. 隐式类型转换(自动发生)

// 整型提升:char/short运算时自动转为int  
char c1 = 100, c2 = 100;  
int sum = c1 + c2;    // char先转为int再相加  

// 算术转换:不同类型混合运算时向高精度转换  
double d = 3.14;  
int i = 10;  
double result = d + i; // i先转double再相加  

// 赋值转换:右侧类型自动转为左侧类型  
int num = 3.1415;     // num=3(小数部分截断)  

2. 强制类型转换(显式控制)

// 基本语法:(目标类型)表达式  
float f = 1.618;  
int n = (int)f;       // n=1  

// 指针类型转换(危险操作!)  
int data = 0x12345678;  
char *p = (char*)&data;  
printf("%x", *p);     // 输出78(小端模式下)  

// 避免符号扩展问题  
unsigned char uc = 0xFF;  
int signed_val = (int)uc;     // 255(无符号扩展)  
int sign_extend = (signed char)uc; // -1(符号扩展)  

四、避坑指南:7个血泪教训

  1. 浮点数判等陷阱

    float f1 = 0.1 + 0.2;  
    float f2 = 0.3;  
    if (f1 == f2) {  // 可能不成立!  
        // 应使用fabs(f1-f2) < 1e-6  
    }  
    
  2. 自增运算符的副作用

    int i = 0;  
    int j = i++ + i++; // 未定义行为!不同编译器结果不同  
    
  3. 逻辑与位运算混淆

    if (x & 0x01)     // 检测最低位是否为1(正确)  
    if (x && 0x01)    // 等效于x != 0(可能非预期)  
    
  4. 移位越界灾难

    int x = 1;  
    x = x << 33;      // 若int为32位,这是未定义行为!  
    
  5. 无符号数减法黑洞

    unsigned int a = 5, b = 10;  
    int diff = a - b; // 结果是大正数(非-5)!  
    
  6. 三元运算符类型不匹配

    int x = 1;  
    float y = x ? 3.14 : 0; // 正确 → y=3.14  
    float z = x ? 3.14 : 0; // 正确但可能警告,建议写成0.0  
    
  7. 宏定义中的优先级灾难

    #define SQUARE(x) x*x  
    int val = SQUARE(2+3); // 展开为2+3*2+3=11  
    

五、总结:运算符使用决策树

选择运算符时

  • 需要位级操作 → 用& | <<
  • 需要逻辑判断 → 用&& ||
  • 快速乘除2的幂 → 用位移运算

类型转换原则

  • 明确需要截断数据 → 用强制转换
  • 混合类型运算 → 依赖隐式转换但需注意精度损失

🚫 绝对避免

  • 对浮点数使用位运算
  • 在复杂表达式中依赖运算符优先级记忆
  • 对有符号数进行位运算(除非完全理解符号位处理)

终极心法:当你写下a + b * c时,想象每个运算符都在发出机器指令的轰鸣声。理解这些底层细节,就是掌握C语言力量的钥匙! ⚙️