Archive for the ‘Programming’ Category

linux indent 命令详解

Thursday, May 8th, 2008

功能说明:调整C原始代码文件的格式。

语  法:indent [参数][源文件] 或 indent [参数][源文件][-o 目标文件]

补充说明:indent可辨识C的原始代码文件,并加以格式化,以方便程序设计师阅读。

参  数:
 -bad或–blank-lines-after-declarations  在声明区段或加上空白行。
 -bap或–blank-lines-after-procedures  在程序或加上空白行。
 -bbb或–blank-lines-after-block-comments  在注释区段后加上空白行。
 -bc或–blank-lines-after-commas  在声明区段中,若出现逗号即换行。
 -bl或–braces-after-if-line  if(或是else,for等等)与后面执行区段的”{”不同行,且”}”自成一行。
 -bli<缩排格数>或–brace-indent<缩排格数>  设置{ }缩排的格数。
 -br或–braces-on-if-line  if(或是else,for等等)与后面执行跛段的”{”不同行,且”}”自成一行。
 -bs或–blank-before-sizeof  在sizeof之后空一格。
 -c<栏数>或–comment-indentation<栏数>  将注释置于程序码右侧指定的栏位。
 -cd<栏数>或–declaration-comment-column<栏数>  将注释置于声明右侧指定的栏位。
 -cdb或–comment-delimiters-on-blank-lines  注释符号自成一行。
 -ce或–cuddle-else  将else置于”}”(if执行区段的结尾)之后。
 -ci<缩排格数>或–continuation-indentation<缩排格数>  叙述过长而换行时,指定换行后缩排的格数。
 -cli<缩排格数>或–case-indentation-<缩排格数>  使用case时,switch缩排的格数。
 -cp<栏数>或-else-endif-column<栏数>  将注释置于else与elseif叙述右侧定的栏位。
 -cs或–space-after-cast  在cast之后空一格。
 -d<缩排格数>或-line-comments-indentation<缩排格数>  针对不是放在程序码右侧的注释,设置其缩排格数。
 -di<栏数>或–declaration-indentation<栏数>  将声明区段的变量置于指定的栏位。
 -fc1或–format-first-column-comments  针对放在每行最前端的注释,设置其格式。
 -fca或–format-all-comments  设置所有注释的格式。
 -gnu或–gnu-style  指定使用GNU的格式,此为预设值。
 -i<格数>或–indent-level<格数>  设置缩排的格数。
 -ip<格数>或–parameter-indentation<格数>  设置参数的缩排格数。
 -kr或–k-and-r-style  指定使用Kernighan&Ritchie的格式。
 -lp或–continue-at-parentheses  叙述过长而换行,且叙述中包含了括弧时,将括弧中的每行起始栏位内容垂直对其排列。
 -nbad或–no-blank-lines-after-declarations  在声明区段后不要加上空白行。
 -nbap或–no-blank-lines-after-procedures  在程序后不要加上空白行。
 -nbbb或–no-blank-lines-after-block-comments  在注释区段后不要加上空白行。
 -nbc或–no-blank-lines-after-commas  在声明区段中,即使出现逗号,仍旧不要换行。
 -ncdb或–no-comment-delimiters-on-blank-lines  注释符号不要自成一行。
 -nce或–dont-cuddle-else  不要将else置于”}”之后。
 -ncs或–no-space-after-casts  不要在cast之后空一格。
 -nfc1或–dont-format-first-column-comments  不要格式化放在每行最前端的注释。
 -nfca或–dont-format-comments  不要格式化任何的注释。
 -nip或–no-parameter-indentation  参数不要缩排。
 -nlp或–dont-line-up-parentheses  叙述过长而换行,且叙述中包含了括弧时,不用将括弧中的每行起始栏位垂直对其排列。
 -npcs或–no-space-after-function-call-names  在调用的函数名称之后,不要加上空格。
 -npro或–ignore-profile  不要读取indent的配置文件.indent.pro。
 -npsl或–dont-break-procedure-type  程序类型与程序名称放在同一行。
 -nsc或–dont-star-comments  注解左侧不要加上星号(*)。
 -nsob或–leave-optional-semicolon  不用处理多余的空白行。
 -nss或–dont-space-special-semicolon  若for或while区段仅有一行时,在分号前不加上空格。
 -nv或–no-verbosity  不显示详细的信息。
 -orig或–original  使用Berkeley的格式。
 -pcs或–space-after-procedure-calls  在调用的函数名称与”{”之间加上空格。
 -psl或–procnames-start-lines  程序类型置于程序名称的前一行。
 -sc或–start-left-side-of-comments  在每行注释左侧加上星号(*)。
 -sob或–swallow-optional-blank-lines  删除多余的空白行。
 -ss或–space-special-semicolon  若for或swile区段今有一行时,在分号前加上空格。
 -st或–standard-output  将结果显示在标准输出设备。
 -T  数据类型名称缩排。
 -ts<格数>或–tab-size<格数>  设置tab的长度。
 -v或–verbose  执行时显示详细的信息。
 -version  显示版本信息。

我的风格

-bad -bap -bbb -nbc -br -lp -ncs -ce -npsl -i8

C99中的restrict关键字

Wednesday, February 6th, 2008

这里的restrict让我觉得有些疑惑,一查原来是C99中增加的关键字

那么restrict的意义是什么呢?

One of the new features in the recently approved C standard C99, is the restrict pointer qualifier. This qualifier can be applied to a data pointer to indicate that, during the scope of that pointer declaration, all data accessed through it will be accessed only through that pointer but not through any other pointer. The ‘restrict’ keyword thus enables the compiler to perform certain optimizations based on the premise that a given object cannot be changed through another pointer. Now you’re probably asking yourself, “doesn’t const already guarantee that?” No, it doesn’t. The qualifier const ensures that a variable cannot be changed through a particular pointer. However, it’s still possible to change the variable through a different pointer.

概括的说,关键字restrict只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径;这样的后果是帮助编译器进行更好的代码优化,生成更有效率的汇编代码。

举个简单的例子

int foo (int* x, int* y)
{
*= 0;
*= 1;
return *x;
}

很显然函数foo()的返回值是0,除非参数x和y的值相同。可以想象,99%的情况下该函数都会返回0而不是1。然而编译起必须保证生成100%正确的代码,因此,编译器不能将原有代码替换成下面的更优版本

int f (int* x, int* y)
{
*= 0;
*= 1;
return 0;
}
 

啊哈,现在我们有了restrict这个关键字,就可以利用它来帮助编译器安全的进行代码优化了

int f (int *restrict x, int *restrict y) { *= 0; *= 1; return *x; }

此时,由于指针 x 是修改 *x的唯一途径,编译起可以确认 “*y=1; ”这行代码不会修改 *x的内容,因此可以安全的优化为

int f (int *restrict x, int *restrict y)
{
*= 0;
*= 1;
return 0;
}

最后注意一点,restrict是C99中定义的关键字,C++目前并未引入;在GCC可通过使用参数” -std=c99″
来开启对C99的支持

volatile变量用法

Wednesday, February 6th, 2008

volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如

操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行

优化,从而可以提供对特殊地址的稳定访问。

使用该关键字的例子如下:

int volatile nVint;

当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指

令刚刚从该处读取过数据。而且读取的数据立刻被保存。

例如:

volatile int i=10;
int a = i;
。。。//其他代码,并未明确告诉编译器,对i进行过操作
int b = i;

volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的

汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间

的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果

i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问

注意,在vc6中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编

代码,测试有无volatile关键字,对程序最终代码的影响:

首先用classwizard建一个win32 console工程,插入一个voltest.cpp文件,输入下面的代码:

#include <stdio.h>
void main()
{
 int i=10;
 int a = i;

 printf(”i= %d\n”,a);
        //下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道
 __asm {
  mov         dword ptr [ebp-4], 20h
 }

 int b = i;
 printf(”i= %d\n”,b);
}

然后,在调试版本模式运行程序,输出结果如下:
i = 10
i = 32

然后,在release版本模式运行程序,输出结果如下:
i = 10
i = 10

输出的结果明显表明,release模式下,编译器对代码进行了优化,第二次没有输出正确的i值。

下面,我们把 i的声明加上volatile关键字,看看有什么变化:
#include <stdio.h>
void main()
{
 volatile int i=10;
 int a = i;

 printf(”i= %d\n”,a);
 __asm {
  mov         dword ptr [ebp-4], 20h
 }

 int b = i;
 printf(”i= %d\n”,b);
}

分别在调试版本和release版本运行程序,输出都是:
i = 10
i = 32

这说明这个关键字发挥了它的作用!

位域

Thursday, October 18th, 2007

一、位域  
   
    有些信息在存储时,并不需要占用一个完整的字节,   而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1   两种状态,   用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:  
   
    struct   位域结构名  
    {   位域列表   };  
   
    其中位域列表的形式为:   类型说明符   位域名:位域长度  
   
    例如:  
   
  struct   bs  
  {  
   int   a:8;  
   int   b:2;  
   int   c:6;  
  };  
   
   
    位域变量的说明与结构变量说明的方式相同。   可采用先定义后说明,同时定义说明或者直接说明这三种方式。例如:  
   
  struct   bs  
  {  
   int   a:8;  
   int   b:2;  
   int   c:6;  
  }data;  
   
    说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。对于位域的定义尚有以下几点说明:  
   
    1.   一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:  
   
  struct   bs  
  {  
   unsigned   a:4  
   unsigned   :0   /*空域*/  
   unsigned   b:4   /*从下一单元开始存放*/  
   unsigned   c:4  
  }  
   
    在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。  
   
    2.   由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。  
     
    3.   位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:  
   
  struct   k  
  {  
   int   a:1  
   int   :2   /*该2位不能使用*/  
   int   b:3  
   int   c:2  
  };  
   
    从以上分析可以看出,位域在本质上就是一种结构类型,   不过其成员是按二进位分配的。  
   
    二、位域的使用  
   
    位域的使用和结构成员的使用相同,其一般形式为:   位域变量名·位域名   位域允许用各种格式输出。  
   
  main(){  
   struct   bs  
   {  
    unsigned   a:1;  
    unsigned   b:3;  
    unsigned   c:4;  
   }   bit,*pbit;  
   bit.a=1;  
   bit.b=7;  
   bit.c=15;  
   printf(”%d,%d,%d\n”,bit.a,bit.b,bit.c);  
   pbit=&bit;  
   pbit->a=0;  
   pbit->b&=3;  
   pbit->c|=1;  
   printf(”%d,%d,%d\n”,pbit->a,pbit->b,pbit->c);  
  }  
   
    上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示位域也是可以使用指针的。  
   
    程序的9、10、11三行分别给三个位域赋值。(   应注意赋值不能超过该位域的允许范围)程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值,赋为0。第15行使用了复合的位运算符”&=”,   该行相当于:   pbit->b=pbit->b&3位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为   3)。同样,程序第16行中使用了复合位运算”|=”,   相当于:   pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。

这也是在ChinaUnix上看了几篇关于C语言’位域(Bit   Fields)’的帖子之后,才想写下这篇文章的。其实在平时的工作中很少使用到’位域’,我是搞服务器端程序设计的,大容量的内存可以让我毫不犹豫的任意’挥霍’^_^。想必搞嵌入式编程的朋友们对位域的使用应该不陌生吧。这里我也仅仅是凭着对C语言钻研的兴趣来学习一下’位域’的相关知识的,可能有些说法没有实践,缺乏说服力。  
   
  具体也不是很清楚当年C语言的创造者为什么要加入位域这一语法支持,那是太遥远的事情了,我们不需要再回顾了,既然大师们为我们创造了它,我们使用便是了。  
   
  毋庸置疑,位域的引入给用户的最大的好处莫过于可以有效的利用’昂贵’的内存和操作bit的能力了。而且这种操作bit位的能力很是方便,利用结构体域名即可对这些bit进行操作。例如:  
   
  struct   foo   {  
    int   a   :   1;  
    int   b   :   2;  
    short   c   :   1;  
  };  
   
  struct   foo   aFoo;  
  aFoo.a   =   1;  
  aFoo.b   =   3;  
  aFoo.c   =   0;  
   
  通过结构体实例.域名即可修改某些bit得值,这些都是编译器的’甜头’。当然我们也可以自己通过一些’掩码’和移位操作来修改这些bit,当然如果不是十分需要,我们是不需要这么做的。  
   
  位域还提供一种叫’匿名’位域的语法,它常用来’填缺补漏’,由于是’匿名’,所以你不能像上面那样去访问它。如:  
  struct   foo1   {  
    int   a   :   1;  
    int       :   2;  
    short   c   :   1;  
  };  
  在foo1的成员a和c之间有一个2   bits的匿名位域。  
   
  在foo结构体的定义中,成员a虽然类型为int,但是它仅仅占据着4个字节中的一个bit的空间;类似b占据2个bit空间,但是b到底是占据第一个int的2个bit空间呢还是第二个int的2个bit空间呢?这里实际上也涉及到如何对齐带有’位域’的结构体这样一个问题。我们来分析一下。  
   
  我们再来看看下面两个结构体定义:  
  struct   foo2   {  
                  char         a   :   2;  
                  char         b   :   3;  
                  char         c   :   1;  
  };  
   
  struct   foo3   {  
                  char         a   :   2;  
                  char         b   :   3;  
                  char         c   :   7;  
  };  
  我们来打印一下这两个结构体的大小,我们得到的结果是:  
  sizeof(struct   foo2)   =   1  
  sizeof(struct   foo3)   =   2  
  显然都不是我们期望的,如果按照正常的内存对齐规则,这两个结构体大小均应该为3才对,那么问题出在哪了呢?首先通过这种现象我们可以肯定的是:带有’位域’的结构体并不是按照每个域对齐的,而是将一些位域成员’捆绑’在一起做对齐的。以foo2为例,这个结构体中所有的成员都是char型的,而且三个位域占用的总空间为6   bit   <   8   bit(1   byte),这时编译器会将这三个成员’捆绑’在一起做对齐,并且以最小空间作代价,这就是为什么我们得到sizeof(struct   foo2)   =   1这样的结果的原因了。再看看foo3这个结构体,同foo2一样,三个成员类型也都是char型,但是三个成员位域所占空间之和为9   bit   >   8   bit(1   byte),这里位域是不能跨越两个成员基本类型空间的,这时编译器将a和b两个成员’捆绑’按照char做对齐,而c单独拿出来以char类型做对齐,这样实际上在b和c之间出现了空隙,但这也是最节省空间的方法了。我们再看一种结构体定义:  
   
  struct   foo4   {  
                  char         a   :   2;  
                  char         b   :   3;  
                  int   c   :   1;  
  };  
   
  在foo4中虽然三个位域所占用空间之和为6   bit   <   8   bit(1   byte),但是由于char和int的对齐系数是不同的,是不能捆绑在一起,那是不是a、b捆绑在一起按照char对齐,c单独按照int对齐呢?我们打印一下sizeof(struct   foo4)发现结果为4,也就是说编译器把a、b、c一起捆绑起来并以int做对齐了。  
   
  通过上面的例子我们发现很难总结出很规律性的东西,但是带有’位域’的结构体的对齐有条原则可以遵循,那就是:”尽量减少结构体的占用空间”。当然显式的使用内存对齐的机会也并不多。^_^

内存对齐与ANSI C中struct型数据的内存布局

Saturday, September 8th, 2007

  当在C中定义了一个结构类型时,它的大小是否等于各字段(field)大小之和?编译器将如何在内存中放置这些字段?ANSI C对结构体的内存布局有什么要求?而我们的程序又能否依赖这种布局?这些问题或许对不少朋友来说还有点模糊,那么本文就试着探究它们背后的秘密。
  首先,至少有一点可以肯定,那就是ANSI C保证结构体中各字段在内存中出现的位置是随它们的声明顺序依次递增的,并且第一个字段的首地址等于整个结构体实例的首地址。比如有这样一个结构体:

  struct vector{int x,y,z;} s;
  int *p,*q,*r;
  struct vector *ps;
 
  p = &s.x;
  q = &s.y;
  r = &s.z;
  ps = &s;
  assert(p < q);
  assert(p < r);
  assert(q < r);
  assert((int*)ps == p);
 
// 上述断言一定不会失败
  这时,有朋友可能会问:”标准是否规定相邻字段在内存中也相邻?”。 唔,对不起,ANSI C没有做出保证,你的程序在任何时候都不应该依赖这个假设。那这是否意味着我们永远无法勾勒出一幅更清晰更精确的结构体内存布局图?哦,当然不是。不过先让我们从这个问题中暂时抽身,关注一下另一个重要问题————内存对齐。
  许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。不过Intel奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。Win32平台下的微软C编译器(cl.exe for 80×86)在默认情况下采用如下的对齐规则: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。Linux下的GCC奉行的是另外一套规则(在资料中查得,并未验证,如错误请指正):任何2字节大小(包括单字节吗?)的数据类型(比如short)的对齐模数是2,而其它所有超过2字节的数据类型(比如long,double)都以4为对齐模数。
  现在回到我们关心的struct上来。ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。嗯?填充区?对,这就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。那么结构体本身有什么对齐要求吗?有的,ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格(但此非强制要求,VC7.1就仅仅是让它们一样严格)。我们来看一个例子(以下所有试验的环境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1,内存对齐编译选项是”默认”,即不指定/Zp与/pack选项):

  typedef struct ms1
  {
     char a;
     int b;
  } MS1;
 

假设MS1按如下方式内存布局(本文所有示意图中的内存地址从左至右递增):
       _____________________________
       |       |                   |
       |   a   |        b          |
       |       |                   |
       +—————————+
 Bytes:    1             4


  因为MS1中有最强对齐要求的是b字段(int),所以根据编译器的对齐规则以及ANSI C标准,MS1对象的首地址一定是4(int类型的对齐模数)的倍数。那么上述内存布局中的b字段能满足int类型的对齐要求吗?嗯,当然不能。如果你是编译器,你会如何巧妙安排来满足CPU的癖好呢?呵呵,经过1毫秒的艰苦思考,你一定得出了如下的方案:

       _______________________________________
       |       |\\\\\\\\\\\|                 |
       |   a   |\\padding\\|       b         |
       |       |\\\\\\\\\\\|                 |
       +————————————-+
 Bytes:    1         3             4


  这个方案在a与b之间多分配了3个填充(padding)字节,这样当整个struct对象首地址满足4字节的对齐要求时,b字段也一定能满足int型的4字节对齐规定。那么sizeof(MS1)显然就应该是8,而b字段相对于结构体首地址的偏移就是4。非常好理解,对吗?现在我们把MS1中的字段交换一下顺序:

  typedef struct ms2
  {
     int a;
     char b;
  } MS2;
   

或许你认为MS2MS1的情况要简单,它的布局应该就是

        _______________________
       |             |       |
       |     a       |   b   |
       |             |       |
       +———————+
 Bytes:      4           1

因为MS2对象同样要满足4字节对齐规定,而此时a的地址与结构体的首地址相等,所以它一定也是4字节对齐。嗯,分析得有道理,可是却不全面。让我们来考虑一下定义一个MS2类型的数组会出现什么问题。C标准保证,任何类型(包括自定义结构类型)的数组所占空间的大小一定等于一个单独的该类型数据的大小乘以数组元素的个数。换句话说,数组各元素之间不会有空隙。按照上面的方案,一个MS2数组array的布局就是:

|<- array[1] ->|<- array[2] ->|<- array[3] …..
__________________________________________________________
|             |       |              |      |
|     a       |   b   |      a       |   b  |………….
|             |       |              |      |
+———————————————————-
Bytes:  4         1          4           1


  当数组首地址是4字节对齐时,array[1].a也是4字节对齐,可是array[2].a呢?array[3].a ….呢?可见这种方案在定义结构体数组时无法让数组中所有元素的字段都满足对齐规定,必须修改成如下形式:

       ___________________________________
       |             |       |\\\\\\\\\\\|
       |     a       |   b   |\\padding\\|
       |             |       |\\\\\\\\\\\|
       +———————————+
 Bytes:      4           1         3


  现在无论是定义一个单独的MS2变量还是MS2数组,均能保证所有元素的所有字段都满足对齐规定。那么sizeof(MS2)仍然是8,而a的偏移为0,b的偏移是4。
  好的,现在你已经掌握了结构体内存布局的基本准则,尝试分析一个稍微复杂点的类型吧。

  typedef struct ms3
  {
     char a;
     short b;
     double c;
  } MS3;
 

我想你一定能得出如下正确的布局图:
        
        padding 
           |
      _____v_________________________________
      |   |\|     |\\\\\\\\\|               |
      | a |\|  b  |\padding\|       c       |
      |   |\|     |\\\\\\\\\|               |
      +————————————-+
Bytes:  1  1   2       4            8

  sizeof(short)等于2,b字段应从偶数地址开始,所以a的后面填充一个字节,而sizeof(double)等于8,c字段要从8倍数地址开始,前面的a、b字段加上填充字节已经有4 bytes,所以b后面再填充4个字节就可以保证c字段的对齐要求了。sizeof(MS3)等于16,b的偏移是2,c的偏移是8。接着看看结构体中字段还是结构类型的情况:

  typedef struct ms4
  {
     char a;
     MS3 b;
  } MS4;


  MS3中内存要求最严格的字段是c,那么MS3类型数据的对齐模数就与double的一致(为8),a字段后面应填充7个字节,因此MS4的布局应该是:
       _______________________________________
       |       |\\\\\\\\\\\|                 |
       |   a   |\\padding\\|       b         |
       |       |\\\\\\\\\\\|                 |
       +————————————-+
 Bytes:    1         7             16


  显然,sizeof(MS4)等于24,b的偏移等于8。
  在实际开发中,我们可以通过指定/Zp编译选项来更改编译器的对齐规则。比如指定/Zpn(VC7.1中n可以是1、2、4、8、16)就是告诉编译器最大对齐模数是n。在这种情况下,所有小于等于n字节的基本数据类型的对齐规则与默认的一样,但是大于n个字节的数据类型的对齐模数被限制为n。事实上,VC7.1的默认对齐选项就相当于/Zp8。仔细看看MSDN对这个选项的描述,会发现它郑重告诫了程序员不要在MIPS和Alpha平台上用/Zp1和/Zp2选项,也不要在16位平台上指定/Zp4和/Zp8(想想为什么?)。改变编译器的对齐选项,对照程序运行结果重新分析上面4种结构体的内存布局将是一个很好的复习。
  到了这里,我们可以回答本文提出的最后一个问题了。结构体的内存布局依赖于CPU、操作系统、编译器及编译时的对齐选项,而你的程序可能需要运行在多种平台上,你的源代码可能要被不同的人用不同的编译器编译(试想你为别人提供一个开放源码的库),那么除非绝对必需,否则你的程序永远也不要依赖这些诡异的内存布局。顺便说一下,如果一个程序中的两个模块是用不同的对齐选项分别编译的,那么它很可能会产生一些非常微妙的错误。如果你的程序确实有很难理解的行为,不防仔细检查一下各个模块的编译选项。