段位结构体与补码、大小端

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

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


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

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

int main()
{
  bit s;
 char *c=(char*)&s;
  cout<

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

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

补码:0000 1011


负数:-7

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

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

补码:1111 0001


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


4.分析原题

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

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

#include
#include
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
PS:赋值前(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)的情况下,必然要遵循这个小端规律~



收点小知识:

一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:   

  struct bs  
  {  
   unsigned a:4  
   unsigned :0 /*空域*/  
   unsigned b:4 /*从下一单元开始存放*/
   unsigned c:4  
  } 

用那个空域就能起隔离作用,只是用空域方便快捷一点,不一定非得用空域,用已命名或者根本就未命名的位域填充(比如上例中写4),保证刚好不跨字节就OK了。

注意,不能跨越两个字节,也就意味着位域最大只能是8了。




个人能力有限,没有深究两个问题:

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 :	and    $0xfffffff8,%eax
   0x8048637 :	or     $0x1,%eax
   0x804863a :	mov    %al,0x14(%esp)
   0x804863e :	movzbl 0x14(%esp),%eax
   0x8048643 :	or     $0x18,%eax
   0x8048646 :	mov    %al,0x14(%esp)
   0x804864a :	movzbl 0x14(%esp),%eax
   0x804864f :	and    $0x1f,%eax
   0x8048652 :	or     $0xffffffa0,%eax
   0x8048655 :	mov    %al,0x14(%esp)



 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
 

你可能感兴趣的:(C,深入浅出C语言)