C语言总结——段位结构体与补码

最近在进行C语言复习,不借助课本死知识,试图直接通过某些方式进行推理验证,来得出一些听过的和没听过的、还有忘记的结论。 

比较浅,适合初学者看。但也有一些不容易发现的小规律能够涨姿势。

1.直接上题,这也算一个面试题吧,让你解释打印结果:

struct bit
{ int a:3;
int b:2;
int c:3;
};

int main()
{
bit s;
char *c=(char*)&s;
cout<<sizeof(bit)<<endl;
*c=0x99;
cout << s.a <<endl<<s.b<<endl<<s.c<<endl;
int a=-1;
printf("%x",a);
return 0;
}

这题简单一点,告诉你输出是多少(或者难一点,也可以直接让你猜输出),然后让你解释输出为什么是:

root@v:/usr/local/C-test/analysis# ./a.out
4
1
-1
-4
ffffffffroot@v:/usr/local/C-test/analysis#

如果直接就看懂了,请无视这篇帖子。。。 

我一分析就分析错了?这都 哪跟哪 ?退一步两步,然后根据答案也没说对~!

这个题不难,答不对其实是一些基础知识淡忘和遗漏造成的。

马虎大意看错行:

要注意,第一个4是sizeof输出的,由于没加文字性描述,容易误认4为第一个输出(最主要原因是最后一个没换行,linux输出容易忽略那一行,认为前三个是abc第四个是-1,其实第五个才是-1),到时候就更摸不着头脑了——当然,有电脑时你可以自己加文字描述,在纸上不会出现命令行干扰,这个问题可以解决。 

2.引用一段关于段位结构的定义(精简版): 

    位结构定义的一般形式为:

     struct位结构名{

          数据类型 变量名: 整型常数;

          数据类型 变量名: 整型常数;

     } 位结构变量;

     其中: 数据类型 必须 是int(unsigned或signed)。 整型 常数 必须是 非负 的整数, 范围 是0~15, 表示二进制位的个数, 即 表示有多少位 。 变量名是选择项, 可以不命名, 这样规定是为了排列需要。

    例如: 下面定义了一个位结构。

struct{

unsigned incon: 8; /*incon占用低字节的0~7共8位*/

unsigned txcolor: 4;/*txcolor占用高字节的0~3位共4位*/

unsigned bgcolor: 3;/*bgcolor占用高字节的4~6位共3位*/

unsigned blink: 1; /*blink占用高字节的第7位*/

}ch;

位结构成员的访问与结构成员的访问方式是相同的,访问上例位结构中的bgcolor成员可写成:

      ch.bgcolor

 

    注意:

    1. 位结构中的成员 可以 定义为unsigned, 也可 定义为signed,  但 当成员长度为1时 , 会被认为是unsigned类型 。因为单个位不可能具有符号。 (实测:int默认)  

    2. 位结构中的成员 不能使用 数组和指针 , 但 位结构变量(不是位结构变量的成员变量) 可以是 数组和指针, 如果是指针, 其成员访问方式同结构指针。

    3. 位结构总长度(位数), 是各个位成员定义的位数之和,  可以超过两个字节。

    4. 位结构成员可以与其它结构成员一起使用。

  例如:

struct info{

char name[8];

int age;

struct addr address;

float pay;

unsigned state: 1;

unsigned pay: 1;

}workers;

上例的结构定义了关于一个工从的信息。其中有两个位结构成员, 每个位结构成员只有一位, 因此只占一个字节但保存了两个信息, 该字节中第一位表示工人的状态, 第二位表示工资是否已发放。由此可见使用位结构可以节省存贮空间。

光参考这些定义,是解决不了这个题的。 下面看看另一个问题,补码~~

3.计算机存储形式——补码

int a=-1;
printf("%x",a);

首先,看到那个printf了吧,其他都是cout,突然来个printf,是不是很突兀?更绝的是,此处定义了一个a,跟前边根本没关系。

其实,它是题目的提示信息(不是提示的话突然搞这么个输出语句不是蛋疼么,看来这和高考一样,有出题和答题技巧),-1输出的ffffffff是提示信息,它提示了你计算机的存储形式——用%x控制输出16进制能看清它在计算机中的的存储形式是补码。

 

那么什么是补码呢?也算基础知识了,这里就不详细说什么原码、反码、补码的定义和区别了,直接上原码补码换算方法:

补码,顾名思义,互补,补全,也可以参考“集合”的定义,一个全集中有子集A,A和否A,两者相补刚好满。说白了这叫模运算。比如二进制中单个位上进行的就是模运算,1+1 == 2,进位10或者不进位0,原来的位上取都取0. 

以八位二进制为例,模为2的八次幂,即1111 1111 + 1,没法用1 0000 0000表示,因为没那么长~~ 

正数(补码和原码相同):+11

二进制:0000 1011

原码:0000 1011

负数:-7

二进制(这里还不涉及符号,只是二进制数):0000 1111 

原码(第一位为符号位,负数符号位1):1000 1111 

小结:正数不变,负数除符号位,变反+1,总之,原码补码相加应该等于模的倍数。

4.分析原题 

有了段位结构体和补码的基础后,再来分析原题:

废话就不多说了,还是设立对照组,利用printf打印参数发现规律和问题。经过多次改进,终于弄出一个比较完整有效的参照实验:

#include<stdio.h>
#include<iostream>
struct bit{
int a:3;
int b:2;
int c:3;
};
//看看int默认是有符号还是无符号?
int main(){
bit s;
char *c = (char*) &s;
printf("before assignment : s is %x\n",*c);
printf("and a/b/c is :\n");
std::cout << s.a << std::endl << s.b << std::endl << s.c << std::endl;
printf("s.a:%x\ns.b:%x\ns.c:%x\n",s.a,s.b,s.c);

*c = 0x99;
printf("after assignment : s is %x\n",*c);

printf("sizeof(bit) s is %d\n",sizeof(bit));


std::cout << s.a << std::endl << s.b << std::endl << s.c << std::endl;
printf("s:%x\n",s);
printf("s.a:%x\ns.b:%x\ns.c:%x\n",s.a,s.b,s.c);

}


打印:
# ./a.out
before assignment : s is ffffffc9
and a/b/c is :
1
1
-2
s.a:1
s.b:1
s.c:fffffffe
after assignment : s is ffffff99
sizeof(bit) s is 4
1
-1
-4
s:8048899
s.a:1
s.b:ffffffff
s.c:fffffffc

S:赋值前(before assignment):s is ffffffc9尾数c9不是固定的,根据源代码的组织不同,可能影响到内存分配从而造成区别。编译中出现过69、89和c9,但是前边的ffffff是固定的。 

可以看到虽然struct bit中只有3+2+3 == 8 个比特位, 但是还是占用了4个字节 ,共32个比特位(但是这不影响用一个char指针就给他们三个赋值,因为他们三个占用的一个字节在整个struct bit中的地址最小( 大端 ),char指针正好指向那个地址)。

由于改进了实验代码,可以直接从结果看出,struct bit中低8位是被赋值0x99了, 但是 高24位 被 缺省 弄成0xffffff了 ,这是从 整个struct bit声明时就存在的。从现在看,无法撼动!!!

特例:除非用指针改——比如*(c+1) = 0x00,手贱的我还是试了,用

*(c + 1) = 0x00;

不成功。怀疑有保护, 不过因为已经证实前边的那堆default产生的ffffff对结果没影响,暂时也就没必要深究了。

s.a,s.b,s.c分别占用几个bit位,就按几个bit位算,不干前边的事。想想也是,那样的话也太悲剧了吧,头一个比特变量(比如s.a)永远受前边影响(s.c:1111 1111 1111 1111 1111 1111 110),没法算准,特例之中还有特例,用std::cout来操作s.a,s.b,s.c是没事了,但是用其他 不匹配的指针操作, 例如 :

printf("before assignment : s is %x\n",*c);
printf("s.a:%x\ns.b:%x\ns.c:%x\n",s.a,s.b,s.c);

的打印结果: 

ffffff99 
s.a:1 
s.b:ffffffff 
s.c:fffffffc

那么再来分析打印结果,我先做个小假设 (蓝色为实际上错误的假设) : 
赋值前:十六进制:0xfffffc9 
二进制: 
1111 1111 1111 1111 1111 1111 1100 1001 
后八位分给s.a、s.b、s.c: 
s.a:110 
s.b:01 
s.c:001 
打印结果是:1、1、-2 
赋值后:十六进制:0xffffff99 
二进制: 
1111 1111 1111 1111 1111 1111 1001 1001 
后八位分给s.a、s.b、s.c: 
s.a:100 
s.b:11 
s.c:001 
打印结果是:1、-1、-4 
明显不对~!!!所以呢?顺序有误,s.a和s.c应该调换一下! 即,赋值后: 
s.a为001, 
s.c为110, 
s.b还是01。 
这样再按补码看(不用补到八位或32位,有几位算几位),110是-2;100是-4;01和001都是1;11是-1。就对上号了 
这个故事告诉我们:结构体中,不光先声明的常规变量地址更低,先声明的比特位也在同地址中更低的地方 ——话说回来,这只是个特例,刚好他们总共才占一个地址,如果扩展到比特位占用多个地址(就是总和大于8bit)的情况下,必然要遵循这个 大端规律 ~

个人能力有限,没有深究两个问题: 
1.我使用*(c + 1) = 0x00;或者*(c - 1) = 0x00;都无法改变struct bit中前边24位中的1,说是保护不知道妥当否,还是方法不对。 
2.既然s.a等都只有两三位,他们在寄存器中是怎么操作的,先从栈中提取出来,放寄存器,补全,操作完,再放回去? 
因为使用了%al低8位操作,又用了$0xfffffff8等补全操作,所以暂且假设是刚好能对s.a,s.b和s.c分别操作而互不影响,具体应该能从这些值中计算,推测一二,但是先写到这吧,没推,扯得有点远。 
有些也没看太懂,比较生偏的movzbl等(也能都的到差不太多的AT&T用法)。

 

=> 0x8048634 <main()+16>: and $0xfffffff8,%eax
0x8048637 <main()+19>: or $0x1,%eax
0x804863a <main()+22>: mov %al,0x14(%esp)
0x804863e <main()+26>: movzbl 0x14(%esp),%eax
0x8048643 <main()+31>: or $0x18,%eax
0x8048646 <main()+34>: mov %al,0x14(%esp)
0x804864a <main()+38>: movzbl 0x14(%esp),%eax
0x804864f <main()+43>: and $0x1f,%eax
0x8048652 <main()+46>: or $0xffffffa0,%eax
0x8048655 <main()+49>: mov %al,0x14(%esp)

 

永不止步步 发表于10-17 09:18 浏览65535次
分享到:

已有0条评论

暂时还没有回复哟,快来抢沙发吧

添加一条新评论

只有登录用户才能评论,请先登录注册哦!

话题作者

永不止步步
金币:67417个|学分:363791个
立即注册
畅学电子网,带你进入电子开发学习世界
专业电子工程技术学习交流社区,加入畅学一起充电加油吧!

x

畅学电子网订阅号