操作符详解
一、操作符的分类
算数操作符:+、-、*、/、%
移位操作符:<<、>> (移动的是二进制位)
按位操作符:&、|、^、~
赋值操作符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=
单目操作符:!、++、--、&、*、+、-、sizrof、(类型)
关系操作符:>、>=、<、<=、==、!=
逻辑操作符:&&、||
条件操作符:? : (唯一的三目操作符)
逗号表达式:,
下标引用:[ ]
函数调用:()
结构成员访问:. 、->
在前面的文章中已经介绍过一部分操作符,接下来一起学习剩下的。
二、二进制和进制转换
我们经常能听到2进制、8进制、10进制、16进制这样的讲法,那是什么意思呢?其实2进制、8进制、10进制、16进制是数值的不同表示形式。
举个例子,比如15这个数字,这其实是它的10进制表示形式,那15的其他进制我们也可以表示出来
15的2进制:1111
15的8进制:17
15的10进制:15
15的16进制:F
当然这里二进制是比较重要的,因为我们对10进制比较熟悉,我们可以通过10进制来理解2进制:10进制中满10进1,10进制的数字每⼀位都是0~9的数字组成。2进制也是如此:2进制中满2进1,2进制的数字每⼀位都是0~1的数字组成。
1、2进制转10进制

2、2进制转8进制
8进制的数字每⼀位是0~7的,0~7的数字,各⾃写成2进制,最多有3个2进制位就⾜够了,⽐如7的2进制是111,所以在2进制转8进制数的时候,从2进制序列中右边低位开始向左每3个2进制位会换算⼀个8进制位,剩余不够3个2进制位的直接换算。比如:2进制的01101011,换成8进制:0153,0开头的数字,会被当做8进制
3、2进制转8进制
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进制表示方法有三种:原码、反码、补码
有符号整数的三种表示方法均有符号位和数值位两部分,2进制序列中,最高位的1位是被当做符号位,剩余的都是数值位。符号位都是用0表示“正”,用1表示“负”。正整数的原、反、补码都相同,负整数的三种表示方法各不相同。
原码:直接将数值按照正负数的形式翻译成2进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。

注意:对于整型来说,数据存放内存中其实存放的是补码。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,原码取反加一的到补码,补码取反加一得到原码,不需要额外的硬件电路。
四、移位操作符
移位操作符是 C 语言中对二进制补码位进行位移操作的运算符,分为 左移<< 和 右移>> 两种,核心作用是把整数的二进制位整体向左 / 向右移动指定位数,是非常高效的位运算。
注意:移位操作符的操作数只能是整数。
1、左移操作符
规则:左边丢弃,右边补 0

直接用一个例子:

不难发现,左移1位有乘2的效果,左移2位有乘4的效果,其实左移的等价效果是:正数左移 n 位 ≈ 乘以2ⁿ(无溢出时)
2、右移操作符
相较于左移操作符,右移操作符就稍复杂一点了。
首先,右移运算的规则分为两种:
1、逻辑右移:左边用0填充,右边丢弃

2、算数右移:左边用原值的符号位填充,右边丢弃

注意:右移运算到底是算术还是逻辑,取决于编译器的实现,常见编译器都是算术右移
同样举个例子:

左移操作符的等价效果是:正数右移 n 位 ≈ 除以2ⁿ(向下取整);负数右移也向下取整。可通过上面的几个样例来理解。
注意:1、移位位数不能为负数:如a<<-1,属于未定义行为
2、移位位数不超过类型位数:32 位 int 移位 32 位(a << 32)是未定义行为(不同编译器结果不同)
3、不要用移位替代乘除法(有例外):正数左移 / 右移无溢出时,等价于乘 / 除 2ⁿ,效率更高;负数右移结果是向下取整(如-7 >> 1 = -4,而-7/2 = -3),不等价
4、char/short 移位会先提升为 int:char a = 1; a << 28 操作的是 int 类型的补码
五、按位操作符
按位操作符同样是是C语言中直接对整数的二进制补码位进行操作的运算符,有:
1、& 按位与
2、| 按位或
3、^ 按位异或
4、~ 按位取反
与移位操作符相同,按位操作符的操作数也必须是整数。
1、按位与 &
规则:两个数的二进制补码对应位都为 1 时,结果位为 1;否则为 0(“同 1 为 1,其余为 0”)

2、按位或 |
规则:两个数的二进制补码对应位只要有一个为 1,结果位为 1;否则为 0(“有 1 为 1,全 0 为 0”)。

3、按位异或 ^
规则:两个数的二进制补码对应位不同时为 1,相同时为 0(“同 0 异 1”)

用途 1:无临时变量交换两个整数
4、按位取反 ~
按位取反是一个单目操作符。
规则:对整数的二进制补码所有位取反(0 变 1,1 变 0),包括符号位。

5、练习
介绍完这几个按位操作符后,有一种云里雾里的感觉,那接下来我们就来几个小题练习实践一下
1)练习1:不能创建临时变量(第三个变量),实现两个整数的交换
对于两个数的交换,我们肯定会想到创建临时变量来进行交换,可有没有方法不创建临时变量来进行交换呢?这里就可以用我们的按位异或来进行计算了。那是如何计算的呢?
我们要先弄清楚a^a,a^0的值是什么:两个相同数字异或的结果是0;0和任何数字异或的结果还是那个数字

正是利用这一点,可以实现我们的无临时变量交换,下面是代码:

这就是异或符号的用途之一
2)练习2:求一个整数存储在内存中的二进制中1的个数(补码中)
法1:我们首先想到类比10进制中求整数中1的个数的方法,利用除运算和模运算,不断对 2 取模获取最低位,再除以 2 舍弃最低位,直到数值为 0。

但是这种方法是有弊端的,仅对正数直接有效,对负数却不行。

我们这里也是可以直接改进一下,可以将n定义为无符号整型 unsigned int,-1的补码是32个1,符号位的1对于无符号整型n来说,已经不是符号位,n认为是无符号数,是一个非常大的整数。

法2:还可以利用按位与来计算,按位与的规则是同“1”为“1”,其余为零,我们可以利用1来与n来进行计算,计算完一次后,用右移操作符将n左移一位,循环32次,这样就能将n的每一位与1的最低位都能比较,n & 1如果等于1,那么计数加一。
#include <stdio.h>
int main()
{
int n = 0;
int count = 0;
int i = 0;
scanf("%d", &n);
for (i = 0; i < 32; i++)
{
if (((n >> i) & 1) == 1)
{
count++;
}
}
printf("%d\n", count);
return 0;
}
//举个例子 比如 n = 13
//13的补码:00000000000000000000000000001101
// 1的补码:00000000000000000000000000000001
//当i = 0
//00000000000000000000000000001101
//00000000000000000000000000000001
//最低位都为1,两者运算结果为1,则count++
//当i = 1
//00000000000000000000000000000110
//00000000000000000000000000000000
//最低位不同,运算结果为0,count不动
//…………以此类推,循环32次以后,也就遍历了13的每一个二进制位
法三:还有一种比较奇葩的代码:n & (n - 1),这个代码可以消去 n 的补码中最右侧的 1,循环执行该操作直到 n=0,循环次数就是 1 的个数,也是非常不容易被想到。

3)练习3:写代码将13⼆进制序列的第5位修改为1,然后再改回0
这个练习就将我们的位操作符基本用到了
#include <stdio.h>
int main()
{
int n = 13;
//把第5位改为1
//13的补码:00000000000000000000000000001101
//我们可以或上一个
// 00000000000000000000000000010000
//两者进行或运算,可以将第五位改成1,
//而这个数可以通过将1左移四位得到,即1<<4;
n |= (1 << 4);
printf("%d\n", n);//修改成功的话,n应该为29
//把第5位改为0
//现在的补码:00000000000000000000000000011101
//我们可以将第5位与上一个0,而其余要想保持不变,应该都与上一个1
//即11111111111111111111111111101111
//而要想得到这个的话,我们可以对00000000000000000000000000010000(即1<<4)按位取反
//即~(1<<4)
n &= (~(1 << 4));
printf("%d\n", n);//修改成功的话,n应该为13
return 0;
}
六、单目操作符
!、++、--、+、-、sizrof、(类型) 这些单目操作符我们已了解过,只有&和*还没了解,因为这两个要在指针才会用到所以学习指针的时候再聊。
七、逗号表达式
逗号表达式,就是逗号隔开的多个表达式。他是从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
我们用具体例子来理解:

八、下标访问[]、函数调用()
1、下标引用操作符
int arr[10];//创建数组
arr[9] = 10;//实⽤下标引⽤操作符。
[ ]的两个操作数是arr和9。
2、函数调用操作符
接受⼀个或者多个操作数:第⼀个操作数是函数名,剩余的操作数就是传递给函数的参数。
九、结构成员访问操作符
在介绍结构成员访问操作符之前,我们先来了解一下结构体是什么
1、结构体
结构体的存在是为了弥补C语言数据类型的局限性。C语言中已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要 名字、年龄、学号、身高、体重等;描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让我们可以自己创造适合的类型。结构体是⼀些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量,如: 标量、数组、指针,甚⾄是其他结构体。
1)结构体类型的声明
struct是结构体的关键字,下面是如何声明结构体
struct 结构体名
{
成员1类型 成员名;
成员2类型 成员名;
...
};
我们可以用一个学生结构体来更清晰的理解
struct Student//结构体的名字
{
char name[20];//名字
int age; //年龄
int high; //身高
float weight; //体重
char id[16]; //学号
};
2)结构体变量的定义和初始化
先前我们只是声明了一个类型,那接下来我们就可以用这个类型来创建变量,定义变量有两种方式:
第一种是声明类型的同时定义变量(全局)
struct Student
{
char name[20];
int age;
int high;
float weight;
char id[16];
}stu1,stu2,stu3; //声明结构体后直接定义
第二种是声明类型后在定义变量(可以像下面一样直接创建,是全局,也可以在主函数内创建,是局部的)
struct Student
{
char name[20];
int age;
int high;
float weight;
char id[16];
};
struct Student stu4;
struct Student stu5;
struct Student stu6;
这两种可以同时使用
创建结构体变量的同时也可以进行初始化,初始化也有两种方式
#include <stdio.h>
struct Student
{
char name[20];
int age;
int high;
float weight;
char id[16];
};
int main()
{
struct Student stu1 = { "张三",20,180,75.5f,"202531235066" };//按顺序初始化
struct Student stu2 = {.age = 20,.name = "张三",.weight = 75.5f,.high = 180,.id = 202531235066};//用操作符.可以按照自己想要的顺序初始化
}
上面第二种初始化的方法就用到了结构体成员访问操作符中的直接访问。
2、结构体成员访问操作符
1)结构体成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。
使用方式:结构体变量.成员名
2)结构体成员的间接访问
结构体成员的间接访问是通过箭头操作符(->)访问的,同样接受两个操作符,但这个操作符是与指针相关的,所以等后面我们学习指针后再来详细了解。
十、操作符的属性:优先级、结合性
我们上面已经了解了这么多操作符,那当这些操作符出现在一起的时候,我们应该按照什么样的顺序去计算呢?这就要讲到C语言操作符的两个重要属性:优先级和结合性,这两个属性决定了表达式求值的顺序。
优先级:优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不⼀样的。
结合性:如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执⾏顺序。⼤部分运算符是左结合(从左到右执⾏),少数运算符是右结合(从右到左执⾏),⽐如赋值运算符( =)
运算符的优先级顺序很多,下⾯是部分比较常用运算符的优先级顺序(按照优先级从⾼到低排列),由于圆括号的优先级最⾼,可以使⽤它改变其他运算符的优先级。

下面是各操作符的优先级和结合性的表格

表格参考:https://zh.cppreference.com/w/c/language/operator_precedence
十一、表达式求值
1、整型提升
C语⾔中整型算术运算总是⾄少以缺省整型类型的精度来进⾏的,为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型,这种转换称为整型提升。说直白点,C 语言中,只要做整型算术运算(+ - * / % & | ^ <<>> 等),所有比 int 小的整数(char、short),都会自动先提升成 int,再计算,也就是说,运算至少以 int 的精度进行,不会用 char/short 直接算。
那如何进行整型提升呢?
1、有符号整数提升是按照变量的数据类型的符号位来提升的
2、⽆符号整数提升,⾼位补0


一个例子:
#include <stdio.h>
int main()
{
char a = 3;
//a存的是补码后8位:00000011
//a提升:
//补充符号位0:00000000000000000000000000000011
char b = 127;
//b存的是补码后8位:01111111
//b提升:
//补充符号位0:00000000000000000000000001111111
char c = a + b;
//用提升后的a和b计算:
//a:00000000000000000000000000000011
//b:00000000000000000000000001111111
//c:00000000000000000000000010000010
//c中只能存8个字节,所以截取后8位:10000010 放到c中
//而%d是以10进制的形式打印有符号的整数
//那c也应该进行整型提升,这里的高位符号位是1,所以补1
//补充符号位1:11111111111111111111111110000010(补码)
//原码: 10000000000000000000000001111110——结果为:-126
printf("%d", c);
return 0;
}
2、算术转换
如果某个操作符的各个操作数属于不同的类型,那么除⾮其中⼀个操作数的转换为另⼀个操作数的类型,否则操作就⽆法进⾏。
下面列举的层次体系被称为寻常算术转换。如果某个操作数的类型在这个列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后再执行运算。

如果有机会,我想为自己办一场葬礼。
不设灵堂,不燃纸钱,
只我披麻戴孝,白绫绕肩,只我知晓。
不需要悲号,也不需要凭吊,
让月光包裹我,让万物祭奠我,送我远行。
一叩首,是挣脱。
再叩首,是剥离。
三叩首,喜迎新我。
就这样,
我会成为自己的祖先,
也是自己的后代。
我继承了我,又背叛了我。
我埋葬了我,又诞生了我
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)