C语言 操作符
一、算数操作符
+ // 加
- // 减
* // 乘
/ // 除
% //取模
程序清单:
#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
4. 位操作符的应用
应用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;
}
// 输出结果:
// 3
// 1
// 3
注意事项:
① 自增分为前置与后置,++前置表示:先自增,后使用;++后置表示:先使用,后自增。(自减也是如此)
② 自增自减会对当前操作的变量直接生效,也就是说,底层存储的二进制也被修改了。
③ 在日常程序中,自增自减正常使用即可。以前在学校的时候,C语言 期末考试会考那些逻辑非常怪的题目,其中就有多个自增自减放在一起使用的,其实没有必要深究,因为一个好的程序压根就不会那么写。
六、关系操作符
注意在字符串比较的时候,不能使用双等号作为比较,它需要 strcmp 字符串函数来操作两个字符串。
>
>=
<
<=
!= // 用于测试“不相等”
== // 用于测试“相等”
七、逻辑操作符
&& // 逻辑与
|| // 逻辑或
程序清单:
#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); // 5
c = a > b ? a : b;
printf("%d\n", c); // 5
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;
}
// 输出结果:
// 6
// 6
注意事项:
在我们平时写出 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;
}
// 输出结果:
// Jack 18 32
// Jack 18 32
// Jack 18 32