第十六章 操作符(详解和灵活运用)

C语言学习之路

第一章 初识C语言
第二章 变量
第三章 常量
第四章 字符串与转义字符
第五章 数组
第六章 操作符
第七章 指针
第八章 结构体
第九章 控制语句之条件语句
第十章 控制语句之循环语句
第十一章 控制语句之转向语句
第十二章 函数基础
第十三章 函数进阶(一)(嵌套使用与链式访问)
第十四章 函数进阶(二)(函数递归)
第十五章 数组进阶
第十六章 操作符(详解及注意事项)



前言

在第六章中我们已经对操作符有了一个基础性的了解,那么本章中将对操作符进行详细地讲解并且更注重对细节的阐述。希望大家能够认真看完。


一、算数操作符

+ //加法
- //减法
* //乘法
/ //除法
% //取模

在上面的算术操作符中,只有进行取模运算时,所运算的数字必须是整数。而其余的运算符针对整数和浮点数都可以进行运算。
但是,我们需要注意的是除法在运算过程中,想要得到一个浮点数,那么该运算式的除数和被除数至少存在一个浮点数。

二、移位操作符

移位操作符在第六章中已经做了详细地讲解,这里就不做过多的解释了,暂时就把第六章中的内容借鉴过来吧。

符号含义
<<二进制位左移
>>二进制位右移

解释:

int a =5;
int b=a<<1;
printf("%d",b);

该代码的含义就是将a的二进制位向左移动一位。
那么其结果是什么呢?
我们首先将其翻译为二进制,再移动。
如何翻译呢?我们需要理解一下翻译的方法,这时候,我们不妨理解一下十进制的计算方法,再对其进行类比。
在这里插入图片描述

(1)左移:

这时候,我们需要明白一下其移动的规则:左侧多余的地方舍去,右侧缺失的地方补充0。
解释:之所以二进制位有32位,是因为int数据类型的大小是4个字节,在前文中的第二章,我们讲过一个字节等于八个比特,一个比特就是一个二进制位,所以一共有32个二进制位。
在这里插入图片描述

这里,我们就已经对其进行了移动,然后再对移动后的二进制序列,进行计算。
在这里插入图片描述
我们会发现,由5变成了10。那么我们经过多次的计算后,便能够得到一个结论,就是 左移一位相当于乘以2 。其实很好理解,以10进制为例,我们将123,在十进制的条件下左移一位,是不是就变成了1230,相当于乘以10,所以这个结论是很好理解的。

int a =5;
int b=a>>1;
printf("%d",b);

(2)右移:

但是!
右移就较为麻烦了
右移分为两种:
一种是算数右移,一种是逻辑右移。
算数右移:右边丢弃,左边补原符号位。
(符号位是最高位,最左侧位,正数是0,负数是1。)

逻辑右移:类似于左移,右侧丢弃,左侧补0。

不同的编译器移动的方式不同,我们可以利用负数右移,来验证我们的编译器是如何移动的,这个时候,我们首先需要更多的了解一下,整数的二进制位的存储形式。

整数的二进制位形式,共有三种:

  • 原码:直接换算得来的二进制位

  • 反码:符号位(最高位即最左侧的一位)不变,其他位取反,即0和1转换。

  • 补码: 反码+1变成补码。

对于负数而言,存储形式为补码,对于正数而言,三种码相同。

但是我们发现,对于正数而言逻辑右移和算数右移的结果是相同的,所以我们只能通过负数来验证。因此我们以数字-1为例:
在这里插入图片描述
在这里插入图片描述
这里可以总结出两条规律:
左移操作符等价于乘以2,右移操作符中的算数右移等价于除以2。
整数的存储方式是以补码的方式存储的,只不过整数的原码,反码,补码相同。当我们通过对补码的移动操作后,还需要转换为原码,从而计算出对应的十进制数。
注意事项:
对于移位操作符来说,不要移动负数。

三、位操作符

位操作符的基本操作在前面的章节中已经做了详细的介绍,这里我们主要提一些注意事项和灵活应用。

符号含义
&按二进制位与
|按二进制位或
^按二进制位异或

注意:位操作符的操作对象,必须是整数。
首先我们需要知道,在编程语言中,0是假,1是真。

(1)与:均为真,才是真

int a=3;
int b=5;
int c=a & b;
printf("%d",c);

图解:
在这里插入图片描述

(2)或:至少有一个真,就是真

int a=3;
int b=5;
int c=a | b;
printf("%d",c);

图解:
在这里插入图片描述

(3)异或:相同为0,不同为1

int a=3;
int b=5;
int c=a ^ b;
printf("%d",c);

在这里插入图片描述
再次注意!!!
这三种运算只能对整数运用。

注意事项:
位操作符的计算对象依旧是整数对应的二进制位补码。
灵活应用:
我们在之前介绍位操作符的时候,在文章结尾提到了一道面试题,这里我们对其进行详细地解答和总结。

题目1:在不创建临时变量的情况下,交换两个变量的值。

//交换a和b的值
int a=3;
int b=5;

法一:利用算术操作符进行交换

int a=3;
int b=5;
a=a+b;
b=a-b;
a=a-b;

解释:
在这里插入图片描述
但是这种方法是有一定的弊端的,为什么呢?
在我们第一次给a赋值为a和b的和时,这里就会存在超出整型范围的风险。倘若a和b的结果都是232那么二者之和已经超出了int的范围,此时该方法就行不通了。

法二:利用位操作符进行交换
铺垫:
规律1:相同的数字异或为0

3^3=0;
5^5=0;

因为两个相同的数字,其补码一定是相同的,所以当二者异或运算时,相同则为假,所以结果都是0。那么全为0的补码对应的数字就是0。
规律2:任何数字和0异或的结果为本身

3^0=3;

解释:
在这里插入图片描述
我们只关注整数补码的最后几位。进行异或计算后,我们发现依旧是非零数字本身。

规律3:异或运算支持交换律

3^3^5=3^5^3

理解起来非常简单,类似于我们的加法交换律。

解题:
有了以上的铺垫,我们就能够轻松地写出这道题的第二种解法。

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

解释:
在这里插入图片描述

题目2:统计一个整数的二进制位中1的个数

法一:利用&来计算

void test01()
{
	int a = 0;
	scanf("%d", &a);
	int c = a;
	int count = 0;//记录1的个数
	for (int i = 0; i < 32; i++)
	{
		int temp = a & 1;
		if (temp % 2 == 1)
		{
			count++;
		}
		a=a >> 1;
	}
	printf("%d的二进制位有%d个1\n", c,count);
}

因为一个整型数据的二进制位有32位,所以我们一共需要判断32次。同时针对某一位二进制位,当与1做&运算时,我们可以通过下图来理解:
法二:利用奇偶来判断

void test02()
{
	int a = 0;
	scanf("%d", &a);
	int c = a;
	int count = 0;//记录1的个数
	for (int i = 0; i < 32; i++)
	{
		if (a % 2 == 1)
		{
			count++;
		}
		a = a >> 1;
	}
	printf("%d的二进制位有%d个1\n", c, count);
}

四、赋值操作符

1、连续赋值:

int a=10;
b=a=a+3;

连续赋值的原理是从右向左依次赋值,所以最终b的结果是13。

五、sizeof操作符

注意事项1:

首先我们这里先点名一个误区,sizeof在使用过程中需要配合小括号来实现,这个时候我们就会误认为sizeof是一个函数。但是sizeof的本质是一个操作符。至于为什么是操作符,在下文中会进行相应的证明。

int a=10;
printf("%zu\n",sizeof(a));
printf("%zu\n",sizeof a);
printf("zu\n",sizeof(int));
printf("zu\n",sizeof int);//这种写法是错误的!

当我们想使用sizeof操作符打印某个变量的所占内存的大小时,我们在双引号中所写的占位符应该是%zu。同时我们发现,当我们利用变量的名称计算内存空间的时候,小括号可以写可以不写,由此我们就能够证明sizeof不是函数,仅仅是一个操作符。
但是,当我们利用对应变量的数据类型来计算内存空间的时候,小括号是不能去掉的。

注意事项2:

int arr[10]={0};
printf("%zu\n",sizeof(arr));
printf("%zu\n",sizeof(arr[10]));

我们通过上述的代码可以计算数组所占空间的大小。当我们通过数组名计算时,此时的数组名代表的是整个数组,而非数组首元素的地址。这是在前文数组进阶中所讲解过的特殊情况之一。这里值得注意的是我们打印数组中某个元素所占内存的大小时,我们在这里输入的是第十一个元素,但是我们在初始化数组的时候,一共仅有10个元素。所以在逻辑上第三行代码是行不通的,但是我们在VS2019中运行时会发现不会报错,并且会给出正确的结果。

其实我们在认定第三行代码错误的逻辑是,访问第11个元素的时候出现了越界访问的问题。但实际上,此处并没有去访问它具体的内容,而是根据我们在括号内所输入的内容确定出数据的存储类型,从而计算出大小。所以我们输入第11个元素的时候,这个元素尽管不存在,但是它的数据类型一定是和前十个元素保持一致的,所以根据这个逻辑编译器就会计算出对应的内存大小,这也就使得系统并未报错。

注意事项3:

short s=10;
int a=2;
printf("%zu\n",sizeof(s=a+5));
printf("%d",s);

我们运行上述代码,观察一下结果:
在这里插入图片描述
我们发现尽管我们在sizeof的小括号内对s进行了赋值运算,但是最后s的打印结果依然是10。由此,我们就能够得出一个结论:sizeof()括号内的表达式并不会执行!
有了这个结论我们就能够很轻松地理解为什么最终s打印出来还是10。
然后我们来解释一下为什么sizeof(s=a+5)的结果是2。尽管表达式不会运算但是编译器知道,倘若表达式运行,最终的计算结果也会存储在s中,而s的数据类型是短整型,也就是说其数据类型是确定的,由此我们就能够得出最终的所占内存结果。
但是我们这里可能会有一个疑惑,a是整型数据但是将整型数据是如何存储在一个短整型中的呢?
当a+2后,我们得到的是一个4个字节的数据,但是我们想要存储进2个字节的短整型中的时候,就会对这个4个字节的数据进行截断,也就是说会保留2个字节,然后再存储进短整型中,当数字较小时,并不会造成数据的丢失,但是可想而知,如果我们的数据较大,此时我们将其截断很有可能会发生数据的丢失。
图示:
在这里插入图片描述

注意事项4:

void test04(int arr[],char arr1[])
{
	printf("%zu\n", sizeof(arr));//1
	printf("%zu\n", sizeof(arr1));//2
}
void test05()
{
	int arr[10] = { 0 };
	char arr1[10] = { 0 };
	printf("%zu\n", sizeof(arr));//3
	printf("%zu\n", sizeof(arr1));//4
	test04(arr, arr1);
	
}

四处的打印结果分别是什么呢?
我们运行上述代码:
在这里插入图片描述
40和10我们很好理解,因为sizeof中的数组名代表的是整个数组而不是数组首元素的地址。
但是4和4我们如何解释呢?
这里就涉及到了前面所提到的数组作为参数的本质了。当我们将一个数组作为参数传入到函数中的时候,这个数组名仅仅是一个指针变量,他的唯一含义就代表一个地址。所以当我们再次将其写入到sizeof中的时候,sizeof就会计算出指针变量的内存大小。

在这里插入图片描述

六、按位取反操作符

int a=0;
printf("%d",~a);

按位取反操作符是~,而这里的位指的是二进制位。而操作的二进制位是整数的补码。
我们可以用图来解释一下:
在这里插入图片描述

例题1:

题目描述:
将a的二进制位中的第n位变成1。
例子:
在这里插入图片描述
思路分析:
在这里插入图片描述

代码实现:

printf("请输入一个数字:\n");
int a=0;
scanf("%d",&a);
printf("请输入n的值:\n");
int n =0;
scanf("%d",&n);
int result = a|(1<<(n-1));
printf("该数字第n位变成1后,结果为:%d\n", result);

例题2:

题目描述:
将a的二进制位中的第n位变成0。
例子:
在这里插入图片描述
思路分析:
在这里插入图片描述
代码实现:

	printf("请输入一个数字:\n");
	int a = 0;
	scanf("%d", &a);
	printf("请输入n的值:\n");
	int n = 0;
	scanf("%d", &n);
	int b = ~(1 << (n - 1));
	int result = a & b;
	printf("该数字第n位变成0后,结果为:%d\n", result);

七、关系操作符

注意事项1:

结构体等自定义的数据类型没有办法进行大小的比较,因为编译器不知道比较的标准是什么,所以无法比较。

注意事项2:

两个等号是判断是否相等的关系操作符,一个等号是赋值操作符。

注意事项3:

判断两个字符串是否相等不能直接比较两个字符串,而是要使用库函数中的字符串比较函数。
当我们真的直接将两个字符串进行比较的时候,实际上比较的是两个字符串的首字母的地址。

八、逻辑操作符

逻辑操作符的基本使用方法再前面已经接讲解了,这里主要是介绍一些注意事项。

注意事项1:逻辑操作符的短路行为

例1:


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

在这里插入图片描述
分析:
第一行:采用逗号表达式的形式从左向右依次定义一系列变量。
第二行:
a采用的是后置加加,也就是说先对a判断是否为真,再对其进行加一操作。
由于a的初始值是0,而在C语言中,0代表假,因此这一部分的判断结果为假,但是我们这里采用的是&&也就是说,所有表达式必须都为真才为真。只要有一个假的,哪怕后续都为真,最终的结果也是假。所以说,当我们判断出一个假,后续的表达式便无需判断,进而提高了代码的运行效率。正是因为后续的表达式不再执行,所以b和d也无法进行加一操作了。
当我们分析完这两行代码后,最终的结果便可想而知了。
例2:

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

在这里插入图片描述
代码分析
第一行:采用逗号表达式的形式定义了一系列变量。
第二行:
首先a采用的是后置加加,所以先进行真假判断,a等于0为假,所以编译器会继续向后判断,直到找到一个真。当执行到b前置加加的运算的时候,先+1再判断。最后b的值为4,为真,故不再进行后续的判断。故d无法进行加一。因此得到了最终的结果。

九、逗号表达式

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

int a = 1;
int b = 2;
int c = (a>b,a=b+10,a,b=a+1);

从左向右依次计算,最终C的值就是13。

十、函数调用操作符和操作数

void test(int x,int y)
{
}
int main()
{
  test(3, 6)//小括号()函数调用操作符
  //操作数:test,3,6(函数名和两个实参一共三个操作数)
  return 0;
}

十一、表达式求值

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

隐式类型转换

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

整型提升的过程:

根据符号位(即最左侧的高位)进行数据的补齐,再进行一系列的运算

示例:

char a=5;
char b=294;
char c=a+b;

我们先分析第一行代码中,如何将整型数据5存储进char的数据类型之中。如下图所示:
在这里插入图片描述
同理,第二行代码中b的存储道理是相同的,如下图所示:
在这里插入图片描述

但是这里我们就发现了一个很严重的问题,当我们将一个4个字节的整型数据进行截断为1个字节的字符型数据的时候,发生了丢失。

那么当a和b中存储了相应的数据后,二者相加时的过程如下图所示:
在这里插入图片描述
此时我们打印该十进制数字。
在这里插入图片描述

printf("%d\n",(int)c);

我们运行上述一系列代码验证最终的结果:
在这里插入图片描述

整型提升的意义:

我们发现,在进行数据的截断操作时,有时候会造成数据的丢失,那么当我们进行了整型提升后,补充了很多高位,从而防止了表达式运行后进1操作时造成的数据丢失。很好地保留了数据。

练习1:

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

我们运行上述代码,发现最终打印的结果只有C。
那么这是为什么呢?
首先我们需要明白abc的赋值均为十六进制数字,我们以a为例:将这个十六进制转化为二进制数字是:1011 0110而在进行表达式的执行时,会发生整型提升,即利用符号位补全32个二进制位。所以补全后的32位的二进制位的最高位是1,而1代表负数,也就是说进行整形提提升后的最终结果是一个负数。因此二者不可能相等。
b与之同理。而c本身的数据类型就是普通的整型,所以不会发生整型提升,因此二者相等的关系是成立的。所以打印了c。

练习2:

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

在这里插入图片描述
sizeof中的表达式虽然不会执行,但是遇到表达式会对字符型数据进行整型提升,因此造成了所占内存的变化。同时,我们也证明了整型提升的存在。

算数转换

当一个表达式中的操作数的数据类型所占的内存空间是大于4个字节的,但是几个操作数的数据类型并不相同,那么此时这些操作数会向着高内存的数据类型进行提升,当所有操作数提升至一个相同的数据类型时,再进行运算。

十二、操作符的属性

操作符的属性分为三个:
1、操作符的优先级
2、操作符的结合性
3、是否控制求值顺序
两个相邻的操作符先执行哪个?取决于他们的优先级,如果两者的优先级相同,则取决于他们的结合性(即从左到右还是从右到左运算)。
当一个表达式过于复杂的时候,不同的编译器就会有不同的计算结果,所以我们再写代码的时候,不要将一个式子写的过于复杂。


总结

今天的学习主要是在操作符的基础认知上进行详解,以及一些注意事项的提示,希望对大家有所帮助。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值