`
cloudtech
  • 浏览: 4593794 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论

C语言之struct大小、首地址与内存对齐

 
阅读更多

被问到如下问题:
给定一个结构体中某个变量地址,可否得到结构体变量的地址?


答案是可以,但是对不同的场合有不同的结果;这与微处理器平台、编译器的处理不可分割。


首先,对于处理器,大尾端、小尾端的因素必须考虑;
其次:

一、ANSIC标准中并没有规定,相邻声明的变量在内存中一定要相邻。

为了程序的高效性,内存对齐问题由编译器自行灵活处理,这样导致相邻的变量之间可能会有一些填充字节。对于基本数据类型(int char),他们占用的内存空间在一个确定硬件系统下有个确定的值,所以,接下来我们只是考虑结构体成员内存分配情况。

1、Win32平台下的微软C编译器(cl.exe for 80×86)的对齐策略:

1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。


2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。


3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。
备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。


2、GNU GCC编译器中,遵循的准则有些区别,对齐模数不是像上面所述的那样,根据最宽的基本数据类型来定。
在GCC中,对齐模数的准则是:对齐模数最大只能是4,也就是说,即使结构体中有double类型,对齐模数还是4,所以对齐模数只能是1,2,4。而且在上述的三条中,第2条里,offset必须是成员大小的整数倍,如果这个成员大小小于等于4则按照上述准则进行,但是如果大于4了,则结构体每个成员相对于结构体首地址的偏移量(offset)只能按照是4的整数倍来进行判断是否添加填充。

譬如:
struct id
{

char ch;
double dd;

}T;
根据以上准则,在windows下,使用VC编译器,sizeof(T)的大小为16个字节;GNU GCC编译器则得到12字节。

二、struct的首地址即为第一个元素的首地址
如下程序,测试环境,GNU/Linux Debian, GCC 4.3.2-1-1

1 #include <stdio.h>
2 #define STRUCT_OFFSET(id, element) ((unsignedlong) &((structid*)0)->element)
3 struct_Test
4 {
5 charch;
6 doubledd;
7 };
8
9 intmain(void)
10 {
11 struct_Test stru;
12
13 printf("the addrress of first ele of struct is %x/n", &stru.ch);
14
15 unsignedlongoffset = STRUCT_OFFSET(_Test, dd);
16
17 printf("the offset of dd is %x, offset = %u/n", &stru.dd, offset);
18 printf("the start addrress of struct caculated from dd is %x/n", (char*)&stru.dd - offset);
19
20 return0;
21 }

$ ./a.out
the addrress of first ele of struct is bfb86124
the offset of dd is bfb86128, offset = 4
the start addrress of struct caculated from dd is bfb86124

其中,整个程序中最关键的部分就是如何求出结构体中某个成员相对于结构体首地址的偏移量。

这里的解决方法是:假设存在一个虚拟地址0,将该地址强制转换成为该结构体指针类型(struct id*)0。那么地址0开始到sizeof(struct)-1长度的内存区域就可以视为一个结构体的内存。

这样结构体中任何一个元素都可以通过对该结构体指针解引用得到。

由于该结构体的起始地址为0,因此任何一个成员的地址应该等于其相对于结构体起始地址的偏移,这也就是计算偏移量的方法:

#define STRUCT_OFFSET(id, element) ((unsignedlong) &((structid*)0)->element)
Linux内核里面的list_entry宏就是这样的。

说明:
1) 前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏移量了呢?

因为有了第1点存在,所以我们就可以只考虑成员的偏移量,这样思考起来简单。想想为什么。
结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,这个宏也在stddef.h中定义,如下:
#define offsetof(s,m) (size_t)&(((s *)0)->m)


例如,想要获得S中c的偏移量,方法为
size_t pos = offsetof(s, dd);// pos等于4

2) 基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。

由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。

但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。

三、有一个影响sizeof的重要参量还未被提及,那便是编译器的pack指令。

它是用来调整结构体对齐方式的,不同编译器名称和用法略有不同,VC6中通过#pragma pack实现,也可以直接修改/Zp编译开关。

#pragma pack的基本用法为:#pragma pack( n ),n为字节对齐数,其取值
为1、2、4、8、16,默认是8,如果这个值比结构体成员的sizeof值小,那么该成员的偏移量应该以此值为准,即是说,结构体成员的偏移量应该取二者的最小值,公式如下:
offsetof( item ) = min( n, sizeof( item ) )

四、还有一点要注意,“空结构体”(不含数据成员)的大小不为0,而是1。

试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢?于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。
如下:
struct S { };
sizeof( S ); // 结果为1

五、含位域结构体的sizeof

位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构体的sizeof,只是考虑到其特殊性而将其专门列了出来。
C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。

分享到:
评论

相关推荐

    C语言之struct大小、首地址与内存对齐[借鉴].pdf

    C语言之struct大小、首地址与内存对齐[借鉴].pdf

    C语言字节对齐详解

    介绍C语言字节对齐的文档,对C语言字节对齐讲述得很清楚,特别适合新手对结构体不清楚的情况下参考

    C语言之GCC中支持的内存对齐指令

    #pragma pack() :取消内存对齐访问 #pragma pack(n) (n=1/2/4/8):按n字节对齐 #pragma pack(2) struct mystruct1 { int a; char b; short c; } struct mystruct2 { int a;; double b; short c; } ...

    解析C语言中结构体struct的对齐问题

    首先看一下结构体对齐的三个概念值: 数据类型的默认对齐值(自身对齐): ...有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的“偏移

    深入理解C语言内存对齐

    “数据项只能存储在地址是数据项大小的整数倍的内存位置上” 例如int类型占用4个字节,地址只能在0,4,8等位置上。 例1: 代码如下:#include &lt;stdio&gt;struct xx{ char b; int a; int c; char d;}; int main(){ ...

    5分钟搞定内存字节对齐

    写出一个struct,然后sizeof,你会不会经常对结果感到奇怪?sizeof的结果往往都比你声明的变量总长度要大,这是怎么回事呢?讲讲字节对齐吧.

    内存对齐(Memory alignment)

         在 C语言之结构体 章节里,对struct的功能和使用进行了详细的说明。「内存对齐」章节作为struct的一个扩充知识。事实也证明,实际开发中,关注结构体内存布局特性的同事寥寥无几。甚至某些同事表示从未去...

    c语言难点分析整理,C语言

    35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈...

    C语言、C++内存对齐问题详解

    主要介绍了C语言、C++内存对齐问题详解,内存对齐的问题主要存在于理解struct和union等复合结构在内存中的分布,需要的朋友可以参考下

    高级C语言详解

    35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈...

    史上最强的C语言资料

    35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈...

    C语言结构体对齐.pdf

    C 语言结构体对齐也是老生常谈的话题了。基本上是面试题的必考题。内容 虽然很基础,但一不小心就会弄错。写出一个 struct,然后 sizeof,你会不会经 常对结果感到奇怪? sizeof的结果往往都比你声明的变量总长度要...

    C语言难点分析整理

    35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈...

    实例讲解C语言编程中的结构体对齐

    主要介绍了C语言编程中的结构体对齐,值得注意的是一些结构体对齐的例子在不同编译器下结果可能会不同,需要的朋友可以参考下

    高级进阶c语言教程..doc

    35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈...

    C语言讲义.doc

    2.10.6 大端对齐与小端对齐 23 2.11 CHAR类型 24 2.11.1 char常量,变量 24 2.11.2 printf输出char 24 2.11.3 不可打印char转义符 24 2.11.4 char和unsigned char 25 2.12 浮点FLOAT,DOUBLE,LONG DOUBLE类型 25 ...

    免费下载:C语言难点分析整理.doc

    35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈...

    高级C语言 C 语言编程要点

    35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈...

Global site tag (gtag.js) - Google Analytics