C语言精华记录——陆(结构、联合、位段、位级操作)

来源:互联网 发布:mac os get ipaddress 编辑:程序博客网 时间:2024/06/06 01:55

结构

数据对齐:

许多计算机系统对基本数据类型的合法地址做出了一些限制要求某种类型对象的地址必须是某个值K(通常是2、4、8)的倍数,这种对齐限制简化了 处理器和存储系统之间接口的硬件设计。

(因为:如果处理器经常从内存中取出8字节,若内存中一个存储器块单位是8个字节。则我们保证将所有double类型数据的地址对齐成8的倍数,那么就可以用一个存储器操作来读写值了! 否则,对象可能被分在两个存储器块中,我们就要执行两次存储器访问!)

保持数据对齐能够提高效率。

 

数据对齐 是与操作系统和硬件相关的。

在IA32平台下,Linux的对齐规则是:2字节数据类型(例如short)的地址必须是2的倍数,而其它数据类型的地址必须是4的倍数。

WINDOWS对齐的要求更严格——任何n字节基本数据对象的地址 都必须是n的倍数,n=2,4,8。  这种要求提高了存储器性能,而代价是浪费了一些空间。

struct S1{    int  i ;     char c ;     int  j ;} ; 

//编译器可能需要在字段的分配中插入间隙,以保证每个结构元素都满足它的对齐要求,而结构本身对它的起始地址也有一些对齐要求。

(故为了满足字段i和j的4字节对齐要求,编译器在字段c和j之间插入一个3字节的间隙[可称之为:内存空洞] 结构的)

此外也要考虑结构体地址的对齐。

故结构体的数据对齐要考虑每个成员的对齐要求还要考虑整个结构体的对齐要求。

★但可归结为一个准则:以结构体中最大数据类型元素的对齐为标准。

结构的总大小要满足最大元素 对齐的倍数

struct S2{     doubled ;       int  i ;       int  j ;       char c ;} ;

//我们以成员中最大数据类型为标准来对齐,由此来计算整个结构的大小。最大对齐规则都满足了,小的对齐规则就好满足了。

此例中:d 占8个字节,i和j分别占4个字节,而c及尾部空洞共占8个字节。

因为:如此时c和上面一样还和空洞共占4字节的话,结构的总大小就不满足最大元素对齐的倍数了。这样若有结构体数组,则下个结构体的首位置就不会对齐了。

(可以这样思考:以最大类型元素的对齐大小为单位 分割结构的存储空间这样最后总的存储空间就是此单位的倍数了。)

 

★【按照 数据对齐严格程度的大小有序排列结构中的成员,可最大限度地减少因边界对齐带来的损失

 

如果你必须确定结构中某个成员的实际位置,可以使用offsetof宏(定义于stddef.h)

offsetof(结构体类型名,成员名) 其返回指定成员据结构体首元素地址的字节数。

struct S1{     char   c ;      double d ;} ;  // offsetof(struct S1, d) ;表达式值为8

 

联合

联合提供了一种方式,能够规避C语言的类型系统,允许以多种类型来引用一个对象

一个联合的总大小等于它最大字段的大小。联合是用不同的字段来引用相同的存储器块。

在某些情况下,联合十分用,但若使用不当,容易引发错误,因为它们绕过了C语言类型系统提供的安全措施。

 

联合可以被初始化,但这个值必须是联合的第1个成员类型。

例: union { inta ; char c[4]; } x = {5} ;

 

应用:

1,我们事先知道一个结构中的两个不同字段是互斥的那么我们将两个字段声明为联合的一部分,以减少存储空间。(对于有较多类似情况的结构,会节省很多空间)

此时,我们一般引入一个枚举类型,来标记这个联合中当前的选择,然后再创建一个结构,包含一个标签字段和这个联合。

typedef enum { N_LEAF, N_INTERNAL } nodetype_t ;//定义枚举struct NODE_T{    nodetype_t  type ; //可标记此结点是叶结点还是内部结点     union      //联合     {     struct{ struct NODE_T * left ; structNODE_T * right } internal ;           double data ;     } info ;}  ;

2,★可以用来访问不同数据类型的位模式

//下面的代码返回的是:float作为unsigned的位表示unsigned float2bit ( float f ){   union    {    float f ;         unsigned  u ;    }tmp ;    tmp.f= f ;    return  tmp.u ;}

//以上,我们以一种数据类型来存储联合中的参数,又以另一种数据类型来访问它。(不同于强制类型转换)

(实际对应的机器码为movl 8(%ebp), %eax

 这就说明了 机器码中没有类型信息,无论参数是float还是unsigned,它们都在相对%ebp偏移量为8的地方,程序只是简单地将它们的值复制到返回值,不修改任何位[而强制类型转换 涉及浮点值的转换时 会修改位])

3,可以深入到 数据内部的字节级来访问

union{   double d ;    unsignedu[2] ;} tmp ;tmp.u[0] = x1 ;tmp.u[1] = x2 ; //分别访问了double型的数据d的低4字节与高4字节。(注意在大端法机和小端法机上的结果相反)

【注意:如果联合中成员的长度相差悬殊,当存储较短成员时会浪费很多空间。

好的方法是:

在联合中存储指向不同成员的指针,而不是存储成员本身,当需要哪个成员时,动态分配空间】

 

位段

C允许程序员整数成员包装到比编译器正常所允许的更小的空间中。这种整数成分成为位段,是通过在结构体的成员声明中使用一个冒号和一个常量表达式(指定了位段所占据的位数)指定的。

(位段就是允许我们自己定义某个整型的数据占据多少位,而对位的解释还是与其数据类型一样)

ANSI标准C允许位段为unsigned int , signed int , int类型,它们分别称为:无符号位段、有符号位段、普通位段(有可能是有符号或无符号的)。

有些编译器允许使用任何整数类型的位段,包括char型。

编译器可以选择地对位段的最大长度加以限制,并指定位段无法跨越的地址边界,当一个字段将跨越一个字的边界时,它可能会移动到下一个字。

【注意:取地址操作符&无法用于位段成员,因为计算机无法对任意长度的字段编址】

关于填充

struct s{     unsigned a :4 ;      unsigned   :2 ;      unsigned b :6 ; }  ; //此位段结构共占4字节。

结构中也可以包含无名位段。作为相邻成员之间的填充。

★注意:编译器是以字(int型大小)为单位,向位段结构分配存储空间的!一次分配一个字的空间(32位机就是32个位),而不是根据定义的位的数量来分配(这是为了数据对齐)。若一个位段结构的总定义空间大于一个字,则再分配一个字。

(如此看来,使用位段不一定能节省空间,有可能还浪费空间。只是它可以对一个字的内部位方便地访问,使具名访问深入到了位级!

 

当然:若是定义char型的位段,则编译器是以字节(char型大小)为单位,来分配空间的!

(这样,我们可以根据需要选用两种不同大小规格的位段结构)

 

★为一个无名字段指定0长度 具有特殊含义,它表示不应该有其它位段被包装到前一个位段所在的区域。如果有,它就被放置。

struct s{   unsigned a :4 ;    unsigned   :0 ;    unsigned b :6 ;  }  ;

【0长度字段就相当于是一个分隔线,把其前和其后的字段分隔到不同的字单位中】

故:上面的unsigneda :4 与后面的空洞合占一个字的空间;unsigned b :6与后面的空洞合占一个字的空间。unsigned a :4,unsigned b :6两个成员分属不同的字空间。整个位段结构占2个字即8字节空间。

移植性问题

使用位段的程序是无法移植的。因为其在不同计算机平台上的实现结果可能不同,依赖于硬件(大端小端,字长)和编译器。

应用:

用于一个结构体数组,由于数组太大,所以要求结构体的成员必须紧密包装,以节省内存。

(只有在内存空间问题上有严格要求的程序 才使用位段。 一般情况不用!)

 

位级操作

位段也是一种位级操作的方式,但位段不可移植(缺点),不过其能通过变量名来访问位,使操作上更简便,程序更清晰。(优点)

我们通过掩码和移位操作可完全取代位段的功能,且具有移植性。只是操作不及位段简单。

(这两种方法都可在位级操作,我们可根据情况,在程序清晰性和可移植性间做出考量)

 

位运算符:按位取反~ 、左移<< 、右移>> 、与运算&(逻辑乘) 、或运算|(逻辑加)、异或运算^(按位加)

注意:移位操作符的优先级问题

例t =a<<2 + a; 对于a<<2这个位操作,优先级要比加法要低,所以这个表达式就成了“t = a<< (2+a)”,而我们实际的意图可能是t= (a<<2) + a;

【涉及移位操作的表达式要警惕其优先级问题!】

常用的位操作

1、取出(检测)某位的值

value & 掩码        (结果为0则某位为0,结果非0则某位为1)

2、设置某位的值

①把某位置1

value |= 掩码

②把某位置0

value &= ~掩码

【注意:以上掩码均意为——把要设置的位置1,其余位置0】

以上两类的操作(一类可对任意位访问,一类可对任意位赋值)组合起来可实现所有的位操作!

掩码的生成

1,可用左移操作(<<)生成所需要的掩码

例:value  &  ( 1<<n )

        value  |=  ( 1<<n )

value  &= ~(1<<n )

2,预先算出包含所有掩码的数组(用于频繁的位级操作)

例:unsigned intmasks[ ] = { 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80 } ;//从0号位到7号位所有的掩码。

value & masks[n] ;

 

【应用联合 可用unsigned的方式访问任意数据类型,再用位级操作,两者结合起来可以访问任意数据内部的任意位,无论它是什么数据类型!如此一来,数据的底层细节可暴露无遗,高级语言对细节的封装和隐蔽亦可暴露。 这就使C具备了汇编的某些特性。】

 


 

原创粉丝点击