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