C语言:位操作符详解

简介: C语言:位操作符详解

前言

在前不久,我们计算机导论的课程讲到了进制转化,原反补三码。但是我周围有同学提出疑问,学习这三个码有什么用啊?

我在c语言中也学习过不少相关知识,他们都是我在学习位操作符时第一次接触到,所以今天我写一篇博客,来解释位操作符。C语言是一门非常灵活的语言,这得益于它丰富的操作符,甚至某些操作符可以精确到二进制位,也就是计算机最小的存储单元。那我们开始吧!

进制介绍

其实我们经常能听到2进制、8进制、10进制、16进制这样的讲法,那是什么意思呢?其实2进制、8进制、10进制、16进制是数值的不同表示形式而已。

比如13的不同形式有:

进制 表达
二进制 1101
八进制 15
十进制 13
十六进制 d

十进制是我们从小到大学习的,我们知晓十进制有以下两大特点:

满10进位

所有数字都由0-9组成

二进制也是如此

满2进位

所有数字都由0-11组成

对于16进制来说,由于10~15这些数字,无法用一位数字表达,于是就引入了字母abcdef来对应。

满16进位

所有数字都由0-9,a-f组成

那这些数字为啥在不同进制下表达形式不同?这就涉及到位权了:

进制的位权

不同进制的最大区别就是每一位的位权不同,从小数点左侧第一位开始,n进制的第x位的位权就是nx-1。想要知道一个数字多大,就用相应的位上的数字乘相应的位权,最后加和。比如二进制的1011,根据上图加和表达的数字就是11。

C语言常见的进制转化

在C语言中,由于数据是以二进制存储,所以二进制可以说的C语言进制转化的核心。在此我们主要说明十转二,二和八互转,二和十六互转。

十进制转二进制:

我们以125为例:

我们只需要每次把数字除2,把余数保留,把商留下来,然后用商继续除2,直到商为0。然后把余数从后到前排列,就得到相应的二进制了。故125的二进制就是1111101。

二进制与八进制和十六进制的互相转化

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

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

对于c语言来说,编译器在处理的时候,会把0开头的数字判定为8进制,0x开头的数字判定为16进制。比如下面的代码,两个数字都是17,在加上不同的前缀后,打印结果也就不同了。

而我们在给内存编址的时候,地址也是16进制的形式,它也带有一个0x的前缀。

原码、反码、补码

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

三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,最高位的一位是被当做符号位,剩余的都是数值位。

转换规则

正整数的原、反、补码都相同。

负整数的三种表示方法各不相同。

原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。

反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。

补码:反码+1就得到补码。

对于int(整形),计算机会给内存开辟4个字节即32个比特来存放a。由于在此a是正数,第一位符号位为0,数值为5,转化为二进制就是101,存在最后。正数的原反补三码相同。

由于在此a是负数,在原码中,第一位是符号位,存放1。

反码:符号位不变,保持为1。其余位按位取反,即0变1,1变0.

补码:在反码的情况下加1。

而补码想要变回原码,也是相同的步骤,即先取反后加一。

数据与内存的关系

首先,我们在内存中存储的数据是以补码的形式存储的。我们用代码定义a,b为5和-5,然后观察其在内存中的值:

由于二进制实在难于分辨,所以编译器在向程序员呈现计算机存储的值的时候,会转为16进制。我们从上图中可见,a的存储是可以理解的,可b的值却不是-00000005。这个fffffffb其实就是-5的补码的16进制形式,由此可以证明,内存存储数据就是以补码的形式。

那为什么内存要存补码?

  • 可以把符号与数值统一处理,把数字的正负放在码值中,不用额外区分。
  • 可以使加法减法统一处理(CPU只有加法计算器)。

第一点其实是容易理解的,那为什么用补码可以统一加减法呢?我们以下面的代码为例:

可见,虽然代码是减法,但是在计算机处理的时候,只做了加法运算。这样可以减少计算机硬件的消耗,只需要在CPU内部做好加法的硬件即可。

移位操作符

移位操作符是针对补码的操作符,他们都直接作用于补码。使用语法如下:

a << x;

a >> x;

前者是左移,后者是右移,a是被移动的值,x表示移动的位数,这里表示的就是把a的补码左移(右移)x位。

左移操作符<<

对于左移操作符,就是补码右侧补0,左侧丢弃。之后a的补码就会发生改变。第三行的两种形式都可以改变a的值,当然也可以用其它变量来接收a左移后的返回值,那这样a本身就不会发生改变。比如

b = a << 1;

右移操作符>>

右移操作符和左移操作符略有不同,右移操作符的效果有两种,分别是算数右移和逻辑右移。

逻辑右移:左边用0填充,右边丢弃

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

在此,具体实现哪一种是由编译器决定的,但是绝大部分编译器是算数右移。

注意:

移位操作符不可以移动负数位,这个标准是没有定义的,会报错。

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

位操作符:&, |, ^, ~

操作符 名称 功能
& 按位与 两数补码按位相比,有0则为0,同1才为1
1 按位或 两数补码按位相比,有1则为1,同0才为0
^ 按位异或 两数补码按位相比,相同为0,相异为1
~ 按位取反 对一个补码,把0变1,把1变0

具体如下:

一个小小的面试题

不能创建临时变量(第三个变量),实现两个数的交换。

对于两数交换,不少人会想到创建第三变量,但是这道面试题直接断了这条思路。

题解1:

这种思维模式,把a和b的值加和,然后分别用b先后储存的值,来反向输出。但是它有一种问题就是,当数字过大时,a+b的值有可能超出int类型的储存范围。在此我稍微讲解一下int类型的存储范围:

int类型的存储范围

int(整型)分为两种类型,分别是int(有符号整型)和unsigned int(无符号整型)。它们的区别就是第一位是符号位还是数值位。

对于无符号整型,它没有符号位,这32位全是数值位,当数字最大的时候就是32位数值位全为1,计算得到4294967295。所以无符号整形的存储范围是0到4294967295

对于有符号整型,它的第一位是符号位,那数值位只有31位,也就是说,当数字最大的时候就是这31位全是1,符号位为0;当数字最小的时候,这31位全是1,符号位为1。计算得到int的存储范围是-2147483674到2147483674

题解2:

在此我给大家解释说明一下:

首先引入两个公式

a ^ a = 0

a ^ 0 = a

这用到了异或的思路,相同为0,相异为1。那么a和a的补码完全相同,异或后就得到0。0与a异或得到a也很好理解。

在上述代码中,a被赋值为a ^ b。之后b = a ^ b就相当于 b = (a ^ b) ^ b。异或是支持交换律的,那这就相当于b = ( b ^ b ) ^ a = 0 ^ a = a。至此我们就完成了把a的值赋给a。

在第三句代码中,a = a ^ b = ( a ^ b) ^ a = b。因为在这里,b已经是a的值了。

由上述可知,在我们得到a ^ b的时候,只要再 ^ a就得到了b,再 ^b就得到a。

这种代码的好处就是不会进位,防止数值过大出现精度丢失的情况。当然,平常写代码还是用创建第三个变量的方法好,这个代码有很多局限性

  • 只能作用于整数
  • 代码的可读性低
  • 代码执行效率也低于三个变量的方法

在此只是为了解决这个面试题而用的这个方式。

逗号表达式

基本语法:

exp1, exp2, exp3, …expN

逗号表达式,就是用逗号隔开的多个表达式。

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

我们用几个例子来了解这个表达式。

int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式

这个表达式从左到右一次执行

第一步a>b是不成立的,直接忽略。也就是说,当逗号表达式中的式子不成立,它就直接不执行,没有意义。

第二步给a赋值,a就变成了12。

第三步给b赋值,由于第二步a的值被改变了,所以这里b=12+1=13。也就是说,前面的表达式改变,是会影响后面的表达式的。

if (a =b + 1, c=a / 2, a > 0, c > 5)

相信不少人第一次在if语句中见到三个表达式。在这个if语句中,第一步和第二步会改变a和c的值,会影响后面if的判断。第三步,不论a是否满足该条件,都完全不影响if语句的判断,这条if语句最终是否执行取决于第四个表达式c > 5是否成立。也就是上面提到的:“整个表达式的结果是最后一个表达式的结果。”

对我来说,这个逗号表达式最大的作用就是简化while以及do-while循环。比如:

int a = 3;
  int b = 5;
  a -= 1;
  b += 1;
  while (a > 0)
  {
    a -= 1;
    b += 1;
  }

这个代码在进入while循环前需要预处理一下,而这个预处理刚好和while循环的内部处理相同,我们就可以写成:

int main()
{
  int a = 3;
  int b = 5;
  while (a -= 1,b +=1,a > 0)
  {
  }
}

或者

int main()
{
  int a = 3;
  int b = 5;
  do
  {
  } while (a -= 1, b += 1, a > 0);
}

看吧,代码是不是简化了很多?

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

想不到吧?你每天都在用的数组访问和调用函数居然也是通过操作符!接下来我们浅浅讲述一下它们的操作数与操作符的关系。

操作数

操作数就是指一个操作符的操作对象,比如3+5,那么+操作符的操作数就是3和5。

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

也就是说,对于下标引用操作符,它的操作数是括号内的索引值以及数组名。

函数调用操作符()

函数调用操作符的操作数也包含函数名,但是函数是可以传多个参数的,每个参数都是一个操作数。

也就是说,函数调用操作符接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

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

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

优先级

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

3 + 4 * 5

比如这个代码,*操作符的优先级就比较高,会先执行4×5,后执行+3。

结合性

如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序。

左结合就是指对于同级操作符,从左到右执行,比如加减法:

3 + 5 - 1

它是左结合的,故先3+5,再-1。

大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符( = )。

a = 3 + 5

就会先执行右边的3+5,再赋值给a。

而在所有操作符中,圆括号()的优先级是最高的,可以用来改变其它操作符的先后顺序,比如:

(3 + 5) * 5

圆括号的优先级高于*,就会先执行3+5。

操作符的优先级与结合性表格在下面的链接中,但我不建议大家记忆,通过平常的代码练习即可。

link

整型提升与算术转换

我们知道,计算机在执行计算的时候,是在cpu中执行计算的。而CPU在计算时至少接受一个int(整型)的长度的数据,然后再计算。当一个数据长度小于int(整型)时,就会把这个数据变长为int(整型)的长度,这个过程叫做整形提升。而当一个数据长度大于int(整型)时,会根据计算的值的数据类型,把那个数据长度较短的数据增长,这个过程叫做算术转换。

整型提升

如图,对于一个char a = 1;

在进入CPU进行计算的时候,就会在前面补0,直到补到32个bit,即一个整型长度的时候,才开始计算。这样可以有效防止进位导致的数据丢失,在上面也讲过,数据的存储是有范围的,char也有相应的范围,当我们计算结果超出这个范围,就会丢失数据。

负数的整型提升

char c1 = -1;

变量c1的二进制位(补码)中只有8个比特位:

1111111

因为 char 为有符号的 char

所以整型提升的时候,高位补充符号位,即为1

提升之后的结果是:

11111111111111111111111111111111

正数的整型提升

char c2 = 1;

变量c2的二进制位(补码)中只有8个比特位:

00000001

因为 char 为有符号的 char

所以整形提升的时候,高位补充符号位,即为0

提升之后的结果是:

00000000000000000000000000000001

算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。

long double

double

float

unsigned long int

long int

unsigned int

int

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

结语

今天虽然标题是位操作符,但是我依然从内存的角度解释了很多问题,我认为c语言的学习是离不开对内存的理解的,更不要说以后在学习指针的时候。相信现在大家对开始的问题也有些答案了

为什么要学进制转换?

因为地址是以16进制形式编码,内存是以2进制储存。

为什么要学三码?

因为内存中存储的是补码,计算时也是以补码形式。

那这篇博客就到此结束啦,下次见各位!

相关文章
|
10天前
|
存储 网络协议 C语言
【C语言】位操作符详解 - 《开心消消乐》
位操作符用于在位级别上进行操作。C语言提供了一组位操作符,允许你直接操作整数类型的二进制表示。这些操作符可以有效地处理标志、掩码、位字段等低级编程任务。
49 8
|
10天前
|
C语言
【C语言】逻辑操作符详解 - 《真假美猴王 ! 》
C语言中有三种主要的逻辑运算符:逻辑与(`&&`)、逻辑或(`||`)和逻辑非(`!`)。这些运算符用于执行布尔逻辑运算。
44 7
|
4月前
|
存储 C语言 索引
【C语言篇】操作符详解(下篇)
如果某个操作数的类型在上⾯这个列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后执⾏运算。
79 0
|
4月前
|
程序员 编译器 C语言
【C语言篇】操作符详解(上篇)
这是合法表达式,不会报错,但是通常达不到想要的结果, 即不是保证变量 j 的值在 i 和 k 之间。因为关系运算符是从左到右计算,所以实际执⾏的是下⾯的表达式。
282 0
|
2月前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
44 10
|
2月前
|
存储 编译器 C语言
【C语言】简单介绍进制和操作符
【C语言】简单介绍进制和操作符
169 1
|
2月前
|
存储 编译器 C语言
初识C语言5——操作符详解
初识C语言5——操作符详解
182 0
|
4月前
|
C语言
C语言操作符(补充+面试)
C语言操作符(补充+面试)
46 6
|
4月前
|
存储 编译器 C语言
十一:《初学C语言》— 操作符详解(上)
【8月更文挑战第12天】本篇文章讲解了二进制与非二进制的转换;原码反码和补码;移位操作符及位操作符,并附上多个教学代码及代码练习示例
61 0
十一:《初学C语言》—  操作符详解(上)
|
5月前
|
C语言
五:《初学C语言》— 操作符
本篇文章主要讲解了关系操作符和逻辑操作符并附上了多个代码示例
45 1
五:《初学C语言》—  操作符