一. struct对齐
在c/c++中,struct结构体的大小不是简单的成员变量所占空间大小的累加,这里面涉及到变量对齐(alignment)的概念,由于计算机中内存的结构,使得cpu从某个特定边界的地址可以一次读取出某几个字节的数据,但是不处于边界的地址上读数据效率就会很低了。比如cpu可以一次性从4倍地址处读取4字节的数据,但是我现在需要读取地址为5开始的4字节的内容,那么cpu就只能分2次内存周期来取数据了。
由于这个原因,不管是汇编编译器还是高级语言的编译器,都会为把特定类型的变量放到特定的边界上,以加快程序的运行速度。比如int型变量(4字节)都会对齐到4倍的地址处,float型的变量都会对齐到8倍的地址出。但是对于 char类型的变量,由于不管怎么对齐,都必须要花一次存储周期来读,因此char类型的变量可以放到任何位置。一般来说,变量的对齐和他们的长度是相同的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
输出分别为8 8 1,前2个是由于这就是由于a为4个字节,必须对齐到4倍地址处,而c为1个字节,为了确保a能够对齐到4倍地指出,因此把c扩展到占用4个字节(这样整个tests被对齐到8倍地址处)。
test3因为只还有一个char类型变量,因此可以放在内存任何位置,不需要补空间。
二. 位域
位域的概念:
在c中,struct和union中的成员变量都可以指定位域,也就是指定某个变量仅仅占用几个bit。
有几个重要的地方:
1.位的长度最多只能为变量内型的长度,比如 int a:29是合法的,但是int a:33是非法的。
2.不能取一个位域变量的地址
3.两个类型的位域变量,如果紧挨着声明,并且位于长度和不超过变量的长度,则两个位域变量共享同一个变量的位空间。
比如:
1 2 3 4 5 | |
则sizeof(test)为4(假设机器字长为4字节)
4.可以用匿名位域变量来占位,如果位域长度指定为0,则强制编译器为下一个位于分配一个新的单元。
1 2 3 4 5 | |
则sizeof(test)为8,因为第二个位域长度为0使得b:2占用一个新的单元。
位域struct一般和union联合使用,用来方便的访问某一个结构的某些位。比如:
1 2 3 4 5 6 7 8 | |
在这个里面有一个特别重要的值得注意的问题:
必要想当然的认为a1就是b的二进制表示的前2个bit,a2是随后的3个bit…
事实上,这个跟机器是否为大端小端有关(big endian or little endian),所谓大端,就是数据的低字节存放在内存的高地址处,所谓小端,就是数据的低字节存放在内存的低地址处。我们一般用的x86系列的pc机都是little endian的。
对于上面那断代码,由于只有一个字节,不存在大端小端问题,但是位域还是跟这个有关的。
假如 d.b=100, 100的二进制表示为01100100,那么a1,a2,a3分别代表哪些位呢?
很多人认为肯定是顺序分配啦,a1=01 ,a2=100,a3=100,但是这个是错误的,由于机器是小端机器,位域排列也是相反的,因此a1=00,a2=001,a3=011.下面有2个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
这段程序应该输出1,因为由于little endian,a被分予了一位,并且为1,而a为unsigned char的,故真值为1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
由于a只有一位,且有符号,因此a要么为0,要么为-1。因此这段程序输出-1
下面我们看一个比较经典的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
其实这段程序的作用就是把一个32位的证书转换成一个ip字符串。程序中p = (char *)in,取得32位整数的首地址,注意:由于是小端机器,对于0×12345678,在内存中的排列应该是:
78 56 34 12
^
|
p
p指向职位0×78的字节处,这样再分析上面那段程序就很简单了。
这个程序里面另外一个很有用的东西就是UC宏,这个宏就是把一个8位的char类型转换成一个正整数,首先强制类型转换成一个int型,这个转换会导致符号扩展,因此最后还需要与0xff来一个逻辑与去除符号位。
三. union作为class
起始大家不太相信,union可以像class,struct一样拥有构造函数,析构函数,成员函数等等。比如
union testu {
int a;
int b;
testu() {
cout<<"union testu"<<endl;
}
};
但是,这里有一个需要注意的问题是:union包含的变量不能有显示的构造函数,所谓显示的构造函数,就是被编译器认为是notrival的,也就是说编译器必要要生成构造函数的类。比如:
1 2 3 4 5 6 7 8 9 | |
那么这段代码肯定编译不通过,提示test2包含了含有构造函数的对象。编译器为什么要组织这种行为呢?站在编译器的角度仔细想想不难发现,这个是和union的结构有关系的,union里面的成员在内存里面并行存放,共享通一片内存区域,那么如果有多构造函数的话,构造函数执行的顺序就是个大问题,因为后面执行的构造函数或许会覆盖前面执行的效果!更近一步的,如果前面一个构造函数执行了很多重要的操作,但是后面一个构造函数被执行了,并且是在同一片内存区域上执行,就会导致前面的状态丢失,使得程序处于不一致状态,这并不是我们想要的,因此编译器就理所当然的禁止这种行为了.