文档章节

关于c语言结构体偏移的一点思考

算法与编程之美
 算法与编程之美
发布于 2013/06/25 22:08
字数 2367
阅读 7155
收藏 158
点赞 7
评论 23

前言

相信大家在c语言程序开发的过程一定都使用过结构体,那么不知你对结构体中成员变量偏移这块是如何理解的?本文将和大家一起分享下,本人最近关于c语言中结构体偏移的一些思考和总结。

另外这篇博文还可以帮你更好的理解这个问题c语言中两种宏定义的区别,关于这个思考有哪些方面的意义,细心的你可能发现本文所属的类别为linux内核设计与实现,而并非 GNU 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

注:此处只是利用了编译器的特性来计算结构体偏移,并未对内存地址0有任何操作,有些同学对此可能还有些疑问,详细的了解该问题可参考关于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

从上述三种情况,相信你应该能体会到我所要表达的意思了。(注:后续某博文将从编译器的角度对该问题进行详细的阐述)

结论

本文通过几个示例描述了c语言结构体有关偏移的一些有意思的事情,希望能够对你有所帮助。为什么会有上述思考,相信有些同学已经看出一些端倪,这也正是后续博文将要描述的主题。

如文中有错误之处,欢迎指出。

引用

[1] http://isis.poly.edu/kulesh/stuff/src/klist/

[2] https://www.kernel.org/doc/Documentation/CodingStyle

[3] http://www.kroah.com/log/linux/container_of.html

如果您对算法或编程感兴趣,欢迎扫描下方二维码并关注公众号“算法与编程之美”,和您一起探索算法和编程的神秘之处,给您不一样的解题分析思路。

© 著作权归作者所有

共有 人打赏支持
算法与编程之美
粉丝 281
博文 80
码字总数 92711
作品 0
成都
程序员
加载中

评论(23)

jude
jude
挺有用的
我土鳖

引用来自“Michael_YY”的评论

楼主去看看linux kernel的 通用链表 的实现源代码,利用一组宏定义,基于C语言结构体成员可以通过偏移来定位的特性,其核心思想就是把链表嵌入结构体,使得关于此链表的功能函数(例如:反转、增、删、查等等)能够达到“通用”的目的!

此技术的核心就在于offsetof宏和container_of宏。
算法与编程之美
算法与编程之美

引用来自“zhcosin”的评论

((struct node_t *)0)->c,
博主你确定这句代码能通过编译?如果我没记错,操作系统禁止对地址为零的内存地址进行解引用操作。

http://my.oschina.net/gschen/blog/140832可参考该博文,里面做了一些解释,谢谢。
算法与编程之美
算法与编程之美

引用来自“陈名才”的评论

这主要由于内存地址对齐的原因所致。

”这“指代的是?
算法与编程之美
算法与编程之美

引用来自“我土鳖”的评论

引用来自“Tocy”的评论

引用来自“justin_cn”的评论

引用来自“Tocy”的评论

这个位处理比较麻烦啊。

你说的位处理的意思是?

C语言中位域,尤其是扯上编译器和操作系统的时候。

同意,之前刚中过招

本文的示例并未使用位域,示例只是阐述了结构体内位移的一种计算方式,示例的意义是为后续博文做铺垫。
算法与编程之美
算法与编程之美

引用来自“zhcosin”的评论

((struct node_t *)0)->c,
博主你确定这句代码能通过编译?如果我没记错,操作系统禁止对地址为零的内存地址进行解引用操作。

上述代码并未对地址0进行解引用操作,只是利用了编译器的特性,来计算结构体内成员变量偏移而已,对内存无任何操作。
我土鳖

引用来自“Tocy”的评论

引用来自“justin_cn”的评论

引用来自“Tocy”的评论

这个位处理比较麻烦啊。

你说的位处理的意思是?

C语言中位域,尤其是扯上编译器和操作系统的时候。

同意,之前刚中过招
zhcosin
zhcosin
((struct node_t *)0)->c,
博主你确定这句代码能通过编译?如果我没记错,操作系统禁止对地址为零的内存地址进行解引用操作。
Tocy
Tocy

引用来自“justin_cn”的评论

引用来自“Tocy”的评论

这个位处理比较麻烦啊。

你说的位处理的意思是?

C语言中位域,尤其是扯上编译器和操作系统的时候。
H
HSHeroes
这主要由于内存地址对齐的原因所致。
关于c语言结构体成员变量访问方式的一点思考

前言 上篇博文(关于c语言结构体偏移的一点思考)对c语言中结构体偏移做了一些思考,发现博文中还有一些小的问题,没有描述的足够清楚,所以才萌生了本篇博文的想法。 为什么不直接将本篇博文作...

算法与编程之美 ⋅ 2013/06/27 ⋅ 11

深入理解C语言结构体成员变量内存分配

欢迎点击「算法与编程之美」↑关注我们! 本文首发于微信公众号:"算法与编程之美",欢迎关注,及时了解更多此系列博客。 1 问题描述 在学习C语言的时候,我们都会频繁的接触到结构体,使用结...

算法与编程之美 ⋅ 04/05 ⋅ 0

C语言之offsetof宏和container_of宏

首先我们要明白一点通过结构体变量来访问结构体中的各个元素时,其本质上是 通过指针的方式来实现访问的,只不过是这个时候编译器帮自动帮我们计算了每个 元素与结构体起始地址之间的偏移量而...

科技小能手 ⋅ 2017/11/12 ⋅ 0

C语言/C加加程序员新手入门学习基础之数据类型分享

请点击此处输入图片描述 关注我们 为什么要首先介绍数据类型? 因为(数据结构就是对数据类型的操作)不论是哪一种语言,都要有其基本数据类型,这些基本的数据类型就像一块块砖,而程序中的...

小辰GG ⋅ 2017/12/30 ⋅ 0

关于c语言内存地址对齐的一点思考

前言 相信大家对内存对齐这个概念一定都比较熟悉,本文将介绍,如何利用内存对齐这一特性来做一些有意思的探索。 至于为什么要使用内存对齐,这是一个比较复杂的问题,简单来说就是提高cpu a...

算法与编程之美 ⋅ 2013/10/09 ⋅ 40

cgo的一些经验

cgo可以在go语言中夹杂着C函数或数据,在使用cgo时,有一些需要注意的: 1、go中的int/int32/int64/uint32/uint64和C语言中的int/int32等是不同的,因此,C语言的函数的参数不能是go语言的i...

徐学良 ⋅ 2015/12/25 ⋅ 0

关于面向过程编程的一些思考

关于面向过程编程与指针的一些思考 长期以来,虽然有C语言的基础,但对C的使用主要停留在书面例子的基础上,没有复杂的功能和调用关系,因此,对面向过程的理解并不深刻。而使用java、js编写...

AlexTuan ⋅ 2016/12/07 ⋅ 0

你为什么看不懂Linux内核驱动源码?

学习嵌入式Linux驱动开发,最核心的技能就是能够编写Linux内核驱动、深入理解Linux内核。而做到这一步的基础,就是你要看得懂Linux内核源码,了解其基本的框架和具体实现,了解其内核API的使...

宅学部落 ⋅ 04/28 ⋅ 0

c内存对齐--影响php变量占用的内存

当在C中定义了一个结构类型时,它的大小是否等于各字段(field)大小之和?编译器将如何在内存中放置这些字段?ANSI C对结构体的内存布局有什么要求?而我们的程序又能否依赖这种布局?这些问题...

clearchen ⋅ 2012/08/05 ⋅ 0

小蚂蚁学习C语言(1)——C语言概述

C语言概述 1. 为什么学习C语言 C的起源和发展 1 第一代语言:机器语言 0101 2 第二代语言:汇编语言 add 1,2 3 第三代高级语言: C语言产生和发展过程: 产生时间:1972-1973 产生地点:美国...

嗜学如命的小蚂蚁 ⋅ 2015/11/26 ⋅ 4

没有更多内容

加载失败,请刷新页面

加载更多

下一页

MySQL 数据库设计总结

规则1:一般情况可以选择MyISAM存储引擎,如果需要事务支持必须使用InnoDB存储引擎。 注意:MyISAM存储引擎 B-tree索引有一个很大的限制:参与一个索引的所有字段的长度之和不能超过1000字节...

OSC_cnhwTY ⋅ 今天 ⋅ 0

多线程(四)

线程池和Exector框架 什么是线程池? 降低资源的消耗 提高响应速度,任务:T1创建线程时间,T2任务执行时间,T3线程销毁时间,线程池没有或者减少T1和T3 提高线程的可管理性。 线程池要做些什...

这很耳东先生 ⋅ 今天 ⋅ 0

使用SpringMVC的@Validated注解验证

1、SpringMVC验证@Validated的使用 第一步:编写国际化消息资源文件 编写国际化消息资源ValidatedMessage.properties文件主要是用来显示错误的消息定制 [java] view plain copy edit.userna...

瑟青豆 ⋅ 今天 ⋅ 0

19.压缩工具gzip bzip2 xz

6月22日任务 6.1 压缩打包介绍 6.2 gzip压缩工具 6.3 bzip2压缩工具 6.4 xz压缩工具 6.1 压缩打包介绍: linux中常见的一些压缩文件 .zip .gz .bz2 .xz .tar .gz .tar .bz2 .tar.xz 建立一些文...

王鑫linux ⋅ 今天 ⋅ 0

6. Shell 函数 和 定向输出

Shell 常用函数 简洁:目前没怎么在Shell 脚本中使用过函数,哈哈,不过,以后可能会用。就像java8的函数式编程,以后获取会用吧,行吧,那咱们简单的看一下具体的使用 Shell函数格式 linux ...

AHUSKY ⋅ 今天 ⋅ 0

单片机软件定时器

之前写了一个软件定时器,发现不够优化,和友好,现在重写了 soft_timer.h #ifndef _SOFT_TIMER_H_#define _SOFT_TIMER_H_#include "sys.h"typedef void (*timer_callback_function)(vo...

猎人嘻嘻哈哈的 ⋅ 今天 ⋅ 0

好的资料搜说引擎

鸠摩搜书 简介:鸠摩搜书是一个电子书搜索引擎。它汇集了多个网盘和电子书平台的资源,真所谓大而全。而且它还支持筛选txt,pdf,mobi,epub、azw3格式文件。还显示来自不同网站的资源。对了,...

乔三爷 ⋅ 今天 ⋅ 0

Debian下安装PostgreSQL的表分区插件pg_pathman

先安装基础的编译环境 apt-get install build-essential libssl1.0-dev libkrb5-dev 将pg的bin目录加入环境变量,主要是要使用 pg_config export PATH=$PATH:/usr/lib/postgresql/10/bin 进......

玛雅牛 ⋅ 今天 ⋅ 0

inno安装

#define MyAppName "HoldChipEngin" #define MyAppVersion "1.0" #define MyAppPublisher "Hold Chip, Inc." #define MyAppURL "http://www.holdchip.com/" #define MyAppExeName "HoldChipE......

backtrackx ⋅ 今天 ⋅ 0

Linux(CentOS)下配置php运行环境及nginx解析php

【part1:搭建php环境】 1.选在自己需要安装的安装包版本,wget命令下载到服务器响应目录 http://php.net/releases/ 2.解压安装包 tar zxf php-x.x.x 3.cd到解压目录执行如下操作 cd ../php-...

硅谷课堂 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部