一、算数操作符
+ // 加 - // 减 * // 乘 / // 除 % //取模
程序清单:
#include <stdio.h> int main() { int a = 10 / 3; printf("%d\n", a); float b1 = 10.0 / 3; // double / int printf("%f\n", b1); float b2 = 10 / 3.0; // int / double printf("%f\n", b2); int c = 10 % 3; printf("%d\n", c); return 0; }
输出结果:
注意事项:
① 对于除法操作符,如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
② 观察第二个输出结果,实际上由于 double / int,所以产生的是 double 类型,那么在以格式化 "%f " 输出时,就会发生自 double 向 float 截断。(C语言 默认使用 double 类型)
③ 取模操作符的两个操作数必须为整数,返回的是整除之后的余数。
二、移位操作符
<< // 左移 >> // 右移
注意:
① 移位操作符针对的是数据在内存中的二进制补码。
② 移位操作符的操作数只能是整数。
int a = 3 << 1 // √ int b = 3.5 << 1 // error int c = 3 << 1.5 // error int d = -3 << 1 // √
1. 数据在内存中的存储
计算机在存储数据的时候是以二进制存储的。二进制有多少位,根据数据的类型决定。比如 int 类型,即 4 字节,即 32 位,那么就有 32 个 0或1 的二进制数据。
① 整数的二进制有三种形式:原码、反码、补码。正整数的原、反、补码是相同的;但负整数的原、反、补码则需要计算。(原码符号位不变,其他位按位取反即可变成反码;反码再 +1 即可变成补码)
② 最终,整数在内存中存储的是补码的二进制。
③ 最高位表示符号位,0表示正号,1表示负号。在原码、反码、补码的转换过程中,符号位不能改变。
④ printf 格式化输出的是数据的原码。
2. 左移操作符
程序清单:
#include <stdio.h> int main() { int a1 = 5; int b1 = a1 << 1; printf("a = %d, b = %d\n", a1, b1); // 5, 10 int a2 = -5; int b2 = a2 << 1; printf("a = %d, b = %d\n", a2, b2); // -5, -10 return 0; }
输出结果:
分析左移的过程:
5 << 1,5 的原、反、补码相同。
-5 << 1,左移操作符对 -5 的补码进行操作。
总结:
① 左移操作符相当于为原数据乘以 2.
② 左移对数据的补码的二进制进行操作:左边丢弃,右边补0.
③ 左移不会对原数据进行直接改变。
如下:a 左移过后,把值赋给了 b,则 b 变成了 10,但 a 还是 5.
int a = 5; int b = a << 1; // a = 5, b = 10
3. 右移操作符
程序清单:
#include <stdio.h> int main() { int a1 = 5; int b1 = a1 >> 1; printf("a = %d, b = %d\n", a1, b1); // 5, 2 int a2 = -5; int b2 = a2 >> 1; printf("a = %d, b = %d\n", a2, b2); // -5, -3 return 0; }
输出结果:
分析右移的过程:
5 >> 1,5 的原、反、补码相同。
-5 >> 1,右移操作符对 -5 的补码进行操作。
总结:
① 针对于正整数时,右移操作符相当于为原数据除以 2;针对于负整数时,不确定。
② 右移对数据的补码的二进制进行操作。它分为两种情况。
a. 算数右移:右边丢弃,左边补原符号位。
b. 逻辑右移:右边丢弃,左边补0.
一个程序到底是算数右移还是逻辑右移,取决于编译器的使用,例如我上面的程序就是放在 VS 底下运行的,所以它就采取了算数右移,我的分析过程也是如此。
③ 同样地,右移不会对原数据进行直接改变。
三、位操作符
& // 按位与 | // 按位或 ^ // 按位异或
注意:
① 位操作符同样是针对的是数据在内存中的二进制补码。
② 位操作符的操作数只能是整数。
1. 按位与
& 规则:两个位都为1,则结果为1;其中一位为0,则结果为0.
#include <stdio.h> int main() { int a = 3; int b = -5; int c1 = a & b; printf("%d\n", c1); // 3 return 0; } //00000000 00000000 00000000 00000011 -> 3的原、反、补码 //10000000 00000000 00000000 00000101 -> 5的原码 //11111111 11111111 11111111 11111010 -> 5的反码 //11111111 11111111 11111111 11111011 -> 5的补码
2. 按位或
| 规则:两个为都为0,则结果为0;其中一位为1,则结果为1.
#include <stdio.h> int main() { int a = 3; int b = -5; int c2 = a | b; printf("%d\n", c2); // -5 return 0; }
3. 按位异或
^ 规则:同为0;异为1.
#include <stdio.h> int main() { int a = 3; int b = -5; int c3 = a ^ b; printf("%d\n", c3); // -8 return 0; }
异或的两个规律:
a ^ a = 0 0 ^ a = a
位操作符的应用
应用1
写一个程序,用来交换两个数。
方法一:
#include <stdio.h> int main() { int a = 3; int b = 5; printf("%d, %d\n", a, b); int tmp = a; a = b; b = tmp; printf("%d, %d\n", a, b); return 0; }
方法二:
#include <stdio.h> int main() { int a = 3; int b = 5; printf("%d, %d\n", a, b); a = a + b; b = a - b; // a+b-b => b = a a = a - b; // a+b-a => a = b printf("%d, %d\n", a, b); return 0; }
方法三:
#include <stdio.h> int main() { int a = 3; int b = 5; printf("%d, %d\n", a, b); a = a ^ b; b = a ^ b; // a^b^b => a^0 => b = a a = a ^ b; // a^b^a => 0^b => a = b printf("%d, %d\n", a, b); return 0; }
统一输出结果:
总结:
① 方法一是创建一个新的变量来实现两数交换的,它最常用、效率高、可读性高。方法二和方法三则没有创建新的变量,虽然看似更高效,但也带来了缺点。
② 方法二,我们知道 int 类型是有范围的,当两数相加相减时超出了 int 类型的范围,就会产生意想不到的截断效果,所以在极端的情况下,这并不合理。
③ 方法三,异或本身对于操作数的要求就是必须为整数,所以对于两个浮点数的交换,也并不合理。
④ 综上所述,如果不是面试问到或者题目问到这样的两数交换,我们还是采用方法一,因为程序要么错,要么对,不能模棱两可。
应用2
写一个程序,求一个整数存储在内存中的二进制中1的个数。
#include <stdio.h> int main() { int a = 13; int count = 0; for (int i = 0; i < 32; i++) { int result = (a >> i) & 1; if (result == 1) { // 某一位结果为1,代表是二进制的值为1 count++; } } printf("整数 %d 在内存中二进制为1的个数为:%d\n", a, count); // return 0; }
输出结果:
思路: 让底层的二进制补码右移的同时,按位与1. 与的结果为 1,则说明当前二进制位是 1.
四、赋值操作符
= += -= *= /= &= ^= |= >>= ><<=
五、单目操作符
! // 逻辑反操作 - // 负值 + // 正值 & // 取地址 sizeof // 操作数的类型长度(以字节为单位) ~ // 对一个数的二进制按位取反 -- // 前置、后置-- ++ // 前置、后置++ * // 间接访问操作符(解引用操作符) (int) // 强制类型转换为int
单目操作符,顾名思义,它只有一个操作数。
sizeof 操作符的使用
程序清单:
#include <stdio.h> void test1(int arr[]) // int* arr { printf("%d\n", sizeof(arr)); } void test2(char ch[]) // char* arr { printf("%d\n", sizeof(ch)); } int main() { int arr[10] = { 0 }; char ch[10] = { 0 }; printf("%d\n", sizeof(arr)); // 40 printf("%d\n", sizeof(ch)); // 10 test1(arr); // 4/8 test2(ch); //4/8 return 0; }
输出结果:(32 位)
总结:
① sizeof 是一个操作符,不是一个函数。
② sizeof 用来求类型 / 变量在内存中储存的大小。
③ sizeof 在操作于数组时,需要明白是针对于整个数组,还是针对于函数接收数组的形参。前者计算的是整个数组内元素所占内存的大小,后者是计算一个指针变量的所占内存的大小。
自增、自减
#include <stdio.h> int main() { int a = 1; int b = a++; // b = a; a = a + 1 int c = ++a; // a = a + 1; c = a printf("%d\n", a); printf("%d\n", b); printf("%d\n", c); return 0; }
输出结果:
注意事项:
① 自增分为前置与后置,++前置表示:先自增,后使用;++后置表示:先使用,后自增。(自减也是如此)
② 自增自减会对当前操作的变量直接生效,也就是说,底层存储的二进制也被修改了。
③ 在日常程序中,自增自减正常使用即可。以前在学校的时候,C语言 期末考试会考那些逻辑非常怪的题目,其中就有多个自增自减放在一起使用的,其实没有必要深究,因为一个好的程序压根就不会那么写。
六、关系操作符
注意在字符串比较的时候,不能使用双等号作为比较,它需要特殊的字符串函数来操作两个字符串。
> >= < <= != // 用于测试“不相等” == // 用于测试“相等”
七、逻辑操作符
&& // 逻辑与 || // 逻辑或
程序清单:
#include <stdio.h> int main() { int i = 0, a = 0, b = 2, c = 3; i = a++ && ++b && c++; int j = 0, x = 1, y = 2, z = 3; j = x++ || ++y || z++; printf("a = %d, b = %d, c = %d\n", a, b, c); printf("x = %d, y = %d, z = %d\n", x, y, z); return 0; }
输出结果:
注意事项:
① 逻辑与表示的 " 两者都 ",所以当前者为否的时候,后面就不计算了。
② 逻辑或表示的 " 两者任意一个 ",所以当前者为真的时候,后面就不计算了。
八、条件操作符
a ? b : c // a 成立,执行 b,否则执行 c
程序清单:
#include <stdio.h> int main() { int a = 3; int b = 5; int c = 0; if (a > b) { c = a; }else { c = b; } printf("%d\n", c); c = a > b ? a : b; printf("%d\n", c); return 0; }
输出结果:
九、逗号表达式
result = exp1, exp2, exp3... // 从左向右依次执行,result 结果为最右边表达式的结果。
程序清单:
#include <stdio.h> int main() { int a = (3, 5, 7); // a = 7 printf("a = %d\n", a); int x = 1; int y = 2; int z = (x > y, x = y + 1, y = x + 1); // z = y printf("x = %d, y = %d, z = %d\n", x, y, z); return 0; }
输出结果:
十、其他操作符
[] // 下标引用操作符 () // 函数调用操作符 . // 结构体变量.成员名 -> // 结构体指针变量->结构体成员
1. 下标引用操作符
程序清单:
#include <stdio.h> int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d\n", arr[5]); printf("%d\n", 5[arr]); // 这样写不会出错,但没有人这么写 return 0; }
输出结果:
注意事项:
在我们平时写出 arr[5] 这样的代码时,看上去很平常,但实际上 [ ] 确实是一个操作符,arr 和 5 是它的两个操作数。
2. 函数调用操作符
swap(a, b); print();
注意事项:
在我们平时写出上面那样的代码时,看上去也很平常,但实际上 () 确实是一个操作符。例如:
第一个 () 有三个操作数,swap、a、b.
第二个 () 只有一个操作数:print.
3. 结构体成员访问操作符
程序清单:
#include <stdio.h> struct Student { char name[20]; // 名字 int age; // 年龄 int studentID; // 学号 }; int main() { struct Student student1 = {"Jack", 18, 32}; struct Student student2 = {"Bruce", 20, 05}; printf("%s %d %d\n", student1.name, student1.age, student1.studentID); struct Student* ps1 = &student1; printf("%s %d %d\n", (*ps1).name, (*ps1).age, (*ps1).studentID); // 先解引用再访问 printf("%s %d %d\n", ps1->name, ps1->age, ps1->studentID); return 0; }
输出结果: