指针篇之六 指针与结构体,小心刻舟求剑

来源:互联网 发布:我的世界mac怎么装mod 编辑:程序博客网 时间:2024/06/02 23:30

    指针与结构没有指针与数组的关系那么复杂,却也有一些需要注意的地方。例如:

    typedef struct

    {

      int a;

      int b;

      int c;

    } MyStruct;

    MyStruct ss={20,30,40};  //声明对象ss,三个成员初始化

    MyStruct *ptr=&ss;          //声明指向对象ss的指针。指针指向的类型是MyStruct

    int *pint=(int*)&ss;           //声明指向对象ss的另一指针。指向的类型和ptr不同

    怎样通过指针ptr访问ss的成员变量?答:ptr->a; ptr->b;ptr->c;  那是否可通过指针pint直接访问ss的成员变量呢?有人会这样实现:

    * pint        //访问成员a

    *(pint +1);     //访问成员b

    *(pint +2) //访问成员c

    这里通过手动偏移指针访问结构体成员,效果和通过结构体指针访问成员变量基本等价。结构体本质就是程序员和C语言约定一段内存空间的长短,以及其中内存元素排列的安排。但通过指针偏移访问struct成员既不正规又有隐患,是一种绕过编译器的“硬编译”:结构体中成员位置或大小必须一成不变,一旦改变就与硬编码的地址增量不匹配,比如在int a前加个int d,那后面所有成员的偏移都会发生变化,这时还以为*pint是访问a,*(pint+1)访问b么?那就真应了一句成语——刻舟求剑。

    即使成员变量大小位置永远不改动,这种方式是否就没问题呢?还是不行!因为还有内存对齐的问题。在内存中,编译器按成员列表顺序分别为每个struct成员变量分配内存,如果系统有内存边界对齐的要求,编译器会自动在不满足对齐要求的成员前添加若干填充字节,这导致成员变量间可能有多的空白字节。如:

    typedef struct

    {

      char a;

      char b;

      int c;

    } MyStruct;

    MyStruct ss={0x20, 0x30 0x40};

    char *pint=&ss

    这时*pint*(pint+1)可以访问ss的成员变量ab,但*( pint +2)是否定能访问到结构成员c呢?不一定!因为cint型,bc之间可能有编译器插入的填充字节,以保证c的字节对齐(见内存对齐),此时*( pint +2)访问的可能是填充字节(如下图,假设pint=0x1000

 

    这也再一次说明了指针灵活与危险并存。所以指针访问结构体成员还是老老实实用”->”号吧,即由编译器根据结构体定义确定成员变量的偏移,以隔绝不同编译器之间内存对齐的差别。但如此也并非万无一失,以下情况仍要小心

    1)如果存在不一致的结构体定义呢?比如给客户开发功能库,API接口参数里包含一个结构体指针,如果某次更新库时修改了API头文件里这个结构体的定义,并重编译了库,但发给客户时只提供了更新库,而忘记提供修改过的头文件,会发生什么?

    2第三方开发者还可能使用不同的编译器默认对齐选项,使得结构体在发行库中与外部调用者那里使用不同对齐方式,这也会导致同样问题。

    3)手动偏移访问结构体成员变量在C语言中很少见,没人会抛开”->”而费力不讨好,但如果要手工编写汇编函数且与C接口存在结构体参数传递,这个问题就无法回避:汇编里访问结构体成员,只能在汇编指令里硬性指定偏移量。这时就要调整汇编中的偏移量值,使其和C编译器的结果保持吻合,否则C和汇编的混合调用就会发生函数参数传递错误。而此时在C里修改这种被汇编和C共享的结构体定义,是牵一发动全身,要特别小心。

 

原创粉丝点击