【C语言】从入门到入土(操作符篇)

194 阅读16分钟

前言:

本篇问您讲解C语言中的各种操作符的介绍,以及举例说明等,让你更好的掌握C语言中的操作符。

简单介绍: C语言运算符是说明特定操作的符号,它是构造C语言表达式的工具。C语言的运算异常丰富,除了控制语句和输入输出以外的几乎所有的基本操作都为运算符处理。除了常见的三大类,算术运算符、关系运算符与逻辑运算符之外,还有一些用于完成特殊任务的运算符,比如位运算符。


C语言操作符详解


一、操作符分类简介

操作符可以分为很多种,有算术运算符、关系运算符与逻辑运算符,位运算符等,那这里我们为了更详细的说明我们又把一些操作符给再细化来说明,分别有算术操作符、移位操作符、位操作符、赋值操作符、单目操作符、关系操作符、逻辑操作符、条件操作符、逗号表达式和其他。

下面开始详解操作符。

在这里插入图片描述


二、操作符优先级

首先先给看一下C语言中操作符的优先级:

这是在网上整理得出的一套操作符优先级,但是要不是在学校考试或者面试的时候一般用不到非常详细的优先级,当我们自己写代码的时候,可以直接用()去让其优先进行计算,让可读性更好。


三、操作符详解

1.算术操作符

算术操作符中有以下这几种:

+   -   *  /  %

首先加减乘这几个就比较简单了

int main()
{
   //最后输出为多少呢?
   int a = 20;
   int b = 10;
   a = a + b;
   b = a - b;
   a = a * b;
   printf("a=%d b=%d",a,b);
}

这里最后输出的答案是a=600 b=20。注意这里变量改变之后,他的内存了就是存放着新的值了。这里的a=a * b的时候,其实a已经是30,b已经是20了。

然后到/ 和 %这两个操作符:

/ 这个操作符就是除以,而 %操作符就是取模,结果是整除得到的余数。

//值得一题的是:

int main()
{
	int a = 7/3;
	printf("%d\n", a);
	double b = 7 / 3;
	printf("%lf\n", b);
	//结果为多少呢?
	
	int ret = 10.0%4.0;
	int tmp =10 % 4;
	//哪个对呢?
	return 0;

}

答案是2 和2.000000,ret会报错,而tem正常输出。为什么double 里面存到的不是2.333333?ret会报错?

这里需要注意的是:

  1. 对于/ 这个操作符来说,/ 两边即除数和被除数都是整数的时候,结果执行整数除法,也就是除得结果才转为double类型。
  2. 如果要执行浮点数除法,那么你除数被除数至少需要一个浮点数。
  3. 而对于 %这个操作符来说,它只能针对整形,其他类型无法进行计算。

2.移位操作符

移位操作符分为:

<<    //左移操作符
>>    //右移操作符
      //只适用于整形!

这里涉及到二进制位的运算,先了解一下预备知识:

对于整数的二进制来说有3种表示形式:原码、反码、补码,而对于正整数来说,原码反码补码都是相同的。但对于负整数来说就有所不同了,而整数在内存中存储的都是二进制中的补码,显示的是原码的值。而二进制的首位是符号位,是0即为正数,1则为负数。

负整数原反补码规律:
原码: 直接按照数字的正负和大小写出二进制序列。 反码:原码符号位不变,其他位按位取反得到(1变0,0变1)。 补码:反码+1。

所以在这里移位操作中,移动的是存储的码即补码。

而左移和右移又有所不同,先看左移操作符:

int main()
{
	int a = 5;
	int b = a << 1;

	printf("a=%d b=%d", a, b);
    //答案是多少呢?
	return 0;
}

答案是a=5 b=10,当a << 1的时候,a的值是不会改变的,而这里

a=5,二进制表示为: 原码:00000000 00000000 00000000 00000101 补码:00000000 00000000 00000000 00000101 当它移动一位的时候,实际上就是这一串数字往左移动一位,即为左边去掉,右边补0。若是负数,最终得到还是要还原为原码,看原码的值。

而右移操作符也和左移差不多,但由于二进制最左边的数为符号数,当向右移的时候,符号位就往右移动了,那么补数应该是什么呢?

首先右移运算分两种:

  1. 逻辑移位 :左边用0填充,右边丢弃。
  2. 算术移位 :左边用原该值的符号位填充,右边丢弃。 在这里插入图片描述

对于两种运算,不同的编译器采用的可能是不同的运算,当前使用的vs2019是采用算术运算,而且大多数的编译器也都采用算术运算,能保留符号位的操作。

最后还有一点,移位操作符未定义移动负数位,如 a >> -1,这是标准未定义的,不同的编译器会产生不同的结果。


3.位操作符

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

首先先看一下他们的算法:

& (按位与):只有对应的两个二进位均为1时,结果位才为1 ,否则为0。 | (按位或):只要对应的二个二进位有一个为1时,结果位就为1。 ^ (按位异或):当两对应的二进位相异时,结果为1,相同为0。

同样的,位运算符也是用二进制中的补码进行运算,得到的原码显示值。

我们来看一下例子:

int main()
{
	int a = 2;
	int b = -3;

	int c = a & b;
	int d = a | b;
	int e = a ^ b;

	printf("c=%d d=%d e=%d\n", c, d, e);
    //结果为多少?
}

答案为c=0 d=-1 e=-1,下面是详解:

//a 
//00000000 00000000 00000000 00000010 原码=补码

//b
//10000000 00000000 00000000 00000011 原码
//11111111 11111111 11111111 11111100 反码
//11111111 11111111 11111111 11111101 补码

//a&b 同时为1才为1
//00000000 00000000 00000000 00000010
//11111111 11111111 11111111 11111101
//———————————————————————————————————
//00000000 00000000 00000000 00000000      原码=0

//a|b 两边有1则为1
//00000000 00000000 00000000 00000010
//11111111 11111111 11111111 11111101
//———————————————————————————————————
//11111111 11111111 11111111 11111111     原码=-1

//a^b 相同为0,相异为1
//00000000 00000000 00000000 00000010
//11111111 11111111 11111111 11111101
//———————————————————————————————————
//11111111 11111111 11111111 11111111     原码=-1

这里再问一道“ 简单 ”的面试题:

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

int main()
{
   //如果没有前提条件,相信大家都很快写出这样的代码
   int a = 20;
   int b = 10;
   int tmp=0;
   tmp = a;
   a = b;
   b = tmp;
}
--------------------------------------------------------------------------
//但条件之下应该如何写呢?

int main()
{
   //这种写法可以解决,相信大家读一下就了解了。
   //局限性是数字的值不能太大
   int a=20;
   int b=10;
   a=a+b;
   b=a-b;
   a=a-b;
}

那这种解法和这里的位运算符有什么关联呢,当然有,用位运算符也可以达到这样的效果!

int main()
{
	int a = 20;
	int b = 10;
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
    //这样子就成功了。
	return 0;
}

事实上,由于 ^ 这个位运算符相同为0,相异为1的算法,就可以得到

任何数与0进行异或运算的时候仍得原数。 任何数自己与自己进行异或运算的时候都得0。

但一般要进行两数交换的时候还是应该使用最前面那种,这里仅仅是讨论这个面试题,第二个第三个可读性都较差,而且第三个只适用于整形。


4.赋值操作符

赋值操作符是一个非常好的操作符,当你的值不是你满意的状态,我们就可以直接去修改他。

int main()
{
   char arr[] = "boy";
   int height=155//你的身高
   int weight=50//你的体重
   //不满意?
   //改!

   height = 180;
   weight = 70return 0;
}

同时还有许多复合赋值符:

//复合赋值符就是把符号简洁化
+=     
//   a=a+b  等价于 a+=b  
-=
//   a=a-b  等价于 a-=b  下同
*=
/=
%=
>>=
<<=
&=
|=
^=

5.单目操作符

单目操作符有:

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

1.逻辑反操作

int main()
{
	int a = 0;
	int b = 1;
	printf("%d %d", !a, !b);
    //值为多少?
}

逻辑反操作就是把数的真变为假,假变为真。而0表示假,其他数表示真。这里的a=0,那么!a就是将假变成真,但是真有很多数可以表示,所以我们统一采用!a=1。b=1,!b=0就是把真变假,所以是0。

2.正值负值

int main()
{
	int a = 5;
	int b = -5;
	int c = -6;
	a = -a;
	b = +b;
	c = -c;
	printf("%d %d %d", a, b,c);
    //值为多少?
	return 0;
}

答案为:-5 -5 6,正值负值操作符就是改变值的符号的,但注意+不能直接改变值的符号,把负数变为正数也可以用负值操作符。

3.取地址与解引用操作符

取地址操作符就是把存储该值的地址拿出来,而解引用就是获取运算符左侧的指针所指向的对象的某个成员。

int main()
{
	int a = 20;
	int b = 0;

	int* p = &a;
	*p = 50;
	b = *p;
	printf("%d %d", a, b);
	//值为多少?
	return 0;
}

答案为50 50,这里就是把存储a的地址取出来放,而p则是指向这块空间的。那这块空间中存放的是20,然后将50赋在了这块空间里面,b取这块空间的值最后也等于50。 在这里插入图片描述 如上图所示,* p就是指向a的地址,把地址的值该了之后,其他变量被赋值的时候使用的也是改变后的值,因为这个地址的值已经改变为50,而a自己也变成50。左值就是空间,右值就是空间的内容。

取地址不仅应用于变量之中,结构体也适用。

int main()
{
	int arr[10] = {0};
	arr;//取首元素地址

	&arr[0];//取首元素地址

	&arr;//取整个数组地址,但显示与前面两个一样
	return 0;
}

4.sizeof

sizeof是操作符,不是函数。而且sizeof是计量变量或类型创建变量的内存大小,和内存中放什么数据没有关系。

代码演示:

int main()
{
	char arr[10] = "abc";
	printf("%d\n", sizeof(arr));//10
	//sizeof关注的是整个数组的内存大小
	
	printf("%d\n", strlen(arr));//3 
	//strlen函数可以计算\0之前的字符串长度
	//关注的内存中是否有\0.计算的是\0之前出现的字符个数
}
int main()
{
	int a = 10;
	printf("%d\n", sizeof(a));//4
	printf("%d\n", sizeof a);//4
    //因为sizeof不是函数,所以后面的()不是函数调用操作符
    
	printf("%d\n", sizeof(int));//4
	//printf("%d\n", sizeof int);//这种vs2019不支持
	//但是一些新式的编译器也是支持的
}
int main()
{
    int a = 5;
	short s = 10;
	printf("%d\n", sizeof(s = a + 2));
	printf("%d\n", s);
	//答案是多少?                
}

答案是2 10,这里sizeof算的还是short类型的大小,a是整形变量,而s是短整型,放不下整形变量,这里就会发生整形截断,把整形中两个字节拿给s,所以计算大小仍计算的是short类型的,并且注意sizeof 内部的表达式是不参与运算的,所以s最后结果仍为10。

5.按位取反

按位取反就是将数的二进制的补码0变成1,1变成0。

int main()
{
	int a = 5;
	//00000000 00000000 00000000 00000101

	a = ~a;
	//11111111 11111111 11111111 11111010   补码
	//11111111 11111111 11111111 11111001   反码
	//10000000 00000000 00000000 00000110   原码
	printf("%d", a);
	//结果为-6
}

6.加加减减

这里只需要分清前置和后置的区别:

前置++,先++,后使用;后置++,先使用,后++。 前置--,先--,后使用;后置--,先使用,后--。

int main()
{
    
	int a = 5;
	int b = ++a;//前置++,先++,后使用
	//所以b和a都变成6

	int c = a++;//后置++,先使用,后++
	//所以c变成6,a再加变成7
	printf("%d %d %d", a, b, c);
     
    int c=(++a)+(++a)+(++a);//只需要简单理解上面的代码,这种就不要写了
    //垃圾代码,可读性差,不同编译器可能存在不同结果

	return 0;
}

7.(类型)

() 可以用于强制类型转换

int main()
{
	int a = (int)3.14;
    //默认写出的浮点数是double的,然后前面放(int)强制类型转换为int
    
	printf("%d\n", a);
	return 0;
}

6.关系操作符

关系操作符有:

>
>=
<
<=
!=      //用于测试“不相等”
==      //用于测试“相等”

关系操作符就比较简单了,需要注意的是:

1.关系操作符是用于比较同类型的。 2. == 是等于操作符,而 = 是赋值操作符。


7.逻辑操作符

逻辑操作符有:

&&     //逻辑与
||     //逻辑或

逻辑操作符只关注真假,如果&&两边都为真,则值为真,有一边是假则为假。只要||两边有一个为真则真,两边都为假才为假。

要区分逻辑与和按位与,逻辑或和按位或!

1&2----->0
1&&2---->1
1|2----->3
1||2---->1

逻辑与和逻辑或可以运用在判断上:

int main()
{
	int Math = 82;
	int Chinese = 99;
	int English = 80;

	if (Math > 90 && Chinese > 90 && English > 90)
	{
		printf("你是优秀学生!");
	}

	if (Math > 95 || Chinese > 95 || English > 95)
	{
		printf("你在其中一科中出类拔萃!");
	}

	return 0;
}

还需要注意的是:

&&操作符左边为假的时候,右边剩下的无论是什么都为假,所以当左边为假的时候,右边就不会再算下去。而||操作符左边为真之后,后面也不用算了。

比如:

int main()
{
	int a = 0;
	int b = 2;
	int c = 3;
	int d = 4;

	int i = a++ && b++ && c++ && d++;
	printf("%d %d %d %d ", a, b, c, d);
	//结果为1 2 3 4,因为a先判断后++,a为0,所以后面不用算了
	return 0;
}

8.条件操作符

exp1 ? exp2 : exp3

注意,这是一个表达式!其意义为exp1是否为真,若为真,则exp2的结果就是整体表达式的结果,否则则为exp3为整体表达式的结果。

int main()
{
	int a = 10;
	int b = 20;

	int max = (b > a ? b : a);
	printf("%d", max);
	//得到最大值
	return 0;
}

9.逗号表达式

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

比如:

int main()
{
   int a = 1;
   int b = 2;
   int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
   //c是多少?
   return 0;
}

答案是c就是13,从左到右都执行,最后结果为最后一个表达式结果,中间a=b+10;将a的值变成12,然后b=a+1,所以b=13,而且这是最后一个表达式,所以c=13。

还有以下例子:

int main()
{
   int a=0,b=1,c=2,d=3;
   if(a=a+1,b=b+c,c>d,d>0)
   //虽然前面的都会进行计算
   //但充当判断条件的时候最后只会以d>0做为判断依据
   return 0;
}

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

1.[ ]下标引用操作符

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

int main()
{
	int arr[10];//创建数组
	
	arr[9] = 10;//实用下标引用操作符。
	//[] 的两个操作数是arr和9,即[]旁边的arr和里面的9都是操作数。
	
    printf("%p    %p",&arr[9],arr+9);
    //arr+9就是arr数组中下标为9的元素的地址
	return 0;
}

2.( ) 函数调用操作符

函数调用操作符接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

比如:


void fun()
{
   printf("world");
}
int main()
{
	char arr[] = "hello";
	printf("%u", strlen(arr));
	//调用函数strlen需要带上()
	fun();
	//这个也是调用fun带上()
	return 0;
}

3.结构成员访问操作符

.     //结构体.成员名
->    //结构体指针->成员名

结构成员访问操作符就是帮助访问一个结构的成员。

下面直接用代码来说明:

#include <stdio.h>
#include <string.h>

struct People//自定义类型
{
	char name[20];
	float height;
	char id[19];
};


void fun1(struct People p)
{
	printf("书名: %s\n", p.name);//结构体.成员名  访问这个结构的其中一个成员
	printf("价格: %f\n", p.height);
	printf("书号: %s\n", p.id);
}

void fun2(struct Book * pb)
{
	printf("书名: %s\n", pb->name);
    //这里的pb是指针,由于指针对地址有指向关系
    //所以这里和(*p).name的写法意义相同,都是指向空间,打印p.name的内容

	printf("价格: %f\n", pb->price);
	printf("书号: %s\n", pb->id);
}

int main()
{
	struct People p = {"张三", 1.65f, "4453xxxxxxxxxxxxx"};
	//这里创建一个结构体p是People类型的
	
	fun(p);
	fun(&p);
	p.height = 1.80f;//由于是数字变量可修改
	
	//但p.name不行,因为创建p结构体的时候name是地址,数据要放在空间里
	//这里可以使用字符串拷贝 -strcpy 
	//同时要引用库函数 - string.h
	strcpy(p.name, "李四");
    //将p里面的nmae改为李四
	
	return 0;
}

好啦,本篇的内容就到这里,小白制作不易,有错的地方还请xdm指正,互相关注,共同进步。

还有一件事: