最近在学习bmp位图文件的格式,目的是做数字图像处理的project。以位图文件为例说明是因为在解析bmp格式时我犯了一个十分低级,而且非常不应该犯的错误,浪费了1天的时间,实在是不应该,但后来还是把问题弄清楚了,所以就算给自己一个警告,把其中缘由说清楚,以免类似错误再犯。
bmp文件格式大致分为4个部分:1、位图文件头;2、位图信息头;3、颜色表(调色板);4、位图像素点阵数据。其中前三个部分都可以用结构体来描述。以位图文件头为例:我把它刻画为:
typedef struct BMP_FILEHEADER
{
WORD bmpType;/*位图标识*/
DWORD bmpSize;/*说明文件的大小,用字节为单位*/
DWORD bmpReserved;/*保留,必须设置为0*/
DWORD bmpOffset;/*从文件头开始到实际的图象数据之间的字节的偏移量*/
DWORD bmpHeaderSize;/*说明BITMAP_INFOHEADER结构所需要的字数*/
DWORD bmpWidth;/*说明图象的宽度,以象素为单位*/
DWORD bmpHeight;/*说明图象的高度,以象素为单位.大多数的BMP文件都是倒向的位图*/
WORD bmpPlanes;/*为目标设备说明位面数,其值将总是被设为1*/
} BMP_FILEHEADER;
我用C语言的fread(...)或fgetc(...)函数将位图文件coin.bmp按字节读到已经设定好的pBmpData数组单元中,然后用强制类型转换的方式把pBmpData的首地址赋值给上述结构体类型指针bmpFileHeader,即bmpFileHeader=(BMP_FILEHEADER*)pBmpData,这样就可以很方便地调用结构体域成员了。但是总出错的地方是除了第一个域成员bmpType输出结果正确外,其他的域成员的结果总是错位2个字节,编译器将数组的第三个和第四个字节忽略了。但是我在TC环境下测试结果却是正确的,这很令我无奈,我自信我的逻辑没有错误,所以开始怀疑是VC++编译器的问题,之后用Borland C++ 测试,结果和VC++一样,所以我肯定这个还是我什么地方没用搞清楚。经过1天的时间,不断地做测试,终于搞清楚了。呵呵,虽然很长,但还是值得的,学习的过程本来就是不断给字节加深印象的过程,受刺激的越勤,反射弧越短。
下面说一下什么是内存对齐。为了优化CPU访问内存的效率,程序语言的编译器在做变量的存储分配时就进行了分配优化处理,优化规则大致原则是这样: 对于n字节的元素(n=2,4,8,...),它的首地址能被n整除,这种原则称为“对齐”,如WORD(2字节)的值应该能被2整除的位置,DWORD(4字节)应该在能被4整除的位置。
对于结构体来说,结构体的成员在内存中顺序存放,所占内存地址依次增高,第一个成员处于低地址,最后一个成员处于最高地址,但结构体成员的内存分配不一定是连续的,编译器会对其成员变量依据 “对齐”原则进行处理。对待每个成员类似于对待单个n字节的元素一样,依次为每个元素找一个适合的首地址,使得其符合上述的“对齐”原则。通常编译器中可以设置一个对齐参数n,但这个n并不是结构体成员实际的对齐参数,VC++6.0中结构体的每个成员实际对齐参数N通常是这样计算得到:N = Min{sizeof(此成员类型),n},结构体中所有成员的对齐参数N的最大值称为结构体的对齐参数。
成员的内存分配规律是这样的:从结构体的首地址开始向后依次为每个成员寻找第一个满足条件的首地址addr,该条件是addr MOD N = 0,并且整个结构的长度必须为各个成员所使用的对齐参数中最大的那个值的最小整数倍,不够就补空字节。
VC++6.0的n值是可选的,可以通过设置改变n的值。具体方法是project->settings->C/C++,选择Category:Code Generation,在Struct member alignment中选择1、2、4、8、16对应的选项。不过这种设置的方法有时不管用;另一种方式就是在程序中直接加上语句:#pragma pack(n)。如:#pragma pack(1)表示按1字节对齐;:#pragma pack(4)表示按4字节对齐,以此类推。
再举个实际的例子。比如:
struct EMP
{
char apple;
int orange;
short pear;
}example;
main()
{
printf("%x/n",&example.apple);
printf("%x/n",&example.orange);
printf("%x/n",&example.pear);
printf("size of struct=%d/n",sizeof(example));
}
这个输出结果是: address of apple=427e30
address of orange=427e34
address of pear=427e38
size of struct=12
其地址分配如下:
0x427e30 | Apple |
填充 | |
填充 | |
填充 | |
0x427e34 | Orange |
0x427e38 | Pear |
填充 | |
填充 |
若加上#pragma pack(1),则输出的结果是: address of apple=427e30
address of orange=427e31
address of pear=427e35
size of struct=7
其地址分配如下:
0x427e30 | Apple |
0x427e31
| Orange |
0x427e35
| Pear |
如果把域成员第一个和第二个颠倒位置,不加#pragma pack(1)结果会怎麽样呢?是不是和第一个结果一样呢?
struct EMP
{
int orange;
char apple;
short pear;
}example;
main()
{
printf("address of orange=%x/n",&example.orange);
printf("address of apple=%x/n",&example.apple);
printf("address of pear=%x/n",&example.pear);
printf("size of struct=%d/n",sizeof(example));
}
输出结果为:address of orange=427e30
address of apple=427e34
address of pear=427e36
size of struct=8
其地址分配如下:
0x427e30
| Orange
|
0x427e34
| Apple |
填充 | |
0x427e36
| Pear |
从上述结果看,对于结构体中域成员顺序及数据类型的不同,编译器所分配的内存地址及大小也不一样,所以合理地排放域成员的顺序对于节省内存空间有很大作用,这个在嵌入式的程序设计上非常管用。上述地址分配原则中的“整个结构的长度必须为各个成员所使用的对齐参数中最大的那个值的最小整数倍,不够就补空字节”的含义是:首先要保证分配的地址符合addr MOD N = 0,然后在此基础上计算内存空间,为最大N值的整数倍。另外通过对结构体域成员地址分配的原则的了解,我找到了一开始所犯错误的原因。