找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

371

积分

0

好友

45

主题
发表于 2025-12-26 11:36:41 | 查看: 29| 回复: 0

本文详细介绍了C语言中的各类操作符,包括算术操作符、移位操作符、位操作符、赋值操作符、单目操作符、关系操作符、逻辑操作符、条件操作符、逗号表达式、下标引用、函数调用、结构成员访问以及表达式求值规则。内容涵盖了操作符的功能、用法以及在内存中的处理方式,如位移操作对二进制补码的影响,位操作符的按位与、按位或、按位异或,以及整数在内存中的存储和位运算的应用等。

文章目录

  • 一、操作符分类
  • 二、算术操作符
  • 三、移位操作符
  • 四、位操作符
  • 五、赋值操作符
  • 六、单目操作符
  • 七、关系操作符
  • 八、逻辑操作符
  • 九、条件操作符
  • 十、逗号表达式
  • 十一、下标引用操作符
  • 十二、函数调用操作符
  • 十三、结构成员访问操作符
  • 十四、表达式求值
  • 总结

一、操作符分类

C语言提供了丰富的操作符,让我们能够灵活地操作数据。操作符就像是工具箱里的各种工具,每种工具都有其特定的用途。

操作符分类:

  1. 算术操作符
  2. 移位操作符
  3. 位操作符
  4. 赋值操作符
  5. 复合赋值操作符
  6. 单目操作符
  7. 关系操作符
  8. 逻辑操作符
  9. 条件操作符
  10. 逗号操作符
  11. 下标引用、函数调用和结构成员

二、算术操作符

算术操作符是我们最熟悉的操作符,它们就像数学中的加减乘除一样直观。

操作符 功能
+ 加法
- 减法
* 乘法
/ 除法
% 取模

重要规则:

  1. *+- 、``** 这三个操作符与平常数学中的运算规则相同,可以作用于整数和浮点数。
  2. 除法操作符/
    • 如果两个操作数都为整数,执行整数除法(取整运算)。
    • 只要有浮点数,执行的就是浮点数除法
  3. 取模操作符%
    • 只能操作整数,返回整除之后的余数。
    • 不能用于浮点数。

示例:

#include <stdio.h>

int main()
{
    // 整数除法
    int a = 7 / 2;        // 结果:3(整数除法)
    printf("7 / 2 = %d\n", a);

    // 浮点数除法
    double b = 7.0 / 2;   // 结果:3.5(浮点数除法)
    printf("7.0 / 2 = %.1f\n", b);

    // 取模运算
    int c = 7 % 2;        // 结果:1(7除以2的余数)
    printf("7 %% 2 = %d\n", c);

    // 错误示例:%不能用于浮点数
    // double d = 7.0 % 2.0;  // 编译错误

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 1


三、移位操作符

移位操作符就像是在二进制世界里“移动”数字,它们直接操作内存中存储的二进制位。理解移位操作符,需要先理解整数在内存中的存储方式。

3.1 原码、反码、补码

整数在内存中存储的是补码(二进制),整数的二进制有三种表现形式:原码、反码、补码

正整数

对于正整数而言,正整数的原码、反码、补码是相同的

示例: int a = 5;

  • 在大多数系统上,int 类型占4个字节、32个比特位。
  • 换算为二进制(原码)为:00000000000000000000000000000101
  • 5的原码、反码、补码相同,都为:00000000000000000000000000000101
  • 开头的第一个数字表示正负,0表示正数,1表示负数

负整数

对于负整数而言,负整数的原码、反码、补码是需要计算的

示例: int a = -5;

  • 原码:换算为二进制(原码)为 10000000000000000000000000000101
  • 反码:原码的符号位不变,其他数字按位取反 → 11111111111111111111111111111010
  • 补码:反码加1就是补码 → 11111111111111111111111111111011

记忆技巧:

  • 正数:原码 = 反码 = 补码。
  • 负数:原码 → 反码(符号位不变,其他位取反)→ 补码(反码+1)。

3.2 移位操作符

操作符 功能
<< 左移操作符
>> 右移操作符

重要说明:

  • 移位操作符的操作数只能是整数,并且移动的是二进制位补码
  • 移动负数位是标准未定义的(不要这样做)。
  • 移动的是补码,改变的是数据在内存中的存储结构,但是打印时还是按照原码进行打印。
  • 对一个数移位操作完成后,当前的数不会改变的,除非把它赋值给另外一个变量。
  • 左移n位相当于给之前的数乘以2的n次方(例如:左移1位相当于乘2,左移2位相当于乘4)。
  • 右移n位相当于给之前的数除以2的n次方(例如:右移1位相当于除2,右移2位相当于除4)。
  • 注意: 这个规律对于非负数成立,对于负数,右移的结果取决于采用算术右移还是逻辑右移。

3.3 左移操作符 <<

移位规则:左边丢弃,右边补0。

示例: int num = 10;

num在内存中的二进制(补码):
00000000000000000000000000001010  (10的二进制)

num左移一位(num << 1):
00000000000000000000000000010100  (20的二进制,相当于10×2)

代码示例:

#include <stdio.h>

int main()
{
    int num = 10;
    printf("num = %d\n", num);           // 输出:10
    printf("num << 1 = %d\n", num << 1); // 输出:20(10×2)
    printf("num << 2 = %d\n", num << 2); // 输出:40(10×4)
    printf("num = %d\n", num);           // 输出:10(num本身没有改变)

    // 需要赋值才能改变num的值
    num = num << 1;
    printf("num = %d\n", num);           // 输出:20

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 2

3.4 右移操作符 >>

右移运算分两种,分别是逻辑移位算术移位。具体采用哪种方式取决于编译器。

1. 逻辑移位:左边用0填充,右边丢弃。

示例: 用-5举例(假设采用逻辑右移)

-5的补码:
11111111111111111111111111111011

逻辑右移后:
01111111111111111111111111111101

2. 算术移位:左边用原该值的符号位填充,右边丢弃。

示例: 用-1举例(假设采用算术右移)

-1的补码(32个全1):
11111111111111111111111111111111

算术右移后(左边用符号位1填充):
11111111111111111111111111111111  (仍然是-1)

重要说明:

  • VS系列编译器在右移的时候,采用的是算术右移
  • 到底是算术右移还是逻辑右移取决于编译器。
  • 对于有符号整数,通常使用算术右移(保持符号)。
  • 对于无符号整数,通常使用逻辑右移(左边补0)。

代码示例:

#include <stdio.h>

int main()
{
    int num = 10;
    printf("num = %d\n", num);           // 输出:10
    printf("num >> 1 = %d\n", num >> 1); // 输出:5(10÷2)
    printf("num >> 2 = %d\n", num >> 2); // 输出:2(10÷4)

    int num2 = -1;
    printf("num2 = %d\n", num2);         // 输出:-1
    printf("num2 >> 1 = %d\n", num2 >> 1); // 输出:-1(算术右移,保持符号)

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 3


四、位操作符

位操作符直接操作二进制位,就像是在二进制世界里进行逻辑运算。它们是底层编程和性能优化的利器。

操作符 功能
& 按位与
| 按位或
^ 按位异或

重要说明:

  • 位操作符的操作数必须是整数,且操作的位是二进制位补码
  • 注意区分位操作符和逻辑操作符:
    • &&& 不同:& 是按位与,&& 是逻辑与。
    • ||| 不同:| 是按位或,|| 是逻辑或。

4.1 按位与 &

规则:全1为1,否则为0。

示例: 1 & 2

1的二进制:00000000000000000000000000000001
2的二进制:00000000000000000000000000000010
按位与:   00000000000000000000000000000000  (结果:0)

代码示例:

#include <stdio.h>

int main()
{
    int num1 = 1;
    int num2 = 2;
    printf("num1 & num2 = %d\n", num1 & num2);  // 输出:0

    // 一个数字&1,如果得到1,说明这个数字的最后一位是1
    int num = 5;  // 二进制:101
    if((num & 1) == 1)
        printf("num的最后一位是1\n");

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 4

4.2 按位或 |

规则:有1为1,否则为0。

示例: 1 | 2

1的二进制:00000000000000000000000000000001
2的二进制:00000000000000000000000000000010
按位或:   00000000000000000000000000000011  (结果:3)

代码示例:

#include <stdio.h>

int main()
{
    int num1 = 1;
    int num2 = 2;
    printf("num1 | num2 = %d\n", num1 | num2);  // 输出:3

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 5

4.3 按位异或 ^

规则:相同为0,不同为1。

重要性质:

  • 对于任意整数 aa ^ a = 0
  • 对于任意整数 a0 ^ a = a
  • 异或运算满足交换律和结合律。

示例: 1 ^ 2

1的二进制:00000000000000000000000000000001
2的二进制:00000000000000000000000000000010
按位异或: 00000000000000000000000000000011  (结果:3)

代码示例:

#include <stdio.h>

int main()
{
    int num1 = 1;
    int num2 = 2;
    printf("num1 ^ num2 = %d\n", num1 ^ num2);  // 输出:3

    // 验证异或的性质
    int a = 5;
    printf("a ^ a = %d\n", a ^ a);      // 输出:0
    printf("0 ^ a = %d\n", 0 ^ a);      // 输出:5

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 6

4.4 位操作符的应用

应用1:不使用临时变量交换两个数

这是一道经典的面试题:不能创建临时变量(第三个变量),实现两个数的交换。

方法:使用异或操作符

#include <stdio.h>

int main()
{
    int a = 10;
    int b = 20;

    printf("交换前:a = %d, b = %d\n", a, b);

    a = a ^ b;  // a现在存储的是a^b的结果
    b = a ^ b;  // b = a^b^b = a^0 = a(原理:b=a^b^b=a^0=a)
    a = a ^ b;  // a = a^b^a = b(原理:a=a^b^a=b)

    printf("交换后:a = %d, b = %d\n", a, b);
    // 输出:交换后:a = 20, b = 10

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 7

原理说明:

int a = 5;  // 101
int b = 3;  // 011

a = a ^ b;  // a = 110 (a^b的结果)
b = a ^ b;  // b = 101 (即原来的a,因为a^b^b=a^0=a)
a = a ^ b;  // a = 011 (即原来的b,因为a^b^a=b)

局限性: 异或操作符只能用于整数,这个方法不适用于浮点数。

应用2:求一个整数存储在内存中的二进制中1的个数

方法1:逐位检查(循环32次)

#include <stdio.h>

int main()
{
    int num = 10;
    int count = 0;  // 计数
    int i = 0;

    for(i = 0; i < 32; i++)
    {
        if(1 == ((num >> i) & 1))  // 一个数字&1,如果得到1,说明这个数字的最后一位是1
        {
            count++;
        }
    }

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

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 8

思考: 这样的实现方式有没有问题?

  • 问题:对于正数,高位都是0,循环32次是浪费的。
  • 问题:对于负数,这种方法可以正确工作。

方法2:使用取模和除法(有问题的方法)

#include <stdio.h>

int main()
{
    int num = 10;
    int count = 0;  // 计数

    while(num)
    {
        if(num % 2 == 1)
            count++;
        num = num / 2;
    }

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

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 9

思考: 这样的实现方式有没有问题?

  • 问题: 对于负数,num / 2 的结果是向零截断的,当num为-1时,-1/2 = 0,循环会立即结束。
  • 问题: 对于负数,num % 2 的结果可能是-1(取决于编译器实现),导致计数错误。
  • 问题: 这种方法对负数无法正确工作,只能用于非负整数。

方法3:优化方法(推荐)

#include <stdio.h>

int main()
{
    int num = -1;
    int count = 0;  // 计数

    while(num)
    {
        count++;
        num = num & (num - 1);  // 每次操作都会消除num最右边的1
    }

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

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 10

原理说明:
num & (num - 1) 这个操作会消除 num 最右边的1。例如:

  • num = 10 (二进制:1010)
  • num - 1 = 9 (二进制:1001)
  • num & (num - 1) = 8 (二进制:1000) - 最右边的1被消除了。

优势:

  • 循环次数等于1的个数,而不是固定的32次。
  • 对正数和负数都有效。
  • 效率更高。

这种方式是不是很好? 达到了优化的效果,但是难以想到。在实际开发中,方法1更直观易懂,方法3更高效。


五、赋值操作符

赋值操作符是一个很棒的操作符,它可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。

赋值操作符=

int weight = 120;  // 体重
weight = 89;       // 不满意就赋值

double salary = 10000.0;
salary = 20000.0;  // 使用赋值操作符赋值

赋值操作符可以连续使用:

int a = 10;
int x = 0;
int y = 20;

a = x = y + 1;  // 连续赋值

这样的代码感觉怎么样?
虽然语法上允许,但这样的代码可读性较差。同样的语义,你看看:

x = y + 1;
a = x;

这样的写法是不是更加清晰爽朗而且易于调试?

5.1 复合赋值符

复合赋值符让代码更加简洁,它们是赋值操作符和其他操作符的组合。

操作符 功能
+= 加等于
-= 减等于
*= 乘等于
/= 除等于
%= 模等于
>>= 右移等于
<<= 左移等于
&= 与等于
|= 或等于
^= 异或等于

示例:

int x = 10;

x = x + 10;   // 传统写法
x += 10;      // 复合赋值,其他运算符一样的道理。这样写更加简洁

x = x * 2;    // 传统写法
x *= 2;       // 复合赋值

x = x >> 1;   // 传统写法
x >>= 1;      // 复合赋值

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 11

说明: 复合赋值符不仅让代码更简洁,而且在某些情况下,编译器可能会生成更高效的代码。


六、单目操作符

单目操作符也就是只接受一个操作数的操作符。它们就像是一元函数,只需要一个参数就能工作。

单目操作符列表: 操作符 功能
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
前置、后置–
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换

6.1 逻辑反操作 !

C99中引入了布尔类型(_Bool),这样和0、1相比可读性更高。使用 ! 逻辑反操作符可以得到相反的布尔类型。

#include <stdio.h>
#include <stdbool.h>

int main()
{
    int flag = 0;

    if(!flag)  // 如果flag为0(假),!flag为1(真)
    {
        printf("flag is false\n");
    }

    bool b = true;
    printf("!b = %d\n", !b);  // 输出:0(false)

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 12

6.2 取地址和解引用操作符

  • &:取地址操作符,获取变量的地址。
  • *``**:解引用操作符(间接访问操作符),通过地址访问值。
    
    #include <stdio.h>

int main()
{
int a = 10;
int *p = &a;  // p存储a的地址

printf("a = %d\n", a);      // 直接访问:10
printf("*p = %d\n", *p);    // 间接访问:10(通过指针p访问a的值)

*p = 20;  // 通过指针修改a的值
printf("a = %d\n", a);      // 输出:20

return 0;

}

![](https://static1.yunpan.plus/attachment/453e934155ad.png)

### 6.3 sizeof 操作符
**`sizeof`**是一个**操作符**,不是函数!它可以求变量(类型)所占空间的大小,单位是字节。

**重要特性:**
*   `sizeof` 是操作符,不是函数。对于类型名必须使用括号(如 `sizeof(int)`),对于变量名可以不用括号(如 `sizeof a`),但建议统一使用括号提高可读性。
*   特殊的是 `sizeof(long)` 在32位下为4字节,而在64位下为8字节,这个取决于编译器的实现。
*   C语言只是规定 `sizeof(long) >= sizeof(int)`。
*   **在C语言中,`sizeof` 操作符中括号内的表达式不参与计算。**

**示例:**
```c
#include <stdio.h>

void test1(int arr[])  // 本质上传过来是 int* arr
{
    printf("%zu\n", sizeof(arr));  // 4/8
    // 数组首元素的地址用指针变量来存储,指针变量的大小是4/8,取决于计算机
}

void test2(char ch[])  // 本质是传过来是 char* ch
{
    printf("%zu\n", sizeof(ch));  // 4/8
}

int main()
{
    int arr[10] = {0};
    char ch[10] = {0};

    printf("%zu\n", sizeof(arr));  // 40 ——数组的大小10*4
    printf("%zu\n", sizeof(ch));   // 10 ——数组的大小10*1

    test1(arr);  // 输出:4或8(指针的大小)
    test2(ch);   // 输出:4或8(指针的大小)

    // sizeof中表达式不参与计算
    int a = 10;
    int sz = sizeof(a++);  // a++不会执行
    printf("a = %d\n", a);  // 输出:10(a没有自增)

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 13

注意: 使用 %zu 来打印 size_t 类型(sizeof 的返回类型)。

6.4 按位取反操作符 ~

~操作符表示对一个数的二进制按位取反,即二进制每一位取反。

应用场景:

  • scanf() 读取失败的时候会返回 EOF(End Of File)。
  • 在大多数系统上,EOF 的值是 -1
  • ~(-1) 对-1的二进制位取反得到0。
  • 0为假就停下来了,也正是这个原因,while(~scanf("%d", &n)) 可以终止循环。
  • 注意: EOF 的具体值由实现定义,但在大多数系统上确实是 -1

示例:

#include <stdio.h>

int main()
{
    int a = 0;
    printf("~a = %d\n", ~a);  // 输出:-1(0的按位取反)

    int b = -1;
    printf("~b = %d\n", ~b);  // 输出:0(-1的按位取反)

    // 实际应用:循环读取直到EOF
    int num;
    while(~scanf("%d", &num))  // 等价于 while(scanf("%d", &num) != EOF)
    {
        printf("读取到:%d\n", num);
    }

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 14

6.5 自增和自减操作符

++-- 是C语言中最容易出错的操作符之一,理解它们对于掌握C语言至关重要。

规则:

  • ++a:前置++,先++后使用
  • a++:后置++,先使用,后++
  • --a:前置–,先–后使用
  • a--:后置–,先使用,后–

示例:

#include <stdio.h>

int main()
{
    int a = 10;

    printf("a = %d\n", a);        // 输出:10
    printf("a++ = %d\n", a++);   // 输出:10(先使用,后++)
    printf("a = %d\n", a);        // 输出:11

    int b = 10;
    printf("++b = %d\n", ++b);   // 输出:11(先++,后使用)
    printf("b = %d\n", b);        // 输出:11

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 15

常见陷阱:

int i = 1;
int ret = (++i) + (++i) + (++i);
// 这个表达式的结果是未定义的,不同编译器可能产生不同的结果
// VS: 12  Linux: 10

6.6 强制类型转换

强制类型转换允许我们显式地将一个类型转换为另一个类型。

int a = (int)3.14;  // 3(将3.14强制转换为int,小数部分被截断)

double b = 3.14;
int c = (int)b;     // 3

注意: 强制类型转换可能导致数据丢失或精度损失,使用时要谨慎。


七、关系操作符

关系操作符用于比较两个值的大小关系,就像数学中的比较符号。

操作符 功能
> 大于
>= 大于等于
< 小于
<= 小于等于
== 用于测试“相等”
!= 用于测试“不相等”

重要提示:

  • == 用于判断相等,= 用于赋值,不要混淆。
  • 判断字符串相等的时候,if (str1 == str2)错误的(如果str1和str2是字符数组),这样比较的是字符串首字符的地址,而不是字符串内容。
  • 正确的做法是用 strcmp 函数来比较字符串的大小,自左向右逐个按照ASCII码值进行比较,直到出现不同的字符或遇 \0 为止。

示例:

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

int main()
{
    int a = 10, b = 20;

    if(a > b)
        printf("a > b\n");
    else if(a < b)
        printf("a < b\n");
    else
        printf("a == b\n");

    // 字符串比较
    char str1[] = "abc";
    char str2[] = "abc";

    // 错误:比较的是地址
    if(str1 == str2)  // 这总是false,因为str1和str2是不同的数组
        printf("相等\n");

    // 正确:使用strcmp函数
    if(strcmp(str1, str2) == 0)  // 比较字符串内容
        printf("字符串相等\n");

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 16


八、逻辑操作符

逻辑操作符用于组合多个条件,它们就像逻辑学中的“与”和“或”。

操作符 名称
&& 逻辑与
|| 逻辑或

重要特性:

  • &&(逻辑与):如果左边为假,就不用算右边了(短路求值)。
  • ||(逻辑或):如果左边为真,就不用算右边了(短路求值)。

短路求值的优势:

  1. 提高效率:如果左边已经能确定结果,就不需要计算右边。
  2. 避免错误:可以利用短路求值来避免某些错误。

示例:

#include <stdio.h>

int main()
{
    int a = 0;
    int b = 10;

    // && 短路求值
    if(a != 0 && b / a > 5)  // a != 0为假,不会执行b/a,避免除零错误
    {
        printf("条件成立\n");
    }

    // || 短路求值
    if(a == 0 || b / a > 5)  // a == 0为真,不会执行b/a
    {
        printf("条件成立\n");
    }

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 17

注意区分:

  • &&&& 是按位与,&& 是逻辑与。
  • |||| 是按位或,|| 是逻辑或。

九、条件操作符

条件操作符是C语言中唯一的三目操作符,它就像是一个简洁的 if-else 语句。

语法: 表达式1 ? 表达式2 : 表达式3

执行逻辑:

  • 如果表达式1为真,执行表达式2,整个表达式的值为表达式2的值。
  • 如果表达式1为假,执行表达式3,整个表达式的值为表达式3的值。

示例:

#include <stdio.h>

int main()
{
    int a = 3;
    int b = 6;
    int c = 0;

    c = a > b ? a : b;
    // a > b 为表达式1,a为表达式2,b为表达式3
    // 该语句的意思为:如果a>b,则将a的值赋给c,否则将b的值赋给c

    printf("c = %d\n", c);  // 输出:6

    // 嵌套使用
    int max = a > b ? (a > 10 ? a : 10) : (b > 10 ? b : 10);

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 18

使用建议:

  • 条件操作符适合简单的条件判断。
  • 复杂的逻辑建议使用 if-else 语句,可读性更好。

十、逗号表达式

逗号表达式就像是一个“序列”,它让我们可以在一个表达式中执行多个操作。

语法: exp1, exp2, exp3, …expN

规则:

  • 逗号表达式就是用逗号隔开的多个表达式。
  • 从左向右依次执行代码,整个表达式的结果是最后一个表达式的结果

示例:

#include <stdio.h>

int main()
{
    int a = 1;
    int b = 2;
    int c = (a > b, a = b + 10, a, b = a + 1);  // 逗号表达式

    printf("c = %d\n", c);  // 输出:13

    // 执行过程:
    // 1. a > b (结果为0,但被丢弃)
    // 2. a = b + 10 (a = 12)
    // 3. a (值为12,但被丢弃)
    // 4. b = a + 1 (b = 13,这是最后一个表达式,所以c = 13)

    printf("a = %d, b = %d\n", a, b);  // 输出:a = 12, b = 13

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 19

实际应用:
逗号表达式常用于 for 循环中:

for(i = 0, j = 10; i < j; i++, j--)
{
    // 循环体
}

十一、下标引用操作符

[]下标引用操作符

操作数:一个数组名 + 一个索引值

int arr[10];    // 创建数组
arr[9] = 10;    // 使用下标引用操作符
// []的两个操作数是arr和9

说明:

  • arr[9] 等价于 *(arr + 9)
  • 数组名在大多数情况下是首元素地址。
  • 通过下标可以访问数组中的任意元素。

十二、函数调用操作符

()函数调用操作符

  • 接受一个或者多个操作数。
  • 第一个操作数是函数名。
  • 剩余的操作数就是传递给函数的参数。
    
    int add(int a, int b)
    {
    return a + b;
    }

int main()
{
int result = add(3, 5);  // ()是函数调用操作符
return 0;
}

![](https://static1.yunpan.plus/attachment/453e934155ad.png)

---

## 十三、结构成员访问操作符

结构成员访问操作符用来访问一个结构体的成员。
*   **`.`**:直接成员访问操作符(用于结构体变量)。
*   **`->`**:间接成员访问操作符(用于结构体指针)。
```c
#include <stdio.h>

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

int main()
{
    struct Student s = {"张三", 20};
    printf("姓名:%s,年龄:%d\n", s.name, s.age);  // 使用.访问成员

    struct Student *p = &s;
    printf("姓名:%s,年龄:%d\n", p->name, p->age);  // 使用->访问成员
    // p->name 等价于 (*p).name

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 20


十四、表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定,有些表达式的操作数在求值的过程中可能需要转换为其他类型。

14.1 隐式类型转换

整型提升

规则:

  • C语言的整型算术运算总是至少以缺省整型类型的精度来进行的。
  • 为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升

整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器的操作数的字节长度,一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU是难以直接实现两个8比特位直接相加运算,所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

示例:

// 实例
char a, b, c;
a = b + c;
// b和c的值被提升为普通整型,然后再执行加法运算
// 加法运算完成之后,结果将被截断,然后再存储于a中

如何进行整型提升:
整型提升是按照变量的数据类型的符号位来提升的。

  • 有符号类型:如果符号位是1,高位补1;如果符号位是0,高位补0。
  • 无符号类型:高位补0。

示例:

#include <stdio.h>

int main()
{
    char a = 0xb6;        // 10110110(符号位是1,表示负数)
    short b = 0xb600;
    int c = 0xb6000000;

    if(a == 0xb6)        // a会被整型提升,变成负数,所以不相等
        printf("a");
    if(b == 0xb600)      // b会被整型提升,变成负数,所以不相等
        printf("b");
    if(c == 0xb6000000)  // c不需要整型提升,所以相等
        printf("c");

    // 输出:c

    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 21

说明:

  • 实例中的 ab 要进行整型提升,但是 c 不需要整型提升。
  • ab 整型提升之后,变成了负数,所以表达式 a==0xb6b==0xb600 的结果是假。
  • 但是 c 不发生整型提升,则表达式 c==0xb6000000 的结果是真。

算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。

类型转换层次(从低到高):

int
unsigned int
long int
unsigned long int
long long int
unsigned long long int
float
double
long double

示例:

int a = 10;
float b = 3.14;
double c = a + b;  
// 执行过程:
// 1. a会被转换为float(因为float在转换层次中比int高)
// 2. 进行float加法运算,结果类型是float
// 3. 如果赋值给double,float结果会被转换为double

但是算术转换要合理,要不然会有一些潜在的问题:

float f = 3.14;
int num = f;  // 隐式转换,会有精度丢失(3.14变成3)

14.2 操作符的属性

复杂表达式的求值有三个影响的因素:

  1. 操作符的优先级
  2. 操作符的结合性
  3. 是否控制求值顺序

两个相邻的操作符先执行哪个,取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。

重要原则: 如果写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。

问题表达式示例

表达式1:

a * b + c * d + e * f

注释: 代码1在计算的时候,由于 *+ 的优先级高,只能保证,* 的计算是比 + 早,但是优先级并不能决定第三个 * 比第一个 + 早执行。

所以表达式的计算顺序就可能是:

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

表达式2:

c + --c;

注释: 同上,操作符的优先级只能决定自减 -- 的运算在 + 的运算的前面,但是我们并没有办法得知,+ 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。

代码3-非法表达式:

#include <stdio.h>

int main()
{
    int i = 10;
    i = i-- - --i * (i = -3) * i++ + ++i;
    printf("i = %d\n", i);
    return 0;
}

说明: 表达式3在不同编译器中测试结果都不同。这是一个非法表达式,结果未定义。

代码4:

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

int main()
{
    int answer;
    answer = fun() - fun() * fun();
    printf("%d\n", answer);  // 输出多少?
    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 22

说明: 这个代码有没有实际的问题?
虽然在大多数的编译器上求得结果都是相同的,但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。

代码5:

#include <stdio.h>

int main()
{
    int i = 1;
    int ret = (++i) + (++i) + (++i);
    printf("%d\n", ret);  // VS:12  Linux:10
    printf("%d\n", i);    // 4
    return 0;
}

C语言操作符完全指南:从算术运算到位操作与表达式求值 - 图片 - 23

说明: 看看同样的代码产生了不同的结果,这是为什么?
简单看一下汇编代码,就可以分析清楚。这段代码中的第一个 + 在执行的时候,第三个 ++ 是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。

总结: 我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。在实际编程中,应该避免写出这样的表达式,使用明确的、可预测的代码。


总结

操作符是C语言的基础,掌握各种操作符的使用方法和特性,对于编写高效、正确的代码至关重要。这不仅涉及到算法与数据结构中的表达式计算,也是理解网络与系统底层原理的重要一环。

关键要点:

  1. 算术操作符:注意整数除法和浮点数除法的区别,% 只能用于整数。
  2. 移位操作符:左移n位相当于乘以2的n次方,右移n位相当于除以2的n次方,注意算术右移和逻辑右移的区别。
  3. 位操作符:直接操作二进制位,是底层编程的利器。
  4. 赋值操作符:注意 === 的区别。
  5. 单目操作符sizeof 是操作符不是函数,++-- 的前置后置区别。
  6. 关系操作符:字符串比较要用 strcmp,不能用 ==
  7. 逻辑操作符:注意短路求值的特性。
  8. 表达式求值:注意整型提升和算术转换,避免写出有歧义的表达式。

最佳实践:

  • 使用括号明确表达式的计算顺序。
  • 避免在表达式中多次修改同一个变量。
  • 利用短路求值提高效率和避免错误。
  • 理解隐式类型转换,必要时使用显式类型转换。

希望这些知识能帮助你在C语言的学习路上走得更远!记住,多写代码、多实践,才能真正掌握这些操作符的精髓。




上一篇:SAP UI5路由参数传递进阶教程:实现视图间数据传递
下一篇:疯狂架构师 AlibabaNacos+性能调优+JVM+Redis+Netty+微服务+源码框架+并发编程
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 09:19 , Processed in 0.352066 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表