ARM_C高级学习笔记(四)结构体详解

(一)结构体概述

  • 结构体类型是一种自定义类型(C语言中的2种类型:原生类型和自定义类型。)
  • 结构体使用时先定义结构体类型再用类型定义变量
  • 两种定义结构体的方式
第一种 结构体定义时需要先定义结构体类型,然后再用类型来定义变量。
第二种 也可以在定义结构体类型的同时定义结构体变量
  • 举例说明
#include <stdio.h>

//结构体定义的第一种
//先定义类型,再定义变量
struct people
{
	char a[20];
	int age;
};
//结构体定义的第二种
//定义类型的同时定义变量
struct student
{
	char a[20];
	int age;
}stu;
//注意和typedef区分,typedef定义的是类型别名stu,上面的stu是变量名
/*typedef struct student
{
	char a[20];
	int age;
}stu;
*/

int main (void)
{
	struct people pep;
	pep.age = 20;
	printf("age = %d.\n", pep.age);
	stu.age = 15;
	printf("age = %d.\n", stu.age);
	
	return 0;
}

  • 数组中元素的访问方式:表面上有2种方式(数组下标方式和指针方式);实质上都是指针方式访问。
  • 结构体变量中的元素访问方式:只有一种,用.或者->的方式来访问。(.->访问结构体元素其实质是一样的,只是C语言规定用结构体变量来访问元素用.用结构体变量的指针来访问元素用->。实际上在高级语言中已经不区分了,都用.
  • 结构体的访问方式有点类似于数组下标的方式

(二)结构体的对齐访问

1.什么是对齐?

对齐跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。比如在32位cpu下,假设一个整型变量的地址为0x00000004,那它就是自然对齐的

2.结构体为何要对齐访问?

  • 结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率,否则会大大降低效率。
  • 内存本身是一个物理器件(DDR内存芯片,SoC上的DDR控制器),本身有一定的局限性:如果内存每次访问时按照4字节对齐访问,那么效率是最高的;如果你不对齐访问效率要低很多。
  • 还有很多别的因素和原因,导致我们需要对齐访问。譬如Cache的一些缓存特性,还有其他硬件(譬如MMU、LCD显示器)的一些内存依赖特性,所以会要求内存对齐访问。
  • 对比对齐访问和不对齐访问:对齐访问牺牲了内存空间,换取了速度性能;而非对齐访问牺牲了访问速度性能,换取了内存空间的完全利用。

3.结构体对齐要点

  • 编译器本身可以设置内存对齐的规则,有以下的规则需要记住:(32位编译器,一般编译器默认对齐方式是4字节对齐。)
  1. 结构体对齐要考虑:结构体整体本身必须安置在4字节对齐处,结构体对齐后的大小必须4的倍数(编译器设置为4字节对齐时,如果编译器设置为8字节对齐,则这里的4是8)
  2. 结构体中每个元素本身都必须对其存放,而每个元素本身都有自己的对齐规则
  3. 编译器考虑结构体存放时,以满足以上2点要求的最少内存需要的排布来算。
  • 实例
#include <stdio.h>
//编译器默认4字节对齐


// 分析过程:(struct mystruct0 为例)
// 首先是整个结构体,整个结构体变量4字节对齐是由编译器保证的,我们不用操心。
// 然后是第一个元素a,a的开始地址就是整个结构体的开始地址,所以自然是4字节对齐的。但是a
// 的结束地址要由下一个元素说了算。
// 然后是第二个元素b,因为上一个元素a本身占4字节,本身就是对齐的。所以留给b的开始地址也是
// 4字节对齐地址,所以b可以直接放(b放的位置就决定了a一共占4字节,因为不需要填充)。
// b的起始地址定了后,结束地址不能定(因为可能需要填充),结束地址要看下一个元素来定。
// 然后是第三个元素c,short类型需要2字节对齐(short类型元素必须放在类似0,2,4,8这样的
// 地址处,不能放在1,3这样的奇数地址处),因此c不能紧挨着b来存放,解决方案是在b之后添加1
// 字节的填充(padding),然后再开始放c。c放完之后还没结束
// 当整个结构体的所有元素都对齐存放后,还没结束,因为整个结构体大小还要是4的整数倍。
struct mystruct0     //一字节对齐         //四字节对齐
{                
	int   a;         // 4                 // 4
	char  b;         // 1                 // 2
	short c;         // 2                 // 2
};

struct mystruct1 
{                    
	char  a;        // 1                  // 4
	int   b;        // 4                  // 4
	short c;        // 2                  // 4
};

int main (void)
{                                                                          // 结果
	printf("sizeof(struct mystruct0) = %d.\n", sizeof(struct mystruct0));  // 8
	printf("sizeof(struct mystruct1) = %d.\n", sizeof(struct mystruct1));  // 12

}

(三)结构体对齐指令

1.gcc支持但不推荐的对齐指令:#pragma pack() #pragma pack(n) (n=1/2/4/8)

  1. #pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候我不希望对齐方式是4,而希望是别的(譬如希望1字节对齐,也可能希望是8,甚至可能希望128字节对齐)。
  2. 常用的设置编译器编译器对齐命令有2种:第一种是#pragma pack(),这种就是设置编译器1字节对齐(有些人喜欢讲:设置编译器不对齐访问,还有些讲:取消编译器对齐访问);第二种是#pragma pack(4),这个括号中的数字就表示我们希望多少字节对齐。
  3. 我们需要#pragma pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n。
  4. #prgma pack的方式在很多C环境下都是支持的,但是gcc虽然也可以不过不建议使用。
  • 实例
#include <stdio.h>
//使用#pragma pack(n)对齐指令
#pragma pack(2)  //两字节对齐
struct stu
{
	char  a;
	int   b;
	short c;
};
#pragma pack()
//编译器默认,不使用#pragma pack(n)对齐指令
struct stu1
{
	char  a;
	int   b;
	short c;
};

int main (void)
{                                                                 // 结果
	printf("sizeof(struct stu) = %d.\n", sizeof(struct stu));     // 8
	
	printf("sizeof(struct stu1) = %d.\n", sizeof(struct stu1));   // 12
	
	return 0;
}

2.gcc推荐的对齐指令__attribute__((packed)) __attribute__((aligned(n)))

  1. __attribute__((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
  2. __attribute__((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)
  • 实例
#include <stdio.h>
struct stu
{
	char  a;
	int   b;
	short c;
}__attribute__((packed));      //取消对齐优化

struct stu1
{
	char  a;
	int   b;
	short c;
}__attribute__((aligned(2)));   //设置两字节对齐
/*
struct stu2
{
	int  a;
	char   b;
	short c;
}__attribute__((aligned(3)));     //error :requested alignment is not a positive power of 2
*/                                //__attribute__((aligned(n))); n需要是2的正次幂
struct stu3
{
	int  a;
	char   b;
	short c;
}__attribute__((aligned(16))) s2; //设置16字节对齐

struct stu4
{
	int  a;
	char   b;
	short c;
};

int main (void)
{                                                                 // 结果
	printf("sizeof(struct stu) = %d.\n", sizeof(struct stu));     // 7
	
	printf("sizeof(struct stu1) = %d.\n", sizeof(struct stu1));   // 12
	
	printf("sizeof(struct stu3) = %d.\n", sizeof(struct stu3));   // 16

	struct stu4 __attribute__((aligned(16))) s1 ;                 //定义变量时使用无效
	printf("sizeof(struct stu4) = %d.\n", sizeof(s1));            // 8
	
	printf("sizeof(struct stu4) = %d.\n", sizeof(s2));            // 16
	return 0;
}

(四)offsetof宏与container_of宏

  • 由结构体指针进而访问各元素的原理
    • 通过结构体整体变量来访问其中各个元素,本质上是通过指针方式来访问的,形式上是通过.的方式来访问的(这时候其实是编译器帮我们自动计算了偏移量)。

1.offsetof宏

  1. offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。
  2. offsetof宏的原理:我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。
  3. 学习思路:第一步先学会用offsetof宏,第二步再去理解这个宏的实现原理。
  • #define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
  • (TYPE *)0 这是一个强制类型转换,把0地址强制类型转换成一个指针,这个指针指向一个TYPE类型的结构体变量。 (实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错)。
  • ((TYPE *)0)->MEMBER (TYPE *)0是一个TYPE类型结构体变量的指针,通过指针指针来访问这个结构体变量的member元素
  • &((TYPE *)0)->MEMBER 等效于&(((TYPE *)0)->MEMBER),意义就是得到member元素的地址。但是因为整个结构体变量的首地址是0,
  1. 实例

2.container_of宏

  1. 作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。
  2. typeof关键字的作用是:typepef(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。
  3. 这个宏的工作原理:
#define container_of(ptr, type, member) ({			\
	const typeof(((type *)0)->member) * __mptr = (ptr);	\
	(type *)((char *)__mptr - offsetof(type, member)); })
  • 第一步,首先定义一个临时的数据类型(通过typeof( ((type *)0)->member )获得)与ptr相同的指针变量__mptr,然后用它来保存ptr的值。
  • 第二步,用(char *)__mptr减去member在结构体中的偏移量,得到的值就是整个结构体变量的首地址(整个宏的返回值就是这个首地址)。
  1. 实例
#include <stdio.h>

// TYPE是结构体类型,MEMBER是结构体中一个元素的元素名
// 这个宏返回的是member元素相对于整个结构体变量的首地址的偏移量,类型是int
#define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)

// ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素的元素名
// 这个宏返回的就是指向整个结构体变量的指针,类型是(type *)

#define container_of(ptr, type, member) ({			\
	const typeof(((type *)0)->member) * __mptr = (ptr);	\
	(type *)((char *)__mptr - offsetof(type, member)); })


struct mystruct
{
	char  a;
	int   b;
	short c;
};

int main(void)
{
	struct mystruct my1;
	my1.a = 12;
	my1.b = 44;
	my1.c = 55;
	
	struct mystruct *pMy1 = NULL;
	pMy1 = &(my1.b);
	printf("my1.b的地址是 %p .\n", pMy1);
	printf("结构体的首地址是 %p .\n", container_of(pMy1, struct mystruct, b));
	// my1.b的地址是 0x7ffd34d35fe0 .
	// 结构体的首地址是 0x7ffd34d35fdc .
    // 0x7ffd34d35fe0 + 4 = 0x7ffd34d35fdc
	
	
	return 0;
}
发布了21 篇原创文章 · 获赞 6 · 访问量 415

猜你喜欢

转载自blog.csdn.net/weixin_44112805/article/details/105210071