C语言笔记归纳9:操作符详解

操作符详解

目录

操作符详解

一、🔧 操作符的分类

二、🔢 进制和进制转换

常见进制及表示

常用进制转换方法

三、💾 底层核心:原码、反码、补码

核心概念

代码示例(int 类型,32 位)

为什么要用补码?

补码与原码的转换技巧

四、⚡ 移位操作符

1. 左移操作符 <<

2. 右移操作符 >>

示例(以 - 1 为例,int 类型 32 位)

重要注意事项

五、🔍 位操作符

1. 按位与 &

2. 按位或 |

3. 按位异或 ^

4. 按位取反 ~

位操作实战技巧(面试高频)

1. 无临时变量交换两个整数

2. 统计二进制中 1 的个数(高效方法)

3. 判断一个数是否是 2 的幂次方

4. 二进制位置 0 或置 1

六、📌 单目操作符

关键示例

1. sizeof 操作符

2. 自增自减操作符

3. 强制类型转换

补充说明

七、📝 逗号表达式

示例

应用场景

八、🔍 下标引用 [] 与函数调用 ()

1. 下标引用操作符 []

示例

2. 函数调用操作符 ()

示例

九、🏗️ 结构体与成员访问操作符 . 和 ->

1. 结构体的定义与初始化

2. 结构体成员访问

示例

3. 嵌套结构体的成员访问

十、⚖️ 操作符的优先级与结合性

1. 优先级

2. 结合性

示例

重要建议

十一、🔢 表达式求值的关键规则

1. 整型提升

示例

2. 算术转换

示例

十二、⚠️ 问题表达式解析(避坑指南)

1. 同一表达式中多次修改同一变量

2. 函数参数的求值顺序不确定

3. 逻辑操作符的短路求值被忽略

4. 赋值操作符与相等判断混淆

避坑总结

十三、🎉 总结


✨ 引言:

操作符是 C 语言的核心语法元素,贯穿于每一行代码的编写与执行。无论是基础的算术运算、高效的位操作,还是复杂的表达式求值,掌握操作符的底层逻辑和使用技巧,都是写出简洁、高效、无 Bug 代码的关键。本文将系统梳理 C 语言所有常用操作符,从分类、底层原理到实战案例,搭配详细代码和通俗解释,让你彻底掌握操作符的方方面面!

一、🔧 操作符的分类

C 语言操作符按功能可分为 12 大类,覆盖编程中的各类需求,一目了然:

  • 算术操作符:+-*/%(基础数学运算)
  • 移位操作符:<<>>(二进制位直接移位)
  • 位操作符:&|^~(二进制位逻辑运算)
  • 赋值操作符:=+=-=*=/=%=<<=>>=&=|=^=(变量赋值与运算结合)
  • 单目操作符:!++--&*+-~sizeof(类型)(仅需一个操作数)
  • 关系操作符:>>=<<===!=(比较大小或相等性)
  • 逻辑操作符:&&||(逻辑判断,支持短路求值)
  • 条件操作符:?:(三目运算符,简洁分支逻辑)
  • 逗号表达式:,(多表达式串联执行)
  • 下标引用:[](数组元素访问)
  • 函数调用:()(函数执行调用)
  • 结构成员访问:.->(结构体成员操作)

二、🔢 进制和进制转换

操作符(尤其是位运算、移位操作)的底层依赖二进制,先掌握进制转换,后续学习会更轻松:

常见进制及表示

进制组成元素C 语言标识示例(数字 15)
二进制0, 1无(直接书写)1111
八进制0-7前缀0017
十进制0-915
十六进制0-9, A-F(a-f)前缀0x/0X0xF

常用进制转换方法

  1. 十进制转二进制:除 2 取余,逆序排列例:将 10 转为二进制

    10 ÷ 2 = 5 余 0  
    5 ÷ 2 = 2 余 1  
    2 ÷ 2 = 1 余 0  
    1 ÷ 2 = 0 余 1  
    

    结果:从下往上读取余数 → 1010

  2. 二进制转十进制:按权展开,求和二进制从右向左,第 n 位的权值为 2^(n-1),将每一位与权值相乘后求和:例:将 1010 转为十进制

    1×2³ + 0×2² + 1×2¹ + 0×2⁰ = 8 + 0 + 2 + 0 = 10
    
  3. 二进制与八进制 / 十六进制转换

    • 二进制转八进制:每 3 位一组(不足补 0),每组对应一个八进制数(利用 421 权值法:3 位二进制对应权值 4、2、1)例:101101 → 010(2) 110(6) 001(1) → 0261
    • 二进制转十六进制:每 4 位一组(不足补 0),每组对应一个十六进制数(利用 8421 权值法:4 位二进制对应权值 8、4、2、1)例:101101 → 0010(2) 1101(13→D) → 0x2D

三、💾 底层核心:原码、反码、补码

计算机中整数以补码形式存储和运算,这是理解位操作、移位操作的关键,必须彻底掌握:

核心概念

  • 符号位:二进制最高位用于表示正负,0 = 正数,1 = 负数(int 类型占 4 字节 = 32 位)
  • 原码:直接将十进制数转换为二进制,最高位为符号位,是最直观的表示形式
  • 反码
    • 正数:反码与原码完全相同
    • 负数:符号位保持不变,其他位按位取反
  • 补码
    • 正数:补码与原码、反码完全相同
    • 负数:补码 = 反码 + 1(计算机存储和运算的最终形式)

代码示例(int 类型,32 位)

#include <stdio.h>

int main()
{
    int a = -10;
    // 原码:10000000 00000000 00000000 00001010(直接转换十进制,最高位为1)
    // 反码:11111111 11111111 11111111 11110101(符号位不变,其他位取反)
    // 补码:11111111 11111111 11111111 11110110(反码加1)

    int b = 10;
    // 原码:00000000 00000000 00000000 00001010(正数符号位为0)
    // 反码:00000000 00000000 00000000 00001010(与原码相同)
    // 补码:00000000 00000000 00000000 00001010(与原码、反码相同)

    return 0;
}

为什么要用补码?

  1. 统一加减法运算:CPU 只有加法器,减法运算可转化为 “加负数的补码”,无需额外硬件支持例:计算 1-1(等价于 1+(-1))
    • 用原码计算:00000000 00000000 00000001 + 10000000 00000000 00000001 = 10000000 00000000 00000010 → 结果为 - 2(错误)
    • 用补码计算:00000000 00000000 00000001 + 11111111 11111111 11111111 = 100000000 00000000 00000000 → 舍弃溢出进位,结果为 00000000 00000000 00000000 → 0(正确)
  2. 避免正负零歧义:原码中 + 0(00000000)和 - 0(10000000)是两个不同的表示,补码中仅存在唯一的零(00000000),避免计算混乱

补码与原码的转换技巧

  • 负数补码 → 原码:补码减 1 后按位取反(或按位取反后加 1,两种方法结果一致)
  • 例:补码 11111111 11111111 11111111 11110110(-10 的补码)减 1 得反码:11111111 11111111 11111111 11110101 → 按位取反(符号位不变)得原码:10000000 00000000 00000000 00001010

四、⚡ 移位操作符

移位操作符直接操作整数的二进制补码,效率极高,操作数只能是整数:

1. 左移操作符 <<

  • 规则:左边的二进制位被直接抛弃,右边补 0
  • 数学意义:正数左移 n 位,等价于乘以 2ⁿ(效率远高于乘法操作)
#include <stdio.h>

int main()
{
    int a = 6;
    // 原码:00000000 00000000 00000000 00000110
    // 反码:00000000 00000000 00000000 00000110
    // 补码:00000000 00000000 00000000 00000110
    int b = (a << 1);
    // 左移1位后补码:00000000 00000000 00000000 00001100
    // 反码:00000000 00000000 00000000 00001100
    // 原码:00000000 00000000 00000000 00001100 → 12
    printf("%d\n", a); // 6(原变量值不变)
    printf("%d\n", b); // 12(6×2¹)
    return 0;
}
#include <stdio.h>

int main()
{
    int a = -6;
    // 原码:10000000 00000000 00000000 00000110
    // 反码:11111111 11111111 11111111 11111001
    // 补码:11111111 11111111 11111111 11111010
    int b = (a << 1);
    // 左移1位后补码:11111111 11111111 11111111 11110100
    // 反码:11111111 11111111 11111111 11110011(补码减1)
    // 原码:10000000 00000000 00000000 00001100 → -12
    printf("%d\n", a); // -6(原变量值不变)
    printf("%d\n", b); // -12(-6×2¹)
    return 0;
}

2. 右移操作符 >>

右移操作符分为两种模式,具体采用哪种由编译器决定(主流编译器对有符号数采用算术右移):

  • 逻辑右移:右边的二进制位被抛弃,左边补 0(多用于无符号数)
  • 算术右移:右边的二进制位被抛弃,左边补原符号位(多用于有符号数)
示例(以 - 1 为例,int 类型 32 位)
#include <stdio.h>

int main()
{
    int num = -1;
    // 原码:10000000 00000000 00000000 00000001
    // 反码:11111111 11111111 11111111 11111110
    // 补码:11111111 11111111 11111111 11111111

    num >>= 1; // 采用算术右移,左边补符号位1
    // 右移后补码:11111111 11111111 11111111 11111111(与原补码相同)
    // 反码:11111111 11111111 11111111 11111110
    // 原码:10000000 00000000 00000000 00000001 → -1

    printf("%d\n", num); // 输出-1(验证算术右移)
    return 0;
}

重要注意事项

  • 不要对整数进行负数位的移位(如a << -1),这属于未定义行为,不同编译器可能产生不同结果
  • 移位操作不会改变原变量的值,需将结果赋值给新变量或原变量(如a = a << 1

五、🔍 位操作符

位操作符直接操作二进制补码的每一位,是底层开发、高效编程的核心工具,核心规则如下:

操作符名称运算规则(两位对比)记忆口诀
&按位与全 1 为 1,有 0 为 0有 0 则 0
``按位或有 1 为 1,全 0 为 0有 1 则 1
^按位异或不同为 1,相同为 0相同为 0,不同为 1
~按位取反0 变 1,1 变 0逐位翻转

1. 按位与 &

#include <stdio.h>

int main()
{
    int a = 3;
    int b = -5;

    int c = a & b;
    // 3的补码:00000000 00000000 00000000 00000011
    // -5的原码:10000000 00000000 00000000 00000101
    // -5的反码:11111111 11111111 11111111 11111010
    // -5的补码:11111111 11111111 11111111 11111011
    // 按位与运算:
    // 00000000 00000000 00000000 00000011
    // 11111111 11111111 11111111 11111011
    // &-----------------------------------
    // 00000000 00000000 00000000 00000011 → 补码(正数原码与补码相同)→ 3

    printf("%d\n", c); // 输出3
    return 0;
}

常见用途:判断整数奇偶性(n & 1,结果为 1 则为奇数,0 则为偶数)、清 0 特定位(与 0 按位与)

2. 按位或 |

#include <stdio.h>

int main()
{
    int a = 3;
    int b = -5;

    int c = a | b;
    // 3的补码:00000000 00000000 00000000 00000011
    // -5的补码:11111111 11111111 11111111 11111011
    // 按位或运算:
    // 00000000 00000000 00000000 00000011
    // 11111111 11111111 11111111 11111011
    // |-----------------------------------
    // 11111111 11111111 11111111 11111011 → 补码
    // 反码:11111111 11111111 11111111 11111010(补码减1)
    // 原码:10000000 00000000 00000000 00000101 → -5

    printf("%d\n", c); // 输出-5
    return 0;
}

常见用途:置 1 特定位(与 1 按位或)

3. 按位异或 ^

#include <stdio.h>

int main()
{
    int a = 3;
    int b = -5;

    int c = a ^ b;
    // 3的补码:00000000 00000000 00000000 00000011
    // -5的补码:11111111 11111111 11111111 11111011
    // 按位异或运算:
    // 00000000 00000000 00000000 00000011
    // 11111111 11111111 11111111 11111011
    // ^-----------------------------------
    // 11111111 11111111 11111111 11111000 → 补码
    // 反码:11111111 11111111 11111111 11110111(补码减1)
    // 原码:10000000 00000000 00000000 00001000 → -8

    printf("%d\n", c); // 输出-8
    return 0;
}

常见用途:交换两个整数(无临时变量)、翻转特定位

4. 按位取反 ~

#include <stdio.h>

int main()
{
    int a = 0;
    // 原码:00000000 00000000 00000000 00000000
    // 反码:00000000 00000000 00000000 00000000
    // 补码:00000000 00000000 00000000 00000000
    int b = ~a;
    // 按位取反后补码:11111111 11111111 11111111 11111111
    // 反码:11111111 11111111 11111111 11111110(补码减1)
    // 原码:10000000 00000000 00000000 00000001 → -1

    printf("%d\n", b); // 输出-1
    return 0;
}

注意:按位取反会翻转所有位(包括符号位),结果必然改变原数的正负性

位操作实战技巧(面试高频)

1. 无临时变量交换两个整数
#include <stdio.h>

int main()
{
    int a = 3;
    int b = 5;
    printf("交换前:a=%d, b=%d\n", a, b);

    a = a ^ b;  // a = 3 ^ 5
    b = a ^ b;  // b = (3 ^ 5) ^ 5 = 3(利用a^a=0,0^a=a)
    a = a ^ b;  // a = (3 ^ 5) ^ 3 = 5

    printf("交换后:a=%d, b=%d\n", a, b); // 输出:a=5, b=3
    return 0;
}
2. 统计二进制中 1 的个数(高效方法)
#include <stdio.h>

int main()
{
    int n = 0;
    int count = 0;
    scanf("%d", &n);

    // 核心原理:n & (n-1) 会清除n二进制中最右边的1
    while (n)
    {
        n = n & (n - 1);
        count++;
    }

    printf("二进制中1的个数:%d\n", count);
    return 0;
}

示例:n=15(1111)

  • 15 & 14 = 14(1110)→ count=1
  • 14 & 13 = 12(1100)→ count=2
  • 12 & 11 = 8(1000)→ count=3
  • 8 & 7 = 0 → count=4 → 循环结束,结果为 4
3. 判断一个数是否是 2 的幂次方
#include <stdio.h>

int main()
{
    int n = 0;
    scanf("%d", &n);

    // 核心原理:2的幂次方二进制中只有一个1,n & (n-1) 结果为0
    if (n > 0 && (n & (n - 1)) == 0)
    {
        printf("是2的幂次方\n");
    }
    else
    {
        printf("不是2的幂次方\n");
    }
    return 0;
}
4. 二进制位置 0 或置 1
#include <stdio.h>

int main()
{
    int n = 13; // 二进制:00000000 00000000 00000000 00001101
    int bit_pos = 4; // 操作第4位(从0开始计数)

    // 1. 将第bit_pos位置1(与1按位或)
    n |= (1 << bit_pos); // 1 << 4 = 16(00000000 00000000 00000000 00010000)
    printf("%d\n", n); // 13 | 16 = 29(00000000 00000000 00000000 00011101)

    // 2. 将第bit_pos位置0(与~(1<<bit_pos)按位与)
    n &= ~(1 << bit_pos); // ~(1<<4) = 0xFFFFFFEF(11111111 11111111 11111111 11101111)
    printf("%d\n", n); // 29 & 0xFFFFFFEF = 13(恢复原值)

    return 0;
}

六、📌 单目操作符

单目操作符仅需一个操作数,功能丰富,包括逻辑判断、自增自减、地址操作等:

操作符名称功能说明
!逻辑非对表达式结果取反:!0=1,! 非 0=0
++自增前置 ++:先自增 1,再使用;后置 ++:先使用,再自增 1
--自减前置 --:先自减 1,再使用;后置 --:先使用,再自减 1
&取地址获取变量的内存地址(后续指针章节详细讲解)
*解引用通过地址访问内存中的变量(后续指针章节详细讲解)
+正号对数值无影响(仅用于强调正数,可省略)
-负号改变数值的符号
~按位取反对二进制补码逐位翻转(已在第五章讲解)
sizeof计算字节数计算变量或数据类型所占的字节大小,返回值类型为 size_t(用 % zu 格式符打印)
(类型)强制类型转换将表达式结果转换为指定类型(慎用,可能丢失精度或导致数据异常)

关键示例

1. sizeof 操作符
#include <stdio.h>

int main()
{
    int a = 10;
    printf("sizeof(a) = %zu\n", sizeof(a));      // 输出4(int类型占4字节)
    printf("sizeof(int) = %zu\n", sizeof(int));  // 输出4
    printf("sizeof(a + 3.14) = %zu\n", sizeof(a + 3.14)); // 输出8(a提升为double类型)

    char arr[10] = {0};
    printf("sizeof(arr) = %zu\n", sizeof(arr));  // 输出10(数组总字节数=元素个数×单个元素字节数)

    return 0;
}
2. 自增自减操作符
#include <stdio.h>

int main()
{
    int a = 5;
    int b = ++a; // 前置++:a先自增为6,再赋值给b → b=6,a=6
    printf("a=%d, b=%d\n", a, b); // 输出:a=6, b=6

    int c = 5;
    int d = c++; // 后置++:c先赋值给d(d=5),再自增为6 → d=5,c=6
    printf("c=%d, d=%d\n", c, d); // 输出:c=6, d=5

    return 0;
}
3. 强制类型转换
#include <stdio.h>

int main()
{
    float f = 3.14f;
    int a = (int)f; // 强制将float类型转换为int类型,丢失小数部分
    printf("a=%d\n", a); // 输出3

    int b = 10;
    double d = (double)b / 3; // 强制转换后进行除法,结果为浮点数
    printf("d=%lf\n", d); // 输出3.333333

    return 0;
}

注意:强制类型转换仅在表达式计算时临时改变类型,不会改变原变量的类型和值

补充说明

单目操作符中的 &(取地址)和 *(解引用)是指针操作的核心,后续指针章节会详细讲解,此处暂不展开。

七、📝 逗号表达式

逗号表达式的形式为 exp1, exp2, exp3, ..., expN,核心规则如下:

  • 从左到右依次执行所有表达式
  • 整个逗号表达式的结果为最后一个表达式的值
  • 逗号的优先级是所有操作符中最低的

示例

#include <stdio.h>

int main()
{
    int a = 1;
    int b = 2;
    // 逗号表达式:先执行a>b(结果为0),再执行a=b+10(a=12),再执行a,最后执行b=a+1(b=13)
    int c = (a > b, a = b + 10, a, b = a + 1);
    printf("c=%d\n", c); // 输出13(最后一个表达式b=a+1的结果)
    printf("a=%d, b=%d\n", a, b); // 输出a=12, b=13

    // 逗号表达式在循环中的应用(简化代码)
    int x = 0, y = 0;
    while (x++, y++, x < 5)
    {
        printf("x=%d, y=%d\n", x, y);
    }
    // 循环执行过程:x从1到4,y从1到4,每次循环先执行x++和y++,再判断x<5
    return 0;
}

应用场景

  • 简化代码:在需要执行多个操作但语法只允许一个表达式的场景(如 for 循环的初始化、条件判断)
  • 批量赋值或计算:依次执行多个赋值或计算操作,取最后一个结果

八、🔍 下标引用 [] 与函数调用 ()

1. 下标引用操作符 []

下标引用操作符用于访问数组元素,格式为 arr[n],核心规则:

  • 操作数为数组名和索引值(n 为索引,从 0 开始)
  • 等价于 *(arr + n)(数组名本质是数组首元素的地址)

示例

#include <stdio.h>

int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    printf("%d\n", arr[2]);      // 输出3(访问索引为2的元素)
    printf("%d\n", *(arr + 2));   // 输出3(与arr[2]等价)
    printf("%d\n", 2[arr]);       // 输出3(等价于arr[2],语法允许但不推荐)

    return 0;
}

说明arr[n] 之所以等价于 *(arr + n),是因为数组名 arr 代表数组首元素的地址,arr + n 表示首元素地址向后偏移 n 个元素的地址,* 解引用后即可访问该地址对应的元素。

2. 函数调用操作符 ()

函数调用操作符用于执行函数,格式为 函数名(实参1, 实参2, ...),核心规则:

  • 操作数为函数名和实参列表(实参个数与函数形参一致)
  • 函数调用时,实参的值会传递给形参,函数执行后返回结果(无返回值的函数返回 void)

示例

#include <stdio.h>

// 定义加法函数
int Add(int x, int y)
{
    return x + y;
}

int main()
{
    printf("Hello World!\n"); // 函数调用,无实参

    int a = 3, b = 5;
    int ret = Add(a, b);      // 函数调用,传递两个实参
    printf("a + b = %d\n", ret); // 输出8

    return 0;
}

说明:函数调用操作符的优先级较高,在表达式中会优先执行。

九、🏗️ 结构体与成员访问操作符 . 和 ->

C 语言中的结构体是自定义数据类型,用于将不同类型的数据组合成一个整体,访问结构体成员需使用 . 或 -> 操作符。

1. 结构体的定义与初始化

#include <stdio.h>

// 结构体声明:描述学生的属性
struct Student
{
    char name[20]; // 姓名
    int age;       // 年龄
    int high;      // 身高
    float weight;  // 体重
    char id[16];   // 学号
}; // 分号不能省略(结构体声明结束标志)

// 定义全局结构体变量并初始化
struct Student s1 = {"张三", 20, 180, 75.5f, "20230901022"};

// 定义全局结构体变量(未初始化,成员为随机值)
struct Student s2, s3, s4;

int main()
{
    // 定义局部结构体变量并初始化(指定成员顺序)
    struct Student s5 = {"李四", 22, 175, 68.0f, "20230901023"};

    // 定义局部结构体变量并指定成员初始化(C99及以上支持)
    struct Student s6 = {
        .age = 21,
        .name = "王五",
        .weight = 72.3f,
        .high = 178,
        .id = "20230901024"
    };

    return 0;
}

2. 结构体成员访问

  • . 操作符:通过结构体变量名访问成员,格式:结构体变量.成员名
  • -> 操作符:通过指向结构体的指针访问成员,格式:结构体指针->成员名

示例

#include <stdio.h>
#include <string.h>

struct Student
{
    char name[20];
    int age;
};

int main()
{
    // 定义结构体变量并初始化
    struct Student s = {"张三", 20};
    printf("姓名:%s,年龄:%d\n", s.name, s.age); // 用.访问成员 → 输出:姓名:张三,年龄:20

    // 修改成员值
    strcpy(s.name, "张三三"); // 字符串赋值需用strcpy函数
    s.age = 21;
    printf("修改后:姓名:%s,年龄:%d\n", s.name, s.age); // 输出:修改后:姓名:张三三,年龄:21

    // 定义结构体指针并指向结构体变量
    struct Student *ps = &s;
    // 用->访问成员
    printf("指针访问:姓名:%s,年龄:%d\n", ps->name, ps->age); // 输出:指针访问:姓名:张三三,年龄:21

    // -> 等价于 (*指针).成员
    printf("等价形式:姓名:%s,年龄:%d\n", (*ps).name, (*ps).age); // 输出同上

    return 0;
}

3. 嵌套结构体的成员访问

如果结构体成员也是结构体类型,需逐层访问:

#include <stdio.h>

// 定义嵌套的结构体
struct Date
{
    int year;
    int month;
    int day;
};

struct Person
{
    char name[20];
    struct Date birthday; // 成员为结构体类型
    int height;
};

int main()
{
    struct Person p = {
        .name = "赵六",
        .birthday = {2000, 9, 10},
        .height = 180
    };

    // 访问嵌套结构体的成员
    printf("姓名:%s\n", p.name);
    printf("生日:%d年%d月%d日\n", p.birthday.year, p.birthday.month, p.birthday.day);
    printf("身高:%dcm\n", p.height);

    return 0;
}

十、⚖️ 操作符的优先级与结合性

复杂表达式的求值顺序由操作符的优先级结合性决定,这是避免表达式求值错误的关键:

1. 优先级

优先级决定了不同操作符在表达式中的执行顺序,优先级高的操作符先执行。例如:

  • 乘法 *、除法 / 的优先级高于加法 +、减法 -
  • 赋值操作符 = 的优先级较低,通常最后执行

常见操作符优先级排序(从高到低)

  1. 圆括号 ()(强制改变优先级)
  2. 单目操作符(!++--~sizeof 等)
  3. 算术操作符(*/% 高于 +-
  4. 移位操作符(<<>>
  5. 关系操作符(>>=<<= 高于 ==!=
  6. 位操作符(& 高于 ^ 高于 |
  7. 逻辑操作符(&& 高于 ||
  8. 条件操作符 ?:
  9. 赋值操作符(=+=-= 等)
  10. 逗号表达式 ,(优先级最低)

2. 结合性

当多个优先级相同的操作符连用时,求值顺序由结合性决定:

  • 左结合:从左到右依次执行(大部分操作符,如 +-*/
  • 右结合:从右到左依次执行(少数操作符,如赋值操作符 =, 单目操作符)

示例

#include <stdio.h>

int main()
{
    // 优先级示例:先算乘法,再算加法
    int a = 3 + 4 * 2;
    printf("a=%d\n", a); // 输出11(3 + (4×2))

    // 结合性示例1:左结合(加法)
    int b = 3 + 4 + 5;
    printf("b=%d\n", b); // 输出12((3+4)+5)

    // 结合性示例2:右结合(赋值)
    int c, d, e;
    c = d = e = 10;
    printf("c=%d, d=%d, e=%d\n", c, d, e); // 输出10,10,10(c=(d=(e=10)))

    // 优先级示例:括号改变顺序
    int f = (3 + 4) * 2;
    printf("f=%d\n", f); // 输出14((3+4)×2)

    return 0;
}

重要建议

十一、🔢 表达式求值的关键规则

表达式求值过程中,除了优先级和结合性,还需注意整型提升算术转换,否则可能出现意想不到的结果:

1. 整型提升

  • 定义:表达式中的字符型(char)、短整型(short)操作数,在参与运算前会自动转换为普通整型(int
  • 原因:CPU 的整型运算器(ALU)通常以int类型为最小运算单位,转换为int可提高运算效率
  • 提升规则
    • 有符号数:按符号位进行符号扩展(正数高位补 0,负数高位补 1)
    • 无符号数:高位补 0

示例

#include <stdio.h>

int main()
{
    char a = 3;    // 二进制:00000011(signed char)
    char b = 127;  // 二进制:01111111(signed char)

    // a和b会进行整型提升
    // a提升后:00000000 00000000 00000000 00000011
    // b提升后:00000000 00000000 00000000 01111111
    char c = a + b;
    // a + b的结果:00000000 00000000 00000000 10000010(int类型)
    // 截断为char类型:10000010(signed char,符号位为1,是负数)

    // 打印时c会再次进行整型提升(符号扩展)
    // 提升后补码:11111111 11111111 11111111 10000010
    // 原码:10000000 00000000 00000000 01111110 → -126
    printf("%d\n", c); // 输出-126

    return 0;
}

注意char类型在不同编译器中可能是signed charunsigned char,会影响整型提升的结果,编程时需注意兼容性。

2. 算术转换

  • 定义:如果表达式中不同类型的算术操作数参与运算,编译器会按规则将它们转换为同一类型后再运算
  • 转换规则(从高到低,按优先级转换)
    1. long double
    2. double
    3. float
    4. unsigned long long
    5. long long
    6. unsigned long
    7. long
    8. unsigned int
    9. int

示例

#include <stdio.h>

int main()
{
    int i = 10;
    float f = 3.14f;
    double d = i + f; // i(int)→ float → 与f相加 → 结果转换为double

    printf("d=%lf\n", d); // 输出13.140000

    unsigned int u = 20;
    int j = -10;
    if (u + j > 0) // j(int)→ unsigned int → 相加结果为unsigned int(无负数)
    {
        printf("u + j > 0\n"); // 会执行此语句
    }
    else
    {
        printf("u + j <= 0\n");
    }

    return 0;
}

注意:无符号数与有符号数混合运算时,有符号数会转换为无符号数,可能导致负数被解释为大数,需格外小心。

十二、⚠️ 问题表达式解析(避坑指南)

有些表达式看似正确,但由于 C 语言标准未明确规定求值顺序或存在副作用,会导致未定义行为(不同编译器结果不同),需坚决避免:

1. 同一表达式中多次修改同一变量

// 未定义行为:i被多次修改,求值顺序不确定
int i = 10;
i = i++ - --i * (i = -3) * i++ + ++i;
printf("i=%d\n", i); // 不同编译器结果不同

2. 函数参数的求值顺序不确定

int fun()
{
    static int count = 1;
    return ++count;
}

int main()
{
    // 未定义行为:fun()的调用顺序不确定
    int answer = fun() - fun() * fun();
    printf("%d\n", answer); // 可能是2-3*4=-10,也可能是其他结果
    return 0;
}

3. 逻辑操作符的短路求值被忽略

int a = 0, b = 5;
// a为假,&&右边的++b不会执行(短路求值),b仍然是5
if (a && ++b)
{
    // 不会执行
}
printf("b=%d\n", b); // 输出5

4. 赋值操作符与相等判断混淆

int x = 5;
// 错误:将赋值操作符=误写为相等判断==
if (x = 10) // x被赋值为10,条件为真
{
    printf("x=10\n"); // 会执行
}

避坑总结

  • 避免在同一表达式中对同一变量进行多次读写操作
  • 函数参数中避免使用有副作用的表达式(如i++
  • 明确逻辑操作符的短路求值特性,不依赖未执行的表达式
  • 严格区分赋值操作符=和相等判断==
  • 复杂表达式拆分为多个简单语句,提高可读性和正确性

十三、🎉 总结

本文覆盖了 C 语言所有常用操作符的知识点,从基础到进阶,从原理到实战,搭配详细代码示例和解释。如果遇到具体问题(如指针操作、结构体复杂应用),欢迎在评论区交流~ 祝大家编程之路少踩坑,多高效!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值