前言
本章将对C语言操作符进行深度的讲解,将每种操作符都单独拿出来精讲。最后添加了些简单的练习题,并配有详细解析。
一、算术操作符
0x00 概览
📌 注意事项:
① 除了 % 操作符之外,其他的几个操作符都可以作用于整数和浮点数;
② 对于 / 操作符,如果两个操作数 都为整数 ,执行整数除法;
③ 对于 / 操作符,只要有浮点数出现 ,执行的就是浮点数除法;
④ 对于 % 操作符的两个数 必须为整数;
0x01 整数除法
📚 定义:对于 / 操作数,如果两个操作数都为整数,执行整数除法;
❓ 整数除法:即一个整数除以另一个整数结果为只保留整数;
💬 代码演示:
int main() { int a = 5 / 2; // 5÷2 = 商2余1 printf("a = %d\n", a); // 👈 输出的结果是什么? return 0; }
🚩 运行结果: a = 2
0x02 浮点数除法
📚 定义:只要有浮点数出现,执行的就是浮点数除法;
❓ 浮点数除法:结果会保留小数部分( 给定对应的%前提下 );
💬 代码演示:
int main() { double a = 5 / 2.0; // 5÷2 = 2.5,有1个浮点数,条件就成立,执行浮点数除法 printf("a = %lf\n", a); // 👈 输出的结果是什么? return 0; }
🚩 运行结果: a = 2.500000
0x03 取模操作符
📚 定义:取模运算即 求两个数相除的余数 ,两个操作数必须为非0整数;
📌 注意事项:
① 两个操作数必须为整数;
② 两个操作数均不能为0(没有意义);
💬 代码演示:
int main() { int a = 996 % 10; // 996 mod 10 = 6 int b = 996 % 100; // 996 mod 100 = 96 printf("%d\n", a); printf("%d\n", b); return 0; }
🚩 运行结果:6 96
❌ 错误演示:
int main() { double a = 5 % 2.0; // ❌ 操作数必须为整数 printf("a = %lf\n", a); return 0; }
🚩 运行结果:error: invalid operands to binary % (have 'int' and 'double')
int main() { int a = 2 % 0; // ❌ 操作数不能为0 printf("%d\n", a); return 0; }
🚩 运行结果:warning: division by zero [-Wdiv-by-zero]
0x04 整除和浮点除的区分
💬 代码演示:我们想得到 1.2
int main() { int a = 6 / 5; printf("%d\n", a); return 0; }
🚩 运行结果: 1 ( 但是运行结果为1 )
❓ 难道是因为我们用的是 %d 打印的原因吗?
int main() { float a = 6 / 5; printf("%f\n", a); return 0; }
🚩 运行结果: 1.000000 ( 仍然不是想要的1.2,运行结果为1.000000 )
(气急败坏,无能狂怒)
💡 解析:其实问题不在于存到a里能不能放的下小数的问题,而是 6 / 5 得到的结果已经是为1了(执行的是整除);
🔑 解决方案:把6改成6.0,或把5改成5.0,也可以都改,让它执行浮点数除法;
int main() { float a = 6 / 5.0; printf("%f\n", a); return 0; }
🚩 运行结果: 1.200000
❓ 虽然代码可以运行,但是编译器报了一个 warning,让我们来瞅瞅是咋回事:
🔑 解析:直接写出的这个数字(6.0或5.0),编译器会默认认为它是 double 类型
那么计算后a的结果也会是 double 类型(双精度浮点数);
如果双精度浮点数的值放到一个单精度浮点数里的话,可能会丢失精度,
好心的编译器就发出了这样的一个警告,这个是正常的;
💡 如果你不想看到这样的警告,你可以这么做:
int main() { float a = 6.0f / 5.0f; // 👈 “钦定” 为float单精度浮点数 printf("%f\n", a); return 0; } int main() { double a = 6.0 / 5.0; // 👈 改成double printf("%lf\n", a); return 0; }
📚 关于精度丢失的现象:
① 有效数字位数超过7位的时候,将会四舍五入,会丢失较多精度;
② 在运行较大数值运算的时候,将有可能产生溢出,得到错误的结果;
二、移位操作符
0x00 概览
📚 概念: 移位操作符分为 "左移操作符" 和 "右移操作符" ;
📌 注意事项:
① 移位操作符的 操作数必须为整数;
② 对于运算符,切勿移动负数位(这是标准为定义的行为);
③ 左移操作符有乘2的效果,右移操作符有除2的效果(左乘2,右除2);
0x01 左移操作符
📚 移位规则:左边丢弃,右边补0 ;(左边的数给👴爬,至于爬多远,还要看操作数是多少)
💬 代码演示:
int main() { int a = 2; int b = a << 1; // 将a的二进制位向左移动1位; printf("b = %d\n", b); // 4 (左移操作符有乘2的效果) /* 00000000000000000000000000000010 0|000000000000000000000000000010+0 (左边丢弃,右边补0) */ return (0); }
🚩 运行结果: b = 4
🔑 图解左移操作符:
0x02 右移操作符
📚 移位规则:两种移位规则;
① 算术右移:右边丢弃,左边补原符号位(通常为算术右移);
② 逻辑右移:右边丢弃,左边补0;
📌 注意事项:
① C编译器中默认为算术右移,如果是 signed 有符号类型时,需要注意;
② 使用 unsigned 无符号类型时,算术右移和逻辑右移的结果是一样的;
int main() { int a = 10; int b = a >> 1; // 把a的二进制位向右移动一位 printf("b = %d\n", b); // 5 (右移操作符有除2的效果) /* 00000000000000000000000000001010 0+0000000000000000000000000000101|0 */ return 0; }
🚩 运行结果: b = 5
🔑 解析: 为了搞懂什么是算术右移,什么是逻辑右移,我们不得不了解整数的二进制表示方式:
0x03 整数的二进制表示方式(初步了解)
📚 负数-1要存放在内存中,内存中存放的是二进制的补码;
📌 整数的二进制表示形式(原反补):
① 原码:直接根据数值写出的二进制序列,即为原码;
② 反码:原码的符号位不变,其他位置按位取反,即为反码(如果不知道什么是按位取反,后面会讲);
③ 补码:反码 + 1,即为补码; (内存中存放的是补码)
📜 -1 的原码、反码、补码:
💬 此时回到上述问题,如果右移时采用逻辑右移:
int main() { int a = -1; int b = a >> 1; printf("b = %d\n", b); return 0; }
🚩 运行结果: b = -1
🔑 图解逻辑右移与算数右移:
❌ 错误演示:操作数不能是负数!
int main() { int num = 10; num >> -1; // ❌ a<<1 ?? 垃圾代码 return 0; }
🚩 运行结果: warning: right shift count is negative [-Wshift-count-negative]
三、位操作符
0x00 概览
📚 位操作符:按位与、按位或、按位异或;
📌 注意事项:位操作符的 操作数必须为整数;
0x01 按位与 &
📚 定义:按2进制按位与,只有对应的两个二进位都为1时,结果位才为1;(必须都为真,结果才为真)
💬 代码演示:按位与的用法
int main() { int a = 3; int b = 5; int c = a & b; // a和b都为真 printf("%d", c); return 0; }
🚩 运行结果: 1
0x02 按位或
📚 定义:只要对应的两个二进位有一个为1时,结果位就为1;(只要有一个为真,结果就为真)
💬 代码演示:按位或的用法
int main() { int a = 0; int b = 5; int c = a | b; // a和b有一个为真 printf("%d\n", c); return 0; }
🚩 运行结果: 5
int main() { int a = 0; int b = 0; int c = a | b; // a和b都为假 printf("%d\n", c); return 0; }
🚩 运行结果: 0
0x03 按位异或 ^
📚 定义:相同为0,相异为1;(上下相同就为假,不同为真)
💡 巧记:觉得按位异或不好记? 试着这么记 👇
" 这对恋人是异性恋吗?是回1,不是回0 " 0 1 是, 1 0 是, 1 1 不是, 0 0 不是;
※ 异或:a⊕b = (¬a ∧ b) ∨ (a ∧¬b) 如果a、b两个值不相同,则异或结果为1,反之结果为0;
💬 代码演示:按位异或的用法
int main() { int a = 3; int b = 5; int c = a ^ b; // a和b不同 printf("%d\n", c); return 0; }
🚩 运行结果: 6
int main() { int a = 3; int b = 3; int c = a ^ b; // a和b相同 printf("%d\n", c); return 0; }
🚩 运行结果: 0
0x04 位操作符的应用
📃 面试题:交换两个 int 变量的值,不能使用第三个变量;
(即a=3,b=5,交换之后a=5,b=3)
1. 临时变量法 - 该题禁止了此方法,但是在工作中建议使用该方法;
int main() { int a = 3; int b = 5; printf("交换前: a = %d, b = %d\n", a, b); int tmp = a; // 创建一个临时变量,存放a a = b; // a变为b b = tmp; // b变为原来的a printf("交换后: a = %d, b = %d\n", a, b); return 0; }
🚩 运行结果: 交换前: a = 3, b = 5;交换后:a=5, b=3
2. 加减交换法 - 存在缺陷:可能会溢出(超过整型的存储极限)
int main() { int a = 3; int b = 5; printf("交换前: a = %d, b = %d\n", a, b) a = a + b; b = a - b; a = a - b; printf("交换后: a = %d, b = %d\n", a, b); return 0; }
🚩 运行结果: 交换前: a = 3, b = 5;交换后:a=5, b=3
🔑 解析:第一步: 3 + 5 = 8,第二步: 8 - 5 = 3,第三步: 8 - 3 = 5,此时,a = 5, b = 3 ;
3. 异或交换法 - 缺点:可读性差,执行效率低下;
int main() { int a = 3; int b = 5; printf("交换前: a = %d, b = %d\n", a, b); a = a ^ b; b = a ^ b; a = a ^ b; printf("交换后: a = %d, b = %d\n", a, b); return 0; }
🚩 运行结果: 交换前: a = 3, b = 5;交换后:a=5, b=3
🔑 解析:
💬 编写代码实现:求一个整数存储在内存中的二进制中1的个数
1. 一般解法 - 模除
int main() { int num = 0; int count = 0; scanf("%d", &num); /* 统计num的补码中有几个1 */ while(num != 0) { if(num % 2 == 1) { count++; } num = num / 2; } printf("%d\n", count); return 0; }
🚩 运行结果: (假设输入3) 2
🔑 解析:
2. 移位操作符 + 按位与 结合的方式解决
💭 思路:
① 利用 for 循环,循环32/64次;
② 每次 if 判断,将 num 右移 i 位的结果与 1 按位与,为真则说明为1,count++;
③ 如果为假,进入下一次循环,最后打印出 count 即可;
int main() { int num = 0; int count = 0; scanf("%d", &num); int i = 0; /* 32位系统,至少循环32次 */ for(i=0; i<32; i++) { if( ((num >> i) & 1) == 1 ) // 如果num右移i位的结果和1按位与,为真 count++; } printf("%d\n", count); return 0; }
🚩 运行结果: (假设输入3) 2