每天一点C / 位和字节
哈喽,我是老吴,继续记录我的学习心得
作为一个高度自律的人,我喜欢跑步,为此受过伤病的困扰,万恶的足底筋膜炎!
作为程序员和写作爱好者,是非常需要锻炼身体。
如果你也喜欢跑步,可以考虑阅读这本书:
如何评估一本书是否值得阅读?
好书那么多,我要避免浪费时间在烂书上。下面以《无伤跑法》这本书为例分享一下我是如何评估一本书的。
1) 检查基本信息:
作者: 戴剑松 / 郑家轩
出版年: 2018-12-1
页数: 268
...
豆瓣评分 8.8 272人评价
该书评价人数虽然不高,但是评分较高。
2) 检查作者的专业度:
南京体育学院 / 体能康复实验中心主任
南京医科大学 / 公共卫生与预防医学博士
知乎运动健身专栏作家(182篇文章)
我去知乎查阅了一下戴老师的文章,质量很高。
3) 查阅目录,确定有自己感兴趣的新知识:
...
第五章 没有伤痛才能轻盈奔跑
第十一节 一网打尽足底痛
...
第五章就是我想重点阅读的章节,里面大概率有我需要的知识。
4) 一本书如果读了 3 章感觉收获不大,就要考虑换书了。
每天一点C / 数组和指针
正文目录:
1. 位相关的运算符
2. 位相关的用法
3. 位字段 (bit field)
4. 怎样判断机器的字节顺序?
5. 怎样将整数转换到二进制或十六进制?
6. 怎样高效地统计整数中为1的位的个数?
7. 相关参考
写作目的:
记录一些 C 语言中位和字节相关的操作。
测试环境:
Ubuntu 16.04
gcc version 5.4.0
1. 位相关的运算符
1) 取反:~
~(10011010) = (01100101)
运算符 ~ 把 1 变为 0,把 0 变为 1。
2) 按位与:&
(10010011) & (00111101) = (00010001)
运算符 & 通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位都为 1,结果才为 1。
3) 按位或:|
(10010011) | (00111101) = (10111111)
运算符 | 通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中有 >=1 的位为 1,结果就为 1。
4) 按位异或:^
(10010011) ^ (00111101) = (10101110)
运算符 ^ 逐位比较两个运算对象。对于每个位,如果两个运算对象中有且只有 1 位 为 1, 结果为 1。
5) 左移:<<
(10001010) << 2 = (00101000)
运算符 << 将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值会被丢弃,用 0 填充空出的位置。
6) 右移:>>
(10001010) >> 2 = (00100010) // 情况1
(10001010) >> 2 = (11100010) // 情况2
运算符 >> 将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出右末端位的值丢。
对于无符号类型,用 0 填充空出的位置。
对于有符号类型,其结果取决于机器。空出的位置可能用 0 填充,也可能用符号位填充。
2. 位相关的用法
1) 什么是掩码?
所谓掩码指的是一些设置为开 (1) 或关 (0) 的位组合。
为什么叫掩码?看下面这个例子:
#define MASK (1<<1)
flags = flags & MASK;
上面这个例子中,只有 MASK 中 为1的位才可见,掩码中的 0 隐藏 (掩盖) 了 flags 中相应的位。
2) 打开 (设置) 位
有时,比如在操作硬件寄存器的情况下,需要打开一个值中的特定位,同时保持其他位不变。这种情况可以使用按位或运算符 | 和一个掩码进行配合:
#define MASK (1<<1)
flags |= MASK;
3) 关闭 (清空) 位
在不影响其他位的情况下关闭指定的位:
#define MASK (1<<1)
flags &= ~MASK;
4) 切换位
切换位指的是打开已关闭的位,或关闭已打开的位:
#define MASK (1<<1)
flags ^= MASK;
5) 检查位
检查某位的值是否为 1:
#define MASK (1<<1)
(flags & MASK) == MASK
注意,掩码至少要与其覆盖的值的宽度相同,要避免符号位带来的意外,最好在代码中使用 unsigned int 操作位和字节。
6) 提取位
移位运算符可用于从较大单元中提取一些位,例如提取 RBG 颜色值:
#define BYTE_MASK 0xff
unsigned long color = 0x123456;
unsigned char blue, green, red;
red = color & BYTE_MASK;
green = (color >> 8) & BYTE_MASK;
blue = (color >> 16) & BYTE_MASK;
3. 位字段 ( bit field )
位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度,在 Linux 驱动中,某些代码使用了位字段:
struct ap_queue_status {
unsigned int queue_empty : 1;
...
unsigned int response_code : 8;
unsigned int pad2 : 16;
} aqs;
给字段赋值:
aqs.queue_empty = 0;
aqs.response_code = 0xff;
所赋的值不能超出字段可容纳的范围。
位字段占用的空间:
struct {
unsigned int autfd : 1;
unsigned int bldfc : 1;
unsigned int undln : 1;
unsigned int itals : 1;
} prnt;
struct {
unsigned int code1 : 2;
unsigned int code2 : 2;
unsigned int code3 : 6;
unsigned int code4 : 8;
#if TEST
unsigned int code5 : 10;
unsigned int code6 : 12;
unsigned int code7 : 24;
#endif
} prcode;
int main(void)
{
printf("%ld %ld\n", sizeof(prnt), sizeof(prcode));
}
测试结果:
4 4 // without TEST
4 12 // with TEST
系统会自动判断出需要几个 byte 的空间来存储数据,在我的机器上测试,一个成员最起码占用 1 个 byte。
位字段的储存顺序:
取决于机器。在有些机器上,存储的顺序是从左往右,而在另一些机器上,是从右往左。另外,不同的机器中两个字段边界的位置也有区别。由于这些原因,位字段通常都不容易移植,我不要求自己写,但是要求自己会看。
4. 怎样判断机器的字节顺序?
演示 demo:
int main(void)
{
int x = 1;
if (*((char *)&x) == 1)
printf("little - endian\n");
else
printf("big - endian\n");
return 0;
}
运行效果:
$ gcc byte_order.c -o byte_order
$ ./byte_order
little - endian
代码解析:
先初始化在内存中占用 4 个字节的 int 变量。
然后获取int 变量中第 1 个字节的地址,等效代码是:char *px = (char *)&x。
最后获取第 1 个字节的值:*px,观察 *px 是否为 1 就可以知道大小端了。
5. 怎样将整数转换到二进制或十六进制?
演示 demo:
进行任意进制数转换的小函数:
#define BUF_SIZE (33)
char *baseconv(unsigned int num,int base)
{
static char retbuf[BUF_SIZE];
char *p;
...
p = &retbuf[sizeof(retbuf)-1];
*p='\0';
do {
*--p="0123456789abcdef"[num % base];
num /=base;
} while(num !=0);
return p;
}
在 main() 中进行测试:
int main(void)
{
int a = 20;
printf("%s\n", baseconv(a, 2));
printf("%s\n", baseconv(a, 16));
return 0;
}
运行效果:
$ gcc int_conv.c -o int_conv
$ ./int_conv
10100
14
代码解析:
首先需要明确的是:整数本来就是以二进制存储的,这里说的转换只是指打印的形式。
在baseconv() 中的缓冲是 static 的,这有2 个作用:1) 将缓冲清 0,2) 只有是 static 的缓冲才能在函数外部被使用。
注意 char *p = &retbuf[sizeof(retbuf)-1] = '\0' 这个操作,这里将缓冲的最高位设置为字符串结束符,同时表明了字符串是从高地址向底地址构造的,函数返回缓冲中有效数据的起始地址。
-
如果你这样打印:
printf("%d %s %s\n", a, baseconv(a, 2), baseconv(a, 16));
会得到这样的结果:
10100 00
。这是因为 baseconv() 中的缓冲是 static 的,
baseconv(a, 2)
将baseconv(a, 16)
冲刷掉了。
6. 怎样高效地统计整数中为1的位的个数?
演示 demo:
统计整数中为1的位的个数的小函数:
static int bitcounts[] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
int bitcount(unsigned int u)
{
int n=0;
for(; u!=0; u>>=4)
n += bitcounts[u & 0x0f];
return n;
}
在 main() 中进行测试:
int main(void)
{
int i = 0;
for (i=0; i<=0x0f; i++)
printf("%d\n", bitcount(i));
return 0;
}
运行效果:
$ gcc bit_counts.c -o bit_counts
$ ./bit_counts
0
1
1
2
1
2
2
3
1
2
2
3
2
3
3
4
代码解析:
许多像这样的位问题可以使用查找表格来提高效率和速度。
这段代码是以每次 4 位的方式计算数值中为1的位的个数。
相关参考
《C Primer Plus 6th》, 15
《你必须知道的 495 个 C语言问题》, 20.7
《C 和指针》, 5.1.3
《C 专家》, NULL
《C 和 C++ 程序员面试秘籍》, 5
《C 语言解惑》, NULL
思考技术,也要思考人生
学习技术,更要学习如何生活。
你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。
对 嵌入式系统 (Linux、RTOS、OpenWrt、Android) 和 开源软件 感兴趣,想和更多人互相交流学习,关注公众号:嵌入式Hacker,一起来学习吧。
关注 / 转发 / 打赏,都是对作者莫大的支持。觉得文章对你有价值的话,不妨点个 在看和点赞 哦。
ps:
欢迎加入我的微信群:先加我微信,我拉你进群,暗号(我最棒的)。
祝工作顺利,家庭幸福,财源滚滚~
推荐阅读
-
C#中结构体和字节数组转换实现
-
c# String扩展 让你在PadLeft和PadRight时不再受单双字节问题困扰
-
用c语言实现,两个int(32位)整数m和n的二进制表达中,有多少个位(bit)不同?
-
C# 2进制、8进制、10进制、16进制...各种进制间的转换(三) 数值运算和位运算
-
C++中 char、short、long、double等在32位机中指针占几个字节?
-
python自测知识点和书籍补充(C4 文本和字节序列)
-
C语言自定义类型详解(结构体、枚举、联合体和位段)
-
编程初学者入门7_公务员面试现场打分。有7位考官,从键盘输入若干组成绩,每组7个分数(百分制),去掉一个最高分和一个最低分,输出每组的平均成绩。(复习冒泡排序+C、Java中局部变量不赋值不能使用))
-
C#实现MD5加密32位,补0和不补0区别
-
【操作系统实验 / C++】在分页管理方式下采用位示图来表示主存分配情况,实现主存分配和回收(附代码)