c语言分层理解(c语言操作符)

谁都懂的道理,不如直接开干,干就完了!

下面我们介绍的是操作符这一板块的内容,这里简单介绍,如果需要深入理解可以点这个链接"c语言符号深度理解和再认识",这里有一些比较难懂的符号,我已经汇总在了一起。

1. 操作符分类

操作符有这么几类:

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

下面对这些操作符一一细细道来。

2. 算术操作符

+ - * / %

这五个都是常见的算术操作符,但是有两个符号需要特别注意:/和%。
看图得结论:


1.除了%操作符,其他的四个操作符可以用于整数和浮点数。




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




3.%操作符的两个操作数必须是整数,返回的是整除之后的余数。


3. 移位操作符

了解移位操作符,首先必须了解一下权值是什么?原码、反码、补码又是什么,怎么得来的?下面一步步展开。

<< 左移操作符
>> 右移操作符
注意:移位操作符的操作数只能是整数。

3.1 初步了解权值是什么

举一个例子就明白:
一个数字10用二进制表示为1010,所表示的意思是:10=1* 23 +0* 22 +1* 21+ 0*20.这里的1010的第一个1的权值就是23。第二个0的权值就是22。用八进制表示10,就是10=12,这里12中1的权值就是81,2的权值是80。下面我们再来了解原反补。

3.2 初步了解原码、反码、补码

一个数分为正数和负数,计算机中是不能识别负号的,所以计算机用补码的形式存储正数和负数,规定最高符号位对数分为正数和负数,正数最高符号位为0,负数为1,这样区分正数和负数。正数的原码、反码、补码都是一样的,负数的原码、反码、补码要进行运算(其中补码=反码+1,反码=原码按位取反(除了最高符号位)。

正数的最高符号位是0,负数的最高符号位是1。

举个例子:

正数10(因为是int类型的,所以有4个字节,也就是32个比特位)
00000000000000000000000000001010 - 10的原码
00000000000000000000000000001010 -10的反码
00000000000000000000000000001010 - 10的补码

正数的原码反码补码都相同

负数-10(32个比特位)
10000000000000000000000000001010 - -10的原码
11111111111111111111111111110101 - -10的反码
11111111111111111111111111110110 - -10的补码

负数的补码=反码+1,反码=原码按位取反(除了最高符号位不变)

3.3 左移操作符

移位规则:左边抛弃,右边补0。

先看现象,在分析:

分析:

10的补码(原码)是:
00000000000000000000000000001010
左移:
00000000000000000000000000010100 = 20(原码=补码)(因为是正数)

再来看一个奇怪的现象:

我们发现左移操作符可以对正数或者负数乘以2。

3.4 右移操作符

移位规则:右移运算分为两种:1.逻辑移位:左边用0填充,右边丢弃;2.算术移位:左边用原来该值的符号位填充,右边丢弃。其中编译器到底用哪一种呢?这是取决于编译器本身,绝大多数是用的算术移位。

还是一样先看现象再分析:

10的补码(原码)是:
00000000000000000000000000001010
右移:
00000000000000000000000000000101 = 5(原码=补码)

再来看一个现象:

我们发现右移操作符可以对正数或者负数除以2。其中是取整方式是向0取整,不懂向取整可以点击文章开始的链接。

注意:对于移位操作符,不要移动负数位,这个是标准为定义。
这段话什么意思?看这个:

4. 位操作符

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

符号用法;

//&-按位与
//比特位同时为1时才为1
#include <stdio.h>
int main()
{
    
    
	int a = 10;
	//00000000000000000000000000001010(原反补相同)
	int b = -3;
	//10000000000000000000000000000011(原码)
	//11111111111111111111111111111100(反码)
	//11111111111111111111111111111101(补码)
	int c = a & b;
	//00000000000000000000000000001010(a)
	//11111111111111111111111111111101(b)
	//00000000000000000000000000001000=8(补码)(最高符号位是0,所以是正数,补码=原码)
	//
	printf("%d\n", c);
	return 0;
}
//|-按位或
//比特位同时为0时才为0
#include <stdio.h>
int main()
{
    
    
	int a = 5;
	//00000000000000000000000000000101 (5的原码也是补码)
	int b = -2;
	//10000000000000000000000000000010 (-2的原码)
	//11111111111111111111111111111101(-2的反码)
	//11111111111111111111111111111110(-2的补码)
	int c = a | b;
	//-2的补码|5的补码
	//11111111111111111111111111111111 (c的补码)
	//11111111111111111111111111111110(反码)
	//10000000000000000000000000000001=-1 (原码)
	printf("%d\n", c);
	return 0;
}
//按位异或
//比特位相同为0,不同为1
#include <stdio.h>
int main()
{
    
    
	int a = -8;
	//10000000000000000000000000001000 (-8的原码)
	//11111111111111111111111111110111(-8的反码)
	//11111111111111111111111111111000 (-8的补码)
	int b = 6;
	//00000000000000000000000000000110(6的原码,也是补码)
	int c = a ^ b;
	//-8的补码^6的补码
	//11111111111111111111111111111110(c的补码)
	//11111111111111111111111111111101(反码)
	//10000000000000000000000000000010=-2(原码)
	printf("%d\n", c);
	return 0;
}

4.1 练习:不能创建临时变量(第三个变量),实现两个数的交换

方法一(用按位异或来解决):

#include <stdio.h>
int main()
{
    
    
	int a = 10;
	//00000000000000000000000000001010(原码=补码)
	int b = 20;
	//00000000000000000000000000010100(原码=补码)
	a = a ^ b;
//a^b=00000000000000000000000000011110=30=a
	b = a ^ b;
	//b=0000000000000000000000011110(a)^b=00000000000000000000000000001010=10;
	a = a ^ b;
	//a=00000000000000000000000000011110(a)^00000000000000000000000000001010(b)=20(a)
	return 0;
}

方法二(用加法):

int main()
{
    
    
	int a = 3;
	int b = 5;
	int c = 0;
	c =a + b;//c=8
	a =c - a;//a=5
	b =c - a;//b=3
	return o;
}

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

5. 赋值操作符

这里再提一次:初始化和赋值的区别:初始化是创建变量的同时给定一个值,赋值时创建变量后对其给定一个值。

如:

#include <stdio.h>
int main()
{
    
    
    int a;
    a = 20;//这是赋值
    int b = 10;//这是初始化
    return 0;
}

5.1 连续赋值问题

#include <stdio.h>
int main()
{
    
    
	int a = 10;
	int b = 5;
	int c = 0;
	c = a = b + 8;
	printf("%d\n", c);
	return 0;
}//最终输出13
#include <stdio.h>
int main()
{
    
    
	int a = 10;
	int b = 5;
	int c = 0;
	a = b + 8;
	c = a;
	printf("%d\n", c);
	return 0;
}最终输出13

通过上述两段代码可以看出:
连续赋值的阅读体验极差,往往给人一种不想看的感觉。

5.2 复合赋值符

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

看一例子吧:

#include <stdio.h>
int main()
{
    
    
    int c = 0;
    c = c + 10;
   //c += 10;(相当于c = c + 10)
    return 0;
}

6. 单目操作符

6.1 单目操作符介绍

!(逻辑反操作)
-(负值)
+(正值)
&(取地址)
sizeof(操作数的类型长度(以字节为单位)
~(对一个数的二进制按位取反)
–(前置、后置–)
++(前置、后置++)
*(间接访问操作符(解引用操作符))
(类型)(强制类型转换)


这里对!(逻辑反操作)解释一下:

#include <stdio.h>
int main()
{
    
    
	int i = 0;
	if (!i)//这里的意思是明确的真假
	{
    
    
		printf("hehe\n");
	}
	if (i == 0)//而这里的i在判断条件中相当于一个数值
	{
    
    
		printf("haha\n");
	}
	return 0;
}

对&(取地址操作符)和*(解引用操作符)解析:

    int arr[10];
    &arr;//取的是整个数组的地址
    //也可以&函数;

提一句:首元素不表示首元素地址的两种情况:

  1. sizeof(数组名),这里的数组名是整个数组的大小空间。
  2. &数组名,这里的数组名是整个数组的大小。
    int a = 10;
    int *p = &a;
    *p = 20;
    //最终a别改为20

通常解引用操作符于指针有关,后期在对其进行理解,这里不在对其过多理解。

对sizeof操作符进行解释(直接看现象):

这里在打印指针的时候不管是什么类型在32位平台下都是4个字节的大小,在64位平台下都是8个字节的大小。

再看一例;

这里错误的原因是sizeof的()应该输入表达式。

再看一例:

这里我们观察到sizeof(s = a + 2)=2;说明计算的是s的空间大小。

6.2 sizeof和数组

还是一样通过现象看本质:

再看一例:

函数传参,传数组名,这里的数组名就是地址,所以在32为平台下是4个字节的大小。

对~(对一个数的二进制按位取反)解析:

#include <stdio.h>
int main()
{
    
    
	int a = 10;
	//a=00000000000000000000000000001010
	//~a=11111111111111111111111111110101
	//最高符号位是1,所以是补码
	//11111111111111111111111111110100(~a的反码)
	//10000000000000000000000000001011=-11(~a的原码)
	printf("%d\n", ~a);
	return 0;
}

对~这个符号了解后再来看个实例:

#include <stdio.h>
//把一个数的第n二进制位改为1,在改为0.
int main()
{
    
    
	int a = 10;
	//00000000000000000000000000001010
	//假设对第四二进制位修改
	//就是用1对其进行左移操作并用按位或进行操作
	int n = 0;
	scanf("%d", &n);
	///把一个数的第n二进制位改为1
	a = a | (1 << (n - 1));
	printf("%d\n", a);
	//把一个数的第n二进制位改为0
	a = a & ~(1 << (n - 1));
	printf("%d\n", a);
	return 0;
}

对前置++和后置++以及前置–和后置–解析:

int main()
{
    
    
	int a = 3;
	int b = ++a;
	//b = ++a等价于a=a+1,b=a;(先自身++后使用))
	printf("b=%d\n", b);//b=4
	return 0;
}
int main()
{
    
    
	int a = 3;
	int c = a++;
	//c = a++等价于c=a,a=a+1;(先使用后自身++)
	printf("c=%d\n", c);//c=3
	return 0;
}
int main()
{
    
    
	int g = 2;
	int d = --g;
	//int d = --g;//等价于g=g-1,d=g;(先自身--后使用)
	printf("d=%d\n", d);//d=1
}
int main()
{
    
    
	int g = 2;
	int e = g--;
	//e = g--等价于e=g,g=g-1;(先使用后自身--)
	printf("e=%d\n", e);//e=2
}

对(类型)强制转化类型符号解析:

#include <stdio.h>
int main()
{
    
    
	float a = 3.14f;
	int b = (int)a;//float类型强制转化为int类型
	return 0;
}

7. 关系操作符

>,>=,<,<=,!=,==

注意:编译过程中==和=不小心写错,导致错误。

8. 逻辑操作符

&& 逻辑与
|| 逻辑或

逻辑操作符就是判断真假,非0为真,0为假。

看例题:

#include <stdio.h>
int main()
{
    
    
	int i = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++ && ++b && d++;
	printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
	return 0;
}//最终输出结果是1234

这里的逻辑与发生短路,逻辑与要从左到右依次执行,碰到0就不用在执行其后面的,这里a++使用的时候是0,在对其进行本身++操作,所以打印a=1,a++使用的时候是0,逻辑与操作在碰到假是时,其后面不再执行,所以后面b,c,d的值都没有变。


#include <stdio.h>
int main()
{
    
    
	int i = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++||++b||d++;
	printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
	return 0;
}最终输出结果是1334

这里也是短路问题,从左到右一次执行,逻辑或在碰到真时不在执行其后面表达式,所以a++使用时候是0,然后就要再看++b,而++b是非0为真,所以不在看d++表达式。

总结:逻辑与符号和逻辑或符号都是从左到右依次执行,而逻辑与操作符号是看表达式是否为假,为假就不再看其后的表达式,为真则继续执行,知道碰到为假的表达式。逻辑或符号是只要碰到为真,则不在执行。

9. 条件操作符

exp1 ? exp2 : exp3

有时if else语句不如条件操作符简洁。

if(a>1)
{
    
    
    b=3;
}
else
{
    
    
    b=1;
}

上面的这段if else语句其实可以用条件操作符,b=(a>1?3:1);

10. 逗号表达式

exp1,exp2,exp3,exp4…

逗号表达式从左到右依次执行,整个表达式结果是最后一个表达式结果。

#include <stdio.h>
int main()
{
    
    
	int a = 1;
	int b = 2;
	int c = (a > b, a = b + 10, a, b = a + 1);
	printf("%d\n", c);
	return 0;
}//最终输出13.

11. 下标引用、函数调用、结构成员

11.1下标引用操作符([])

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

11.2函数调用操作符

#include <stdio.h>
void test1()
{
    
    
	printf("hehe\n");
}
void test2(const char* str)
{
    
    
	printf("%s\n", str);
}
int main()
{
    
    
	test1(); //实用()作为函数调用操作符。
	test2("hello bit.");//实用()作为函数调用操作符。
	return 0;
}

11.3结构成员

12. 表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。

12.1隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型
提升。

整型提升的意义:

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purposeCPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
也就是char类型和short类型在运算时首先都要转化为int类型的再进行计算。
无符号整型提升,高位直接补0,有符号高位补1。

例如:

#include <stdio.h>
int main()
{
    
    
	char a = 5;
	//00000000000000000000000000000101=5
	//CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度
	//所以是32位,但是a变量是char类型的所以发生截断
	//00000101=a  
	char b = 126;
	//00000000000000000000000001111110=126
	//01111110=b(发生截断)
	char c = a + b;
	//整型提升
	//(a的最高符号位是0,所以在补齐的时候补0)
	//(b的最高符号位是0,所以在补齐的时候补0)
	//a+b=00000000000000000000000000000101+00000000000000000000000001111110
	//00000000000000000000000010000011=c
	//10000011=c(发生截断)
	printf("%d\n", c);
	//%d是十进制的方式打印有符号整数
	//c的最高符号位是1,所以在补齐的时候补1
	//11111111111111111111111100000011(补码)
	//11111111111111111111111100000010(反码)
	//10000000000000000000000011111101=-125(原码)
	return 0;
}

再看一例:

#include <stdio.h>
int main()
{
    
    
	char a = 0xb6;
	//10110110=a
	//整型提升后
	//11111111111111111111111110110110(a最高符号位是1,所以补1)
	short b = 0xb600;
	//1011011000000000=b
	//整型提升后
	//11111111111111111011011000000000
	int c = 0xb6000000;
	if (a == 0xb6)//会发生整型提升不会再等于0xb6
		printf("a");
	if (b == 0xb600)//会发生整型提升
		printf("b");
	if (c == 0xb6000000)//int类型不会发生整型提升
		printf("c");
	//最终输出c
	return 0;
}

再看一例:

#include <stdio.h>
int main()
{
    
    
	char c = 1;
	printf("%zu\n", sizeof(c));
	printf("%zu\n", sizeof(+c));
	printf("%zu\n", sizeof(-c));
	//printf("%zu\n",sizeof(c+1));//最终输出也是4.
	return 0;
}
//只要short和char类型的运算,就会发生整型提升。

输出结果:

c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以sizeof(+c) 是4个字节.表达式 -c也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof© ,就是1个字节.

12.2算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。

float f = 3.14;//由double类型转换为float
int num = f;//由float类型转换为int类型

由高精度类型向低精度类型转化会有精度丢失问题的出现。在这里注意只有short类型和char类型转换为int类型时才是整型提升(原因是CPU内整型运算器(ALU)的操作数的字节长度,一般就是int的字节长度,同时也是CPU的通用寄存器的长度),而int类型转化为double等高精度不叫做整型提升,运算时也不遵循整型提升的运算规则。

举一个int类型转换为double和float类型的例子:

可见只是变为了浮点数并补0.

猜你喜欢

转载自blog.csdn.net/m0_46343224/article/details/126111140