【嵌入式C编程】快速通关秘籍五:结构体与位运算章节

内容提要:

  1. 结构体变量的应用
  2. 结构体数组的应用
  3. 结构体指针的应用
  4. 共用体、枚举的应用
  5. typedef的使用
  6. 结构体的内存分配
  7. 位运算

知识详解01:结构体变量的定义

  1. 在实际问题中,一组数据往往具有不同的数据类型;例如在学生登记表中,姓名应为字符型、学号可为整型或字符型、年龄应为整型、性别应为字符型、成绩可为整型或实型.
  2. 显然不能用一个数组来存放这一组数据;因为数组中各元素的类型和长度都必须一致
  3. 为了解决这个问题,C语言中给出了另一种构造数据类型——结构体
  4. 结构体的本质
    • 将不同类型的数据组合成一个有机的整体,以便于引用.如:一个学生有"学号/姓名/性别" 等属性
    • 在没学结构体之前,我们是如下定义的:
      • int  num; 
      •  char name[20];
      •  char sex;        
      • 学生信息的一般表示法
      • 这样的定义是不是很不方便呢?于是C语言就发明了结构体。
    • 那么用我们新学的结构体咋定义呢?
      • struct student    
      • {
        •  int num;
        •  char name[20];
        •  char sex;
      • };
      • 学生信息的结构体表示法
      • struct为结构体关键字,student就是这结构体的类型名,而 num,name, sex就是该结构体的成员,他们可以是不同类型的,注意在定义类型的时候不要对结构体成员num,name, sex赋初值。其次就是在大括号后面要有分号“;”。
  1. 定义结构体变量的方式
    • 我们要用结构体,是不是要有一个结构体类型呀,然后用这个类型去定义一个变量,就像int 一样,有了这样的类型,再用int 去定义一个变量。结构体其实也是一样。那又怎样去定义结构体变量呢?有三种方法。
      • 第一种方法:先定义结构体类型再定义变量名

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第1张图片

      • 其中struct是关键字,boy是结构体标记(用于为结构体命名),struct boy代表该结构体的类型, 大括弧里面就是结构体的成员。注意大括弧的后面一定要有一个分号‘;’(一定要记住)。到此为止我们有了"struct boy"这样的类型了, struct boy 就等价于我们的int一样,接下来我们就要定义一个这样类型的变量,定义格式如下:struct boy lucy;这个lucy我们所要的结构体变量。
      • 第二种方法:在声明类型的同时定义变量

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第2张图片

      • 在下面可以根据我们的需要定义变量,如以下格式:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第3张图片

      • 第三种方法:直接定义结构体类型变量

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第4张图片

      • 这种定义的特点:结构体变量只能在定义结构体类型的时候定义,不能再定义其他的变量。这样的定义的结构体类型是没有名字的。当然这样的定义也是用的,比如我们用typedef重新为这样的结构体类型命个名不就可以了,至于原先有没有名字都无所无了,因为我已经给他起了一个新的名字。(比如你以前的小名叫狗蛋,上学的时候你就用大名张三了,对于你是不是就用张三,你肯定不会在学校用狗蛋这个名字吧)
    • 结构体的初始化

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第5张图片

知识详解02:结构体变量的应用

  1. 以先声明结构体类型再定义变量的类型为例

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第6张图片

    • 在内存中各占25个字节(4+20+1=25),我们暂且认为至少要用25个字节,其实要比这25个字节要多,这一点留着下午给大家解释。
    • 结构体中的成员,可以像普通变量那样单独使用。
      • 这时可以在黑板上以板书的形式讲解:如何引用结构体变量,如Lucy.num = 100;

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第7张图片

      • 带着学生分析这张图片
    • 成员名可与程序中的变量名相同,二者代表不同对象

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第8张图片

    • 结构体变量的成员引用:结构体变量名.成员名
      •  如:Lucy.num = 101;
    • 不能将结构体变量作为整体进行操作.以下是错误的:
      • printf(“%d,%s,%c”,Lucy);错误
      • scanf(”%d  %s  %c”,&Lucy); 错误
      • printf(“%d,%s,%c”, Lucy.num,   Lucy.name,  Lucy.sex);正确
      • scanf(“%d  %s  %c”, &Lucy.num,   Lucy.name,  &Lucy.sex); 正确
    • 当结构体变量的成员也是结构体类型时,引用必须用最底层的成员变量,如下
      • Lucy.birthday.month = 12;
      • 带着学社写一个结构体嵌套的例子
      • #include
      • #include
      • #include
      • struct teacher
      • {
      • int age;
      • };
      • struct stu
      • {
      • char name[20];
      • int age;
      • char sex;
      • struct teacher one;
      • };
      • int main(void)
      • {
      • int i=0;
      • struct stu lilei;
      • lilei.one.age=10;
      • strcpy(lilei.name,"lilei");
      • printf("%s\n",lilei.name);
      • printf("%d\n",lilei.one.age);
      • return 0;
      • }
    • 可以引用结构体变量成员的地址,也可以引用结构体变量的地址,例如:
      • scanf(“%d”,&Lucy.num);   //输入Lucy.num的值
      • printf(“%d”,&Bob);   //输出Bob的首地址
      • 目的:主要用作函数参数,传递结构体变量的地址.
  • 允许具有相同类型的结构变量可以相互赋值,其它情况不允许对结构变量直接赋值
    • Bob = Lucy;

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第9张图片

  • 带着学生浏览代码。路径“00_基础代码”中的“5_1_1struct.c”“5_1_2struct.c”

知识详解03:结构体数组的定义

    对于结构体数组,我们先回想一下整型数组,然后举例我们要统计咱们班30个人的姓名,学号 ,成绩,如果我们用结构体变量来实现是不现实的。那么我们就准备用结构体数组来完成这事。结构体数组就是同一类型的结构体变量的集合,内存分布上是连续的。

  1. 一个结构体变量中可以存放一组数据;如一个学生的学号、姓名、成绩等数据.如果有10个学生的数据需要参加运算显然应该用数组,这就是结构体数组(如果定义10个结构体变量太麻烦了)
  2. 结构体数组与以前介绍过的数值型数组不同之处:每个数组元素都是一个结构体类型的数据,它们都分别包括各个成员(分量)项
  3. 以下以直接定义结构体数组为例:
    • struct student
    •    { 
    •                 int  num;
    •                 char name[10];
    •                 char age;
    •     }edu[2];
    • 我们定义了一个结构体类型是struct student,它有三个成员分别是num, name, age。用这样的结构体类型定义了一个结构体数组edu[2],该数组有两个元素,分别是 edu[0],  edu[1].那如何给数组元素中的成员赋值呢?
      • 结构体数组的引用:
      • edu[0].num = 101;
      • strcpy( edu[0].name,“Lucy” );
      • edu[0].age = 24;

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第10张图片

    • 我们可以改结构体类型去定义一个的新的结构体数组 struct student  sunplus[10]; 

知识详解04:结构体数组的应用

  1. 练习:学生平均成绩统计
    •  struct student
    • {
    •        int   num;
    •        char  name[20];
    •        float score;
    • }edu[2]={ {101,”Lucy”,78},{102,”Bob”,59.5} };
  1. 练习:结构体数组排序
    • 从键盘输入5名学生的信息(学号、姓名、成绩)存入一个结构体数组中,将其按成绩高到低排序并输出

知识详解04:结构体指针的定义

    结构体指针和其他类型的指针都是一样的理解,在32位平台不管啥类型的指针都占4个字节的空间。

  1. 结构体指针就是指向结构体变量的指针;
  2. 如果一个指针变量中保存了结构体变量的首地址,那么这个指针变量就指向该结构体变量.
  3. 通过结构体指针即可访问该结构体变量,这与数组指针和函数指针的情况是相同的
  4. 结构指针变量说明的一般形式为:
    • struct  结构体名   *结构体指针变量名 
    • struct  student    *p = &Lucy;//假设事先定义了 struct  student   Lucy ; 

知识详解05:结构体指针的应用

  1. 利用结构体指针变量,就能很方便地访问结构体变量的各个成员,以下3种形式等价:
    • Lucy.num = 101;
    • (*p).num = 101;
    • p->num = 101;    
    • 注意:“->” 称为指向运算符
      • *p等价于结构体变量,所以对于引用结构体变量中的成员我们用 Lucy.num = 101; (*p).num = 101;点引用
      • 对于结构体指针去引用里面的成员: p->num = 101;
  1. 例:结构体指针的使用

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第11张图片

    • 接下来的例子加深学生对结构体的理解

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第12张图片

知识详解06:共用体定义

     我们回顾一下,结构体是一个构造数据类型,结构体变量里面有好多的成员,结构体变量的大小是不是各成员之和呀。那接下来我们要讲共用体,先从名字上来看,共用共用就是共同使用。意思是啥呢?这共用体和结构体差不多的,他也有好几个成员,不过他的这些成员不是各自占用一块内存,而是所有成员占用同一块内存,那这个共用体的大小是多大呢?共用体的大小是他这些成员当中占内存长度最大的那个大小。好了对共用体有了简单的认识,那么我们接下详细看一下共用体。

一个工程实例

*.h中

 typedef struct 

{

u8 emailflag;  //是否启用邮件,0-->关闭,1--->启用

char serveraddr[30];      //邮件服务器地址   smtp.sina.com

char title[256];   //邮件标题

char emailfrom[80];   //[email protected]

char emailpwd[100]; //密码

char emailto[80];  //接收地址

}EmailMsg;

*.c中

EmailMsg emailmsg={0,{"0"},{"嵌入式监控系统告警"},{"0"},{"0"},{"0"}};//

共用体的几方面应用:

1.可以知道大小端。

2.如果有许多不同类型的临时变量,可以用共用体减少空间。

3.方便一些数据类型转换。

  1. 共用体
    • 使几个不同的变量共占同一段内存的结构称为 “共用体”类型的结构.
    • 共用体与结构体定义、引用型式相类似,只是定义时将关键词struct换成union
      • union data                 union data
      • {    short  i;              {       short  i;
      •        char  ch;     或              char  ch;
      •        float  f;                         float  f;
      •  }a,b;                          }; union data  a,b;
    • 大家思考一下:
      • 我们在定义结构体变量时刻可以对变量里面的所有成员进行初始化,那么对于共用体而言,我们能够对共用体变量的所有成员进行初始化吗?答案是不行的。大家想一下我们共用体的特点,所有的成员共用同一内存空间,如果我们对所有成员进行初始化,是不是它就放不下的,如果我们向里写入3,4,5最终内存中只有5。如果我偏要初始化,那只能初始化其中一个成员。(赋值也是一样)
  1. 共用体与结构体的比较
    • 结构体变量所占内存长度是各成员占的内存长度之和,每个成 员分别占有自己的内存单元.
    • 共用体变量所占的内存长度等于最长的成员的长度,成员共用 一段内存,彼此互相覆盖;即每一个瞬间只有一个成员起作用。故对于同一个共用体型变量,给一个新的成员赋值就“冲掉”了原 有成员的值
      • 上面定义的“共用体”变量a、b各占4个字节(因为一个实型变量占4个字节),而不是各占7个字节(2+1+4)
  1. 共用体变量的引用同结构体相似
    • 简单的提一下:共用体的引用和结构体的引用是一样的。
    • 只有先定义共用体变量才能引用它,而且不能引用共用体变量,而只能引用共用体变量中的成员.
      •  a.i  (引用共用体变量中的整型变量i)
      • a.ch (引用共用体变量中的字符变量ch)
      • a.f  (引用共用体变量中的实型变量f)

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第13张图片

  1. 共用体特点
    • 同一内存段可以用来存放几种不同类型的成员,但每一瞬间只能存放其中一种,而不是同时存放几种.
    • 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员就失去作用.
    • 共用体变量的地址和它的各成员的地址都是同一地址.
    • 不能在定义共用体变量时对它初始化
  1. 我们来看一下特殊的例子,有助于我们更好的分析共用体

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第14张图片

知识详解07:枚举的定义

        枚举类型它像结构体类型一样,也得现有一个类型,然后再用这个类型区定义一个结构体变量,大家还记得定义枚举类型的关键字叫啥?(enum)

  1. 枚举
    • 将变量的值一一列举出来,变量的值只限于列举出来的值的范围内
      • 枚举,枚举,一一列举
      • 对于普通的变量我们可以对赋予任何(该字节所允许的范围内)值,而枚举类型的变量就只能赋予规定的值。
  1. 枚举类型定义的一般形式:
    • enum  枚举名  { 枚举值表 };
      • enum是关键字,
    • 在枚举值表中应列出枚举变量应该取的所有可用值,也称为枚举元素
      • (接下来我们就去定义一个week这样的枚举类型)
  1. 定义枚举类型 week:
    • enum week {sun,mon,tue,wed,thu,fri,sat};
      • 注意:sun, mon等等,是不需要交双引号“”,如“mon”的。 sun, mon其实常量(枚举常量),常量值sun==0,  mon==1以此递增。
      • 有了这样的枚举类型,那如何去定义枚举变量呢?
  1. 定义枚举类型的变量workday、weekday:
    • enum week  workday,weekday;
      • workday,weekday它们就是我们前面定义的枚举类型的两个变量。
      • 那我们如何给这两个变量赋值呢?workday = tue;就这样赋值, workday的实际值就是2.

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第15张图片

      • 为什么会这样呢?因为tue就是一个枚举常量(常量,符号常量)
      • 注意在给枚举变量workday赋值的时候,尽量对其赋值枚举类型列表里面的枚举常量。因为枚举的本意就是这样的。(当然你也就赋其他值,这样与定义一个普通的变量没有什么差别, 何苦还这么麻烦用枚举呢 )
      • 但是有人会说我就要给他赋其他值(20),可不可以呀?答案是可以的,但是你尽量别这么干,因为这样的是没有意义的,因为你完全可以定义一个普通的整型变量来完成这件事,何苦还这么麻烦用枚举呢。而且枚举还有一个好处,那就是在我们用枚举类型去定义一个变量的时候,别人看你的代码就知道给这个枚举变量赋的值应该在枚举值表的范围内。
    • workday与weekday只能取sun….sat中的一个
  1. 枚举值是常量,不是变量,不能在程序中用赋值语句再对它赋值.例如对枚举weekday的元素再作以下赋值: 
    • sun=5; mon=2; sun=mon; 都是错误的.

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第16张图片

  1. 枚举元素本身由系统定义了一个表示序号的数值,从0 开始顺序定义为0,1,2…如在week中,sun值为0,mon值为1, …,sat值为6
  2. 枚举的扩展:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第17张图片

知识详解08:typedef的使用

    typedef我在前面可能说过,它的作用就是给已有的类型重新定义一个名字。大家以前在看别人的代码的时候是否注意到这样定义变量:INT16    a;或者INT32    a;一看大家就知道这两个类型都是整型的,不同的是INT16是16位,INT32是32位的整型。它有什么好处呢?(方便移植)

    

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第18张图片

    接下来我们就去学习如何用typedef去给已有的类型重新取一个新的名字。

    用typedef定义类型的方法:

  1. 用typedef声明新的类型名代替已有类型名
  2. typedef语句的的一般形式为:
    • typedef       原数据类型        新的类型名
      • 在32位平台我们给int重新取名INT32
      • typedef   int    INT32;//(这个比较简单,我们来看一个复杂点的)

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第19张图片

    • 新的类型名DATE代替了上面的结构体类型,此后可以用DATE来定义该结构体型变量:
      • DATE birthday;
      • DATE *p;(p为指向该结构体型数据的指针)
  1. 一个简单的类型名代替复杂的类型表示方法
    • 声明 NUM 为整型数组类型:
      • typedef int  NUM[5];  

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第20张图片

    • 声明 STRING 为字符指针类型:
      • typedef char *  STRING;

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第21张图片

    • 声明 POINTER 为指向函数的指针类型
      • typedef int (*POINTER)(int );

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第22张图片

    • 以上这三种typedef的应用是不是有点复杂,别怕。我会教大家一个固定的步骤去定义,就很简单了。加油!!!
  1. 用typedef定义类型的方法:
    • 先按定义变量的方法写出定义体
      • (如:int  i)
    • 将变量名换成新类型名
      • (例如:int  COUNT)
    • 在最前面加typedef
      • (例如:typedef  int  COUNT)
    • 然后可以用新类型名去定义变量
    • 备注:按照以上的步骤分别重新定义一下上页PPt谈到的类型。
  1. 特点
    • 用typedef可以声明各种类型名,但不能用来定义变量.
    • 用typedef只是对已经存在的类型增加一个类型名,而没有创造新的类型.
    • 当不同源文件中用到同一类型数据时,常用typedef声明一些数据类型,把它们单独放在一个文件中,然后在需要用到它们的文件中用#include命令把它们包含进来
    • 使用typedef有利于程序的通用与移植

知识详解09:结构体的内存分配

    今天上午的时候我在讲结构体变量大小的时候,是不是说过:“结构体变量大小是所有数据成员大小之和”,这样的讲只是方便大家理解结构体。其实结构体变量在内存分配的时候是有一个分配原则的,就是对齐原则(几个字节几个字节为单位为你分配内存空间,并且在存储的时候有一个对齐的概念)

  1. 让学生敲一下下面图片的代码,看看实际结构体的大小是多大?

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第23张图片

  1. C语言结构体对齐也是老生常谈的话题了。基本上是面试题的必考题。内容虽然很基础,但一不小心就会弄错。写出一个struct,然后sizeof,你会不会经常对结果感到奇怪?sizeof的结果往往都比你声明的变量总长度要大,这是怎么回事呢?
  2. 有人给对齐原则做过总结,具体在哪里看到现在已记不起来,这里引用一下前人的经验(在没有#pragma pack宏的情况下):
    • 原则1、数据成员的对齐规则:
      • 结构体(struct)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存放在offset为该数据成员大小的整数倍的地方(比如int在32位机为4字节,则要从4的整数倍地址开始存储)
    • 原则2、结构体作为成员的对齐规则:(重要!)
      • 如果一个结构体B里嵌套另一个结构体A,则结构体A应从offset为A内部最大成员的整数倍的地方开始存储。(struct B里存有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储。),结构体A中的成员的对齐规则仍满足原则1、原则2。
      • 注意:
        • 1.结构体A所占的大小为该结构体成员内部最大元素的整数倍,不足补齐。
        • 2.不是直接将结构体A的成员直接移动到结构体B中
    • 原则3、收尾工作:
      • 结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐
    • 这三个原则具体怎样理解呢?我们看下面几个例子,通过实例来加深理解。
    • 练习1:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第24张图片

    • 分析:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第25张图片

      • 其中最大的类型为long,4个字节,那么他就是以4个字节为单元存储 ,而a占4字节,b占2字节, 最后两个字节为填充字节,由于原则3。注意:*为填充字节
    • 练习2:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第26张图片

    • 分析:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第27张图片

      • 其中最大的类型为int,4个字节,那么他就是以4个字节为单元存储。而a为4个字节offset=4x0,b为1字节offset=1x4,c为2字节offset=2x3.由于原则1所以b后的1个字节要补齐
    • 练习3:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第28张图片

    • 分析:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第29张图片

      • 其中最大的类型为int,4个字节,那么他就是以4个字节为单元存储。而a为1个字节offset=1x0,b为4字节offset=4x1,由于原则1所以a后3个字节要补齐,c为2字节offset=2x4.由于原则3所以c的后2字节要补齐。
    • 练习4:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第30张图片

    • 分析:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第31张图片

      • 其中最大的类型为double,8个字节,那么他就是以8个字节为单元存储。e占2字节,offset=2x0;f类型为int,4字节,所以offset=4x1,由于原则1所以e后2字节补齐。g类型double,8字节, offset=8x1。h为类型short,2字节,offset=4x4。i为结构体类型A,而A中的成员最大为double,8字节,所以i的offset=8x3.接下来分析i中的a,a的类型为int,4字节,offset=8x3+4x0;其中8x3是i现对于整个结构体B的首地址的偏移量,而其中的4x0是a相对于i的首地址的偏移量,b为类型double,8字节,offset=8x3+8x1;由于原则1所以a后面的4字节补齐。c类型为float,4字节,offset=8x3+4x4;由于原则3所以c后面的4个字节补齐。
    • 练习5:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第32张图片

    • 分析

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第33张图片

      • 1.其中最大的类型是int,4个字节,那么它就以4个字节为单元存储
      • 2. a占4个字节,offset=4x0
      • 3. b占1个字节,offset=1x4
      • 4. 由于c是嵌套的结构体类型struct A,所以我们就要分析 c应该从哪儿开始存放呢?这时就要看原则2, c中的最大类型为short,2个字节,故offset=2x3, c中的成员e占一个字节,其offset=2x3+1x0( 2x3表示c偏离B的位置, 1x0 表示e偏离c的位置,所以e偏离B的位置为 2x3+1x0 )
      • 5.f占2个字节,其offset=2x3+2x1
      • 6.d占1字节,其offset=1x10
      • 7.最后一个字节补齐,原则3
    • 练习6:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第34张图片

    • 分析:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第35张图片

      • 1. 其中最大的类型是int,4个字节,那么它就以4个字节为单元存储
      • 2. a占4个字节,offset=4x0
      • 3. b占1个字节,offset=1x4
      • 4.由于c是嵌套的结构体类型struct A,所以我们就要分析 c应该从哪儿开始存放呢?这时就要看原则2, c中的最大类型为int,4个字节,故offset=4x2, c中的成员e占4个字节,其offset=4x2+4x0(  4x2表示c偏离B的位置,  4x0表示e偏离c的位置,所以e偏离B的位置为4x2+4x0)
      • 5.f占2个字节,offset=4x2+2x2,由于原则2中的注意1,所以后2个字节补齐
      • 6.d占2个字节,offset=2x8,由于原则3,后2个字节补齐
  1.  还有一种常见的情况,结构体中含位域字段。位域成员不能单独被取sizeof值。C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。
    • 使用位域的主要目的是压缩存储,其大致规则为:
      • 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
      • 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
      • 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;
      • 如果位域字段之间穿插着非位域字段,则不进行压缩;
      • 整个结构体的总大小为最宽基本类型成员大小的整数倍
  1. 还是让我们来看看例子
    • 例1:
      • struct A
      • {
      •                  char f1 : 3;
      •                  char f2 : 4;
      •                  char f3 : 5;
      • };
      •                           a           b                 c
      • A的内存布局:111,    1111 *,   11111 * * *
      •  位域类型为char,第1个字节仅能容纳下f1和f2,所以f2被压缩到第1个字节中,而f3只能从下一个字节开始。因此sizeof(A)的结果为2。
    • 例2:
      • struct B
      • {
      •                 char f1 : 3;
      •                 short f2 : 4;
      •                 char f3 : 5;
      • };
      • 由于相邻位域类型不同,不同类型不存放在一起,相同类型不超标会存放在一起,在VC6中其sizeof为6,在Dev-C++中为2。
    • 例3:
      • struct C
      • {
      •                 char f1 : 3;
      •                 char f2;
      •                 char f3 : 5;
      • };
      • 非位域字段穿插在其中,不会产生压缩,在VC6和Dev-C++中得到的大小均为3。
  1. 考虑一个问题,为什么要设计内存对齐的处理方式呢?如果体系结构是不对齐的,成员将会一个挨一个存储,显然对齐更浪费了空间。那么为什么要使用对齐呢?体系结构的对齐和不对齐,是在时间和空间上的一个权衡。对齐节省了时间。假设一个体系结构的字长为w,那么它同时就假设了在这种体系结构上对宽度为w的数据的处理最频繁也是最重要的。它的设计也是从优先提高对w位数据操作的效率来考虑的。有兴趣的可以google一下,人家就可以跟你解释的,一大堆的道理。
  2. 最后顺便提一点,在设计结构体的时候,一般会尊照一个习惯,就是把占用空间小的类型排在前面,占用空间大的类型排在后面,这样可以相对节约一些对齐空间

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第36张图片

知识详解10:位运算

    接下来我们一起来学习位运算,先举个例子,比如单片机中的点灯程序,是不是用位运算来做的呀。

  1. 位运算是指按二进制位进行的运算,因为在系统软件中,常要处理二进制位的问题.
  2. 例如:
    • 将一个存储单元中的各二进制位左移或右移一位,两个数按位相加等.
  1. C语言提供的位运算符:有以下几个,其中除~外其它的运算符均为双目运算.
    • 运算符                 含义                   运算符                 含义      
    •     &                    按位与                ~                        取反 
    •      |                      按位或                <<                      左移
    •     ∧                     按位异或             >>                      右移
  1. 参加运算的两个数据,按二进制位展开,然后进行相应的运算,如:“&、或、异或、非”等
    • 用板书的形式说一下每个运算符
    •  & 常用来将一个单元清零、取一个数中的某些指定位、保留指定位操作
    •  | 常用来将一个数的某些位置1 
    •  ^ 判断两个位值,不同为1,相同为0,常用来使特定位翻转等
    •  ~ 是一个单目(元)运算符,用来对一个二进制数按位取反,即将0变1用来配合其它位运算符使用的,用来设置屏蔽字 
  1. 移位运算
    • << 将一个数的各位二进制位全部左移,高位左移后溢出,舍弃不起作用.
      • 例如:a =<<2  将a的二进制数左移2位,右补0  
      • 若a=15,即二进制数00001111
      • 左移2位得00111100,(十进制数60)
    • >>如果原来符号位为0(正数)则左边移入0.
      • 如果符号位为1(负数),则左边移入1还是0要取决于系统,移入0的称为“逻辑右移”,移入1的称为“算术右移.
      • 让大家自己写一下右移程序,看看自己系统是“算术右移”,“逻辑右移”
    • 例如:
      • a的值是八进制数113755: 
      • a:1001011111101101  (用二进制形式表示)
      • a>>1: 0100101111110110  (逻辑右移时)
      • a>>1: 1100101111110110  (算术右移时)
    • 在有些系统中,a>>1得八进制数045766,而在另一些系统上可能得到的是145766。Turbo C和其他一些C编译采用的是算术右移,即对有符号数右移时,如果符号位原来为1,左面移入高位的是1
  1. 有两种方法可以改变一个或几个二进制位赋值和改变其值
    • 使用前面讲过的位操作指令,通过&、|、 ^、~ 的组合来实现二进制按位赋值或改变其值
    • 使用位段的方法
      • C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域” ( bit field);利用位段能够用较少的位数存储数据.
  1. 例题分析:
    • 05_p1图片有问题:可

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第37张图片

    • 给其中的位赋值

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第38张图片

    • 对比下一张图片

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第39张图片

  1. 对于位段成员的引用如下:
    • data.a =2;等;但要注意赋值时,不要超出位段定义的范围;如段成员a定义为2位,最大值为3,即(11)2,所以data a =5,就会取5的低两位进行赋值,就得不得想要的值
  1. 关于位段的定义与引用有几点重要说明:
    • 位段成员的类型必须指定为unsigned或int类型
      • 注意可以是char, unsigned char或short, unsigned short。就是不能使float
    • 一个位段必须存放在一个存储单元中,不能跨两个单元;如果第一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段
      • int是4个字节的存储单元, short是2个字节的存储单元, char是1个字节的存储单元(让大家明白啥叫存储单元)

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第40张图片

      • 先不看上面的结果,假设可以跨位段,上面的结果是8,而不是12。

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第41张图片

      • 而实际结果是这样的

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第42张图片

    • 位段的长度不能大于存储单元的长度,也不能定义位段数组

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第43张图片

      • 为什么会出错呢?因为 char是1个字节的存储单元,而a占9位超过了char存储单元,所以报错。

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第44张图片

    • 如一个段要从另一个字开始,可以定义:
      • unsigned a:1;
      • unsigned b:2;
      • unsigned :0;
      • unsigned c:3;(另一个单元)
      • 由于用了长度为0的位段,其作用是使下一个位段从下一个存储单元开始存放;将a、b存储在一个存储单元中,c另存在下一个单元(“存储单元”视不同的编译系统而异).
      • 例子:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第45张图片

      • 分析:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第46张图片

      • 由于a的类型为short int,2字节,所以offset=2x0;unsigned short :0;让起始地址从下一个位段开始,故offset=2x1;c的类型为char,1个字节,offset=1x4;d的类型为int,4字节,offset=4x2;
    • 可以定义无意义位段,如:
      • unsigned a: 1;
      • unsigned  : 2;
      • unsigned b: 3
      • 例子:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第47张图片

      • 分析:

【嵌入式C编程】快速通关秘籍五:结构体与位运算章节_第48张图片

点赞+关注,技术交流,wulianjishu666

你可能感兴趣的:(嵌入式C语言开发,c语言,数据结构,算法)