👉 欢迎阅读这篇文章 👇

1、操作符的分类

  • 算术操作符:+、-、*、/、%
  • 移位操作符:<<、>>
  • 位操作符:&、|、^
  • 赋值操作符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、 |=、^=
  • 单⽬操作符:!、++、--、&、*、+、-、~、sizeof、(类型)
  • 关系操作符:>、>=、<、<=、==、!=
  • 逻辑操作符:&&、||
  • 条件操作符: ? :
  • 逗号表达式:,
  • 下标引用:[ ]
  • 函数调用:( )
  • 结构成员访问: . 、->

上述操作符中部分与二进制有关,下面铺垫一些二进制和进制转换的知识

2、 二进制和进制转换

2进制、8进制、10进制、16进制只是数值的不同表现形式。
比如:数值15的各种进制的不同表现形式

15的2进制:1111
15的8进制:17
15的10进制:15
15的6进制:F
16进制的数值之前写:0x
8进制的数值之前写:0

下面重点介绍二进制:
首先还是要从10进制来看

  • 10进制中满10进1
  • 10进制中的数字每一位都是0~9组成的

二进制也一样

  • 2进制中满2进1
  • 2进制中的数字每一位都是0~1组成的

2.1 进制转换

2.1.1 2进制转10进制

10进制的123表示的值是一百二十三,因为10进制的每一位都有权重,10进制的数字从右到左是个位、十位、百位,分别每一位的权重是 10 0 10^0 100 10 1 10^1 101 10 2 10^2 102
在这里插入图片描述

2进制和10进制是一样的,只不过2进制每一位的权重从右到左分别是 2 0 2^0 20 2 1 2^1 21 2 2 2^2 22
比如2进制的1101
在这里插入图片描述

2.1.2 10进制转2进制

125转为2进制的过程如下
在这里插入图片描述

即为:1111101

2.1.3 2进制转8进制

8进制的数字由0~7组成,0~7的数字,各自写成2进制,最多有3个2进制位就可以了,所以在2进制转8进制的时候,从2进制序列中右边低位开始向左每3个2进制位会换算成一个8进制位,剩余不够3个2进制位的拿0补。
在这里插入图片描述
2进制01101011转换为8进制为0153,0开头的数字以表示为8进制数字

2.1.4 8进制转2进制

把8进制数的每一位,都替换成对应的3 位2进制数(不足 3 位的,高位补 0 凑够 3 位)。把所有转换后的2进制数,按原来的顺序从左到右拼起来。

2.1.5 2进制转16进制

16进制的数字是由0~9、a~f组成的0~9, a~f的数字,各⾃写成2进制,最多有4个2进制位就⾜够了,⽐如 f 的⼆进制是1111,所以在2进制转16进制的时候,从2进制序列中右边低位开始向左每4个2进制位换算成1个16进制位,不够4个的拿0补。
在这里插入图片描述

2.1.6 16进制转换为2进制

把16进制数的每一位,都替换成对应的4 位2进制数(不足 4 位的,高位补 0 凑够 4 位)。把所有转换后的2进制数,按原来的顺序从左到右拼起来。

3、原码、反码、补码

整数的2进制表示方法有三种,即原码、反码、补码

3.1 有符号整数

有符号整数的三种表示方法均有符号位和数值位,2进制序列中,最高位的1位被当作符号位,剩余的都是数值位。符号位用0表示正,用1表示负。
在这里插入图片描述
正整数的原、反、补码都相同
负整数的三种表示方法各不相同

原码:直接将数值按照正负数的形式翻译成二进制就是原码
反码:保持原码的符号位不变,其他位次按位取反即可
补码:反码+1得到补码

由补码得到原码也可以:取反,+1

3.2无符号整数

无符号整数的三种2进制表示相同,没有符号位,每一位都是数值位
在这里插入图片描述
对于整形来说:数据存放内存中其实存放的是补码。

4、移位操作符

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

移位操作符是作用于数值的补码,而打印的是数值的原码

注意:移位操作符的操作数只能是整数

4.1左移操作符

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

#include <stdio.h>

int main()
{
	int num = 10;
	int n = num << 1;
	printf("n = %d\n", n);
	printf("num = %d\n", num);
	return 0;
}

在这里插入图片描述

在这里插入图片描述

4.2右移操作符

移位规则:⾸先右移运算分两种:

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

#include <stdio.h>

int main()
{
	int num = -10;
	int n = num >> 1;
	printf("n = %d\n", n);
	printf("num = %d\n", num);
	return 0;
}

在这里插入图片描述

在这里插入图片描述

警告:对于移位运算符,不要移动负数位

例如:

int num = 10;
num >> -1;//error

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

位操作符有:

  • &:按位与
  • |:按位或
  • ^:按位异或
  • ~:按位取反

注:他们的操作数必须是整数。

5.1 &:按位与

对应的补码二进制位,有0则为0,同时为1才为1

int main()
{
	int a = 10;
	int b = -7;
	int c = a & b;
//a中的补码:00000000000000000000000000001010
//b中的补码:11111111111111111111111111111001
//c中的补码:00000000000000000000000000001000
	printf("%d", c);//8
	return 0;
}

5.2|:按位或

对应的补码二进制位,有1则为1,同时为0才为0

int main()
{
	int a = 10;
	int b = -7;
	int c = a | b;
//a中的补码:00000000000000000000000000001010
//b中的补码:11111111111111111111111111111001
//c中的补码:11111111111111111111111111111011
//c中的原码:10000000000000000000000000000101
	printf("%d", c);//-5
	return 0;
}

5.3^:按位异或

对应的补码二进制位,相同则为0,不同则为1

int main()
{
	int a = 10;
	int b = -7;
	int c = a ^ b;
//a中的补码:00000000000000000000000000001010
//b中的补码:11111111111111111111111111111001
//c中的补码:11111111111111111111111111110011
//c中的原码:10000000000000000000000000001101
	printf("%d", c);//-13
	return 0;
}

5.4~:按位取反

对应的补码二进制位,1变为0,0变为1

int main()
{
	int a = 0;
//a中的补码:00000000000000000000000000000000
//按位取反后a中的补码:11111111111111111111111111111111
//按位取反后a中的原码:10000000000000000000000000000001
	printf("%d", ~a);//-1
	return 0;
}

5.5应用1

不创建临时变量,实现两个整数的交换。

可以想到一种方法

#include <stdio.h>

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("a = %d b = %d\n", a, b);
	a = a + b;
	b = a - b;
	a = a - b;
	printf("a = %d b = %d\n", a, b);
	return 0;
}

这种方法当数很大的时候,可能存在潜在问题,a+b可能会溢出。

由按位异或可以得到

  • 0 ^ a a a= a a a
  • a a a ^ a a a= a a a
  • 按位异或满足交换律: a a a ^ b b b ^ a a a= a a a ^ a a a ^ b b b= b b b

可以使用按位异或的方法进行交换,代码如下:

#include <stdio.h>

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("a = %d b = %d\n", a, b);
	a = a ^ b;
	b = a ^ b;//a ^ b ^ b = a
	a = a ^ b;//a ^ b ^ a = b
	printf("a = %d b = %d\n", a, b);
	return 0;
}

在这里插入图片描述

5.6练习

练习1:编写代码实现:求⼀个整数存储在内存中的⼆进制中1的个数。

方法1:
首先想到一位一位的判断是否为1,类比10进制的时候的%10/10得到最后一位判断后再消去最后一位,2进制就可以利用%2/2的方法。
代码:

//方法1
#include <stdio.h>

int main()
{
	int a = 13;
	//00000000000000000000000000001101原码
	//00000000000000000000000000001101补码
	int count = 0;
	while (a)
	{
		if (a % 2 == 1)
		count++;
		a /= 2;
	}
	printf("%d", count);//3
	return 0;
}

这种方法的问题:

  • 无法处理负数,假设为-1,由负数的取模运算可知,最终结果的正负取决于第一个运算数,这里就会得到-1,永远不会满足if条件,就不会计数,导致出错。
    在这里插入图片描述
  • 效率较低

方法2:
想要得到一个数的最后一位可以使用n & 1的方法,这里就可以利用循环的方法,按位与1后,将这个数进行右移,把每一位都化成最后一位,进行判断。
代码如下:

#include <stdio.h>

int main()
{
	int n = 13;
	//00000000000000000000000000001101原码
	//00000000000000000000000000001101补码
	int count = 0;//计数
	for (int i = 0;i < 32;i++)
	{
		if ((n >> i) & 1 == 1)
			count++;
	}
	printf("%d", count);
	return 0;
}

这种方法需要循环32次,效率还是较低

方法3:
利用n = n & (n-1),这个表达式的作用是去掉二进制中最右边的1。当最终为0时,就去掉了所有的1,去掉几次就与几个0,不想要将所有32位数字都循环。
代码如下:

#include <stdio.h>

int main()
{
	int n = 13;
	//00000000000000000000000000001101原码
	//00000000000000000000000000001101补码
	int count = 0;//计数
	while (n)
	{
		count++;
		n = n & (n - 1);
	}
	printf("%d", count);
	return 0;
}

练习2:⼆进制位置0或者置1
编写代码将13⼆进制序列的第5位修改为1,然后再改回0

132进制序列: 00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为000000000000000000000000000001101

分析:

  • 为了将第5位置为1(0变1),就要考虑按位或,按位或的另一个操作数的第5位应该是1,其他位是0就不会改变其他位的数字,即:00000000000000000000000000010000,这个数字是1<<4
  • 为了将第5位再置为0(1变0),就要考虑按位与,按位与的另一个操作数的第5位就应该是0,其他的就应该是1,即:11111111111111111111111111101111,这个数字是~1<<4

代码如下:

#include <stdio.h>

int main()
{
	int n = 13;
	n = n | (1 << 4);
	printf("%d\n", n);
	n = n & ~(1 << 4);
	printf("%d\n", n);
	return 0;
}

在这里插入图片描述

6、单目操作符

单目操作符有这些:
!、++、--、&、*、+、-、~、sizeof、(类型)

其中这些操作符只有&、*还没有介绍,这两个在介绍完指针后再介绍。

7、逗号表达式

exp1,exp2,wxp3,...expN

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

例:

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

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

8.1下标引用操作符

操作数:一个数组名+一个索引值(下标)

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

8.2函数调用操作符

接受⼀个或者多个操作数:第⼀个操作数是函数名,剩余的操作数就是传递给函数的参数。至少有一个操作数。

9、结构成员访问操作符

9.1结构体

C语言提供了很多的内置类型,如char、short、int、long、float、double但是只有这些内置类型不够描述一些复杂的对象。比如要描述一个学生,需要描述名字、年龄、学号、⾝⾼、体重等。C语言为了解决这个问题,定义了结构体这一数据类型,让程序员可以⾃⼰创造适合的类型。

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

9.1.1结构的声明

struct tag
{
member-list;
}variable-list;
  • struct是结构体的关键字
  • tag是自定义的名字
  • member-list成员列表可以是一个或者多个
  • 最后的分号不要丢
  • variable-list是定义的变量,可有可无。

描述一个学生:

struct student//结构体类型声明
{
	char name[20];//姓名
	int age;//年龄
	float score;//分数
	//这里不允许初始化,只是类型的声明
};

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

结构体变量的定义

struct student//声明类型
{
	char name[20];//姓名
	int age;//年龄
	float score;//分数
}s1,s2,s3;

//全局变量
struct student s4;
struct student s5;

int main()
{
	//局部变量
	struct student s6;
	struct student s7;
	struct student s8;
	return 0;
}

上方代码定义了8个变量

结构体变量的初始化

struct student
{
	char name[20];//姓名
	int age;//年龄
	float score;//分数
};

int main()
{
	//局部变量
	struct student s1 = {"zhangsan",18,85.0f};//初始化
	struct student s2 = { "lisi",19,90.0f };//初始化
	struct student s3 = { .age=18,.name="wangwu",.score=86.0f};//指定顺序初始化
	return 0;
}

结构体嵌套

struct student//结构体声明
{
	char name[20];//姓名
	int age;//年龄
	float score;//分数
};

struct test//结构体声明
{
	int x;
	int y;
	struct student s;//结构体嵌套
};

int main()
{
	//局部变量
	struct student s1 = { "zhangsan",18,85.0f };//初始化
	struct student s2 = { "lisi",19,90.0f };//初始化
	struct student s3 = { .age = 18,.name = "wangwu",.score = 86.0f };//指定顺序初始化
	struct test t1 = { 10,20,{"liliu",20,90.0f} };//结构体嵌套初始化
	struct test t2 = { .s.name = "liuwu",.s.age = 18,.s.score = 80.0f,.x = 10,.y = 20 };;//结构体嵌套初始化,指定顺序初始化
	return 0;
}

9.2结构成员访问操作符

9.2.1结构成员的直接访问

结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所⽰:打印定义的结构体成员

struct student
{
	char name[20];//姓名
	int age;//年龄
	float score;//分数
};

struct test
{
	int x;
	int y;
	struct student s;
};

int main()
{
	//局部变量
	struct student s1 = { "zhangsan",18,85.0f };//初始化
	struct student s2 = { "lisi",19,90.0f };//初始化
	struct student s3 = { .age = 18,.name = "wangwu",.score = 86.0f };//指定顺序初始化
	struct test t1 = { 10,20,{"liliu",20,90.0f} };//结构体嵌套初始化
	struct test t2 = { .s.name = "liuwu",.s.age = 18,.s.score = 80.0f,.x = 10,.y = 20 };//结构体嵌套初始化,指定顺序初始化
	//结构体成员的打印
	printf("%s ,%d ,%.1f\n", s1.name, s1.age, s1.score);//.操作符访问成员,操作数位变量名和结构体成员
	printf("%s ,%d ,%.1f\n", s2.name, s2.age, s2.score);
	printf("%s ,%d ,%.1f\n", s3.name, s3.age, s3.score);
	printf("%d ,%d ,%s ,%d ,%.1f\n", t1.x, t1.y, t1.s.name, t1.s.age, t1.s.score);
	return 0;
}

在这里插入图片描述
使⽤⽅式:结构体变量.成员名

9.2.2结构体成员的间接访问

指针介绍完后更新

10、操作符的属性

C语言的操作符具有两个重要属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。

  • 优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是不⼀样的。
  • 如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执⾏顺序。⼤部分运算符是左结合(从左到右执⾏),少数运算符是右结合(从右到左执⾏),⽐如赋值运算符( = )。

运算符的优先级顺序很多,下⾯是部分运算符的优先级顺序(按照优先级从⾼到低排列)

  • 圆括号 ()
  • 自增运算符++、自减运算符--
  • 单目运算符+-
  • 乘法*、除法\
  • 加法+、减法-
  • 关系运算符><
  • 赋值运算符=

由于圆括号的优先级最⾼,可以使⽤它改变其他运算符的优先级。

详细的优先级如下,使用时可以查询:
在这里插入图片描述
https://zh.cppreference.com/c/language/operator_precedence

11、表达式求值

11.1整型提升

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

为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型,也就是说char、short 这种「比 int 小的整型」,只要参与运算,会自动先变成 int 类型这个自动转换过程,称为整型提升

只有

  • char / signed char / unsigned char
  • short / unsigned short

只要它们参与任何运算,立刻提升为 int

例如:

char a,b,c;
...
a = b + c;

b和c的值先被提升为普通整型,然后再执行加法运算。
加法运算完成后再被截断变回char类型,存储到a中。

如何进⾏整体提升呢?

  1. 有符号整数提升是按照变量的数据类型的符号位来提升的,也就是在⾼位补充符号位
  2. ⽆符号整数提升,⾼位补0

负数的整型提升

char c1 = -1;
//char为有符号的
//10000001原码
//11111111补码
//11111111111111111111111111111111 高位补1,整型提升

正数的整形提升

char c1 = 1;
//char 是有符号的
//00000001原码
//00000001补码
//00000000000000000000000000000001 高位补0,整型提升

例:

#include <stdio.h>

int main()
{
	char a = 10;
	//00000000000000000000000000001010截断
	//00001010存储到a中
	char b = 120;
	//00000000000000000000000001111000截断
	//01111000存储b中

	char c = 0;

	c = a + b;

	//a
	//00001010
	//00000000000000000000000000001010整型提升
	//b
	//01111000
	//00000000000000000000000001111000整型提升
	//运算
	//00000000000000000000000010000010结果-截断
	//10000010存储c中
	printf("%d", c);
	//10000010再次提升
	//11111111111111111111111110000010补码
	//10000000000000000000000001111110原码

	return 0;
}

上述代码的过程:
a是一个char类型的数据,给他赋值了一个整数13,要先截断成8位二进制序列后储存到a中。b同理。然后执行运算,执行运算之前要先将a和b进行整形提升,提升为32位二进制序列,然后进行运算,运算完成后赋值给c,再次截断为8位二进制序列储存到c中。接着要将c进行传参到printf函数中,需要将c再次整形提升至32位二进制序列后转为原码打印

11.2 算术转换

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

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

如果某个操作数的类型在上⾯这个列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后执⾏运算。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐