C语言——操作符

目录

前言

分类

二进制和进制转换

进制

进制转换

二进制转十进制

十进制转二进制

二进制转八进制

二进制转十六进制

原码、反码、补码

移位操作符

<< 左移操作符

>>右移操作符

位操作符:&、|、^、~

&  按位与

​编辑

|  按位或

^ 按位异或

​编辑

~按位取反

应用

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

编写代码实现:统计⼀个整数存储在内存中的⼆进制中1的个数

找出单身狗

单⽬操作符

逗号表达式

下标访问[]、函数调⽤()

[ ] 下标引⽤操作符

函数调⽤操作符 ()

结构成员访问操作符

结构体

结构的声明

结构体变量的定义和初始化

结构成员访问操作符

结构体成员的直接访问

结构体成员的间接访问

操作符的属性:优先级、结合性


前言

    在前面,我们见到了 +, -, * ,/ , < , > , =这些符号,其实它们的名字叫做操作符。按照功能的不同我们可以对它进行一个简单的分类

分类

算术操作符: + - * / %
移位操作符: << >>
位操作符: &, | ,^ ,~
赋值操作符: = += -= *= /= %= <<= >>= &= |= ^=
单⽬操作符: !、 ++ -- & * + - ~ sizeof ( 类型 )
关系操作符: > >= < <= == !=
逻辑操作符: && ||
条件操作符: ? :
逗号表达式: ,
下标引⽤: []
函数调⽤: ()
结构成员访问: . ->
这里面的部分操作符与二进制有关,所以我们需要先了解二进制以及进制转换的一些知识,开始咯~

二进制和进制转换

进制

   我们经常听到的2进制、8进制、10进制、16进制是数值的不同表⽰形式。

以我们最熟悉的十进制为例:

10进制中满10进1
10进制的数字每⼀位都是0~9的数字组成
二进制:
2进制中满2进1
2进制的数字每⼀位都是0~1的数字组成
八进制:
8进制中满8进1
8进制的数字每⼀位都是0~7的数字组成
十六进制:
16进制中满16进1
为了避免混淆16进制数字每⼀位是0~9, a~f 的
后面的a~f表示十六进制的时候可以是大写,也可以是小写。

进制转换

从前面我们可以看出,进制的原理是差不多的,接下来我们就来学习进制转换。

进制的每⼀位是有权重的。
举个例子:十进制的123
10进制的数字从右向左是个位、⼗位、百位....,分别每⼀位的权重是 10^0 , 10^1 , 10^2...
其他的进制也是类似的,比如二进制 每⼀位的权重,从右向左是: 2^0 , 2^1, 2^2 ...

二进制转十进制

二进制转十进制,我们只需要把二进制每一位乘以它对应的权重就可以了。

比如二进制1011

十进制转二进制

我们只需要将十进制数不断地除二,直到结果为0,每一次得到的余数从下向上写就是转换出的二进制。比如十进制125

二进制转八进制

8进制的数字每⼀位是0~7的,0~7的数字各⾃写成2进制,最多有3个2进制位就足够了。
所以我们可以从2进制序列中右边低位开始向左 每3个2进制位 会换算 ⼀个8进制位 ,剩余不够3个2进制位的直接换算,也可以在前面加一个0.
比如二进制的11110101--->0365(八进制) 0开头的数字,会被当做8进制,我们把0称为前导符。

二进制转十六进制

与二进制转换为八进制类似,16进制的数字每⼀位是0~9, a~f 的,0~9, a~f的数字,各⾃写成2进制,最多有4个2进制位就⾜够了,所以我们可以从2进制序列中右边低位开始向左每4个2进制位会换算⼀个16进制位,剩余不够4个⼆进制位的直接换算。

比如二进制的11110101---->0xf5(十六进制),16进制表⽰的时候前⾯加0x (前导符)

 

我们可以在编译器上打印来验证我们的结果是否正确!

将二进制的11110101转换为十进制就是245

我们可以用%o以八进制无符号形式输出整数,可以使用%x(%X)以十六进制无符号形式输出整数。当x是小写的时候,十六进制的a~f以小写形式输出,是大写就用大写形式输出。

但是它们都不会输出前导符,我们可以自己加上,也可以在%后加上#号。

代码如下:

#include<stdio.h>
int main()
{
	int a = 245;
	printf("%o\n", a);
	printf("%x\n", a);
	printf("%X\n", a);
	printf("----------\n");
	printf("0%o\n", a);
	printf("0x%x\n", a);
	printf("0x%X\n", a);
	printf("----------\n");
	printf("%#o\n", a);
	printf("%#x\n", a);
	printf("%#X\n", a);
	return 0;
}

原码、反码、补码

      整数的2进制表⽰⽅法有三种,即原码、反码和补码 。
      有符号整数的三种表⽰⽅法均有 符号位 数值位 两部分,2进制序列中, 最⾼位 是被当做符号 位,剩余的都是数值位。
       符号位都是⽤ 0表⽰“正” ,⽤ 1表⽰“负”。
正整数的原、反、补码都相同。
负整数的三种表⽰⽅法各不相同,它的转换方法如下:
原码 :直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码 :将原码的 符号位不变 ,其他位依次 按位取反 就可以得到反码。

补码反码+1就得到补码。

例:

-1(整型,4个字节,32个比特位)

原码:10000000 00000000 00000000 00000001

反码:11111111 11111111 11111111 11111110

补码:11111111 11111111 11111111 11111111

如果已知补码想要得到原码:

方法一:补码先减1再符号位不变,其他位按位取反

方法二:补码直接符号位不变,其他位取反后加1

补码:11111111 11111111 11111111 11111111

原码:10000000 00000000 00000000 00000001

对于整形来说:数据 存放内存 中其实存放的是 补码
   原因:在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于,使⽤补码,可以将 符号位和数值域统⼀处理 ;同时,加法和减法也可以统⼀处理( CPU只有加法器 )此外,补码与原码相互转换,其运算 过程是相同的,不需要额外的硬件电路。

移位操作符

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

<< 左移操作符

移位规则:左边抛弃、右边补0
例:
#include<stdio.h>
int main()
{
	int n = 2;
	//正数原码,反码,补码相同
	//00000000 00000000 00000000 00000010
	// //移位规则:左边抛弃、右边补0
	//00000000 00000000 00000000 00001000--8
	//左边舍弃,右边补两位
	int ln = n << 2;
	printf("%d\n", ln);
	return 0;
}

>>右移操作符

移位规则:
右移运算分两种;
1. 逻辑右移:左边⽤0填充,右边丢弃
2. 算术右移:左边⽤原该值的符号位填充,右边丢弃
(由编译器自己决定)
我们可以使用简单的代码来验证一下VS2022使用哪一种右移运算:
#include<stdio.h>
int main()
{
	int a = 2;
	//00000000 00000000 00000000 00000010
	//正数原码反码补码相同
	int b = a >> 1;
	printf("%d\n", b);
	return 0;
}

#include<stdio.h>
int main()
{
	int a = -1;
	//10000000 00000000 00000000 00000001
	//11111111 11111111 11111111 11111110
	//11111111 11111111 11111111 11111111-->补码
	int b = a >> 1;
	printf("%d\n", b);
	return 0;
}

第一个运行结果为1,第二个运行结果为-1,我们用注释简单分析一下第二个代码

#include<stdio.h>
int main()
{
	int a = -1;
	//10000000 00000000 00000000 00000001
	//11111111 11111111 11111111 11111110
	//11111111 11111111 11111111 11111111-->补码
	int b = a >> 1;
	//1. 逻辑右移:左边⽤0填充,右边丢弃 
	//01111111 11111111 11111111 11111111-->补码
	// 符号位变为0,认为是正数,输出结果是一个很大的数
	//不符合
	//2. 算术右移:左边⽤原该值的符号位填充,右边丢弃 
	//11111111 11111111 11111111 11111111-->补码
	//10000000 00000000 00000000 00000001-->原码,值为-1,满足
	printf("%d\n", b);
	return 0;
}

我们可以知道VS2022使用的是算术右移的方式。

位操作符:&、|、^、~

位操作符操作数必须是整数

按位与

规则:两个整数补码对应的二进制位有0则为0,两个同时为1则为1

例:

#include<stdio.h>
int main()
{
	int a = -3;
	//10000000 00000000 00000000 00000011-->  -3原码
	//11111111 11111111 11111111 11111100 
	//11111111 11111111 11111111 11111101-->  -3补码
	int b = 5;
	//00000000 00000000 00000000 00000101-->  5补码
	//11111111 11111111 11111111 11111101-->  -3补码
	//00000000 00000000 00000000 00000101--》  5
	printf("%d\n", a & b);
	return 0;
}

|  按位或

规则:两个整数补码对应的二进制位只要有1就为1,两个同时为0则为0

例:

//|  按位或
#include<stdio.h>
int main()
{
	int a = -3;
	//10000000 00000000 00000000 00000011-->  -3原码
	//11111111 11111111 11111111 11111100 
	//11111111 11111111 11111111 11111101-->  -3补码
	int b = 5;
	//00000000 00000000 00000000 00000101-->  5补码
	//11111111 11111111 11111111 11111101-->  -3补码
	//11111111 11111111 11111111 11111101--》 a|b补码
	printf("%d\n", a | b);
	return 0;
}

如果我们定义一个整型变量num,那么num | 0=num

我们可以用一个简单的代码来进行验证:

#include<stdio.h>
int main()
{
	int num = 0;
	scanf("%d", &num);
	printf("%d\n", num | 0);
	return 0;
}

说明我们的结论是正确的。

^ 按位异或

规则:两个整数补码对应的二进制位相同为0,不相同为1

例:

#include<stdio.h>
int main()
{
	int a = -3;
	//10000000 00000000 00000000 00000011-->  -3原码
	//11111111 11111111 11111111 11111100 
	//11111111 11111111 11111111 11111101-->  -3补码
	int b = 5;
	//00000000 00000000 00000000 00000101-->  5补码
	//11111111 11111111 11111111 11111101-->  -3补码
	//11111111 11111111 11111111 11111000--》 a^b补码
	//10000000 00000000 00000000 00000111    //补码取反加1可以得到原码
	//10000000 00000000 00000000 00001000--》 a^b原码---》-8
	printf("%d\n", a ^ b);
	return 0;
}

~按位取反

规则:一个整数补码按二进制位全部取反,输出原码

这里我们可以看到 ~ 只有一个操作数

例:

#include<stdio.h>
int main()
{
	int a = -3;
	//10000000 00000000 00000000 00000011-->  -3原码
	//11111111 11111111 11111111 11111100 
	//11111111 11111111 11111111 11111101-->  -3补码
	//~全部补码二进制位取反
	//00000000 00000000 00000000 00000010--》符号位变为0,为正数
	printf("%d\n", ~a);
	//2
	return 0;
}

应用

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

一般来讲,我们如果想要交换俩个变量的话,我们会选择创建第三个变量来进行。现在有什么办法不创建临时变量(第三个变量),实现两个整数的交换呢?就需要刚刚了解到的知识进行应用。
由前面我们可以知道,定义一个变量a,那么a^a=0,a^0=0,我们可以用一个代码进行验证
#include<stdio.h>
int main()
{
	int a = -3;
	int b = a ^ a;
	printf("b=%d\n", b);
	int c = a ^ 0;
	printf("c=%d\n", c);
	return 0;
}

为了满足题目要求,我们就可以写出下面的代码

#include<stdio.h>
int main()
{
	int a = -3;
	int b = 5;
	printf("交换前:a=%d,b=%d\n", a, b);
	a = a ^ b;
	b = a ^ b;//b=a^b^b=a^0=a
	a = a ^ b;//a=a^a^b=b
	printf("交换后:a=%d,b=%d\n", a, b);
	return 0;
}

编写代码实现:统计⼀个整数存储在内存中的⼆进制中1的个数

我们知道十进制的数除2每次得到的余数就可以得到二进制原码,我们可以写出这样一个代码

#include<stdio.h>
int main()
{
	int num = 0;
	int count = 0;
	scanf("%d", &num);
	int tmp = num;
	while (num)//直到num=0停止
	{
		if (num % 2 == 1)//余数为1
			count++;
		num = num / 2;//下一次除2
	}
	printf("%d在内存中存储1的个数:%d\n", tmp, count);
	//使用num值会变化
	return 0;
}

我们测试了几个值,发现正数的结果是正确的,而负数的结果是错误的,我们知道

-1

原码:10000000 00000000 00000000 00000001

反码:11111111 11111111 11111111 11111110

补码:11111111 11111111 11111111 11111111

我们知道整数在内存中存放的是补码,所以-1应该是32个1,那么为什么会出现上面的结果呢?

我们来调试一下,发现第一次进去后,num%2==0,num/2之后num变成了0,就跳出了循环。

那么这个方法是不合理的,我们可以使用操作符来解决这个问题。
如果我们定义一个整型变量num,那么num&num==num,我们可以使用num&1判断num在内存中存储的最后一位是不是1。

有一种方法是将num的二进制位每一次向左边移动一位,右边补0

解决题目我们就可以写出下面的代码

#include<stdio.h>
int main()
{
	int num = 0;
	int count = 0;
	scanf("%d", &num);
	int tmp = num;
	for (int i = 0; i < 32; i++)
	{
		if (num & (1 << i))//结果为1为真
			count++;
	}
	printf("%d在内存中存储1的个数:%d\n", tmp, count);
	return 0;
}

不要写成   if ((num & (1 << i)) == 1)    //这样需要考虑到优先级的问题,后面会讲解

代码优化

#include<stdio.h>
int main()
{
	int num = 0;
	int count = 0;
	scanf("%d", &num);
	int tmp = num;
	while (num)
	{
		count++;
		num = num & (num - 1);
	}
	printf("%d在内存中存储1的个数:%d\n", tmp, count);
	return 0;
}

找出单身狗

题目:一组数组有只有一个数字单独出现,其他数字成对出现,找出这个数字(单身狗)

这个问题我们可以使用^这个操作符,我们知道a^a=0,a^0(在第一个应用中有讲解)

#include<stdio.h>
int main()
{
	int arr[7] = { 2,5,3,8,5,2,8 };
	int dog = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		dog = dog ^ arr[i];
	}
	printf("单身狗是%d\n", dog);
	return 0;
}

单⽬操作符

单⽬操作符:
!、 ++ -- +(正号) -(负号) ~ sizeof ( 类型/变量 ) ,&(取地址操作符),*(解引用操作符)
单⽬操作符的特点是 只有⼀个操作数 ,&和*会在指针部分进行讲解。

逗号表达式

形式: exp1, exp2, exp3, …expN
逗号表达式,就是⽤逗号隔开的多个表达式。
逗号表达式: 从左向右依次执⾏ 整个表达式的结果是最后⼀个表达式的结果
例:
#include<stdio.h>
int main()
{
	int a = 2;
	int b = 3;
	int c = 0;
	int n = (b = a, c = a + b, b);
	//       b=2    c=4        2
	printf("%d\n", n);
	return 0;
}

下标访问[]、函数调⽤()

[ ] 下标引⽤操作符

操作数:⼀个 数组名 + ⼀个 索引值(下标)
这个相信大家都不陌生,我们写一个简单的代码
#include<stdio.h>
int main()
{
	int arr[6] = { 1,2,3,4,5,6 };
	printf("%d\n", arr[3]);
	//arr数组名,3下标
	return 0;
}

函数调⽤操作符 ()

接受 ⼀个或者多个操作数 :第⼀个操作数是 函数名 ,剩余的操作数就是 传递给函数的参数
当函数参数没有的时候就只有一个操作数
例:
#include<stdio.h>
void menu()
{
	printf("****1.play****\n");
	printf("****0.exit****\n");
}
int main()
{
	menu();
	//没有参数,()只有menu函数名这一个操作数
	return 0;
}
#include<stdio.h>
int Add(int x,int y)
{
	return x + y;
}
int main()
{
	int a = 3;
	int b = 5;
	printf("add:%d\n", Add(a, b));
	//有参数,()有Add函数名和参数a,b多个操作数
	return 0;
}

结构成员访问操作符

这里首先需要知道的知识就是结构体

结构体

结构体类型是一种 自定义类型
结构体是⼀些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量,
如: 标量、数组、指针,甚⾄是其他结构体。

结构的声明

结构体类型声明的一般形式:

struct 结构体名
{
   成员列表;
}结构体类型变量列表;
举一个简单的例子,描述一个学生,我们需要知道他的姓名,性别,年龄,学号,我们可以写这样一个结构体
struct Student
{
	char name[20];//姓名
	char sex[10];//性别
	int age;//年龄
	char id[10];//学号
}student1,student2;//末尾有分号
//变量

结构体变量的定义和初始化

结构体变量我们可以直接在结构体后面进行定义和初始化,也可以在所需要使用的函数中进行定义和初始化

#include<stdio.h>
struct Student
{
	char name[20];//姓名
	char sex[10];//性别
	int age;//年龄
	char id[10];//学号
}student1 = { "lihua","male",18,"202052" }, student2 = { "zhangli","female",20,"202053" };//末尾有分号
//变量
int main()
{
	printf("student1:%s,%s,%d,%s\n", student1.name, student1.sex, student1.age, student1.id);
	printf("student2:%s,%s,%d,%s\n", student2.name, student2.sex, student2.age, student2.id);
	return 0;
}

#include<stdio.h>
struct Student
{
	char name[20];//姓名
	char sex[10];//性别
	int age;//年龄
	char id[10];//学号
};//末尾有分号
int main()
{
	struct Student student1 = { "lihua","male",18,"202052" },student2 = { "zhangli","female",20,"202053" };
	//结构体变量student1,student2
	printf("student1:%s,%s,%d,%s\n", student1.name, student1.sex, student1.age, student1.id);
	printf("student2:%s,%s,%d,%s\n", student2.name, student2.sex, student2.age, student2.id);
	return 0;
}

结构成员访问操作符

结构体成员的直接访问

结构体成员的直接访问是通过点操作符( . )访问的。
点操作符接受 两个操作数
使⽤⽅式:结构体变量.成员名
比如上面代码的student1.name,student2.id……

结构体成员的间接访问

有时候我们得到的不是⼀个结构体变量,⽽是得到了⼀个指向结构体的指针,我们可以通过->操作符来间接访问结构体成员
使⽤⽅式: 结构体指针->成员名

#include<stdio.h>
struct Student
{
	char name[20];//姓名
	char sex[10];//性别
	int age;//年龄
	char id[10];//学号
}student1 = { "lihua","male",18,"202052" }, student2 = { "zhangli","female",20,"202053" };//末尾有分号
//变量
int main()
{
	struct Student* pf1 = &student1;
	struct Student* pf2 = &student2;
	printf("student1:%s,%s\n", pf1->name, pf1->id);
	printf("student1:%s,%s\n", pf2->name, pf2->id);
	return 0;
}

操作符的属性:优先级、结合性

优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是不⼀样的。
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执⾏顺序。⼤部分运算符是左结合(从左到右执⾏),少数运算是右 结合(从右到左执⾏)
常见优先级
高• 圆括号( ()
   • ⾃增运算符( ++ ),⾃减运算符( --
   • 单⽬运算符( + -
   • 乘法( * ),除法( /
   • 加法( + ),减法( -
   • 关系运算符( < > 等)
低• 赋值运算符( =
具体的可以看下面的表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值