C语言 操作符学习

一、操作符分类

  • 算术操作符
  • 移位操作符
  • 位操作符
  • 赋值操作符
  • 单目操作符
  • 关系操作符
  • 逻辑操作符
  • 条件操作符
  • 逗号表达式
  • 下标引用、函数调用和结构成员

二、算术操作符

 	+		-		*		/		%
  1. 除了 % 操作符外,其他的几个操作符可以作用于整数和浮点数。
  2. 对于 / 操作符,如果两个操作符都是整数,就执行整数除法。而只要有浮点数,就执行浮点数除法。
  3. % 操作符的两个操作符必须为整数。返回的是整除之后的余数。

三、移位操作符

	<<	左移操作符
	>>	右移操作符

左移操作符 移位规则:

左边抛弃、右边补 0

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
	int num = 10;
	printf("num is %d\n", num);
	printf("num<<1 is %d\n", num << 1);
	return 0;
}

右移操作符 移位规则:

首先右移运算分两种:

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

我们拿 -1 来举个例子,看一下逻辑移位和算术移位的区别:

在这里插入图片描述
我们也可以在编译器上写代码试一下,查看一下编译器上默认是那种右移方式。

算术右移的二进制码没有改变,逻辑右移的二进制码发生改变。

	int i = -1;
	printf("%d\n", i >> 1);

警告:对于移位运算符,不要移动负数位,这个标准是未定义的,计算机也不知道怎么操作。

	int i = 1;
	printf("%d\n", i >> -1); //error

四、位操作符

位操作符有:

	&	按位与
	|	按位或
	^	按位异或
	注:它们的操作数必须是整数。

按位与 &

比较两个数据的二进制,二者都为真(1),结果才有1。其中一个是假(0),结果只能是0。

	int a = 3;
	int b = 5;
	int c = a & b;
	//a的二进制位:0011
	//b的二进制位:0101
	//按位与的结果:0001
	printf("%d\n", c); //1

按位或 |

和上面是反过来的。二者其中有个真(1),结果就是1。都是假(0),结果才是0。

	int a = 3;
	int b = 5;
	int c = a | b;
	//a的二进制位:0011
	//b的二进制位:0101
	//按位与的结果:0111
	printf("%d\n", c); //7

按位异或^

按二进制位异或。二者的二进制位相同就是假(0),相异不同就是真(1)。

	int a = 3;
	int b = 5;
	int c = a ^ b;
	//a的二进制位:0011
	//b的二进制位:0101
	//按位与的结果:0110
	printf("%d\n", c); //6

练习题一:

不能创建临时变量(第三个变量),实现两个数据的交换。

方法一:

	int a = 3;
	int b = 5;
	
	a = a + b; //a=8
	b = a - b; //b=8-5
	a = a - b; //a=8-3
	
	printf("%d\n", a);
	printf("%d\n", b);

方法二:异或
相当于先把 a 赋值成 a 和 b 的转换器。b和转换器异或,结果是a;a和转换器异或,结果是b。

	int a = 3;
	int b = 5;
	
	a = a ^ b;	
	b = a ^ b;
	a = a ^ b;

	printf("%d\n", a);
	printf("%d\n", b);

练习题二:

编写代码实现:求一个整数存储在内存中的二进制中 1 的个数。

答案解析:
可以把整数的二进制位的最后一位,和 1 的二进制位进行按位与 & 操作。结果是 1 就表示该二进制位上有 1 。
把上述操作放在循环中就可以实现查找整个二进制的 1 的个数。

缺点就是,一定要循环32次,效率不高。

//编写代码实现:求一个整数存储在内存中的二进制中 1 的个数。
	//把这个整数的二进制位的最后一位,和1进行按位与操作。结果是1就表示该位上有1。
	//可以把二进制放在循环中,比较一次就向右移动一位二进制。
	int i = 0;
	int count = 0;
	int num = -1;
	for (i = 0; i < 32; i++) {
		if( ((num >> i) & 1) == 1) {
			count++;
		}
	}
	printf("%d\n", count);

究极蛇皮之优化:

依次从右向左将 1 取出来,直到为零时跳出循环。相比于第二种方法减少了循环次数。

num-1,相当于取出右边的 1 会导致,前面的高位退位。然后把退位的数据(num-1)和原来的数据(num)取余,把二进制退位导致不相同的真假去掉。

//优化
	int num = 10; //1010
	int count = 0;
	//只要非0,二进制一定至少有一个1。
	while (num) {
		count++;
		
		//num-1导致二进制退位,变成9(1001)。
		//然后10和9按位与,把二者二进制中不同的位数变成0。
		//最后num被赋值为8(1000)。
		num = num & (num - 1);
		
		//8(1000)参与循环,num-1退位变成7(0111)。
		//按位与会导致全部变成0000,num被赋值0,程序结束。
		//至此count累计为2。
	}
	printf("%d\n", count);

五、赋值操作符

简单赋值操作符:

赋值操作符是一个很常用的操作符。

	int weight = 120; //初始化的时候赋值
	weight = 87; //进行重新赋值
	double salary = 1000.0;
	salary = 2000.0;

赋值操作符可以连续使用,例如:

	int a = 10;
	int b = 20;
	int c = 30;
	a = b = c + 1; //连续赋值
	printf("%d\n", a);

其实连续赋值很少用,因为看着不方便,而且不易于调试代码。
相同代码可以执行同样的功能,却又不会有上述的缺点。

	int a = 10;
	int b = 20;
	int c = 30;
	b = c + 1; 
	a = b;
	printf("%d\n", a);

复合的赋值操作符:

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

就是把普通的运算符和赋值符合并简写了,效果都是一样的,而且这样写会更加简洁。

	int x = 10;
	int y = 10;
	x = x + 10;
	y += 10;
	printf("%d\n", x);
	printf("%d\n", y);

六、单目操作符:

单目操作符,就是只能有一个数据参与运算的操作符。
都有什么单目操作符呢?

	!			逻辑反操作
	-			负值
	+			正值
	&			取地址
	sizeof		操作数的类型长度(以字节为单位)
	~			对数据的二进制按位取反
	--			前置、后置--
	++			前置、后置++
	(类型)		强制类型转换

演示代码:

	int a = 10;
	printf("%d\n", !10); //任何非0都为真,对真(10)取反,结果等于假(0)
	printf("%d\n", -10); //就单纯的把数据变成负数

	int* p = &a;	//取地址,需要用指针变量来接收。
	printf("%p\n", p);	//输出地址
	printf("%d\n", *p);	//*p解引用,输出原来的值

	printf("%d\n", sizeof(a)); //a的类型是int,所以sizeof计算出有4个字节
	//还能有不同的格式
	printf("%d\n", sizeof(int));
	printf("%d\n", sizeof a); //可以执行吗?
	printf("%d\n", sizeof int); //可以执行吗?

sizeof 是一个操作符,不是函数!

我们也可以使用 sizeof 来求数组元素的个数:
sizeof(arr)表示数组整体的字节长度(40),sizeof(arr[0])表示数组首元素的字节长度(4)。

	int arr[] = { 1,2,3,4,5 };
	printf("%d\n", sizeof(arr) / sizeof(arr[0]));

sizeof 和数组练习题:

请问程序1,2,3,4处输出结果是多少?

#include <stdio.h>

void test1(int arr[]) {
	printf("%d\n", sizeof(arr)); //3
}
void test2(char ch[]) {
	printf("%d\n", sizeof(ch)); //4
}
int main() {
	int arr[10] = { 0 };
	char ch[10] = { 0 };
	printf("%d\n", sizeof(arr)); //1
	printf("%d\n", sizeof(ch)); //2
	test1(arr);
	test2(ch);

	return 0;
}

运行结果:

答案解析:
1 和 2 很好算,分别是 40 和 10。
3 和 4 输出结果都是 4 或者 8(取决于计算机32/64位系统)。

int 类型在内存中占用 4 个字节,arr 数组创建了 10 个 int 类型空间,那就一共要 40 个字节来存放数据。
char 类型在内存中占用 1 个字节,ch 数组创建了 10 个 char 类型空间,那就一共要 10 个字节来存放数据。

至于 3 和 4,是因为它们由指针变量来接收的数据,sizeof 计算指针变量一般结果都是 4。
我们可以看一下 sizeof 单独计算指针变量的代码:

	int* p = NULL;

	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(p));

结果都是 4。

+ + 和 - - 运算符:

前置 + + 和前置 - - :先让数据自增或者自减,然后再进行操作。

	int a = 10;
	//先对 a 进行自增,然后赋值给 x。a为11,x为11。
	int x = ++a;
	printf("%d\n", a);
	printf("%d\n", x);
	//先对a进行自减,然后赋值给 y。a为10,y为10。
	int y = --a;
	printf("%d\n", a);
	printf("%d\n", y);

后置 + + 和后置 - - :先让数据参加操作,然后再进行自增或者自减。

	int a = 10;
	//先把a的值赋值给x,然后a自行+1。此时a=11,x=10。
	int x = a++;
	printf("%d\n", a);
	printf("%d\n", x);
	//先把a的值赋值给y,然后a自行-1。此时a=10,y=11。
	int y = a--;
	printf("%d\n", a);
	printf("%d\n", y);


七、关系操作符

	>		大于
	>=		大于等于
	<		小于
	<=		小于等于
	!=		不相等
	==		相等

关系运算符比较简单,就是字面意思。不过要注意,编程过程中可能把 == 和 = 写混。


八、逻辑操作符

	&&		逻辑与		需要两边都是真,才是真
	||		逻辑或		两边都是假,才是假

逻辑与 && 和逻辑或 || 的特点:

让我们看一段代码,请问输出结果是多少?

	int i = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++ && ++b && d++;
	printf(" a=%d\n b=%d\n c=%d\n d=%d\n", a, b, c, d);

结果如下:

为什么呢?
因为逻辑与 && 的特点,它只要在表达式中判断第一个值(a++)的结果,如果是假(0),那么它不继续判断(++b,和d++)了。
所以a++被执行判断过一次,后置++,a=1。结果就是1、2、3、4。


我们看一下段代码,请问输出结果是多少?

	int i = 0, a = 0, b = 2, c = 3, d = 4;
	//i = a++ && ++b && d++;
	i = a++ || ++b || d++;
	printf(" a=%d\n b=%d\n c=%d\n d=%d\n", a, b, c, d);

结果如下:

逻辑或 || 。
它先判断a++,发现a是假(因为是后置++,所以a=0先参与运算);就判断下一位++b,发现b是真(因为是前置++,所以b=2先加一再参与运算)。
有一个真就可以不继续判断了,所以d++没有执行到。最终结果是1、3、3、4。

小总结:
通过上面两个例子,我们发现逻辑与 && 和逻辑或 || 有个特点,只要前面一个判断为假/真,逻辑与&&/逻辑真||就会停止判断下去。
逻辑与 && 就好像张三和李四一起才能完成任务,如果前者张三没有去,后者李四去不去都无所谓了(不判断了)。
逻辑或 || 就好像,张三和李四任意一个人就可以完成任务,如果前者张三去了,后者李四去不去都无所谓了。


九、条件操作符

		exp1 ? exp2 : exp3;
exp1:是一个判断表达式,结果为真,就执行exp2;结果为假,就执行exp3。

例题:
这是一个简单的判断大小代码,请问怎么改写成条件表达式?

	int a = 10;
	if (a>5)
		a = 1;
	else 
		a = -1;

改写:注意条件表达式有返回值,需要有个变量接收。

	int a = 10;
	int b = a > 5 ? 1 : -1;

练习题:
使用条件表达式实现找到两个数的较大值。

答案代码:


十、逗号表达式

	exp1, exp2, exp3, ...expN

逗号表达式,就是用括号隔开的多个表达式。
从左往右依次执行,整个表达式的结果是最后一个表达式的结果。

请问下面代码分别输出的结果是多少?

	int a = 1;
	int b = 2;
	int c = (a > b, a = b + 10, a, b = a + 1);
	printf("%d\n", c);
	int a = 3;
	int b = 5;
	int c = 0;
	int d = (c = 5, a = c + 3, b = a - 4, c += 5);
	printf("%d\n", d);

我们通过上面两个题,是不是感觉逗号表达式没什么大用啊!最后面那个表达式才把值赋值给前面的变量。
其实逗号表达式用于代码的优化,使得程序更加美观。

例如:循环外和循环内都有同样的代码要执行。

	a = get_val();
	count_val(a);
	while (a > 0) {
		a = get_val();
		count_val(a);
	}

优化:代码还是一样执行,while判断放在逗号表达式最后也不会报错。

	while (a = get_val(), count_val(a), a > 0) {
	
	}

十一、下标引用、函数调用和结构成员

1. [ ] 下标引用操作符

操作数:一个数组名 + 一个索引值

	int	arr[10]; //创建数组
	arr[9] = 10; //实用下标引用操作符
	[ ] 的两个操作数是 arr 和 9。

2. ( ) 函数调用操作符

接收一个或者多个操作数,
第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

void test1() {
	printf("hehe\n");
}
void test2(const char * str) {
	printf("%s\n", str);
}
int main() {
	test1();  //使用()作为函数调用的操作符
	test2("hello~");  //使用()作为函数调用的操作符
	return 0;
}

3. 访问一个结构的成员

	.结构体  .成员名
	->结构体指针  ->成员名
struct Stu {
	char name[10];
	int age;
	char sex[5];
	double score;
};
void set_age1(struct Stu stu) {
	stu.age = 18;
}
void set_age2(struct Stu* pStu) {
	pStu->age = 18;	//结构成员访问
}
int main() {
	struct Stu stu;
	struct Stu* pStu = &stu;  //结构成员访问

	stu.age = 20;	//结构成员访问
	set_age1(stu);

	pStu->age = 20;	//结构成员访问
	set_age2(pStu);
	return 0;
}

十二、表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。

有些表达式的操作数在求值的过程中,可能需要转换为其他类型。

隐式类型转换

C的整型算术运算,总是至少以默认的整型类型精度来进行的。

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升

什么是整型提升?

表达式的整型运算,要在CPU的相应运算器中执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是 int 的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使是两个 char 类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU,难以直接实现两个 8 bite 字节直接相加运算。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int 或 unsigned int,然后才能送入CPU去执行运算。

	//实例1
	char a,b,c;
	a = b + c;

b 和 c 的值被提升为普通整型,然后再执行加法运算。

加法运算完成之后,结果被截断,然后再存储于 a 中。

如何进行整型提升呢?

整型提升是按照变量的数据类型的符号位来提升的

	//负数的整型提升
	char c1 = -1;
	变量c1的二进制为(补码)中只有 8 个比特位:11111111
	因为 char 为有符号的 char
	所以整型提升,高位补符号位 1 。
	提升之后结果是:
	11111111111111111111111111111111

	//正数的整型提升
	char c2 = 1;
	变量 c2 的二进制为(补码)中只有8个比特位:00000001
	因为 char 为有符号的 char
	所以整型提升,高位补符号位 0。
	提升之后结果为:
	00000000000000000000000000000001

	//无符号整型提升,高位补 0 即可

说了这么多,我们下面看一下例子:

实例1:请问最后输出结果是多少?

int main() {
	char a = 3;
	char b = 127;
	char c = a + b;
	printf("%d\n", c); 
	return 0;
}

实例1解析:
a = 3,在二进制中为 00000011。
b = 127,在二进制中为 01111111。

根据整型提升理论,char类型的 a 和 b 在参与运算要提升至 int 型参与运算。
a就变成00000000000000000000000000000011;
b就提升为00000000000000000000000001111111;
参与运算,结果c为00000000000000000000000010000010。
运算结束,c被截断为char,10000010。

这时你想把二进制转算成整型输出了?其实还早。因为二进制最前面符号位出现了 1,表示这个整型是负数。我们都知道,负数的整型,二进制是补码参与运算,我们要把补码转换成原码才能是最后的结果。
c:补码10000010,补码-1得到反码10000001;符号位不变,其它取反得到原码11111110。

OK(^ o ^)/~,根据原码11111110,我们换算 c 结果是-126。


实例2
请问下列代码最终输出结果是多少?

	char a = 0xb6;
	short b = 0xb600;
	int c = 0xb6000000;
	if (a == 0xb6) {
		printf("a");
	}
	if (b == 0xb600) {
		printf("b");
	}
	if (c == 0xb6000000) {
		printf("c");
	}

因为 a,b 要进行整型提升,所以 a,b 提升后的结果和原本数据不一样,if 判断为假不输出。
c 不需要整型提升,则 if 判断结果为真。
程序最后输出结果是:c


实例3

试着输出一下下列代码,体会一下整型提升~

	char c = 1;
	printf("%u\n", sizeof(c));
	printf("%u\n", sizeof(+c));
	printf("%u\n", sizeof(-c));

代码中 c 没有参与运算的时候,就没有发生整型提升,sizeof(c)的结果是 1 个字节。
当参与运算,例如(+c)或者(-c),就会发生整型提升,所以 sizeof(+c)和 sizeof(-c)结果是 4 个字节。


十二、算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数类型转换为另一个操作数类型,否则不能进行运算。下面的层次体系称为寻常算术转换(类型由大到小排列)。

	long double
	double
	float
	unsigned long int
	long int
	unsigned int
	int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另一个操作数的类型之后,再执行运算。

注意:算术转换要合理,不然会有一些潜在的问题。

	float f = 3.14;
	int num = f;	//转换给一个比自己小的数据类型,会导致精度丢失。

十三、操作符的属性

复杂表达式的求值有三个影响的因素。

  1. 操作符的优先级 -> 就是谁先谁后。
  2. 操作符的结合性
    例如:L-R 的结合性,表示语句从左向右执行。
  3. 是否控制求值顺序
    例如:逻辑或 | |,前面为真,后面就不执行了。

下面举些例子(优先级从高到低):

具体还是要自行百度…


一些问题表达式

	//表达式的求值部分由操作符的优先级决定。
	//表达式1
	a*b + c*d + e*f

表达式中的 * 号确实比 + 号优先级高,计算会比 + 号早。但是不能保证第三个 * 号会比第一个 + 号早参加运算。

所以表达式的计算顺序可能是这样的:

	a*b
	c*d
	a*b + c*d
	e*f
	a*b + c*d + e*f

或者:
	a*b
	c*d
	e*f
	a*b + c*d
	a*b + c*d + e*f

	//表达式2
	c + --c;

同上,操作符的优先级导致自减 - - 的运算在 + 的前面,但是我们没有办法得知,+ 操作符的左操作数的获取,是在右操作数操作之前还是之后。因此,结果是不可预测的,是有歧义的。


	//代码3-非法表达式
	int main()
	{
		int i = 10;
		i = i-- - --i * ( i = -3 ) * i++ + ++i;
		printf("i = %d\n", i);
		return 0;
	}

此段代码更是离谱,在不同编译器中的测试,都是不同结果。

	值  编译器
	—128  Tandy 6000 Xenix 3.2
	—95  Think C 5.02(Macintosh)
	—86  IBM PowerPC AIX 3.2.5
	—85  Sun Sparc cc(K&C编译器)
	—63  gcc,HP_UX 9.0,Power C 2.0.0
	4  Sun Sparc acc(K&C编译器)
	21  Turbo C/C++ 4.5
	22  FreeBSD 2.1 R
	30  Dec Alpha OSF1 2.0
	36  Dec VAX/VMS
	42  Microsoft C 5.1

//代码4
int fun()
{
  	static int count = 1;
  	return ++count;
}
int main()
{
	  int answer;
	  answer = fun() - fun() * fun();
	  printf( "%d\n", answer);//输出多少?
	  return 0;
}

该代码也是有问题的!

我们可以通过操作符的优先级得知,answer = fun() - fun() * fun(); 先算乘法,再算减法。

但是函数的调用先后顺序无法通过操作符的优先级确定!我们不知道哪个 fun() 会先调用,这会导致结果不一致。


//代码5
#include <stdio.h>
int main()
{
	int i = 1;
	int ret = (++i) + (++i) + (++i);
	printf("%d\n", ret);
	printf("%d\n", i);
	return 0;
}

这段代码在 Linux 和 window 环境下的编译器结果都不一致。
相同的代码产生不同结果,这是为什么?

这和汇编代码有点关系,代码段的第一个 + 在执行的时候,第三个 ++ 是否要执行呢?
这是不确定的,因为依靠操作符的优先级和结合性,无法判断第一个 + 和第三个前置 ++ 的先后顺序。

总结:我们写出的代码,如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是有问题的!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值