1. 操作符的分类
2. ⼆进制和进制转换
3. 原码、反码、补码
4. 移位操作符
5. 位操作符:&、|、^、~
6. 单⽬操作符
7. 逗号表达式
8. 下标访问[]、函数调⽤()
9. 结构成员访问操作符
10. 操作符的属性:优先级、结合性
11. 问题表达式求值
书中自有颜如玉,书中自有黄金屋
一、操作符分类
其中有些操作符我们已经学过了,但是我们今天在介绍一部分,今天的这些部分都是与二进制有关的所以我们今天来先结束二进制。
二、二进制和进制转换
我们在学数学中偶尔会听到“2进制,8进制,10进制,16进制”,那这些是什么意思呢?
其实这些进制的表示都是对一个数的不同表达形式罢了。
我们来举20数值的的不同进制的表达形式
我们来类比10进制来学2进制
在10进制中:
1.10进制满10进1
2. 只能出现0~9的数字
则在2进制中
1.2进制满2进1
2.只能出现0~2的数字
2.1进制的转换
10进制的123表示的值是253,为什么是这个值呢?这是因为10进制每一位都是有权重的
我们知道2进制和10进制是非常类似的,那么如果是2进制的1101应该怎么理解呢?
2.1.1
10进制转2进制数字
方法:除2取余法
2.2 2进制转8进制和16进制
8进制的数字的每一位都是0~7,0到7的数字各自写成2进制,最多3个二进制数就够了。
我们拿7来举个例子如下:
所以我们可以知道从二进制序列中每3个二进制位转化位1个8进制位。
2.2.2 二进制位转换位16进制(与2进制位转换成16进制位的方法是一样的)
三、原码、反码、补码
整数的2进制有三种表示的方法:原码、反码、补码
对于有符号的整数(负数)的三种表示方法都有符号位和数值位两部分。
我们用 0 表示正 ,用1表示负。
正整数的原码,反码,补码都是一样的
负整数的原码,反码,补码都是不一样的
原码:将数字直接按照正负数的规则翻译成二进制得到的就是原码
反码:原码的符号位不变,其他数值位按位取反就可以的到反码
补码:反码加1就是补码
补码直接取反+1也可以得到原码
对于整形来说:内存中储存的是补码
这是为什么呢?
在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于,使⽤补码,可以将符号位和数值域统⼀处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
举个例子帮大家了解:
四、位移操作符
1.<<:左移操作符
2.>>右移操作符
注意:位移操作符操作的只能是整数
4.1左移操作符
规则:左边抛弃、右边补0
#include<stdio.h>
int main()
{
// 正整数的原码反码补码相同
//10的原码为 00000000000000000000000000001010
//左移操作符的规则:左边边抛弃右边补0 ;
//00000000000000000000000000001010 变为 00000000000000000000000000010100
//我们根据操作猜测为4 + 16 = 20
int a = 10;
int b = a << 1;
printf("%d", b);
return 0;
}
打印出来的结果就是20,说明我们对了。
4.2 右移操作符
规则:右移操作符运算分为两种
1.逻辑右移:左边用0填充,右边丢弃
2.算术右移:左边用原来的符号填充,右边丢弃.(一般用这个)
int main()
{
int num = -10;
//-10的原码反码补码不一样的
//原码10000000000000000000000000001010
//反码11111111111111111111111111110101
//补码11111111111111111111111111110110
// 11111111111111111111111111111101 //算术右移 (正常)
// 原码 10000000000000000000000000000011 =-3
// 00111111111111111111111111111101 //逻辑右移
// 原码 11000000000000000000000000000011 //这里我们发现逻辑右移出了点问题
int num2 = num >> 2;
printf("%d", num2);
return 0;
}
这里我们需要注意的是:对于位移操作符不能移动负数位(这个标准是没有定义的)。
int main()
{
int num = -10;
int num2 = num >> -2; //error
printf("%d", num2);
return 0;
}
五、位操作符:& 、|、 ^、 ~
注意:这里的操作数必须为整数
直接用代码来实践
#include<stdio.h>
int main()
{
int num1 = 5; //5的原码(就是补码) 00000000000000000000000000000101
int num2 = -6;
//-6 的原码 1000000000000000000000000000110
//反码 1111111111111111111111111111001
//补码 1111111111111111111111111111010
printf("%d ", num1 & num2);
//5的原码 0000000000000000000000000000101
//6的补码 1111111111111111111111111111010
//&后得 0000000000000000000000000000000
//原码为 0000000000000000000000000000000
//0
printf("%d ", num1 | num2);
//5的原码 0000000000000000000000000000101
//6的补码 1111111111111111111111111111010
//|后得 1111111111111111111111111111111(符号位不变)
//原码为 1000000000000000000000000000001
//-1
printf("%d ", num1 ^ num2);
//5的原码 0000000000000000000000000000101
//6的补码 1111111111111111111111111111010
//^后的 1111111111111111111111111111111
//原码 1000000000000000000000000000001
//-1
printf("%d ", ~5);//对一个数得
//5的原码 0000000000000000000000000000101
//~后得 1111111111111111111111111111010
//原码 1000000000000000000000000000110
//-6
return 0;
}
我们来出一道题目:
实现两个变量得值交换,但是不能创建第三个变量
int main()
{
int a = 10;
int b = 20;
a = a ^ b; // a^b^b =a ,a^b^a = b
b = a ^ b; //这里我们可以通过^操作符来进行不创建变量来来交换数字
a = b ^ a;
printf("a=%d b=%d", a, b);
return 0;
}
六、单目操作符
单目操作符的表面意思就是对一个数字进行操作
这里我们只剩 * 和 & 没有见过等我们学习指针的时候来解释
七、逗号表达式
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果
int main()
{
int a = 10;
int b = 20;
int c = (a = b + a, b < a, b = a + b);
//这里的c的值是多少呢?
//从左向右执行得到的是c=50
printf("%d", c);
return 0;
}
八、下标访问[]、函数调用()
8.1 []下标引用操作符
操作数:数组名+[常量数字]
int main()
{
int arr[10] = { 1,2,3,4,5,6,7 };
//[]下标引用操作符
//arr 数组名
//arr+[常数],就可以拿到素组元素对应的下标
printf("%d", arr[2]);
return 0;
}
8.2 函数调用操作符
函数调用操作符()
函数名加上()就可以调用我们所写的函数
void text1(void)
{
printf("咯咯哒\n");
}
void text2(char* pr)
{
printf("%s\n", pr);
}
int main()
{
//()是函数调用操作符
//text1是函数名
text1();
//text加上()就可以调用函数
text2("ikun");
return 0;
}
九、结构成员操作符
9.1 结构体
C 语言给我们提供了一系列的类型 int ,char ,double …也给我们提供了对应的数组。但是这些类型往往都是单一的,如果我们想描述一本书,一个学生,只有这些单一的类型是不够的。
对于以一个学生:名字,年龄,学号…
C语言为了解决这种问题给我们提供了一种自定义的数据类型,让我们可以自由的设计自己需要的情况。
9.1.结构体的声明
9.1.2 结构体变量的定义和初始化
struct nt
{
int x;
int y;
}p1;//全局变量(声明的时候同时定义)
struct p
{
char name[20];
int age;
};
int main()
{
struct nt p2 = { 1,2 }; //局部变量
printf("%d %d\n", p2.x, p2.y);
struct p p3 = { "lik",{10} };//顺序变量初始化
printf("%s %d\n", p3.name,p3.age);
struct p p4 = { .age = 20 , .name = "wkl" };//指定变量初始化
printf("%s %d\n", p4.name, p4.age);
return 0;
}
9.2 结构体成员访问操作符
struct s
{
int a;
int b;
};
int main()
{
struct s s1 = { {10},{20} };
printf("%d %d", s1.a, s1.b); // .的左右两边有两个数值
}
使用方式:结构体变量 . 成员名
9.2.2 结构体变量的间接访问
结构体也是有指针的,那么如果我们拿到的是结构体的指针是如何使用的呢?
struct s
{
int a;
int b;
};
int main()
{
//int a = 10 ;
//int * pr = &a;
struct s s1 = { {10},{20} };
struct s* pr = &s1;
//这里我们需要在引入一个操作符,(->)
printf("%d %d", pr->a, pr->b); // ->的左右两边有两个数值
}
使用方式:结构体指针 -> 成员名
这里我们主要了解的是操作符,在以后的学习中我们更详细的了解一下。
综合举例
struct s
{
char name[15];
int age;
};
void print_(struct s S1) //直接访问
{
printf("%d %s\n", S1.age, S1.name);
}
void prrr_(struct s* prr)//间接访问
{
printf("%d %s", prr->age, prr->name);
}
int main()
{
struct s s1 = { "lisi",20 };
struct s* pr = &s1;
print_(s1);
prrr_(pr);
}
十、操作符的属性:优先级、结合性
在数学中我们通过数字的一些规则可以先进行数字的先后操作顺序,那么在C语言中我们也有自己的一套方法。
即:操作符的优先级和结合性,通过这两个方法我们就可以确定表达式的计算顺序。
10.1 优先级
int main()
{
int a = 5 + 5 * 5;
return 0;
}
对于该数值a答案是多少呢? 是先计算5 + 5 然后 再5,还是先计算 55 再加5呢?
操作符的优先级不同决定了数值的不同,我们肯定知道的是 / * 的优先级是大于 + -的。所以这里是先计算的是 5 +5
10.2 结合性
当两个操作符的优先级相同的时候优先级就没办法确定先算那个了?
int main()
{
int num = 5 * 5 / 3;
return 0;
}
这里的计算值为 8 我们发现就是先计算的是5 * 5 然后再/3。
这里就可以知道,当两个操作符的优先级相同时,运算顺序是从左到右的(大部分)只有赋值运算符=是从右到左的
我们需要记住的优先级顺序如下:
()的运算符最高我们可以运用()来改变其它运算符的优先级
其它运算符优先级我们可以参考其表
https://zh.cppreference.com/w/c/language/operator_precedence
十一、问题表达式解析
11.1 表达式1
这里的的计算顺序不同,也就可能会导致一些问题(这个问题恰好是满足)
11.2 表达式 2
int main()
{
int i = 20;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("%d", i);
return 0;
}
这里我们发现,在不同的编译器上的结果是不同的,说明编译器遇见这种复杂的代码也出现了问题。
总结;在我们学习了操作符的优先级和结合性之后,我们也不能写那种太过复杂的代码
写代码的时候还是需要一步一步的写,