0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

周立功教你学程序设计结构体:内存对齐和基本数据类型

AGk5_ZLG_zhiyua 来源:未知 作者:电子大兵 2017-09-01 13:47 次阅读

第二章为程序设计技术,本文为2.2.1 内存对齐和2.2.2 基本数据类型

我们知道,数组和指针是相同类型有序数据的集合,但很多时候需要将不同类型的数据捆绑在一起作为一个整体来对待,使程序设计更方便。在C语言中,这样的一组数据被称为结构体。

>>>2.2.1内存对齐

虽然所有的变量最后都会保存到特定地址的内存中,但相应的内存空间必须满足内存对齐的要求。主要出于两个方面的原因:

  • 平台原因:不是所有的硬件平台(特别是嵌入式系统中使用的低端微处理器)都能访问任意地址上的任意数据,某些硬件平台只能访问对齐的地址,否则会出现硬件异常。

  • 性能原因:如果数据存放在未对齐的内存空间中,则处理器访问变量时需要做两次内存访问,而对齐的内存访问仅需要一次访问。

在32位微处理器中,处理器访问内存都是按照32位进行的,即一次读取或写入都是4个字节,比如,地址0x0 ~ 0xF这16字节的内存,对于微处理器来说,不是将其看作16个单一字节,而是4个块,每块4个字节,详见图2.4。

图2.4 内存空间示意图

显然,只能从0x0、0x4、0x8、0xC等地址为4的整数倍的内存中一次取出4个字节,并不能从任意地址开始一次读取4个字节。假定将一个占用4字节的int类型数据存放到地址0开始的4字节内存中,其示意图详见图2.5。

图2.5 按内存对齐的方式存储int数据

由于int类型数据存放在块0中,因此CPU仅需一次内存访问即可完成对该数据的读取或写入。反之,如果将该int类型数据存放在地址1开始的4字节内存空间中,其示意图详见图2.6。

图2.6 按内存未对齐的方式存储int数据

此时,数据存放在块0和块1两个块中,若要完成对该数据的访问,必须经过两次内存访问,先通过访问块0得到该数据的3个字节,再通过访问块1得到该数据的1个字节,最后通过运算,将这几个字节合并为一个完整的int型数据。由此可见,若数据存储在未对齐的内存空间中,将大大降低CPU的效率。但在某些特定的微处理器中,它根本不愿意干这种事情,这种情况下,就出现系统异常,直接崩溃了。内存对齐的具体规则如下:

(1)结构体各个成员变量的内存空间的首地址必须是“对齐系数”和“变量实际长度”中较小者的整数倍。假设要求变量的内存空间按照4字节对齐,则内存空间的首地址必须是4的整数倍,满足条件的地址有0x0、0x4、0x8、0xC……

(2)对于结构体,在其各个数据成员都完成对齐后,结构体本身也需要对齐,即结构体占用的总大小应该为“对齐系数”和“最大数据成员长度” 中较小值的整数倍。

一般来说,对齐系数与微处理器的字长相同,比如,32位微处理器的对齐系数是4字节,变量的实际长度与其类型相关,计算类型长度的方法如下:

该程序的输出为:1、4、4、4、8。假定CPU为32位微处理器,对齐系数为4,结构体变量data的定义如下:

结构体的各个成员都是从结构体首地址(其由编译器保证必然满足内存对齐的要求,假定为0)开始计算,按照定义的顺序依次存放各个成员,详见表2.1。

表2.1依次存放各个成员

实际存放位置使用[x,y]表示,x表示起始地址,y表示结束地址。如果x与y相等,则直接使用[x]表示。以成员b为例,其长度为2,小于对齐系数,因此按照2字节对齐,就要求其地址必须是2的倍数,地址0已经被成员a占用,则只能使用满足要求的邻近的内存空间[2,3]存放成员b。而空间[1]由于不满足存放成员b的要求,则只能被弃用。特别地,对于数组成员c,存放时不能将其看作一个整体,即长度为2的成员,应该分别看作两个成员c[0]和c[1]。由此可见,实际存放位置为[0,24],1、6、7、17、18、19部分内存空间被弃用。

当所有成员存放完毕后,则结构体本身也需要对齐,即结构体的大小也应该为对齐字节数的整数倍,对齐字节数取长度最长的成员和“对齐系数”的较小值。在这里,其长度最长的成员为double类型的成员d,其长度为8,大于对齐系数,因此结构体本身也要按照4字节对齐,其占用的空间大小必须是4的整数倍。虽然当前存放位置为[0,24],只占用了25个字节。由于必须满足4的整数倍,因此实际上结构体占用的空间是28个字节,即[0,27]。验证结构体占用空间大小的方法如下:

虽然所有成员的总长度为19个字节,但结构体实际占用了28个字节,多余的9个字节空间为内存对齐弃用的空间,即1、6、7、17、18、19、25、26、27,分为4个段:[1],[6,7],[17,19],[25,27]。查看表2.1可知,这些浪费空间的前面,存放的都是char型数据,由于char型数据只占用一个字节,往往使得其紧接着的空间不能被其它长度更长的数据使用。

为了降低内存浪费的概率,应该在char型数据之后,存放长度最小的成员。即在定义结构体时,应按照长度递增的顺序依次定义各个成员。优化示例结构体的定义如下:

类似地,依次存放各个成员,详见表2.2。

表2.2依次存放各个成员

所有成员实际存放位置为[0,19],中间的地址为5的内存空间被弃用。由于结构体占用的大小为20个字节,已经是4的整数倍,因此无需再做额外的处理。结构体只浪费了1个字节空间,使用率达到95%。显然,通过优化结构体成员的定义顺序,在同样满足内存对齐的要求下,可以大大地减少内存的浪费。

>>>2.2.2基本数据类型

1.范围值校验

如果有min≤value≤max,则check()范围值校验函数需要3个int型参数value、min和max。如果value合法,则返回true,否则返回false,详见程序清单2.10。

程序清单 2.10 rangeCheck()范围值校验函数的实现(1)

  • 代码整洁之道

rangeCheck是一个非常具有描述性的名字,因为它较好地描述了函数要做的事,所以好名字的价值怎么评价都不过分。如果每个示例都让你感到深合己意,那就是整洁代码。函数越短小,功能越集中,就越容易取一个好名字。名字长一些并不可怕,长而具有描述性的名字,比短而令人费解的名字更好。选择具有描述性的名字能帮助程序员理清模块的设计思路,追索好名字往往会使代码重构得更好。

从代码整洁之道的角度来看,最理想的函数参数个数是0(零参数函数),其次是单参数函数,再次是双参数函数,因尽量避免三参数函数。如果需要三个以上的参数,需要有足够的理由,否则无论如何也不要这样做,因为参数带有太多的概念性。

从测试的角度来看,参数甚至更叫人感到为难,因为编写确保参数的各种组合运行正常的测试用例,且测试覆盖所有可能值的组合是令人生畏的事情。输出参数比输入参数还要难以理解,因为人们习惯性地认为,信息通过参数输入函数,通过返回值从函数中输出,输出参数往往让人苦思之后才会觉得恍然大悟。如果函数看起来需要两个、三个或三个以上的参数,说明其中的一些参数就应该封装为结构体类。比如:

由此可见,减少函数参数的最佳方法是一个函数只做一件事,“函数要么做什么事,要么回答什么事!”两者不可兼得。函数应该修改某个对象的状态,或返回该对象的有关信息,两样都干常常会出现混乱。

2.类型与变量

由于有了结构体,因此可以将rangeCheck()的形参min和max转移到结构体中,不仅减少了一个形参,而且处理起来更方便。比如:

该声明描述了一个由两个int类型变量组成的结构体,不仅创建了实际数据的对象range,而且描述了该对象是由什么组成的,因为它勾勒出了结构体是如何存储数据的。显然,range是struct _Range类型的结构体变量,如果在该结构体定义前添加typedef:

此时,range就变成了该结构体的类型,即range等同于struct _Range。习惯的写法是将类型名的首字符大写,将变量名的首字符小写。有了Range类型,即可同时定义一个Range类型的变量range和一个指向Range *类型的指针变量pRange,当然也可以省略类型名_Range。比如:

注意,结构体有两层含义,一层含义是“结构体布局”,结构体布局告诉编译器是如何表示数据的,但它并未让编译器为数据分配空间。下一步是创建一个结构体变量,即结构体的另一层含义,其定义如下:

编译器执行这行代码便创建了一个结构体变量range,编译器使用Range为该变量分配空间:一个int类型的变量min和一个int类型的变量max,这些存储空间都与一个名称range结合在一起。

3.初始化

假设value值的有效范围为0~9,在这里可以使用名为newRangeCheck的宏方便地将结构体初始化。比如:

使用方法如下:

宏展开后如下:

其相当于:

从本质上来看,.min和.max的作用相当于Range结构体的下标。虽然Range是一个结构体,但range.min和range.max都是int类型的变量,因此可以象使用其它int类型变量那样使用它,比如,&(range.min)。

由此可见,如果初始化一个静态存储期的结构体,初始化列表中的值必须是常量表达式。如果是自动存储期,初始化列表中的值可以不是常量。

4.接口与实现

(1)传递结构体成员

只要结构体成员是一个具有单个值的数据类型,比如,int、char、float、double或指针,便可将它作为参数传递给接受该特定类型的函数,rangeCheck()的实现详见程序清单2.11。

程序清单 2.11 rangeCheck()函数的实现(2)

其调用形式如下:

rangeCheck()既不知道也不关心实参是否是结构体的成员,它只要求传入的数据是int类型。如果需要在被调函数中修改主调函数中成员的值,就要传递成员的地址。

(2)传递结构体

虽然传递一个结构体比一个单独的值复杂,但标准C同样允许将结构体作为参数使用,rangeCheck()函数的实现详见程序清单2.11。

程序清单 2.12 rangeCheck()函数的实现(3)

其调用形式如下:

虽然通过这种方法能够得到正确的结果,但它的效率很低,因为C语言的参数传址调用方式要求将参数的一份拷贝传递给函数。假设结构体的成员是一个占用128字节的数组,甚至更大的数组。如果要将它作为参数进行传递,则必须将所占用的字节数复制到堆栈中,以后再丢弃。

(3)传递结构体的地址

假设有一组这样的数据,存储在结构体成员数组中。其数据结构如下:

显然,只要将结构体的地址(int *)&st作为实参传递给iMax()的形参,即可求出数组中元素的最大值,详见程序清单 2.13。

程序清单 2.13 求数组中元素的最大值范例程序

下面还是以范围值校验器为例,定义一个指向该结构体的指针变量pRange,其初始化、赋值与普通指针变量是一样的:

和数组不一样,结构名并不是结构体的地址,因此要在结构名前加上&运算符,因此这里的pRange为指向Range结构体变量range的指针变量。虽然pRange、&range和&range.min的类型不一样,但它们的值相等,那么下面的关系恒成立:

由于.运算符比*运算符的优先级高,因此必须使用圆括号。这里着重理解pRange是一个指针,pRange->min表示pRange指向结构体的首成员,所以pRange->min是一个int类型的变量,rangeCheck()函数的实现详见程序清单2.14。

程序清单 2.14 rangeCheck()函数的实现(4)

rangeCheck()使用指向Range的指针pRange作为它的参数,将地址&range传递给该函数,使得指针pRange指向range,然后通过->运算符获取range.min和range.max的值。注意,必须使用&运算符获取结构体的地址,和数组名不同,结构体名只是其地址的别名。

其调用形式如下:

(4)用函数指针调用

如果需要增加一个奇偶校验器对value值进行偶校验,其数据结构如下:

oddEvenCheck()函数的实现详见程序清单 2.15。

程序清单 2.15 oddEvenCheck()函数的实现

当系统需要多个校验器后,在运行时调用者将根据实际情况决定调用哪个函数,根据依赖倒置原则,最好的方法是用函数指针隔离变化。无论什么校验器,其相同的处理部分是value值的合法性判断,因此将其抽象为模块。而可变的是value值和校验参数,由外部传入的参数应对。由于各种校验器的类型不一样,因此必须使用“void *pData”作为形参才能接受任意类型的数据,即将Range *pRange和OddEven*pOddEven泛化成了void *pData。Validate类型的定义如下:

其中,pData为指向任意校验器参数的指针,value为待校验的值,通用校验器的接口详见程序清单 2.16。

程序清单 2.16 通用校验器接口(validator.h)

以范围值校验器为例,其调用形式如下:

这次传递给函数的是一个指向结构体的指针,指针比整个结构体要小得多,所以将它压到堆栈上的效率要高很多,validator接口的实现详见程序清单 2.17。

程序清单 2.17 validator接口的实现(validator.c)

由于pRange、pOddEven与pData的类型不同,因此需要对pData强制类型转换,才能引用相应结构体的成员。注意,在这里,作者并没有提供完整的代码,请读者补充完善。


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C语言编程
    +关注

    关注

    6

    文章

    90

    浏览量

    20976
  • 程序设计
    +关注

    关注

    3

    文章

    258

    浏览量

    30181
  • 周立功
    +关注

    关注

    38

    文章

    130

    浏览量

    37036
  • 结构体
    +关注

    关注

    1

    文章

    124

    浏览量

    10746

原文标题:周立功:结构体使你的程序设计更方便——内存对齐和基本数据类型

文章出处:【微信号:ZLG_zhiyuan,微信公众号:ZLG致远电子】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    新书创作谈:立功教授数十年之心血力作《程序设计数据结构

    ` 近日,立功教授公开了数十年之心血力作《程序设计数据结构》,此书在4月28日落笔,电子版已无偿性分享到电子工程师与高校群体,在致远电子公众号后台回复关键字【
    发表于 05-15 18:04

    【完整资料】《程序设计数据结构立功数十年心血力作

    ,是立功和团队的读书笔记和程序设计实践的心得。《程序设计数据结构》重点阐述了三大方向内容。C语言学习中的痛点:针对当前工程师在C语言学习
    发表于 05-16 16:43

    Java基本数据类型之间的运算规则是什么?

    Java基本数据类型Java变量的使用说明Java基本数据类型之间的运算规则
    发表于 11-04 09:59

    浅析C51的基本数据类型和扩充数据类型

    本数据类型:扩充数据类型
    发表于 11-22 06:05

    C预处理与C语言基本数据类型

    指令表:注意:宏名的书写由标识符与两边各两条下划线构成。C语言基本数据类型不同操作系统中数据类型所占字节数图解数据类型的其他分类:变量常量(字面量和const常量)void(特殊类型
    发表于 12-21 08:29

    Java的基本数据类型与条件结构

    《Java基础入门》第二篇1 基本数据类型,运算符与表达式,条件结构,循环结构...
    发表于 12-23 08:02

    C语言教程之C语言基本数据类型与顺序程序设计讲解

    本文档的主要内容详细介绍的是C语言教程之C语言基本数据类型与顺序程序设计讲解。在程序运行时,其值不能被改变的量称为常量。常量可分为三种:整型常量,实型常量,字符型常量,符号常量
    发表于 10-26 16:48 3次下载

    C语言程序设计教程之基本数据类型和运算符及表达式的资料说明

    本文档详细介绍的是C语言程序设计教程之基本数据类型和运算符及表达式的资料说明主要内容包括了:1 C的数据类型,2 常量与变量,3 数据类型的转换,4 算术运算符,5 赋值运算,6 关系
    发表于 01-25 15:44 0次下载
    C语言<b class='flag-5'>程序设计</b>教程之基<b class='flag-5'>本数据类型</b>和运算符及表达式的资料说明

    Java程序设计教程之数据类型及其运算的详细资料说明

    本文档的详细介绍的是Java程序设计教程之数据类型及其运算的详细资料说明主要内容包括了:1标识符和关键字 ,2常量和变量 ,3基本数据类型 ,4运算符 ,5表达式 ,6数组
    发表于 02-22 10:27 5次下载
    Java<b class='flag-5'>程序设计</b>教程之<b class='flag-5'>数据类型</b>及其运算的详细资料说明

    51单片机学习笔记(9)——C51的基本数据类型和扩充数据类型

    本数据类型:扩充数据类型
    发表于 11-14 13:36 1次下载
    51单片机学习笔记(9)——C51的基<b class='flag-5'>本数据类型</b>和扩充<b class='flag-5'>数据类型</b>

    Struct结构数据类型

    Struct类型是一种由多个不同数据类型元素组成的数据结构,其元素可以是基本数据类型,也可以是Struct、数组等复杂数据类型以及PLC
    的头像 发表于 07-25 17:02 2339次阅读

    C语言-基本数据类型与位运算

    这篇文章作为基础知识点,总结C语言的基本数据类型有哪些,浮点数的精度,整数变量的空间范围,变量定义语法,变量命名规则,浮点数打印格式,基本数据类型printf对应的打印、位运算的知识点。
    的头像 发表于 08-14 09:56 1226次阅读

    结构数据类型Struct介绍

    Struct类型是一种由多个不同数据类型元素组成的数据结构,其元素可以是基本数据类型,也可以是Struct、数组等复杂数据类型以及PLC
    的头像 发表于 01-30 10:15 1153次阅读

    本数据类型分享

    本数据类型本数据类型:包括位、位序列、整数、浮点数、日期时间。此外字符也属于基本数据类型,请参见文档String与WString。 1.位和位序列 2.整数数据类型 3.浮点型实
    的头像 发表于 06-13 14:14 5310次阅读
    基<b class='flag-5'>本数据类型</b>分享

    javascript的基本数据类型有哪些

    JavaScript 是一种动态的、面向对象的编程语言,广泛应用于 Web 开发中。在 JavaScript 中,有七种基本数据类型(Primitive Types),它们分别是 Undefined
    的头像 发表于 12-03 11:17 306次阅读