Bootstrap

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;
}

输出结果:

1-1

注意事项:

① 对于除法操作符,如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。

② 观察第二个输出结果,实际上由于 double / int,所以产生的是 double 类型,那么在以格式化 "%f " 输出时,就会发生自 double 向 float 截断。(C语言 默认使用 double 类型)

1-2

③ 取模操作符的两个操作数必须为整数,返回的是整除之后的余数。

二、移位操作符

<<				// 左移
>> 				// 右移

注意:

① 移位操作符针对的是数据在内存中的二进制补码。
② 移位操作符的操作数只能是整数。

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 的原、反、补码相同。

1-3

-5 << 1,左移操作符对 -5 的补码进行操作。

1-4

总结:

① 左移操作符相当于为原数据乘以 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 的原、反、补码相同。

1-5

-5 >> 1,右移操作符对 -5 的补码进行操作。

1-6

总结:

① 针对于正整数时,右移操作符相当于为原数据除以 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的补码

1-7

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;
}

1-8

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;
}

1-9

异或的两个规律

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;
}

统一输出结果:

1-10

总结:

① 方法一是创建一个新的变量来实现两数交换的,它最常用、效率高、可读性高。方法二和方法三则没有创建新的变量,虽然看似更高效,但也带来了缺点。

② 方法二,我们知道 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;
}

输出结果:

2-1

思路: 让底层的二进制补码右移的同时,按位与1. 与的结果为 1,则说明当前二进制位是 1.

2-2

四、赋值操作符

= 
+= 
-= 
*= 
/= 
&= 
^= 
|= 
>>= 
><<=

五、单目操作符

! 					// 逻辑反操作
- 					// 负值
+ 					// 正值
& 					// 取地址
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 位)

2-3

总结:

① 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;
}

输出结果:

2-4

注意事项:

① 逻辑与表示的 " 两者都 ",所以当前者为否的时候,后面就不计算了。
② 逻辑或表示的 " 两者任意一个 ",所以当前者为真的时候,后面就不计算了。

八、条件操作符

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;
}

输出结果:

2-5

十、其他操作符

[]            // 下标引用操作符
()			  // 函数调用操作符
.			  // 结构体变量.成员名
->			  // 结构体指针变量->结构体成员

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
;