C语言中的位操作

本文详细介绍了C语言中的位操作,包括位逻辑运算符(如掩码、打开和关闭位、切换位、检查位)和位移运算符。位操作在底层开发中非常重要,如控制硬件和处理二进制数据。位域的概念也被提及,它是C语言中压缩数据的一种方式,允许更高效地利用内存。文章还讨论了位移运算符的不同类型以及位域在结构体中的使用和限制。

位操作

C语言常用于底层开发,它可以与硬件通信并且可以嵌入汇编语言,因此经常需要进行位操作,例如一台IBM PC通过向端口发送指令来控制硬件,控制代码通过读取指令字节上某个位来打开设备,其它位可能储存发送的信息。这就需要提取位上的信息。

进制

日常生活中我们常常使用十进制,原因可能得益于我们有10个手指头和脚趾头,计算机则使用二进制,因为信号的打开和关闭只有两种状态,很容易实现。八进制常用于小型设备或者古老的计算机,现在已经很少使用,目前常用的是十六进制,因为十六进制用F对应二进制1111,FF对应11111111,刚好一个字节,因此用十六进制描述二进制非常方便。用不同进制表述浮点时会有区别,例如1/3不能用十进制精确描述但三进制可以,二进制很擅长描述1/2,1/4,1/8这种2的n次幂,但不能精确表示2/5,3/7,因此当用double进行计算时常常可以看到1/10的输出结果为0.9999999999999999,但只要精度足够也可以用于日常和商业。

位运算符

位运算符分为逻辑运算符和位移运算符,逻辑运算符优先级低于关系运算符和赋值运算符,位移运算符优先级低于加减运算符高于关系运算符,但&=, >>=等赋值运算符优先级与++,+=相同。单纯的逻辑运算不会改变变量的值,例如flags<<2不会改变flags的值,它只返回运算后的结果,只有<<=才会改变flags的值。

位逻辑运算符

位逻辑运算符有:
按位非
& 按位与
| 按位或
^ 按位异或
&= 按位与后赋值
|= 按位或后赋值
^= 按位异或后赋值
^和&逻辑相反,两个值不同为真,两个相同为假,也可以用两个数相加得到结果,即0^0=0,0^1=1,1^0=1,1^1=0
这些逻辑都是从生活现象中总结出来的,对于异或逻辑,例如结婚两人性别必须是异性,同性不允许结婚。位逻辑在数字电路中应用更加广泛,还有一门学科叫数字逻辑,是电子专业必修课之一。在计算机中位运算用法大体如下:

掩码

使用&将一个二进制值的其它位隐藏起来,只保留指定的位,例如mask位00000010,flags & mask的效果如下:
在这里插入图片描述
在设置网卡ip的窗口中,掩码常常为255.255.255.0,即将ip地址的低位隐藏起来,只保留高位,如图:
在这里插入图片描述

打开和关闭位

将二进制某个位打开可以使用|,假设flags是00001111,mask是10000000,flags | mask结果为:10001111,最高位1被打开其它位不变。将二进制某个位关闭可以使用flags & ~mask,假设mask为00000001,结果为00001110,最低位被关闭。只需使用不同的运算符,同一个mask既可以用来打开又可以用来关闭位。

切换位

打开已关闭的位或者关闭已打开的位,可以使用^切换,因为1与任何位异或都会取反,0与任何位异或结果不变,如下:
1^1=0
1^0=1
0^0=0
0^1=1
如果flash为00000000,mask为11000000,那么flashs ^ mask的结果是11000000,最高两位被取反,其它位不变。

检查位

如果我们想比较两个字节中的位,不能使用flags1flags2这样的方式,因为比较的是字节的整体值,比较位应使用(flags1 & mask)(flags2 & mask)的形式,mask为某个位的掩码,由于&优先级低于赋值因此要用括号括起来。

位移运算符

位移运算符包含:

<< 按位左移
>> 按位右移
<<+ 按位左移后赋值
>>+ 按位右移后赋值

由于基本数据类型最小是1字节,当我们需要存取位数据时常常需要进行左移或右移,例如颜色值通常用三个字节表示,如FF00FF,当我们需要获取红色时需要使用flags>>=4将红色取出。用左移一位相当于将值乘以2,右移一位相当于将值除以2,但是右移与系统有关,因为涉及到负数,有些系统使用0填充空出的位,有些系统使用1填充空出的位,现在的编译器比较智能,如果发现值为正则用0填充,为符则用1填充,测试代码如下:

#include<stdio.h>
#include<stdio.h>
int main(void)
{
	unsigned a = 0x000040;
	int b = 0x000040;
	printf("%d,%d,%d,%d",a>>1,b>>1,4>>1,-4>>1);
	return 0;
}#include<stdio.h>
#include<stdio.h>

int main(void)
{
	unsigned a = 0x000040;
	int b = 0x000040;
	printf("%d,%d,%d,%d",a>>1,b>>1,4>>1,-4>>1);
	return 0;
}

有些高级语言除了智能填充还提供了>>>运算符进行无符号右移,即便值为负也可以进行无符号右移,当然我们也可以手动实现,不过在实际运用中有符号右移并不多。

位域

位运算符虽然强大但当我们需要精确读写位数值时并不直观,而且比较繁琐,有没有一种数据类型可以直观操作位数值呢?有的,C语言提供了位域,位域不是一种数据类型而是C语言压缩数据的一种方案。C语言中最小的整数类型是char,也就是1字节,实际工作中可能用不了这么大的范围,例如enum和bool,bool用一个二进制位储存就行,enum通常不会超过4位,使用short和int储存会造成空间的浪费。为了进一步节省内存开销,C语言支持在结构体中使用位域来压缩空间,方法是在成员后面用冒号指定所占的位,即bit大小,例如:

#include<stdio.h>

struct Student
{
	char name[20];
	int age:8;
	int sex:1;
};

int main(int argc, char* argv[])
{
	printf("%d",sizeof(struct Student));
	return 0;
}

对于一个学生的年龄来说不会超过255,所以将age指定位8bit,对于性别来说不会超过1bit,原本Student需要至少28个字节储存,测试的结果为24,压缩了4个字节,为什么结果不是22呢?因为C语言标准规定:

  • 位域的宽度不能超过它所依附的数据类型的长度

这里age和sex类型为int,因此指定的位宽度不成超过sizeof(int)

  • 只有有限的几种数据类型可以用于位域

C99支持short,int,unsigedint,bool,实际上现在的编译器额外还支持char以及 enum 类型

  • 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof() 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止
  • 如果两个紧邻成员的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始
  • 当使用位域时,由于位域成员不占用完整的字节,所以不能使用&获取位域成员的地址。

虽然不能获取位域成员的地址,但用访问成员值是没有问题的。
当相邻的成员类型不同时,每个编译器也会采取不同的方案,GCC 会压缩存储,而 VC/VS 不会,加上成员之间本来就有间隙,使用了位域的结构体大小完全由编译器决定,相对于union使用的覆盖技术,位域则采用另一种思路来节省空间。这说明当成员类型不同或跨边界时,我们并不能精确的控制位值,只能作为压缩的参考值,具体储存方式取决于编译器。如果我们需要精确控制位数值,就需要避免编译器自动对齐,我们可以使用相同的数据类型成员,并且严格控制跨越边界。例如当我们处理颜色值时,可以定义一个描述颜色的位域color:

typedef struct
{
	unsigned b:8;
	unsigned g:8;
	unsigned r:8;
} color;

这里每个int占用1个字节,3个字节加起来也不会超过一个int,当我们需要控制字节的每一位时,可以将每个成员定义为unsigned char,大小为1个bit,如下:

#include<stdio.h>
#include<stdlib.h>

typedef struct
{
	unsigned char b1 : 1;
	unsigned char b2 : 1;
	unsigned char b3 : 1;
	unsigned char b4 : 1;
	unsigned char b5 : 1;
	unsigned char b6 : 1;
	unsigned char b7 : 1;
	unsigned char b8 : 1;
} byte;

int main(void)
{
	byte byte1 = { 0,0,0,0,0,0,0,1 };
	unsigned char* bp = &byte1;
	char str[50] = "";
	printf("%zd\n", sizeof(byte1));
	printf("%08s\n", itoa(*bp, str, 2));
	return 0;
}

此例中结构体byte为一个字节,其中成员b1到b8代表位值,用该类型定义变量byte1并进行初始化,为显示结果,用一个unsigned char类型指针bp指向byte1,然后调用itoa()以二进制形式输出字节值,注意char类型转换为int会在前面添加24个1导致输出一个负数,因此这里要用unsigned char。运行程序结果显示byte1的长度为1字节,值为1000000,为什么是这个结果呢?因为编译器将结构体第1个成员放入字节的低位,第8个成员放入字节高位,这里初始化时将最高位设置为1,因此将字节视为char时输出1000000,这与初始化顺序相反。默认初始化顺序总是从第一个成员开始,强迫我们从低位写到高位,让人别扭,如果要从高位初始化到低位,可以写成:
byte byte1 = { .b8=0, .b7 = 0, .b6 = 0, .b5 = 0, .b4 = 0, .b3 = 0, .b2 = 0, .b1 = 1 };
虽然符合二进制书写习惯,但太繁琐,为了方便书写我们可以改用字符串进行初始化,如下:

#include<stdio.h>
#include<stdlib.h>

typedef struct
{
	unsigned char b1 : 1;
	unsigned char b2 : 1;
	unsigned char b3 : 1;
	unsigned char b4 : 1;
	unsigned char b5 : 1;
	unsigned char b6 : 1;
	unsigned char b7 : 1;
	unsigned char b8 : 1;
} byte;

void setByte(byte* b, char* str)
{
	b->b8 = str[0] - '0';
	b->b7 = str[1] - '0';
	b->b6 = str[2] - '0';
	b->b5 = str[3] - '0';
	b->b4 = str[4] - '0';
	b->b3 = str[5] - '0';
	b->b2 = str[6] - '0';
	b->b1 = str[7] - '0';
}

printByte(byte* b)
{
	unsigned char* p = b;
	printf("%d", b->b8);
	printf("%d", b->b7);
	printf("%d", b->b6);
	printf("%d", b->b5);
	printf("%d", b->b4);
	printf("%d", b->b3);
	printf("%d", b->b2);
	printf("%d", b->b1);
	printf("\n");
}

int main(void)
{
	byte byte1;
	setByte(&byte1, "00101001");
	printByte(&byte1);

	unsigned char* bp = &byte1;
	char str[50] = "";
	printf("%08s\n", itoa(*bp, str, 2));

	return 0;
}

这里声明byte1时不进行初始化,而是通过函数setByte()初始化,setByte()中将字符串从左到右解释为高位和低位以符合书写习惯,为方便输出字节内容还增加了一个printByte ()函数,它也按照从高位到低位的顺序输出,比较两种输出方式,它们结果相同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值