一、结构体的概述
C语言中有两种类型:原生类型和自定义类型。结构体类型是一种自定义类型
1、结构体使用
定义结构体是需要先声明结构体的类型,然后再用结构体类型来定义结构体变量,不过也可以在定义的同时定义结构体变量。如:
# include<stdio.h>
//定义类型
struct people
{
char name[20];
int age;
};
//定义类型的同时定义变量
struct student
{
char name[20];
int age;
}s1;
//将类型struct student 重命名s1 ,s1是一个类型名,不是变量
typedef struct student
{
char name[20];
int age;
}s1;
int main(void)
{
struct people p1; //使用结构体类型定义变量
p1.age = 20;
printf (“p1.age = %d \n”,p1.age);
return 0;
}
2、从数组到结构体的进步之处
结构体可以认为是从数组发展而来的,其实数组和结构体都算是数据结构的范畴了,数组就是最简单的数据结构;结构体比数组更复杂一些;链表,哈希表之类的比结构体有复杂一些;而二叉树、图又更复杂一些。
数组有两个明显的缺陷:
- 第一个是定义时必须明确给出大小,且这个大小在以后不能修改;
- 第二个是数组要求所有的元素类型必须一致。
在更加复杂的数据结构中,就致力于解决数组的这两个缺陷。结构体是用来解决数组的第二个缺陷的,可以将结构体理解为其中元素类型不相同的数组。结构体完全可以取代数组,只是在通常简单的情况下,数组使用起来更为简单方便。
3、结构体变量中的元素如何访问
数组元素的访问方式,表面上看有两种:下标方式和指针方式。但实质上都是指针方式。结构体变量中元素的访问方式只有一种,用句号.
或者箭头->
的方式访问。这两种访问结构体元素的实质是一样的,当使用指针的时候完全可以使用句号访问,只是写法复杂,可行性不强,因此就使用箭头来代替。使得访问形式看起来更加简洁。结构体对于成员的访问本质上还是使用地址进行访问。
二、结构体的对齐访问
上面讲过结构体中元素的访问,本质上使用的还是指针方式,结合这个元素在整个结构体中的偏移量和这个元素的类型来访问。但是实际上结构体的元素的偏移量比我们想想的还要复杂,因为结构体要考虑元素的对其访问,结构体实际占用的字节数与所有成员的字节数的总和不一定相等。
1、结构体为何要对其访问
访问结构体元素时需要对其访问,主要是为了配合硬件,也就是说硬件身物理上的限制,因此对其排布和访问可以提高访问效率。
struct student
{
char name[20];
int age;
};
内存本身是一个物理器件(DDR内存芯片,soc上的DDR控制器),本身有一定的局限性,如果内存每次访问按照4字节访问,那么效率是最高的;如果不对齐访问,效率要低很多。
还有很多别的因素和原因,导致我们需要对其访问。如:cache 的一些缓存特性,还有其他硬件(如MMU、LCD显示器)的一些内存依赖特性,所以会要求内存对齐访问。
2、结构体对齐的规则和运算
编译器本身可以设置内存对齐的规则。32位编译器,一般编译器默认对齐方式是4字节对齐。
1、当编译器结构体设置为4字节对齐时,结构体整体必须从4字节对齐处存放,结构体对齐后的大小必须为4的倍数。如果编译器设置为8字节对齐,则这里的4就是8.
2、结构体中每个元素本身也必须对齐存放。
3、编译器在考虑以上两点的情况下,实现以最少内存来开辟结构体空间。
3、手动对齐
使用 #pragma 进行对齐的就是手动对齐
#pragma用于告诉编译器,程序员自己希望的对齐方式,比如,虽然编译器默认的对齐方式是4字节对齐,但是我们不希望4字节对齐,而是希望实现别的对齐方式,如1字节对齐、8字节对齐,128字节对齐,这个时候就必须使用#pragma进行手动对齐。
常见的设置手动对齐的命令有两种。
- 第一种#pragma pack(),这种就是设置编译器1字节对齐,不过也可以认为是设置为不对齐或者取消对齐。
- 第二种是#pragma pack(4),这个括号中的数字表示希望以多少字节对齐。
我们需要以#pragma pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n.
#include <stdio.h>
#pragma pack(4)
struct struct1
{
int a;
char b;
short c;
};
struct struct2
{
char a;
int b;
short c;
};
struct struct3
{
int a;
struct struct1 s1;
double b;
int c;
}Mys3;
struct stu
{
char sex;
int length;
char name[10];
};
#pragma pack()
int main(void)
{
printf("sizeof(struct struct1) = %d \n",sizeof(struct struct1));
printf("sizeof(struct struct2) = %d \n",sizeof(struct struct2));
printf("sizeof(struct struct3) = %d \n",sizeof(Mys3));
printf("sizeof(struct stu) = %d \n",sizeof(struct stu));
return 0;
}
对于 #pragma pack手动对齐来说,在很多C语言环境下都是支持的自然GCC也支持,如果不是有特殊需求的话,不建议使用。
4、GCC推荐的对齐指令:
- attribute((packed))和_attribute_((aligned(2)))
使用_attribute_((packed))和_attribute_((aligned(2)))时,直接放在类型定义的后面,那么该类型就以指定的方式进行对齐。packed的作用就是取消对齐,aligned(n)表示对齐方式
#include <stdio.h>
struct struct1
{
int a;
char b;
short c;
}_attribute_((packed));
struct struct2
{
char a;
int b;
short c;
}_attribute_((packed));
int main(void)
{
printf("sizeof(struct struct1) = %d \n",sizeof(struct struct1));
printf("sizeof(struct struct2) = %d \n",sizeof(struct struct2));
return 0;
}
#include <stdio.h>
struct struct1
{
int a;
char b;
short c;
}_attribute_((aligned(n)));
struct struct2
{
char a;
int b;
short c;
}_attribute_((aligned(1024)))Mystruct1;
int main(void)
{
printf("sizeof(struct struct1) = %d \n",sizeof(struct struct1));
printf("sizeof(struct struct2) = %d \n",sizeof(struct struct2));
return 0;
}
- _ attribute_((aligned(n)))他的作用是让结构体类在整体上按照n字节对齐。
三、offsetof宏与container_of宏
1、通过结构体指针访问各结构体成员的原理
通过结构体变量来访问其中各个元素,本质上是通过指针的方式来访问的,形式上是句点.
的方式来访问的(这时候其实编译器帮助我们自动计算了偏移量)。
(1)、offsetof宏
offsetof宏的作用:
计算结构中某个元素相对结构体首字节地址的偏移量,其实质是通过编译器来帮我们计算的。
#include<stdio.h>
#define offsetof(TYPE,MEMBER) ((int) & ((TYPE*)0)->MEMBER)
struct Mystruct
{
char a;
int b;
short c;
};
/*
TYPE是结构体类型,MEMBER是结构体中一个元素的元素名
这个宏返回的是MEMBER元素相对整个结构体变量的首地址
的偏移量,类型是int。
*/
int main(void)
{
struct Mystruct s1;
s1.b = 12;
int *P = (int *)((char*)&s1 + 4);
printf("*p = %d\n",*p);//这种方法是自己根据结构体对齐计算得出的。
return 0;
}
offsetof宏的原理:
我们虚拟一个TYPE类型的结构体变量,然后用TYPE,MEMBER的方式来访问MEMBER元素,继而得到MEMBER相对整个变量首地址的偏移量。
简单说明:(TYPE *)0是一个强制类型转换,把0 地址强制转换成一个指针,这个指针指向一个一个TYPE类型的结构体变量,实际上这个结构体变量可能不存在,但是只要不去解引用这个指针就不会出错。
(3)、container_of宏
#define container_of(ptr,type,member)(\
{const typeof(((type* )0)->member) *_mptr = (ptr); \
(type *)((char *)_mptr - offsetof(type,member);)
})