struct位域,对齐以及union作为class需要注意的地方

一. 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
struct tests {
  char c;
  int a;
};
struct test2 {
  int a;
  char c;
};
struct test3 {
  char c;
};
cout<<sizeof(tests)<<endl;
cout<<sizeof(test1)<<endl;
cout<<sizeof(test2)<<endl;

输出分别为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
struct test {
  int a:2;
  int b:3;
  int c:4;
};

则sizeof(test)为4(假设机器字长为4字节)
4.可以用匿名位域变量来占位,如果位域长度指定为0,则强制编译器为下一个位于分配一个新的单元。

1
2
3
4
5
struct test {
    int a:2;
    signed :0;
    int b:2;
}

则sizeof(test)为8,因为第二个位域长度为0使得b:2占用一个新的单元。
位域struct一般和union联合使用,用来方便的访问某一个结构的某些位。比如:

1
2
3
4
5
6
7
8
union {
     struct {
	unsigned char a1:2;
	unsigned char a2:3;
	unsigned char a3:3;
    }x;
    unsigned char b;
  }d;

在这个里面有一个特别重要的值得注意的问题:
必要想当然的认为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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(int argc, char** argv) {
  union{
    struct{
      unsigned char a:1;
      unsigned char b:2;
      unsigned char c:3;
    }d;
    unsigned char e;
  } f;
  f.e = 1;
  printf("%d/n",f.d.a);
  return 0;
}

这段程序应该输出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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(int argc, char** argv) {
  union{
    struct{
      char a:1;
      char b:2;
      char c:3;
    }d;
    char e;
  } f;
  f.e = 1;
  printf("%d/n",f.d.a);
  return 0;
}

由于a只有一位,且有符号,因此a要么为0,要么为-1。因此这段程序输出-1

下面我们看一个比较经典的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//假设硬件平台是intel x86(little endian)
char *inet_ntoa(uint32_t in) {
  static char b[18];
  register char *p;
  p = (char *)in;
  #define UC(b) (((int)b)&0xff)
  (void) snprintf(b, sizeof(b),
  "%d. %d. %d. %d", UC(p[0]), UC(p[1]), UC(p[2]), UC(p[3]));
  return (b);
}
 
int main() {
  printf("%s, %s", inet_ntoa(0x12345678), inet_ntoa(87654321));
}

其实这段程序的作用就是把一个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
class test1 {
  int a;
public:
  test1() { }
};
union test2 {
  test1 t;
  int a;
};

那么这段代码肯定编译不通过,提示test2包含了含有构造函数的对象。编译器为什么要组织这种行为呢?站在编译器的角度仔细想想不难发现,这个是和union的结构有关系的,union里面的成员在内存里面并行存放,共享通一片内存区域,那么如果有多构造函数的话,构造函数执行的顺序就是个大问题,因为后面执行的构造函数或许会覆盖前面执行的效果!更近一步的,如果前面一个构造函数执行了很多重要的操作,但是后面一个构造函数被执行了,并且是在同一片内存区域上执行,就会导致前面的状态丢失,使得程序处于不一致状态,这并不是我们想要的,因此编译器就理所当然的禁止这种行为了.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值