C语言中结构体偏移及结构体成员变量访问方式的问题讨论

c语言结构体偏移
示例1

我们先来定义一下需求:

已知结构体类型定义如下:

struct node_t{
 char a;
 int b;
 int c;
};

且结构体1Byte对齐

#pragma pack(1)

求:

结构体struct node_t中成员变量c的偏移。

注:这里的偏移量指的是相对于结构体起始位置的偏移量。

看到这个问题的时候,我相信不同的人脑中浮现的解决方法可能会有所差异,下面我们分析以下几种可能的解法:

方法1

如果你对c语言的库函数比较熟悉的话,那么你第一个想到的肯定是offsetof函数(其实只是个宏而已,先姑且这样叫着吧),我们man 3 offsetof查看函数原型如下:

 #include <stddef.h>

  size_t offsetof(type, member);

有了上述的库函数,我们用一行代码就可以搞定:

offsetof(struct node_t, c);

当然这并非本文探讨的重点,请继续阅读。

方法2

当我们对c语言的库函数不熟悉的时候,此时也不要着急,我们依然可以使用我们自己的方法来解决问题。

最直接的思路是:【结构体成员变量c的地址】 减去 【结构体起始地址】

我们先来定义一个结构体变量node:

struct node_t node;

接着来计算成员变量c的偏移量:

(unsigned long)(&(node.c)) - (unsigned long)(&node)
&(node.c)为结构体成员变量c的地址,并强制转化为unsigned long;

&node为结构体的起始地址,也强制转化为unsigned long;

最后我们将上述两值相减,得到成员变量c的偏移量;

方法3

按照方法2的思路我们在不借助库函数的情况下,依然可以得到成员变量c的偏移量。但作为程序员,我们应该善于思考,是不是可以针对上面的代码做一些改进,使我们的代码变得更简洁一些?在做具体的改进之前,我们应该分析方法2存在哪些方面的问题。

相信不用我多说,细心的你一定已经察觉到,方法2中最主要的一个问题是我们自定义了一个结构体变量node,虽然题目中并未限制我们可以自定义变量,但当我们遇到比较严且题目中不允许自定义变量的时候,此时我们就要思考新的解决方法。

在探讨新的解决方法之前,我们先来探讨一个有关偏移的小问题:

小问题

这是一道简单的几何问题,假设在座标轴上由A点移动到B点,如何计算B相对于A的偏移?这个问题对于我们来说是非常的简单,可能大部分人都会脱口而出并得到答案为B-A。

那么这个答案是否完全准确呢?比较严谨的你觉得显然不是,原因在于,当A为坐标原点即A=0的时候,上述答案B-A就直接简化为B了。

这个小小的简单的问题,对于我们来说有什么启示呢?

我们结合方法2的思路和上述的小问题,是不是很快就得到了下面的关联:

(unsigned long)(&(node.c)) - (unsigned long)(&node)

B - A
我们小问题的思路是当A为坐标原点的时候,B-A就简化为B了,那么对应到我们的方法2,当node的内存地址为0即(&node==0)的时候,上面的代码可简化为:

(unsigned long)(&(node.c))
由于node内存地址==0了,所以

node.c  //结构体node中成员变量c

我们就可以使用另外一种方式来表达了,如下:

((struct node_t *)0)->c
上述代码应该比较好理解,由于我们知道结构体的内存地址编号为0,所以我们就可以直接通过内存地址的方式来访问该结构体的成员变量,相应的代码的含义就是 获取内存地址编号为0的结构体struct node_t的成员变量c。

此时,我们的偏移求法就消除了struct node_t node这个自定义变量,直接一行代码解决,:

(unsigned long)(&(((struct node_t *)0)->c))
上述的代码相对于方法2是不是更简洁了一些。

这里我们将上面的代码功能定义为一个宏,该宏的作用是用来计算某结构体内成员变量的偏移(后面的示例会使用该宏):

#define OFFSET_OF(type, member) (unsigned long)(&(((type *)0)->member))

使用上面的宏,就可以直接得到成员变量c在结构体struct node_t中的偏移为:

OFFSET_OF(struct node_t, c)

示例2

和示例1一样,我们先定义需求如下:

已知结构体类型定义如下:

struct node_t{
 char a;
 int b;
 int c;
};

int *p_c,该指针指向struct node_t x的成员变量c

结构体1Byte对齐

#pragma pack(1)

求:

结构体x的成员变量b的值?

拿到这个问题的时候,我们先做一下简单的分析,题目的意思是根据一个指向某结构体成员变量的指针,如何求该结构体的另外一个成员变量的值。

那么可能的几种解法有:

方法1

由于我们知道结构体是1Byte对齐的,所以这道题最简单的解法是:

*(int *)((unsigned long)p_c - sizeof(int))
上述代码很简单,成员变量c的地址减去sizeof(int)从而得到成员变量b的地址,然后再强制转换为int *,最后再取值最终得到成员变量b的值;

方法2

方法1的代码虽然简单,但扩展性不够好。我们希望通过p_c直接得到指向该结构体的指针p_node,然后通过p_node访问该结构体的任意成员变量了。

由此我们得到计算结构体起始地址p_node的思路为:

【成员变量c的地址p_c】减去【c在结构体中的偏移】

由示例1,我们得到结构体struct node_t中成员变量c的偏移为:

(unsigned long)&(((struct node_t *)0)->c)
所以我们得到结构体的起始地址指针p_node为:

(struct node_t *)((unsigned long)p_c - (unsigned long)(&((struct node_t *)0)->c))
我们也可以直接使用示例1中定义的OFFSET_OF宏,则上面的代码变为:

(struct node_t *)((unsigned long)p_c - OFFSET_OF(struct node_t, c))
最后我们就可以使用下面的代码来获取成员变量a,b的值:

p_node->a

p_node->b
我们同样将上述代码的功能定义为如下宏:

#define STRUCT_ENTRY(ptr, type, member) (type *)((unsigned long)(ptr)-OFFSET_OF(type, member))

该宏的功能是通过结构体任意成员变量的指针来获得指向该结构体的指针。

我们使用上面的宏来修改之前的代码如下:

STRUCT_ENTRY(p_c, struct node_t, c)

p_c为指向结构体struct node_t成员变量c的指针;

struct node_t结构体类型;

c为p_c指向的成员变量;

注:

上述示例中关于地址运算的一些说明:

int a = 10;
int * p_a = &a;

设p_a == 0x95734104;

以下为编译器计算的相关结果:

  • p_a + 10 == p_a + sizeof(int)*10 =0x95734104 + 4*10 = 0x95734144
  • (unsigned long)p_a + 10 == 0x95734104+10 = 0x95734114
  • (char *)p_a + 10 == 0x95734104 + sizeof(char)*10 = 0x95734114

从上述三种情况,相信你应该能体会到我所要表达的意思了。

结构体成员变量访问方式
访问结构体成员变量?如此简单的问题,有什么可以思考的呢?很纳闷也很奇怪。既然这样,那就带着这个奇怪的问题继续阅读吧。

示例3

我们的探讨还是从一个简单的示例开始:

已知结构体类型定义如下:

struct node_t {
 char a;
 int b;
 int c;
};

且结构体1Byte对齐:

#pragma pack(1)

接下来我们探讨几种访问该结构体成员变量c的方式:

情形1

如果程序中定义了一个struct node_t类型的变量node如下:

struct node_t node;

那么我们就可以直接通过下面的方式来访问成员变量c:

node.c

情形2

如果程序中定义了一个指向struct node_t类型的指针p_node如下:

struct node_t node;
struct node_t *p_node = &node;

或者在堆上分配了一块类型为struct node_t的内存如下:

struct node_t *p_node= (struct node_t *)malloc(sizeof(struct node_t));

那么我们就可以使用下面的方式来访问成员变量c:

p_node -> c;

情形3

上述两种访问方式都是比较常见的,也是大家所熟悉的,下面我们来探讨一种大家不是特别熟悉也不是很常见的情形:

如果程序中只给定了一个内存地址数值addr_node,且该地址addr_node起始的一段内存,指向一块类型为struct node_t的内存,addr_node声明如下:

unsigned long addr_node;

此时,我们如何根据这块内存地址来访问成员变量c呢?

由于我们知道了该结构体的起始地址addr_node,所以我们对其进行强制类型转换,从而得到一个指向该结构体的指针p_node:

struct node_t *p_node = (struct node_t *)addr_node;

接下来我们就可以通过情形2的方式来访问成员变量c了;

情形3要传达的意思是,我们可以通过一个具体的内存地址数值来访问我们的结构体成员变量;

关于情形3的一点说明

((struct node_t *)0)->c
我们通过内存地址0来访问结构体struct node_t成员变量c,但这里面有几点需要说明一下:

1. 我们并未对内存地址0做过任何内存相关操作,如解引用、赋值等,即内存地址编号0开始的一段内存无任何变化;

2. 我们只是利用了编译器的特性来帮助我们计算结构体的偏移,仅仅是利用了编译器的特性来计算而已;

3. 善于利用编译器的一些特性来优化我们的程序或系统;

结论

本文主要介绍了c语言中关于访问结构体成员变量的几种方式,并对通过内存地址数值直接访问结构体成员变量做了说明,解释了上篇博文中可能产生疑问的一个问题。

(0)

相关推荐

  • C语言中交换int型变量的值及转换为字符数组的方法

    不使用其他变量交换两个整型的值: #include <stdio.h> void main(){ int a = 3; int b = 4; a = a ^ b;//使用异或交换 b = b ^ a; a = a ^ b; printf("%d, %d\n", a, b); a = a - b;//使用加减交换 b = a + b; a = b - a; printf("%d, %d\n", a, b); a ^= b ^= a ^= b; printf

  • C语言变量类型的深入分析

    C语言是强类型语言,定义变量时必须声明变量的类型,赋值的时候也只能是同种类型变量赋值. 一.变量的类型告诉编译器怎么处理这个变量的数据. 虽然c语言是强类型语言,但是不同类型的变量通过类型转换也可以赋值,甚至指针变量可以转化为int类型,转化为char类型.从本质上来说,变量类型只是告诉编译器应该怎么处理这个变量,所以不同变量可以通过显示类型转换来赋值.理解这点对我们理解指针的转型非常重要.例如 int a = 10; int **ptr = &a; int b = (int)(*ptr); /

  • C语言基础知识变量的作用域和存储方式详细介绍

    变量的作用域和存储方式 1.简述变量按作用域的分类 变量按作用域分:分为全局变量和局部变量 全局变量:在所有函数外部定义的变量叫做全局变量 全局变量的使用范围:从定义位置开始到下面整个程序结束 局部变量:在一个函数内部定义的变量或者函数的形式参数统称为局部变量 局部变量的使用范围:在函数内部定义的变量只能在本函数内部进行使用 2.简述变量按存储方式的分类 静态变量 自动变量 寄存器变量[寄存器就是cpu内部可以存储数据的一些硬件东西] 3.简述全局变量和局部变量命名冲突的问题 1>在一个函数内部

  • C语言 指针变量作为函数参数详解

    在C语言中,函数的参数不仅可以是整数.小数.字符等具体的数据,还可以是指向它们的指针.用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁. 像数组.字符串.动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合. 有的时候,对于整数.小数.字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值. 有些初学者可能会使用

  • C语言中变量与其内存地址对应的入门知识简单讲解

    先来理解理解内存空间吧.请看下图: 如上图所示,内存只不过是一个存放数据的空间,就好像我的看电影时的电影院中的座位一样.电影院中的每个座位都要编号,而我们的内存要存放各种各样的数据,当然我们要知道我们的这些数据存放在什么位置吧.所以内存也要象座位一样进行编号了,这就是我们所说的内存编址.座位可以是遵循"一个座位对应一个号码"的原则,从"第1号"开始编号.而内存则是按一个字节接着一个字节的次序进行编址,如上图所示.每个字节都有个编号,我们称之为内存地址.好了,我说了这

  • 深入探讨C语言中局部变量与全局变量在内存中的存放位置

    C语言中局部变量和全局变量变量的存储类别(static,extern,auto,register) 1.局部变量和全局变量在讨论函数的形参变量时曾经提到,形参变量只在被调用期间才分配内存单元,调用结束立即释放.这一点表明形参变量只有在函数内才是有效的,离开该函数就不能再使用了.这种变量有效性的范围称变量的作用域.不仅对于形参变量,C语言中所有的量都有自己的作用域.变量说明的方式不同,其作用域也不同.C语言中的变量,按作用域范围可分为两种,即局部变量和全局变量.1.1局部变量局部变量也称为内部变量

  • C语言 常量,变量及数据详细介绍

    一.数据 图片文字等都是数据,在计算机中以0和1存储. (一)分类 数据分为静态数据和动态数据. ①. 静态数据:一些永久性的的数据,一般存储在硬盘中,只要硬盘没坏数据都是存在的.一般以文件的形式存储在硬盘上,电脑关机重启后依然存在. ②. 动态数据:程序运行过程中,动态产生的的临时数据,一般存储在内存中,内存的存储空间一般较小,计算机关闭后这些数据就会被清除.软件或者电脑关闭则这些临时数据会被清除. ③. 静态数据和动态数据可以转换. ④. 注意:为什么不把动态数据存放到硬盘?因为直接访问内存

  • C语言中结构体偏移及结构体成员变量访问方式的问题讨论

    c语言结构体偏移 示例1 我们先来定义一下需求: 已知结构体类型定义如下: struct node_t{ char a; int b; int c; }; 且结构体1Byte对齐 #pragma pack(1) 求: 结构体struct node_t中成员变量c的偏移. 注:这里的偏移量指的是相对于结构体起始位置的偏移量. 看到这个问题的时候,我相信不同的人脑中浮现的解决方法可能会有所差异,下面我们分析以下几种可能的解法: 方法1 如果你对c语言的库函数比较熟悉的话,那么你第一个想到的肯定是of

  • C语言中的自定义类型之结构体与枚举和联合详解

    目录 1.结构体 1.1结构的基础知识 1.2结构的声明 1.3特殊的声明 1.4结构的自引用 1.5结构体变量的定义和初始化 1.6结构体内存对齐 1.7修改默认对齐数 1.8结构体传参 2.位段 2.1什么是位段 2.2位段的内存分配 2.3位段的跨平台问题 2.4位段的应用 3.枚举 3.1枚举类型的定义 3.2枚举的优点 3.3枚举的使用 4.联合 4.1联合类型的定义 4.2联合的特点 4.3联合大小的计算 1.结构体 1.1结构的基础知识 结构是一些值的集合,这些值称为成员变量.结构

  • go语言中布隆过滤器低空间成本判断元素是否存在方式

    目录 简介 原理 数据结构 添加 判断存在 哈希函数 布隆过滤器大小.哈希函数数量.误判率 应用场景 数据库 黑名单 实现 数据结构 初始化 添加元素 判断元素是否存在 简介 布隆过滤器(BloomFilter)是一种用于判断元素是否存在的方式,它的空间成本非常小,速度也很快. 但是由于它是基于概率的,因此它存在一定的误判率,它的Contains()操作如果返回true只是表示元素可能存在集合内,返回false则表示元素一定不存在集合内.因此适合用于能够容忍一定误判元素存在集合内的场景,比如缓存

  • C语言中斐波那契数列的三种实现方式(递归、循环、矩阵)

    目录 一.递归 二.循环 三.矩阵 <剑指offer>里讲到了一种斐波那契数列的 O(logN) 时间复杂度的实现,觉得挺有意思的,三种方法都记录一下. 一.递归 一般来说递归实现的代码都要比循环要简洁,但是效率不高,比如递归计算斐波那契数列第n个元素. long long Fibonacci_Solution1(unsigned int n) { // printf("%d ", n); if (n <= 0) return 0; if (n == 1) retur

  • Java中成员方法与成员变量访问权限详解

    记得在一次面试的笔试题中,有的面试官会要求写出具体的像pullic这些访问限定符的作用域.其实,平常我都没去系统的考虑这些访问限定符的作用域,特别是包内包外的情况,OK,笔试不行了. 这是java基本的知识,也是公司看重的,那没办法啦,我的脑袋记不住东西,那我只能把这些东西写下来方便自己温故知新,不废话了,贴代码了. 代码如下: package com.jaovo; /** *_1_ 成员变量访问权限的求证 * public private protected default(默认的权限) *自

  • 解析C++中派生的概念以及派生类成员的访问属性

    C++继承与派生的概念.什么是继承和派生 在C++中可重用性是通过继承(inheritance)这一机制来实现的.因此,继承是C++的一个重要组成部分. 前面介绍了类,一个类中包含了若干数据成员和成员函数.在不同的类中,数据成员和成员函数是不相同的.但有时两个类的内容基本相同或有一部分相同,例如巳声明了学生基本数据的类Student: class Student { public: void display( ) //对成员函数display的定义 { cout<<"num: &qu

  • 详解C语言的结构体中成员变量偏移问题

    c语言中关于结构体的位置偏移原则简单,但经常忘记,做点笔记以是个记忆的好办法 原则有三个: a.结构体中的所有成员其首地址偏移量必须为器数据类型长度的整数被,其中第一个成员的首地址偏移量为0, 例如,若第二个成员类型为int,则其首地址偏移量必须为4的倍数,否则就要"首部填充":以此类推 b.结构体所占的总字节数即sizeof()函数返回的值必须是最大成员的长度的整数倍,否则要进行"末尾填充": c.若结构体A将结构体B作为其成员,则结构体B存储的首地址的偏移量必须

  • 详解Go语言中数组,切片和映射的使用

    目录 1.Arrays (数组) 2.切片 2.1 make创建切片 3.映射Map Arrays (数组), Slices (切片) 和 Maps (映射) 是常见的一类数据结构 1.Arrays (数组) 数组是定长的. 长度不可改变. 初始化 package main import ( "fmt" ) func main() { var scores [10]int scores[0] = 99 fmt.Printf("scoers:%d\n", scores

  • 浅谈Go语言中的结构体struct & 接口Interface & 反射

    结构体struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套: go中的struct类型理解为类,可以定义方法,和函数定义有些许区别: struct类型是值类型. struct定义 type User struct { Name string Age int32 mess string } var user User var user1 *User = &User{} var user2 *User = new(User) struct使用 下面示例中user1和

  • 详解Swift语言中的类与结构体

    类 在 Swift 中类是建立灵活的构建块.类似于常量,变量和函数,用户可以定义的类的属性和方法.Swift给我们提供了声明类,而无需用户创建接口和实现文件的功能.Swift 允许我们创建类作为单个文件和外部接口,将默认在类一次初始化来创建. 使用类的好处: 继承获得一个类的属性到其他类 类型转换使用户能够在运行时检查类的类型 初始化器需要处理释放内存资源 引用计数允许类实例有一个以上的参考 类和结构的共同特征: 属性被定义为存储值 下标被定义为提供访问值 方法被初始化来改善功能 初始状态是由初

随机推荐