一 : 结构体概述
1 :结构体类型是一种自定义类型
(1)C语言的两种类型:原生类型和自定义类型
2 :结构体使用时先定义结构体类型再用类型定义变量
(1)结构体需要先定义结构体类型再用类型定义变量
#include <stdio.h>
struct s1 //定义结构体类型
{
char name[20];
int age;
};
int main(void)
{
struct s1 p1; //定义结构体变量
p1.age = 12;
printf("p1.age = %d\n",p1.age);
return 0;
}
(2)也可以在定义结构体的同时定义结构体变量
//定义结构体的同时定义结构体变量
#include <stdio.h>
struct s1 //定义结构体
{
char name[20];
int age;
};
struct s2 //定义结构体的同时定义结构体变量
{
char name[20];
int age;
}p2;
int main(void)
{
struct s1 p1; //定义结构体变量
p1.age = 12;
p2.age = 15;
printf("p1.age = %d\n",p1.age);
printf("p2.age = %d\n",p2.age);
return 0;
}
#include <stdio.h>
//将类型struct student 重命名为s1,s1是一个类型名,而不是变量
typedef struct student
{
char a[20];
int age;
}s1;
int main(void)
{
return 0;
}
3 :从数组到结构体的进步之处
(1)结构体可以认为是从数组发展而来的。其实数组和结构体算是数据结构的范畴了,数组就是最简单的数据结构、结构体比数组更复杂一些,链表、哈希表又比结构体复杂了一些;二叉树、图等又复杂一些
(2)数组有2个明显的缺陷:第一个是定义时必须给定明确的大小,且这个大小在以后不能更改;第二个是数组要求所有的元素类型必须一致。更复杂的数据结构就致力于解决数组的这两个缺陷
(3)结构体可以理解为是用来解决数组的第二个缺陷的,可以将结构体理解为一个其中元素类型可以不相同的数组。其实结构体可以完全取代数组,只是在数组可用的范围内数组比结构体更简单。
//结构体和数组存储相同的数据
#include <stdio.h>
struct s1
{
int a;
int b;
int c;
};
int main(void)
{
int a[3] = {67,89,72}; //数组存储三个学生的分数
struct s1 p1; //结构体存储三个学生的分数
p1.a = 67;
p1.b = 89;
p1.c = 72;
printf("a[0] = %d\na[1] = %d\na[2] = %d\n",a[0],a[1],a[2]); //输出结果67,89,72
//输出结果67,89,72
printf("p1.a = %d\np1.b = %d\np1.c = %d\n",p1.a,p1.b,p1.c);
return 0;
}
4 :结构体变量中的元素如何访问?
(1)数组中元素的访问方式:表面上有两种方式(数组下标方式和指针方式);实质上都是指针方式访问
(2)结构体变量中的元素访问方式:只有一种:用.或者->的方式来访问(.和->访问结构体的实质其实是一样的,只是C语言规定用结构体变量来访问元素用.,用结构体变量的指针来访问用->。实际上在高级语言中已经不区分了都用.)
(3)结构体的访问方式有点类似于数组下标的访问方式
思考 :结构体变量的.或者->的访问方式的实质是什么?其实本质上还是用指针来访问的
struct s1
{
int a;
int b;
int c;
};
p1.a = 67; 编译器内部还是转成指针方式访问 int *p = s; *(p+0) = 67;
p1.b = 89; *(p+1) = 89;
p1.c = 72; *(p+2) = 72;
//指针式访问和.访问对比
#include <stdio.h>
struct s4
{
int a;
double b;
char c;
};
int main(void)
{
struct s4 mystruct ;
mystruct.a = 12; //int *p = (int *)&s1; *p = 12;
mystruct.b = 4.44;
mystruct.c = 'a';
printf("mystruct.a = %d\nmystruct.b = %lf\nmystruct.c =
%c\n",mystruct.a,mystruct.b,mystruct.c);
int *o = (int *)&mystruct;
*o = 12;
printf("*o = %d\n",*o);
double *e = (double *)(&mystruct+4);
*e = 12.44;
printf("*e = %lf\n",*e);
char *u = (char *)(&mystruct+12);
*u = 'q';
printf("*u = %c\n",*u);
}
二 :结构体的对齐访问
1 :举例说明什么是结构体对齐访问
(1)上面讲过结构体中元素的访问其实本质上还是用指针方式,结合这个元素在整个结构体中的偏移量和这个元素的类型来进行访问的
(2)实际上结构体元素的偏移量比我们上面讲的还要复杂,因为结构体要考虑元素的对齐访问,所以实际上占的字节数和本身的类型所占的字节数不一定完全一样。(譬如char c 实际所占的字节数可能是1,可能是2,也可能是3也可能是4......)
(3)一般来用.的方式来访问结构体元素时,是不用考虑结构体的元素对齐的。因为编译器会帮忙处理这个细节,但是因为C语言本身是一个很底层的语言,而且做嵌入式开发经常需要从内存的角度,以指针方式来处理结构体以及其中的元素,因此还是要需要掌握结构体对齐规则
2 :结构体为何要对齐访问
(1)结构体中元素对齐访问主要是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率,否则会大大降低效率
(2)内存本身是一个物理器件(DRR内存芯片,soc上的DRR控制器),本身有一定的局限性
(3)还要很多其他的因素和原因,导致需要对齐访问。譬如Cache的一些缓存特性,还有其他硬件(譬如MMU、LCD显示屏)的一些内存依赖特性,所以会要求内存对齐访问
(4)对比对齐访问和不对齐访问:对齐访问牺牲了内存空间,换取了速度性能;不对齐访问牺牲了速度性能,换取了对内存的完全应用
3:编译器对齐规则
(1)编译器本身可以设置内存对齐的规则,有以下的规则需要记住:
第一个:32位编译器,一般编译器默认对齐方式是四个字节对齐
#include <stdio.h>
//分析过程:
//首先是整个结构体,整个结构体变量4字节对齐是由编译器保证的,程序员本身不用操心
//然后是第一个元素a,a的开始地址就是整个结构体的开始地址,所以自然是4字节对齐的,但是a的
//结束地址要由下一个元素说了算
//然后是第二个元素b,因为上一个元素a本身就占四个字节,本身就是对齐的。所以留给b的开始地址也是
//4字节对齐地址,所以b可以直接放(b放的位置就决定了a一共占4个字节,因为不需要填充)
//b的起始地址确定了,结束地址不能确定(可能需要填充),结束地址要看下一个元素来决定,然后是第
//三个元素c,short类型需要两字节对齐(short类型元素必须放在0、2、4、8这样的地址处),因此c不能
//紧贴着b来放,解决方案是在b之后添加一字节的填充(padding),然后再开始放c,c放完之后还没有
//结束。当整个结构体的所有元素都对齐存放后,还没有结束,因为整个结构体大小还要是4的整数倍,如果
//不是4的整数倍,则需要填充到4的整数倍为止
struct mystruct
{
//一字节对齐 //四字节对齐
int a; 4 4
char b; 1 2
short c; 2 2
};
//分析
//结构体对齐是4字节对齐的
//首先看第一个元素a(也是结构体的首地址),元素a是char类型的元素所以本身是1字节对齐的,但是由于下
//一个元素是int类型(4个字节),所以第一个元素除了本身占一个字节之外还需要三个字节的填充。由于第
//第一个元素a需要填充三个字节(牺牲内存来提高内存访问的效率和性能),所以第二个元素b的起始地址
//是从4开始的,又因为int本身就占四个字节,所以b的结束地址不需要填充所占的内存就是4个字节
//第三个元素c就可以紧跟着元素b后面开始存放,然后又因为结构体本身需要是4字节的倍数,虽然元素c本身
//只有只需要两个字节但是还是需要填充两个字节总共就是12个字节刚好是4的倍数完成结构体对齐
typedef struct mystruct1
{
//一字节对齐 //四字节对齐
char a; 1 4
int b; 4 4
short c; 2 4
}mys1;
typedef struct mystruct2
{
//一字节对齐 //四字节对齐
int a; 4 4
struct mystruct s1; 7 8 //结构体本身按四字节对齐
double b; 8 8
int c; 4 4
}mys2;
struct stu
{
//一字节对齐 //四字节对齐
char sex; //1 //4
int length; //4 //4
cahr name[10]; //10 //12
};
int main(void)
{
//输出打印sizeof计算的大小来证明猜测
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct));
printf("sizeof(mys1) = %d\n",sizeof(mys1));
printf("sizeof(mys2) = %d\n",sizeof(mys2));
printf("sizeof(struct stu) = %d\n",sizeof(struct stu));
return 0;
}
总结结构体对齐的分析要点和关键:
(1)结构体对齐要考虑:结构体整体本身必须安置在4字节对齐处,结构体对齐后的大小必须是4的倍数(编译器设置为4字节对齐时,如果编译器设置为8字节对齐,则这里的4是8)
(2)结构体中的每个元素本身都必须对齐存放,而每个元素本身都有自己的对齐规则
(3)编译器考虑结构体存放时,以满足以上两点要求的最少内存需要的排布来计算
4 :gcc支持但不推荐的对齐指令 :#pragma pack() #pragma pack(n) (n=1/2/4/8)
(1)#pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候不希望是4,而希望是别的(譬如希望是 1字节对齐,也可能是8,甚至可能是128字节对齐)
(2)常用的设置编译器对齐命令有两种:
第一种是:#pragma pack(),这就是编译器一字节对齐(编译器不对齐访问或取消编译器对齐访问)
第二种是:#pragma pack(4),这个括号中的数字就表示希望多少字节对齐
(3)需要#pragma pack (n)开头,以#pragma pack()结尾,定义一个区间,这个区间的对齐参数就是n
#include <stdio.h>
#pragma pack(1)
struct mystruct
{
char a;
int b;
char c[10];
};
#pragma pack()
#pragma pack(2)
struct mystruct1
{
char a;
int b;
char c[10];
};
#pragma pack()
#pragma pack(4)
struct mystruct2
{
char a;
int b;
char c[10];
};
#pragma pack()
int main(void)
{
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct)); //15
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct1)); //16
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct2)); //20
return 0;
}
(4)#pragma pack的方式在很多C语言环境下都是支持的,gcc虽然也可以不过不建议使用
5 :gcc推荐的对齐指令 __attribute__((packed)) __attribute__((aligned(n)))
(1) __attribute__((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问
(2) __attribute__((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让结构体变量整体进行n字节对齐(注意是结构体变量整体进行n字节对齐,而不是结构体内部元素也要n字节对齐)
#include <stdio.h>
struct mystruct
{
char a;
int b;
char c[10];
}__attribute__((packed));
struct mystruct1
{
char a;
int b;
char c[10];
}__attribute__((aligned(1)));
struct mystruct2
{
char a;
int b;
char c[10];
}__attribute__((aligned(2)));
struct mystruct3
{
char a;
int b;
char c[10];
}__attribute__((aligned(4)));
struct mystruct4
{
char a;
int b;
char c[10];
}__attribute__((aligned(8)));
struct mystruct5
{
char a;
int b;
char c[10];
}__attribute__((aligned(1024)));
int main(void)
{
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct)); //15
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct1)); //20
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct2)); //20
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct3)); //20
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct4)); //24
printf("sizeof(struct mystruct) = %d\n",sizeof(struct mystruct5)); //1024
return 0;
}