自定义类型-结构体详解

✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
📃个人主页:@rivencode的个人主页
🔥系列专栏:玩转C语言
💬保持学习、保持热爱、认真分享、一起进步!!

目录

  • 一.结构体介绍-自定义类型
  • 二.结构体的声明
    • 1.结构体声明
    • 2.结构体变量的定义和初始化
    • 3.结构体变量访问成员
    • 4.结构体的自引用
  • 三.结构体数组
  • 四.结构体与指针及函数传参
    • 1.指向结构体变量的指针
    • 2.指针访问成员变量
    • 3.STM32寄存器映射
    • 4.结构体传参
  • 五.结构体在内存的存储
    • 1.结构体内存对齐
    • 2.内存对齐的原因
    • 3.修改默认对齐数
    • 4.实现offsetof宏
  • 六.枚举
    • 1.定义枚举类型
    • 2.枚举的使用
    • 3.用枚举限定STM32模式的取值
    • 4.枚举的优点
  • 七.联合体-共用体
    • 1.联合体的定义及初始化
    • 2.用联合体判断计算机存储方式
    • 3.联合体大小的计算
  • 八.结尾

一.结构体介绍-自定义类型

在此之前我们已经知道,C语言中有 int char short long float doudble 等数据类型这些类型在C语言中被称为内置类型,也就是说是C语言自己的数据类型。但如果我们要表示一个复杂对象比如一个学生的信息,内容可能会包含姓名(char name[]),年龄(int age),成绩(float score)等信息,为了方便管理要把这些变量组合成一个整体对学生这一个对象进行描述,而这个整体就称为结构体。但面对不同对象时要想描述他们是不是得定义其他的结构体的类型啊,所以说结构体是一个复杂对象,而这个复杂对象是由我们自己来声明的类型,所以结构体被称为自定义类型,常见的自定义类型还有:枚举、联合体、位段等。

在学习结构体之前要注意一下几点:

1.结构体是自定义类型,说白了它是一个与 int,char…一样是一个类型,所以当你声明一个结构体类型时,这个类型并不会分配空间,只有结构体变量才会分配空间和地址。

2.结构体类型始终只是一个类型,所以拿一个结构体类型创建一个变量,数组,指针变量等,跟int类型创建变量不会有太大区别。

3.结构体定义要放在函数之上(类似于全局结构体,结构体声明之下的函数都可以使用),或者函数之中(类似于局部结构体,函数内部使用),如果放在函数之下会检测不到而报错。

二.结构体的声明

1.结构体声明

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,结构的成员可以是标量、数组、指针,甚至是其他结构体。

声明结构体格式:

struct tag
{
 member-list;//成员变量列表
}variable-list;//结构体变量列表

接下来就用结构体来描述一个学生

struct stu
{
	char name[20];
	char gender[10];
	int age;
	float score;
};


1)typedef 重命名结构体类型

typedef  unsigned int  uint32_t;
typedef struct stu
{
	char name[20];
	char gender[10];
	int age;
	float score;
}stu;


重定义后 stu 就等于 struct stu 也是一个结构体的类型,怎么做的目的就是为了方便定义结构体变量

2.结构体变量的定义和初始化

1.结构体变量的定义
定义结构体变量一般格式:

struct 结构体标签  结构体变量名

struct stu s1;

1)也可以直接在声明结构体的同时定义结构体变量,不过定义是全局变量

直接在声明结构体的同时定义结构体变量,这个还可以是指针,数组等

typedef 重命名结构体类型定义变量

typedef struct stu
{
	char name[20];
	char gender[10];
	int age;
	float score;
}stu;

int main()
{
	struct stu  s1;
	stu  s2;
	return 0;
}

2)声明匿名结构体,定义匿名结构体变量

匿名结构体没有结构体标签(结构体名),定义变量时只能在声明结构体的同时定义全局的结构体变量,否则不能再定义结构体变量

struct
{
	char name[20];
	char gender[10];
	int age;
	float score;
}s1,s2,s3;

typedef 重定义匿名结构体类型

你会发现stm32很多库文件里面的结构体都是这样定义的,目的为了使代码更简洁一点。

2.结构体变量的初始化

1)声明结构体的同时初始化结构体变量

2)结构体变量的初始化放在定义之后

效果一样的,只不过一个是全局变量一个是局部变量,但建议使用第二种,少用全局变量。

3)定义后逐个赋值

4)定义时乱序赋值
初始化赋值,可以不考虑顺序

结构体嵌套初始化

3.结构体变量访问成员

结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数。

struct stu
{
	char name[20];
	char gender[10];
	int age;
	float score;
};


int main()
{
	struct stu s={"王二麻子", "男", 22, 98.8};
	printf("%s %s %d %0.2lf\n",s.name,
		                                        s.gender,
		                                        s.age,
		                                        s.score);
	return 0;
}

如果结构体的成员本身是一个结构体,则需要继续用.运算符,直到最低一级的成员。

4.结构体的自引用

数据结构链表的节点

代码1:

struct Node
{
 int data;
 struct Node next;
};

这个显然不行,sizeof(next)是多大嘞,那不是层层套娃,无限大了。

正确的方式

struct Node
{
 int data;
 //next存放的是下一个节点的地址
 struct Node* next;//该结构体类型的指针变量
};

这里就不过多阐述,以后会出数据结构相关的文章

三.结构体数组

如果我们想存储一个班的信息,一个结构体变量肯定办不到,这里就可以定义一个学生结构体数组,数组的元素的类型就是一个结构体类型,而数组的每个元素是一个结构体(一个同学的信息)。

打印一个简易学生表

typedef struct stu
{
	char name[20];
	char gender[10];
	int age;
	float score;
}stu;

int main()
{
	int i;
	struct stu stu[3]={{"李四", "男", 22, 98.8},{"张三","男",21,95.5},{ "翠花", "女", 20, 99} };
	int sz=sizeof(stu)/sizeof(stu[0]);
	printf("%s %s %s %s\n","姓名","性别","年龄","得分");
	for(i=0; i<sz; i++)
	{
			printf("%s  %s  %d  %0.2lf\n",stu[i].name,
		                                                  stu[i].gender,
		                                                  stu[i].age,
		                                                  stu[i].score);
	}
	 return 0;
}

四.结构体与指针及函数传参

这里一定要记住一个点,结构体类型与int char…类型都只是一个类型,只不过结构体是一个自定义的复杂类型,那些定义和使用结构体数组,指针,函数传参等与其他类型没有很大区别.

1.指向结构体变量的指针

定义形式一般为
struct 结构体标签* 指针名;
 struct Stu* p;

看图就明白的透透的

2.指针访问成员变量

指针访问成员变量 一般使用 -> 操作符

3.STM32寄存器映射

作为《什么是寄存器》文章的补充
回顾一下,寄存器映射是给具有特定功能的内存单元取别名的过程,这个别名就叫寄存器,今天就把GPIOB 的所有寄存器地址打印出来,看一看到底是不是具有特定功能的内存单元的地址。


typedef unsigned int    uint32_t;
typedef short  int      uint16_t;
//外设基地址
#define PERIRH_BASE             ((unsigned int)0x40000000)
//总线基地址
#define APB1PERIRH_BASE  	    PERIRH_BASE 
#define APB2PERIRH_BASE      (PERIRH_BASE+0x10000) 
#define AHBPERIRH_BASE       (PERIRH_BASE+0x20000)
//GPIO 外设基地址
#define RCC_BASE               (AHBPERIRH_BASE+0x1000)  
#define GPIOA_BASE           (APB2PERIRH_BASE+0x0800)
typedef struct
{
	uint32_t CRL;
	uint32_t CRH;
	uint32_t IDR;
	uint32_t ODR;
	uint32_t BSRR;
	uint32_t BRR;
	uint32_t CLKR;
}GPIO_TypeDef;
#define  GPIOA  ((GPIO_TypeDef*)GPIOA_BASE)
int main()
{
	printf("%p\n",&GPIOA->CRL);
	printf("%p\n",&GPIOA->CRH);
	printf("%p\n",&GPIOA->IDR);
	printf("%p\n",&GPIOA->ODR);
	printf("%p\n",&GPIOA->BSRR);
	printf("%p\n",&GPIOA->BRR);
	printf("%p\n",&GPIOA->CLKR);
	return 0;
}

4.结构体传参

前面我们已经学过整型变量的传参,参数分为值传递和址传递。

值传递:形参是实参的一份临时拷贝,函数执行完成时,分配给形参的内存自动销毁。
址传递:传的是变量的地址,通过地址可以访问,并且可以修改变量的值。

接下来我们的重点是讨论,结构体传参到底是值传递好还是址传递好
直接上代码:

分别值传递,址传递封装两个函数打印一个结构体成员变量的值

typedef struct stu
{
	char name[20];
	char gender[10];
	int age;
	float score;
}stu;

void print1(stu  s)
{
	printf("%s\n",s.name);
	printf("%s\n",s.gender);
	printf("%d\n",s.age);
	printf("%0.2lf\n",s.score);
}
void print2(stu* p)
{
    printf("%s\n",p->name);
	printf("%s\n",p->gender);
	printf("%d\n",p->age);
	printf("%0.2lf\n",p->score);
}
int main()
{
	 struct stu s={"王二麻子", "男", 22, 98.8};
	 print1(s);
     print2(&s);
	 return 0;
}


结果可以发现都可以实现功能。

但是看图

值传递,传的是整个结构体而结构体有很多成员变量会占很大的空间,由于传的结构体很大,一是传递时间更久,二是内存的浪费,如果结构体很大,那要重新开辟很大的内存空间

而值传递传的是一个结构体的地址,我们知道凡是是地址再32位平台上都是四个字节,这样就减少的内存的浪费,以及时间会更快,而指针会指向结构体变量通过解引用操作就可以访问该结构体

分别利用函数通过值传递与址传递初始化结构体的成员

  • 函数值传递


你会发现函数值传递并不能修改原本结构体成员变量的值,因为值传递形式参数是实参的临时拷贝,是在内存的另外一个空间存储tmp这个临时变量,所以&tmp与&S,他们两个互不打扰,所以并不会改变s结构体成员变量的值。

  • 函数址传递

    址传递相当于与传过去的是结构体变量的地址用tmp接收,tmp就相当于是该结构体的指针,通过指针解引用操作就可以修改结构体成员变量。

  • 总结
    函数传参的时候,参数是需要压栈的。
    如果值传递一个结构体对象的时候,结构体过大,参数压栈(临时变量的存储)的的系统开销比较大,所以会导致性能的下降

所以建议结构体传参,不管是用来访问结构体成员,还是修改结构体成员都建议传地址过去

五.结构体在内存的存储

1.结构体内存对齐

先看一个代码

比较结构体变量 s1 ,s2 的大小

struct S1
{
	char c1;
	int a;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int a;
};

int main()
{
	struct S1 s1={0};
	struct S2 s2={0};
	printf("%d\n",sizeof(s1));
	printf("%d\n",sizeof(s2));
	return 0;
}

看结果是不是感觉有点意外

这里为什么会出现这种结果,那就要涉及到结构体的内存对齐了

先看内存对齐规则:

这里的大小都是以字节为单位
1. 第一个成员在相对于结构体变量地址偏移量为0的地址处。

2. 其他成员变量要对齐到某个数字(成员变量的对齐数)的整数倍的地址处。
成员变量的对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数,)是成员变量的最大对齐数的整数倍

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

接下来分析s1,s2 结构体变量的大小
s1结构体变量的大小


这里看他们成员变量的地址,也能看出存在内存对齐

s2结构体变量的大小

struct S3
{
	char c1;
	int a;
	char c2;
};
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
int main()
{
   printf("%d\n", sizeof(struct S4));
  return 0;
}


s4嵌套结构体变量大小具体分析:

2.内存对齐的原因

1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取(例如4的倍数)某些特定类型的数据,否则抛出硬件异常。

2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访

结构体的内存对齐是拿空间来换取时间的做法,原因就是现在科技发达了内存是越来越大的而且越来越便宜,总的来说时间要比空间更具性价比

那我们在声明结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。

struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};

就跟我们前面讲的 s1 s2,一样虽然成员变量是一样的但是就位置不一样导致,结构体大小不同。

3.修改默认对齐数

格式: #pragma pack(对齐数)

4.实现offsetof宏

offsetof:是一个带参宏可以求出,结构体成员变量地址相对于结构体变量起始地址的偏移量。


用法:

求出的偏移量完美吻合

自己实现offsetof宏

#define OFFSETOF(type,number)          (int) (&( *(type*) 0 ).number)
struct S2
{
	char c1;
	char c2;
	int a;
};
 
int main()
{
	printf("%d\n",OFFSETOF(struct S2,c1));
	printf("%d\n",OFFSETOF(struct S2,c2));
	printf("%d\n",OFFSETOF(struct S2,a));
	return 0;
}


详细分析:


如果对带参宏的不是很理解的请看——>预编译指令

六.枚举

枚举就是把可能的取值一 一列举。

1.定义枚举类型

格式:

//关键字—>enum
//类型名 ->enum 枚举名
enum 枚举名
{
//各个元素一逗号分隔
元素1,
元素2,
元素3,
....
元素n //最后一个元素不加逗号
};//记得加 ;

就比如一周有七天,就可以一 一列举:

enum DAY
{
	MON=1,//星期一
	TUE,
	WED, 
	THU, 
	FRI,
	SAT, 
	SUN
};

enum DAY则是一个枚举类型,{}里面的是枚举常量

  • 这些可能取值都是常量,默认从0开始,依次递增1,当然在定义的时候也可以赋初值。

  • 没有赋值的枚举元素,其值为前一元素加1


所以枚举常量的值我们想赋值什么就赋值什么

2.枚举的使用


这里注意枚举变量赋值只能赋值枚举常量,不能直接赋值一个常数

typedef重命名枚举类型名

3.用枚举限定STM32模式的取值

  • 速度

  • 模式


这里请参考 STM32新手入门-自己写库函数点亮LED

4.枚举的优点

1. 增加代码的可读性和可维护性
2. #define定义的标识符(没有具体的类型,只有在预编译阶段会进行替换)而枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)–同名
4. 便于调试而宏定义无法进行调试
5. 使用方便,一次可以定义多个常量

这里有关于#define请参考—>C语言预处理指令-单片机必备技能 有详细解析

七.联合体-共用体

在C语言中,可以定义不同数据类型的数据共占同一段内存空间,以满足某些特殊的数据处理要求,这种数据构造类型就是联合体也叫共用体。

1.联合体的定义及初始化

联合体也是一种自定义数据类型,和结构体类型一样,它也是由各种不同类型的数据组成,这些数据叫作联合体的成员变量。不一样的是成员共用一块空间
格式:

union 联合体名
{
   //成员变量列表
}变量列表;

联合体的所有成员在内存中具有相同的首地址,共占同一段内存空间,这些成员可以相互覆盖,所以在某一个时刻只能使用一个成员变量,联合体的目的就是为了节省空间。

联合体的大小:至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

成员变量的访问也和结构体一样用 (.) 来访问

其他的像什么匿名联合体,tepedef 重命名类型等与结构体类似这里就不在赘述啦。

2.用联合体判断计算机存储方式

计算机存储方式分为两种:大端存储 与 小端存储

大端存储: 是指数据的低位(从右往左由低到高) 存储在内存的高地址中,而数据的高位存储到内存的低地址中。
小端存储:是指数据的低位(从右往左由低到高) 存储在内存的低地址中,而数据的高位存储到内存的高地址中。

例如:
定义一个整型变量 int a=0x44332211;
看看变量a在内存中如果存储

接下来就要判断计算机存储方式是小端还是大端

定义一个整型变量a

int a=1;

方法一:
用指针的方式

int Check_Sys1()
{
	int a=1;
	return *(char *)&a;
}
int main()
{
	int ret=Check_Sys2();
	if (1==ret)
	{
		printf("小端模式\n");
	}
	else
   	{
		printf("大端模式\n");
	}
  return 0;
}

方式二:
用联合体的方式


int Check_Sys2()
{
	union un
	{
		char a;
		int b;
	};
	union un u;
	u.b=1;
	return u.a;
}
int main()
{
	int ret=Check_Sys2();
	if (1==ret)
	{
		printf("小端模式\n");
	}
	else
   	{
		printf("大端模式\n");
	}
  return 0;
}


细细品味感觉妙极了

3.联合体大小的计算

1. 联合的大小至少是最大成员的大小。
2. 当最大成员大小不是最大对齐数(是各个成员对齐数中最大的对齐数)的整数倍的时候,就要对齐到最大对齐数的整数倍

例:

八.结尾

文章到这就结束了常见的自定义类型基本都介绍完毕,要想学好STM32单片机结构体有关知识一定要学扎实,觉得文章对你有所帮助的话就赶快点赞收藏叭!!! 持续分享更多干货!!!!

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2023年12月20日
下一篇 2023年12月20日

相关推荐