打印
[牛人杂谈]

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

[复制链接]
1997|14
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
dongnanxibei|  楼主 | 2016-10-16 09:39 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

最近在进行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;
}

沙发
dongnanxibei|  楼主 | 2016-10-16 09:40 | 只看该作者
这题简单一点,告诉你输出是多少(或者难一点,也可以直接让你猜输出),然后让你解释输出为什么是:
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, 表示二进制位的个数, 即 表示有多少位 。 变量名是选择项, 可以不命名, 这样规定是为了排列需要。

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


使用特权

评论回复
板凳
dongnanxibei|  楼主 | 2016-10-16 09:41 | 只看该作者
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;


使用特权

评论回复
地板
dongnanxibei|  楼主 | 2016-10-16 09:42 | 只看该作者

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

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

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,总之,原码补码相加应该等于模的倍数。


使用特权

评论回复
5
dongnanxibei|  楼主 | 2016-10-16 09:42 | 只看该作者

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


使用特权

评论回复
6
dongnanxibei|  楼主 | 2016-10-16 09:43 | 只看该作者
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



使用特权

评论回复
7
dongnanxibei|  楼主 | 2016-10-16 09:46 | 只看该作者
那么再来分析打印结果,我先做个小假设 (蓝色为实际上错误的假设) :
赋值前:十六进制: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)的情况下,必然要遵循这个 大端规律 ~

使用特权

评论回复
8
dongnanxibei|  楼主 | 2016-10-16 09:47 | 只看该作者
个人能力有限,没有深究两个问题:
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)


使用特权

评论回复
9
gejigeji521| | 2016-10-16 11:44 | 只看该作者
结构体和联合体是C语言的难点重点

使用特权

评论回复
10
capturesthe| | 2016-10-16 15:31 | 只看该作者
dongnanxibei 发表于 2016-10-16 09:40
这题简单一点,告诉你输出是多少(或者难一点,也可以直接让你猜输出),然后让你解释输出为什么是:
如果 ...

用的知识不少,在单片机编程里面用的多吗

使用特权

评论回复
11
yiyigirl2014| | 2016-10-17 10:37 | 只看该作者
学习这个,就是需要多练习,多试验。

使用特权

评论回复
12
yiyigirl2014| | 2016-10-17 10:37 | 只看该作者
capturesthe 发表于 2016-10-16 15:31
用的知识不少,在单片机编程里面用的多吗

我觉得单片机就跟PC上的区别就是显示方法。。其实差不多的。

使用特权

评论回复
13
john_lee| | 2016-10-17 14:02 | 只看该作者
1. 位结构中的成员 可以 定义为unsigned, 也可 定义为signed,  但 当成员长度为1时 , 会被认为是unsigned类型 。因为单个位不可能具有符号。 (实测:int默认)  

长度为1的位域,类型是 int,从来就是!
1.我使用*(c + 1) = 0x00;或者*(c - 1) = 0x00;都无法改变struct bit中前边24位中的1,说是保护不知道妥当否,还是方法不对。

struct bit的变量s,只有一个字节(sizeof (s) == 1),没有什么“前边24位”的说法。你看到结果是“ffffffc9”觉得奇怪,很简单,把“char *c=(char*)&s;” 改一下:unsigned char *c=(unsigned char*)&s; 再看看结果,就应该明白了。
2.既然s.a等都只有两三位,他们在寄存器中是怎么操作的,先从栈中提取出来,放寄存器,补全,操作完,再放回去?

基本上就是如此。
比较生偏的movzbl等

movzbl 指令把“源操作数”(一个byte,助记符中的b)复制到“目的操作数”(一个32位字,助记符中的l),并进行“零”扩展(助记符中的z),就是用0填充目的操作数的高24bits。对应的 intel 格式助记符为:movzx。

使用特权

评论回复
14
稳稳の幸福| | 2016-10-17 14:43 | 只看该作者
段位的应用可以方便的实现数据的快速使用,直接可以给某几位起个变量名。

使用特权

评论回复
15
643757107| | 2016-10-22 12:23 | 只看该作者
基础知识如果不牢固,做起项目来,处处碰壁。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

201

主题

3587

帖子

16

粉丝