C语言——操作符
前言
在前面,我们见到了 +, -, * ,/ , < , > , =这些符号,其实它们的名字叫做操作符。按照功能的不同我们可以对它进行一个简单的分类
分类
• 算术操作符: + 、 - 、 * 、 / 、 %
• 移位操作符: << >>
• 位操作符: &, | ,^ ,~
• 赋值操作符: = 、 += 、 -= 、 *= 、 /= 、 %= 、 <<= 、 >>= 、 &= 、 |= 、 ^=
• 单⽬操作符: !、 ++ 、 -- 、 & 、 * 、 + 、 - 、 ~ 、 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以小写形式输出,是大写就用大写形式输出。
但是它们都不会输出前导符,我们可以自己加上,也可以在%后加上#号。
代码如下:
代码语言:javascript代码运行次数:0运行复制#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
例:
代码语言:javascript代码运行次数: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使用哪一种右移运算:
代码语言:javascript代码运行次数:0运行复制#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,我们用注释简单分析一下第二个代码
代码语言:javascript代码运行次数: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;
//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
例:
代码语言:javascript代码运行次数: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补码
//00000000 00000000 00000000 00000101--》 5
printf("%d\n", a & b);
return 0;
}
| 按位或
规则:两个整数补码对应的二进制位只要有1就为1,两个同时为0则为0
例:
代码语言:javascript代码运行次数: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
我们可以用一个简单的代码来进行验证:
代码语言:javascript代码运行次数:0运行复制#include<stdio.h>
int main()
{
int num = 0;
scanf("%d", &num);
printf("%d\n", num | 0);
return 0;
}
说明我们的结论是正确的。
^ 按位异或
规则:两个整数补码对应的二进制位相同为0,不相同为1
例:
代码语言:javascript代码运行次数: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 11111000--》 a^b补码
//10000000 00000000 00000000 00000111 //补码取反加1可以得到原码
//10000000 00000000 00000000 00001000--》 a^b原码---》-8
printf("%d\n", a ^ b);
return 0;
}
~按位取反
规则:一个整数补码按二进制位全部取反,输出原码
这里我们可以看到 ~ 只有一个操作数
例:
代码语言:javascript代码运行次数: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,我们可以用一个代码进行验证
代码语言:javascript代码运行次数: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;
}
为了满足题目要求,我们就可以写出下面的代码
代码语言:javascript代码运行次数: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每次得到的余数就可以得到二进制原码,我们可以写出这样一个代码
代码语言:javascript代码运行次数:0运行复制#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
解决题目我们就可以写出下面的代码
代码语言:javascript代码运行次数: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) //这样需要考虑到优先级的问题,后面会讲解
代码优化
代码语言:javascript代码运行次数:0运行复制#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(在第一个应用中有讲解)
代码语言:javascript代码运行次数: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
逗号表达式,就是⽤逗号隔开的多个表达式。
逗号表达式: 从左向右依次执⾏ , 整个表达式的结果是最后⼀个表达式的结果 。
例:
代码语言:javascript代码运行次数:0运行复制#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;
}
下标访问[]、函数调⽤()
[ ] 下标引⽤操作符
操作数:⼀个 数组名 + ⼀个 索引值(下标)
这个相信大家都不陌生,我们写一个简单的代码
代码语言:javascript代码运行次数:0运行复制#include<stdio.h>
int main()
{
int arr[6] = { 1,2,3,4,5,6 };
printf("%d\n", arr[3]);
//arr数组名,3下标
return 0;
}
函数调⽤操作符 ()
接受 ⼀个或者多个操作数 :第⼀个操作数是 函数名 ,剩余的操作数就是 传递给函数的参数
当函数参数没有的时候就只有一个操作数
例:
代码语言:javascript代码运行次数:0运行复制#include<stdio.h>
void menu()
{
printf("****1.play****\n");
printf("****0.exit****\n");
}
int main()
{
menu();
//没有参数,()只有menu函数名这一个操作数
return 0;
}
代码语言:javascript代码运行次数: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 结构体名
{
成员列表;
}结构体类型变量列表;
举一个简单的例子,描述一个学生,我们需要知道他的姓名,性别,年龄,学号,我们可以写这样一个结构体
代码语言:javascript代码运行次数:0运行复制struct Student
{
char name[20];//姓名
char sex[10];//性别
int age;//年龄
char id[10];//学号
}student1,student2;//末尾有分号
//变量
结构体变量的定义和初始化
结构体变量我们可以直接在结构体后面进行定义和初始化,也可以在所需要使用的函数中进行定义和初始化
代码语言:javascript代码运行次数:0运行复制#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……
结构体成员的间接访问
有时候我们得到的不是⼀个结构体变量,⽽是得到了⼀个指向结构体的指针,我们可以通过->操作符来间接访问结构体成员
使⽤⽅式: 结构体指针->成员名
代码语言:javascript代码运行次数:0运行复制#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;
}
操作符的属性:优先级、结合性
优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是不⼀样的。
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执⾏顺序。⼤部分运算符是左结合(从左到右执⾏),少数运算是右 结合(从右到左执⾏)
常见优先级
高• 圆括号( () )
• ⾃增运算符( ++ ),⾃减运算符( -- )
• 单⽬运算符( + 和 - )
• 乘法( * ),除法( / )
• 加法( + ),减法( - )
• 关系运算符( < 、 > 等)
低• 赋值运算符( = )
具体的可以看下面的表
发布者:admin,转转请注明出处:http://www.yc00.com/web/1754465713a5164417.html
评论列表(0条)