当前位置: 首页 > news >正文

C语言操作符详解:从基础到进阶

在C语言编程中,操作符是构建表达式的基本元素,它们决定了程序如何处理数据。理解操作符的工作原理、优先级和结合性,对于编写高效、无错误的代码至关重要。本文将深入探讨C语言中的各类操作符,从基础概念到高级用法,全面提升你的编程技能。

1. 操作符的分类

C语言中的操作符可以分为以下几类:

  • 算术操作符:用于执行基本的数学运算
  • 关系操作符:用于比较两个值
  • 逻辑操作符:用于组合条件表达式
  • 位操作符:用于对二进制位进行操作
  • 赋值操作符:用于给变量赋值
  • 单目操作符:只需要一个操作数
  • 条件操作符:用于条件判断
  • 逗号操作符:用于顺序执行多个表达式
  • 其他操作符:如下标引用、函数调用、结构成员访问等

2. 算术操作符

算术操作符用于执行基本数学运算,包括:
在这里插入图片描述

2.1 除法操作符的特性

除法操作符 / 的行为取决于操作数的类型:

当两个操作数都是整数时,执行整数除法,结果为商的整数部分(小数部分被舍弃)
当任一操作数为浮点数时,执行浮点除法,结果为准确的商

int a = 5, b = 2;
float c = 5.0, d = 2.0;

printf("%d\n", a / b);    // 输出: 2 (整数除法)
printf("%.1f\n", a / d);  // 输出: 2.5 (浮点除法)
printf("%.1f\n", c / b);  // 输出: 2.5 (浮点除法)
printf("%.1f\n", c / d);  // 输出: 2.5 (浮点除法)

2.2 取模操作符

取模操作符 % 只能用于整数操作数,返回除法运算后的余数:

printf("%d\n", 7 % 3);    // 输出: 1
printf("%d\n", -7 % 3);   // 输出: -1 (取决于编译器实现)
printf("%d\n", 7 % -3);   // 输出: 1 (取决于编译器实现)

注意:C89/C90标准对于负数取模的结果是不确定的,在C99中规定结果的符号应该与被除数相同。

3. 移位操作符

移位操作符用于对二进制位进行左移或右移操作:
在这里插入图片描述

3.1 左移操作符

左移操作符 << 将一个数的所有位向左移动指定的位数:

  • 左边的位被丢弃
  • 右边补充0
unsigned int a = 5;  // 二进制: 00000000 00000000 00000000 00000101
unsigned int b = a << 2;  // 二进制: 00000000 00000000 00000000 00010100 (值为20)

左移 n 位在数学上等同于乘以 2^n(前提是不发生溢出):

int x = 3;
printf("%d\n", x << 1);  // 输出: 6 (3 * 2^1)
printf("%d\n", x << 3);  // 输出: 24 (3 * 2^3)

3.2 右移操作符

右移操作符 >> 将一个数的所有位向右移动指定的位数。右移有两种类型:

  1. 逻辑右移:左边用0填充,右边丢弃
  2. 算术右移:左边用符号位填充,右边丢弃

对于无符号数,总是进行逻辑右移。对于有符号数,大多数C编译器实现的是算术右移,但标准并没有规定。

unsigned int a = 20;  // 二进制: 00000000 00000000 00000000 00010100
unsigned int b = a >> 2;  // 二进制: 00000000 00000000 00000000 00000101 (值为5)

int c = -16;  // 二进制: 11111111 11111111 11111111 11110000 (补码表示)
int d = c >> 2;  // 可能是: 11111111 11111111 11111111 11111100 (值为-4)
                 // 算术右移,保持符号位

警告:不要对负数进行移位操作,也不要移动超过操作数位宽的位数,这会导致未定义行为:

int a = 1;
int b = a << -1;  // 未定义行为
int c = a >> 32;  // 对于32位int,这是未定义行为

4. 位操作符

位操作符用于对整数的二进制位执行操作:
在这里插入图片描述

4.1 按位与

按位与操作符 & 对两个操作数的对应位执行逻辑与操作。只有当两个对应位都为1时,结果位才为1:

int a = 12;  // 二进制: 00001100
int b = 10;  // 二进制: 00001010
int c = a & b;  // 二进制: 00001000 (值为8)

按位与的常见用途:

  • 清除特定位(与0进行与操作)
  • 判断某位是否置位(与掩码进行与操作)

4.2 按位或

按位或操作符 | 对两个操作数的对应位执行逻辑或操作。如果至少有一个对应位为1,则结果位为1:

int a = 12;  // 二进制: 00001100
int b = 10;  // 二进制: 00001010
int c = a | b;  // 二进制: 00001110 (值为14)

按位或的常见用途:

  • 设置特定位(与1进行或操作)
  • 组合标志

4.3 按位异或

按位异或操作符 ^ 对两个操作数的对应位执行异或操作。如果两个对应位不同,则结果位为1;如果相同,则为0:

int a = 12;  // 二进制: 00001100
int b = 10;  // 二进制: 00001010
int c = a ^ b;  // 二进制: 00000110 (值为6)

按位异或的特性使其在以下场景非常有用:

  • 不使用临时变量交换两个整数
  • 查找只出现一次的数字
  • 简单的加密/解密

使用异或交换两个变量

void swap(int *a, int *b) {
    *a = *a ^ *b;
    *b = *a ^ *b;  // 现在b等于原来的a
    *a = *a ^ *b;  // 现在a等于原来的b
}

注意:上述代码在 a 和 b 指向同一内存位置时会出问题(结果为0)。

4.4 按位取反

按位取反操作符 ~ 反转操作数的每一位,即将0变为1,将1变为0:

unsigned char a = 5;  // 二进制: 00000101
unsigned char b = ~a;  // 二进制: 11111010 (值为250)

4.5 实战:计算整数中1的个数

以下是使用位操作来计算一个整数二进制表示中1的个数的三种方法:

// 方法1:简单循环
int count_bits1(int num) {
    int count = 0;
    while (num) {
        if (num & 1) count++;
        num >>= 1;
    }
    return count;
}

// 方法2:遍历所有位
int count_bits2(int num) {
    int count = 0;
    for (int i = 0; i < sizeof(int) * 8; i++) {
        if (num & (1 << i)) count++;
    }
    return count;
}

// 方法3:Brian Kernighan算法
int count_bits3(int num) {
    int count = 0;
    while (num) {
        num &= (num - 1);  // 清除最低位的1
        count++;
    }
    return count;
}

方法3是最高效的,因为它的时间复杂度与数字中1的个数相关,而不是与整数的位数相关。

5. 赋值操作符

赋值操作符用于将值存储到变量中:
在这里插入图片描述

5.1 赋值表达式

赋值表达式不仅执行赋值操作,还产生一个值。该值是存储的值:

int a, b, c;
a = b = c = 10;  // 连续赋值

上面的表达式从右向左求值:首先c被赋值为10,然后b被赋值为c(即10),最后a被赋值为b(即10)。

5.2 复合赋值操作符

复合赋值操作符组合了算术、位或移位操作与赋值操作,使代码更加简洁:

x += 10;  // 等价于 x = x + 10;
y *= 3;   // 等价于 y = y * 3;
z &= mask; // 等价于 z = z & mask;

使用复合赋值操作符通常生成更高效的代码,因为变量只需被评估一次。

6. 单目操作符

单目操作符只需要一个操作数:
在这里插入图片描述

6.1 自增和自减操作符

前置和后置自增/自减操作符的区别在于它们的求值顺序:

  • 前置:先增加/减少变量,然后使用新值
  • 后置:先使用原值,然后增加/减少变量
int a = 5;
int b = ++a;  // a先自增为6,然后b被赋值为6
printf("a=%d, b=%d\n", a, b);  // 输出: a=6, b=6

int x = 5;
int y = x++;  // y先被赋值为5,然后x自增为6
printf("x=%d, y=%d\n", x, y);  // 输出: x=6, y=5

提示:除非需要后置操作符的特性,否则优先使用前置操作符,因为它通常更高效(不需要存储临时原值)。

printf("%zu\n", sizeof(int));  // 输出int类型占的字节数(通常为4)
int a = 10;
printf("%zu\n", sizeof a);     // 同上,对变量使用时括号可选
printf("%zu\n", sizeof(a + 1.0)); // 输出double类型的大小(通常为8)
                              // 因为int与double计算,结果为double

注意:sizeof 是编译时操作符,它不实际计算表达式的值:

int i = 10;
size_t size = sizeof(i++);  // i不会自增
printf("i = %d, size = %zu\n", i, size);  // 输出: i = 10, size = 4

当 sizeof 用于数组时,它返回整个数组的字节大小,而不是数组的指针大小:

int arr[10];
printf("%zu\n", sizeof(arr));      // 输出: 40 (假设int为4字节)
printf("%zu\n", sizeof(arr[0]));   // 输出: 4
printf("%zu\n", sizeof(arr)/sizeof(arr[0])); // 数组元素个数: 10

然而,**当数组作为函数参数传递时,它会退化为指针,**此时 sizeof 返回指针的大小:

void func(int arr[]) {
    printf("%zu\n", sizeof(arr));  // 输出: 4或8 (指针大小)
}

7. 关系操作符

关系操作符用于比较两个值,返回0(假)或1(真):
在这里插入图片描述
最常见的关系操作符错误是将等于操作符 == 误写为赋值操作符 =:

if (x = 5) {  // 赋值操作,而非比较!
    // 这部分代码将总是执行,因为赋值表达式的值为5
}

一个防止这类错误的技巧是将常量放在左侧:

if (5 == x) {  // 如果错写为赋值,会编译错误
    // ...
}

8. 逻辑操作符

逻辑操作符用于组合多个条件表达式:
在这里插入图片描述
与按位操作符不同,逻辑操作符专为处理布尔条件而设计,特点有:

  1. 短路求值:
  • 对于 &&,如果左操作数为假,则不计算右操作数
  • 对于 ||,如果左操作数为真,则不计算右操作数
  1. 任何非零值都被视为真,零被视为假
int a = 5, b = 0;
if (a && b) {  // a为真,b为假,表达式为假
    // 不执行
}

if (a || b) {  // a为真,b不会被求值,表达式为真
    // 执行
}

短路求值可以用来避免潜在的错误和编写更高效的代码:

// 防止除零错误
if (divisor != 0 && dividend / divisor > 10) {
    // 安全操作
}

// 防止空指针访问
if (ptr != NULL && ptr->value > 0) {
    // 安全操作
}

9. 条件操作符

条件操作符 ? : 是C语言中唯一的三元操作符,它提供了一种简洁的条件表达式方式:

condition ? expression1 : expression2

如果条件为真,计算并返回表达式1;否则,计算并返回表达式2:

int max = (a > b) ? a : b;  // 获取两个数中的较大值

条件操作符可以嵌套,但过多嵌套会降低代码可读性:

int abs_value = (x >= 0) ? x : -x;  // 求绝对值
char grade = (score >= 90) ? 'A' : (score >= 80) ? 'B' : (score >= 70) ? 'C' : 'F';

10. 逗号操作符

逗号操作符 , 允许在一个表达式中连接多个表达式,从左到右计算,整个表达式的值是最右边表达式的值

int a, b, c;
c = (a = 10, b = 5, a + b);  // c = 15

逗号操作符常用于for循环中初始化多个变量或在循环中执行多个操作:

for (i = 0, j = 10; i < j; i++, j--) {
    // i从0递增,j从10递减,直到它们相遇
}

需要注意的是,逗号操作符的优先级很低,通常需要括号确保正确的求值顺序:

a = 1, 2;  // a = 1,然后2被丢弃
a = (1, 2);  // a = 2,表达式结果是最后一个值

11. 成员访问操作符

成员访问操作符用于访问结构、联合或类的成员:
在这里插入图片描述

struct Person {
    char name[50];
    int age;
};

struct Person person = {"John", 30};
struct Person *ptr = &person;

printf("%s, %d\n", person.name, person.age);  // 使用.操作符
printf("%s, %d\n", ptr->name, ptr->age);      // 使用->操作符
// ptr->age 等价于 (*ptr).age

12. 表达式求值

表达式求值的过程受三个因素影响:

  1. 操作符优先级:决定哪个操作符先被处理
  2. 操作符结合性:当优先级相同时,决定计算顺序(从左到右或从右到左)
  3. 求值顺序:某些操作符保证特定的求值顺序

12.1 操作符优先级

下表列出了C语言操作符的优先级,从高到低排列:
在这里插入图片描述

当遇到复杂表达式时,使用括号可以明确指定计算顺序,提高代码可读性。

12.2 隐式类型转换

在C语言中,当表达式中包含不同类型的操作数时,会发生隐式类型转换。了解这些规则对于避免意外的结果至关重要:

  1. 整型提升:将较小的整型(char, short)转换为int
  2. 寻常算术转换:在二元运算中,将"较低"类型的操作数转换为"较高"类型

类型从低到高的排序:

  • int
  • unsigned int
  • long
  • unsigned long
  • long long
  • unsigned long long
  • float
  • double
  • long double
char c = 'a';
int i = c + 1;  // c被提升为int,然后加1

float f = 1.2;
int j = 10;
double result = f * j;  // j被转换为float,结果再转换为double

整型提升的例子

char a = 0xfb;  // 二进制: 11111011,有符号char
unsigned char b = 0xfb;  // 二进制: 11111011,无符号char

// 比较表达式中,a和b都会被提升为int
if (a == b) {
    printf("Equal\n");
} else {
    printf("Not equal\n");  // 这里会执行!
}

这是因为有符号的a在提升为int时,符号位会扩展,成为一个负数,而无符号的b扩展高位都是0。

13. 常见陷阱和最佳实践

13.1 避免未定义行为

以下是一些涉及操作符的未定义行为:

  1. 在一个表达式中多次修改同一个变量:
i = i++ + ++i;  // 未定义行为
  1. 依赖求值顺序:
printf("%d %d\n", i++, i);  // 未定义的求值顺序
  1. 有符号整数溢出:
int i = INT_MAX + 1;  // 有符号整数溢出
  1. 除以零:
int result = 10 / (x - x);  // 除以零

13.2 最佳实践

  1. 使用括号明确优先级:即使你熟悉优先级规则,使用括号也能让代码更易读.
if ((x > 0) && (y > 0))  // 比 if (x > 0 && y > 0) 更清晰
  1. 避免在同一表达式中多次修改变量:
i++;
j = i;  // 比 j = i++ 更清晰
  1. 小心自增/自减操作符:
// 而不是 array[i++] = value;
array[i] = value;
i++;
  1. 意识到副作用:副作用是指表达式求值时对程序状态的更改,如赋值或递增
  2. 警惕整数溢出:
// 检查加法是否会溢出
if (a > INT_MAX - b) {
    // 处理溢出
} else {
    c = a + b;  // 安全加法
}

总结

C语言的操作符系统强大而灵活,但也容易导致复杂度和潜在错误。深入理解操作符的工作原理、优先级和结合性,以及可能的陷阱,是成为熟练C程序员的关键一步。通过遵循最佳实践和编写清晰的代码,你可以充分利用操作符的强大功能,同时避免常见的编程错误。
记住,好的代码不仅仅是能够正确工作,更应该易于阅读和维护。因此,即便有简写的方法,有时选择更明确、更清晰的表达方式可能更合适。
希望本文对你理解和使用C语言操作符有所帮助!

http://www.dtcms.com/a/122071.html

相关文章:

  • Vue3中watch监视ref对象方法详解
  • win10开机启动文件夹所在位置
  • MQTT-Dashboard-数据集成
  • JS 箭头函数
  • 深度了解向量引论
  • 【Linux】——文件(下)
  • 基础环境配置
  • 使用Python的Schedule库实现定时任务,并传递参数给任务函数
  • 【教学类-102-06】蛋糕剪纸图案(留白边、沿线剪)05——Python制作1图2图6图
  • linux kernel arch 目录介绍
  • 函数作为参数传递
  • 人力外包解决方案:重构企业用人成本的最优配置
  • VUE中的pinia
  • 使用切面的权限注解,可以重复修饰同一个接口
  • vue3腾讯云直播 前端拉流(前端页面展示直播)
  • Green-AI-Resources开源程序是用于环境可持续 AI 开发和部署的精选研究、工具和最佳实践集合
  • centos-LLM-生物信息-BioGPT安装
  • RecyclerView 和 ListView从 设计理念、性能优化 和 扩展能力 三个维度展开分析
  • 基于开源 AI 大模型 AI 智能名片 S2B2C 商城小程序的京城首家无人智慧书店创新模式研究
  • 编码常见的 3类 23种设计模式——学习笔记
  • python处理excel文件
  • 127.0.0.1本地环回地址(Loopback Address)
  • LeetCode 相交链表题解:双指针的精妙应用
  • 我的NISP二级之路-04
  • 系统分析师(二)--操作系统
  • CD24.【C++ Dev】类和对象(15)初始化列表(下)和对象隐式类型转换
  • 深入理解Spring是如何解决循环依赖的
  • [250409] GitHub Copilot 全面升级,推出AI代理模式,可支援MCP | Devin 2.0 发布
  • 数据库管理工具实战:IDEA 与 DBeaver 连接 TDengine(一)
  • Vue2-实现elementUI的select全选功能