当前位置: 首页 > article >正文

【重生之我要苦学C语言】操作符详解、结构体与表达式求值过程

操作符详解

操作符的分类

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

二进制和进制转换

2进制、8进制、10进制、16进制是数值的不同表示形式

比如:数值15的各种进制的表示形式:

  • 15的2进制:1111
  • 15的8进制:17
  • 15的10进制:15
  • 15的16进制:F5

16进制的数值之前写:0x
16进制:0~15(10 ~15分别用a ~f表示)
8进制的数值之前写:0
8进制:0~7

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

二进制转化成十进制

10进制的数字从右向左是个位、十位、百位.…,分别每一位的权重是10°,10’,10…如下图:

		       百位	   十位	  个位		
	10进制的位	1	     2	    3		
	权重	   10^2	   10^1	   10^0	
	权重值	   100	     10	     1		
	求值	   1*100 + 2*10* +	3*1  =	123	

.
2进制的每一位的权重,从右向左是: 2°,2, 2*2…

2进制的位 1 1

1 0 权重 2^3 2^2 2^1 2^0 权重值 8 4 2 1

	2进制的位	1	     1       0      1		
	权重值	    8        4       2      1	
	权重	   2^3       2^2     2^1    2^0 
	求值	   1^8 +     1^4  +	 0^2  + 1^1  =	13	

十进制转二进制

	2	 125 余数为1	
	2     62 余数为0,
	2     31 余数为1
	2	  15 余数为1.	
	2	   7 余数为1,		
    2	   3  余数为1
    2	   1  余数为1	
           0
  由下往上依次所得的余数就是10进制转换出的2进制	

10进制的125转换的2进制:1111101

2进制转8进制和16进制

2进制转8进制

8进制的数字每一位是0~ 7 的 ,0~7的数字,各自写成2进制,最多有3个2进制位就足够了,比如7的二进制是
111,所以在2进制转8进制数的时候,从2进制序列中右边低位开始向左每3个2进制位会换算一个8进制位,剩余不够3个2进制位的直接换算

如:2进制的 01101011,换成8进制:0153,0开头的数字,会被当做8进制。

 2进制	0	1	1	0	1   0	1   1	
 8进制	    1   	    5	        3	

如果想将八进制转换成二进制,将以上过程倒过来即可

2进制转16进制

16进制的数字每一位是0~ 9,a ~ f 的,0~ 9,a~f的数字,各自写成2进制,最多有4个2进制位就足够了,比如f的二进制是1111,所以在2进制转16进制数的时候,从2进制序列中右边低位开始向左每4个2进制位会换算一个16进制位,剩余不够4个二进制位的直接换算

如:2进制的01101011,换成16进制:0x6b,16进制表示的时候前面加0x

2进制	0	1   1   0 	1   0   1   1	
16进制	            6	            b

如果想将八进制和十六进制转化为十进制,有以下两种方法:

  • 直接算:
    如:0x6b
    6 * 16+12 * 1
  • 将八进制和十六进制转化为二进制后,再进行转化
    (八进制转化为十六进制同理,可借助二进制来完成)

原码、反码、补码

整数的2进制表示方法有三种,即原码、反码和补码
有符号整数的三种表示方法均有符号位和数值位两部分,2进制序列中,最高位的1位是被当做符号位,剩余的都是数值位
符号位都是用0表示“正”,用1表示“负”

在这里插入图片描述

正整数的原、反、补码都相同
例:
0 0000000 00000000 00000000 00001010 ——原码
0 0000000 00000000 00000000 00001010 ——反码
0 0000000 00000000 00000000 00001010 ——补码

负整数的三种表示方法各不相同,需要计算:

原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码
补码:反码+1就得到补码
补码得到原码也是可以使用:取反,+1(或-1,取反)的操作

例:
1 0000000 00000000 00000000 00001010 ——原码
1 1111111 11111111 11111111 11110101——反码
1 1111111 11111111 11111111 11110110——补码

无符号整数的三种2进制表示相同没有符号位,每一位都是数值位

在这里插入图片描述

原码:对外展示
反码:中间状态
补码:内存中存储

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

例:
原码:1+(-1)
00000000 00000000 00000000 00000001——1
10000000 00000000 00000000 00000001——(-1)


10000000 00000000 00000000 00000010——(-2)
显然是错误的
补码:
00000000 00000000 00000000 00000001——1
11111111 11111111 11111111 11111111——(-1)


00000000 00000000 00000000 00000000——0

移位操作符

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

注:移位操作符的操作数只能是整数,是双目操作符

左移操作符

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

假设num=10
00000000 00000000 00000000 00001010——原码
00000000 00000000 00000000 00001010——反码
00000000 00000000 00000000 00001010——补码

将num向左移动一位:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int num = 10;
	int n = num << 1;
	printf("n= %d\n", n);
	printf("num= %d\n", num);
	return 0;
	
}

结果为:

在这里插入图片描述

移位具体过程如下:

在这里插入图片描述

由输出结果可知,移位并不改变num本身的值
如果想要num改变:

num = num << 1;
或
num <<= 1;

右移操作符

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

  • 1.逻辑右移:左边用0填充,右边丢弃
  • 2.算术右移:左边用原该值的符号位填充,右边丢弃
  • 常见的编译器上采用的都是算术右移
  • 到底使用哪种右移方式,C语言标准没有规定,取决于编译器

num=10,将num向右移动一位,采用算术右移:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int num = 10;
	int n = num >> 1;
	printf("n= %d\n", n);
	printf("num= %d\n", num);
	return 0;
}

结果为:
在这里插入图片描述

num=-1时,采用逻辑右移:

在这里插入图片描述
移位后仍是补码,应变为原码

注意:对于移位运算符,不要移动负数位,这是标准未定义的

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

位操作符有:

&	     //按位与——(双目操作符)二进制位
|	     //按位或
^	     //按位异或
~	     //按位取反——单目操作符

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

区分:

&&逻辑与 ——双目操作符
||逻辑或

按位与&:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int a = 3;
	int b = -5;
	int c = a & b;//按位与
	//00000000 00000000 00000000 00000011 3的原码
	//00000000 00000000 00000000 00000011 3的反码
	//00000000 00000000 00000000 00000011 3的补码
	//
	//10000000 00000000 00000000 00000101 -5的原码
	//11111111 11111111 11111111 11111010 -5的反码
	//11111111 11111111 11111111 11111011 -5的补码
	//
	//00000000 00000000 00000000 00000011 3的补码
	//11111111 11111111 11111111 11111011 -5的补码
	//00000000 00000000 00000000 00000011 补码 
	// 对应二进制位上有0,则为0;两个同时为1,则为1
	printf("c= %d\n", c);
	return 0;
}

输出为:

在这里插入图片描述

按位或| :

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int a = 3;
	int b = -5;
	int c = a | b;//按位或
	//00000000 00000000 00000000 00000011 3的补码
	//11111111 11111111 11111111 11111011 -5的补码
	//11111111 11111111 11111111 11111011 补码
	// 对应二进制位上有1,则为1;两个同时为0,则为0
	printf("c= %d\n", c);
	return 0;
}

结果为:

在这里插入图片描述

按位异或^ :

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int a = 3;
	int b = -5;
	int c = a ^ b;//按位异或
	//00000000 00000000 00000000 00000011 3的补码
	//11111111 11111111 11111111 11111011 -5的补码
	//11111111 11111111 11111111 11111000 补码
	// 对应二进制位上相同为0,相异为1
	printf("c= %d\n", c);
	return 0;
}

输出为:

在这里插入图片描述

按位取反~ :

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int a = 0;
	int b = ~a;
	printf("%d\n", b);//按位取反
	printf("%d\n", a);//按位取反
	//00000000 00000000 00000000 00000000
	//11111111 11111111 11111111 11111111
	//将补码的所有二进制位取反
	return 0;
}

输出为:

在这里插入图片描述

练习1:不创建临时变量,实现两个整数的交换

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int a =3;
	int b = 5;
	//将a和b的内容交换一下
	printf("交换后:a=%d b=%d\n", a, b);
	a = a + b;
	b = a - b;//得到原来的a
	a = a - b;//得到原来的b
	printf("交换后:a=%d b=%d\n", a,b);
	return 0;
}

缺点:当a,b都很大的时候,这种方法可能存在溢出的风险
优点:效率高,代码的可读性更好

应改为:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int a =3;
	int b = 5;
	//将a和b的内容交换一下
	printf("交换后:a=%d b=%d\n", a, b);
	a = a ^ b;//a'=a^b
	b = a ^ b;//得到原来的a  b=a'^b=a^b^b=a
	a = a ^ b;//得到原来的b  a=a'^a=a^b^a=b
	printf("交换后:a=%d b=%d\n", a,b);/
	return 0;
}
     a^a = 0
     0^a = a
     3^3^5 = 5
     3^5^3 = 5
     //异或是支持交换律的

缺点:

  • 1.这种方法只能针对整数交换
  • 2.这种方式可读性不高

练习2 :求一个正数存储在内存中的二进制中1的个数

方法一:

因为存储方式为二进制,故取出每一位数字应当%2

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int count = 0;
	int n = 15;
	while (n) {
		if (n % 2 == 1) {
			count++;
		}
		n /= 2;
	}
	printf("%d", count);
	return 0;
}

当n为正数时,成立,但是当n为负数时,却不成立,故该方法有局限性

方法二:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int count = 0;
	int n = -1;
	int i = 0;
	for(i = 0;i < 32;i++) {
		if ((n >> i) & 1 == 1) {
			count++;
		}
	}
	printf("%d", count);
	return 0;
}
   n&1——>1  最低位是1
   n&1——>0  最低位是0

假设n为:
00010101 00010101 01010001 00010011
1:
00000000 00000000 00000000 00000001
所以只需要在每次判断最低位之后,向右移动一位,使当前位置成为最低为即可

方法三:

先看一个式子:

n=n&(n-1)

假设n=14,即1110
n-1:1101
按位与后:n=1100
n-1: 1011
按位与后:n=1000
n-1: 0111
按位与后:n=0000

这个表达式会将n的二进制表示中最右边的1去掉
所以,在n变成0之前,这个表达式能执行几次,说明n的二进制表达式中有几个1

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int count = 0;
	int n = -1;
	int i = 0;
	while (n) {
		count++;
		n = n & (n - 1);
	}
	printf("%d", count);
	return 0;
}

练习3:判断n是否为2的幂次方数

2:0010
4:0100
8:1000
16:10000
32:100000
找规律:凡是2的幂次方,其二进制表达式中只含一个1
即判断:

if ((n & (n - 1)) == 0) 

即可

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int n = 8;
	if ((n & (n - 1)) == 0) {
		printf("yes");
	}
	else printf("no");
	return 0;
}

练习4:二进制位置0或者置1

编写代码将13(n)二进制序列的第五位改为1,再改回为0
00001101——>00011101
按位或00010000(1向左移动四位)即可

n|(1<<4)

代码如下:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int n = 13;
	n |= (1 << 4);
	printf("%d", n);
	return 0;
}

改回来:
00011101——>00001101
按位与11101111(1向左移动四位后按位取反)即可

n & ~(1 << 4);

总代码如下:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int n = 13;
	n |= (1 << 4);
	n &= ~(1 << 4);
	printf("%d", n);
	return 0;
}

逗号表达式

exp1,exp2,exp4,……expN

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

例1:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	int a = 1;
	int b = 2;
	int c = (a > b, a = b + 10, a, b = a + 1);
	printf("%d", c);
	return 0;
	
}

过程如下:

a>b 不成立,但对a,b的值没有影响
a=b+10,a变为12
a,对a,b的值没有影响
b=a+1,b=13
将b的值赋给c
所以c应该为13

例2:

if (a = b + 1, c = a / 2, d > 0) {
	//真正为if语句判断的是d>0
}

例3:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
	a = get_val();
	count_val(a);
	while (a > 0) {
		a = get_val();
		count_val(a);
	}
	return 0;
}

冗余
用逗号表达式改写:

while (a = get_val(), count_val(a),a > 0) {
	
}

下标访问操作符[]

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

arr[5]   //arr和5是[]的两个操作数

函数调用操作符

接受一个或多个操作数:第一个操作数是函数名,第二个操作数就是传递给函数的参数

printf("hehe");//()为函数调用操作符
//printf和hehe是()的两个操作数
Add(35;//调用自定义函数
//Add,3,5为()的操作数
//()最少有一个操作数,就是函数名

结构成员访问操作符

结构体

C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学生,这时单一的内置类型是不行的,描述一个学生需要名字、年龄、学号、身高、体重等……
C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。

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

结构的声明,定义与初始化
 struct tag{
 
	member-list;	
 }variable-list;

struct:关键字
tag:名字
member-list:成员列表(1个/多个成员)
variable-list:变量列表(可有可无)

例:
描述一个学生:
名字——char name[20];
年龄——int age;
性别——char sex[6];//男/女/保密 一个汉字占两个字符
学号——char id[12];

#include <stdio.h>
//声明一个结构体类型
struct Stu {
	char name[20];
	int age;
	char sex[6];
	char id[12]
}s4;//s4是一个全局结构体变量
struct Stu s3;//s3是一个全局结构体变量
int main(){
	struct Stu s1={"张三",20,,2024010203};//s1是一个局部结构体变量
	//按照默认顺序初始化
	struct Stu s2={.age=22,.id="2024010203",.name="lisi",sex="女"};//s2是一个局部结构体变量
	//指定顺序初始化
	return 0;
}

结构体中是存放多个成员的,初始化也使用{}

结构体嵌套:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//声明一个结构体类型
struct Point {
	int x;
	int y;
};
struct Data {

	int n;
	struct Point p;
	double d;
};
int main(){
	struct Data data = { 100,{10,20},3.14 };
	return 0;
}

结构成员访问操作符

结构体成员的直接访问

结构体成员的直接访问通过点操作符(.)进行访问,点操作符接受两个操作数

printf("%s\n", s1.name);//结构体变量.结构体成员的名字
s1.age=18printf("%d\n", s1.age);

输入

struct Stu s1 = { 0 };
scanf("%s %d %s %s", s1.name, &(s1.age), s1.sex, s1.id);
//name,sex,id为数组名,本来就是地址,不需要取地址,age是整型变量,需要取地址
结构体成员的间接访问

->是依赖指针的,会在后期指针部分进行说明

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

优先级(相邻操作符)

优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行
各种运算符的优先级是不一样的

在这里插入图片描述
https://zh.cppreference.com/w/c/language/operator_precedence

结合性

如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符(=)

常见运算符优先级从高到低:

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

圆括号优先级最高,可以改变其他运算符的优先级

表达式求值

整型提升

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

如何进行整型提升?

  • 1.有符号整数提升是按照变量的数据类型的符号位来提升的
  • 2.无符号整数提升,高位补0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
	int a = 10;
	short s = 5;
	int b = s + a;//short会被转化为int类型,再计算

	char c1 = 5;
	char c2 = 126;
	char c3 = c1 + c2;//char会被转化为int类型,再计算
	//char:-128~127  126+5=127+1+1+1+1=-125
	//char类型也属于整型,因为字符在存储的时候,存储的是ASCII码值
	//5:00000000 00000000 00000000 00000101
	//c1:00000101
	//126:00000000 00000000 00000000 01111110
	//c2:01111110
	//发生整型提升
	//c1:00000000 00000000 00000000 00000101
	//c2:00000000 00000000 00000000 01111110
	//c1+c2:00000000 00000000 00000000 10000011
	//c3:10000011
	//%d打印十进制的有符号整数
	//c3发生整型提升:11111111 11111111 11111111 10000011(补码)
	//原码:10000000 00000000 00000000 01111101
	//-125 
	return 0;
}

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

算术转换

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

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

如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算

问题表达式

虽然操作符又优先级和结合性,但是依然可能写出一些没办法确定唯一计算路径的表达式

例1:

a* b + c * d + e * f;
//只能确定先计算乘法再计算加法,但无法确定乘法之间的先后顺序

例2:

c + --c;
//操作符的优先级只能决定自减--的运算在+的运算的前面,但是没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的

例3:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
	int i = 1;
	int ret = (++i) + (++i) + (++i);
	printf("%d\n", ret);
	printf("%d\n", i);
	return 0;
}

在VS(msvc)中,输出为:

在这里插入图片描述

而在devC++(gcc)中,输出为:

在这里插入图片描述

END……

不管前方的路有多苦,只要走的方向正确,
不管多么崎岖不平,都比站在原地更接近幸福。
—《千与千寻》


http://www.kler.cn/a/372140.html

相关文章:

  • sql进阶篇
  • R语言中的Lasso回归:全面指南与实战案例
  • Go语言有哪些常用语句?
  • 【刷题11】CTFHub技能树sql注入系列
  • docker中使用ros2humble的rviz2不显示问题
  • 电源完整性
  • Docker部署学习
  • SQL语言基础
  • 【Linux】centos7内核编译6.11.3版本及其所出现的问题解决方案(升级make、升级gcc)
  • 包和模块(上) python复习笔记
  • 汇流箱组件:光伏汇流采集装置 参数介绍
  • RTT工具学习
  • AI技术的应用前景如何?它会不会改变我们的生活和工作方式?
  • 阿里云VPC机器如何访问公网
  • Vue.observable vs Vuex:何时使用轻量级状态管理?
  • 【python】flash-attn安装
  • 【Clickhouse】客户端连接工具配置
  • 面试 Java 基础八股文十问十答第二十九期
  • Javaee:阻塞队列和生产者消费者模型
  • Brainpy的jit编译环境基础
  • 【LeetCode】跳跃游戏ⅠⅡ 解题报告
  • 如何在Linux系统中使用Netcat进行网络调试
  • Transformer中的Encoder
  • 基于STM32G0的USB PD协议学习(3)
  • 基于微信小程序的图书馆座位预约系统+LW示例参考
  • 数据结构算法学习方法经验总结