携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情
前言
C语言中除了诸如int、char等等的内置类型,还有自定义类型,主要是这几种:结构体、联合、位段和枚举。
本文就来分享一波作者对自定义类型的学习心得与见解。本篇属于第一篇,主要介绍结构体,后续还有,可以期待一下。
笔者水平有限,难免存在纰漏,欢迎指正交流。
结构体
什么是结构体
结构是一些值的集合,这些值称为成员变量。
结构是一种复合类型,结构的每个成员可以是不同类型的变量。其实可以和数组比较一下,数组是相同类型的元素的集合,而结构体是各种类型(可以不同)元素的集合。
结构的声明
结构中的成员可以是任何一种C类型,也可以是结构。
struct tag
{
member-list;
}variable-list;//分号不能丢
比如:
struct any
{
int a;
char o[10];
int*p;
};
这样就声明了一种结构类型名为struct any,其中包含三个成员。
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}student;
可以直接再结构声明后面加上变量名,在声明结构的同时也创建了该结构类型的变量。在这里,struct Stu
才是完整的类型名。
注意:在声明结构的时候只是在创建一种结构数据类型,并没有定义变量。
结构声明内的成员不能是自己。
struct book
{
int a;
struct book poo;
};//无限套娃了,如果要是可以的话占用内存会无穷大
结构体的自引用是依靠指针实现的。
struct node
{
int date;
struct node *next;//指向下一个相同结构的变量
};
注意别漏了下面花括号后的分号。
这样写代码,可行吗?
typedef struct
{
int data;
Node* next;
}Node;
不行。不过这里还有一个typedef将该匿名结构体重命名成了Node,但是不能在这个结构体内部使用这一名字来声明成员,因为typedef重命名是在后面起作用的,而Node* next
在重命名的前面,所以是不行的。
解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;
特殊的声明
在声明结构的时候,可以不完全的声明。
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上面的两个结构在声明的时候省略掉了结构体标签,这样一来就是一次性声明了,属于匿名结构体,也就是说没有标识,下次还能再用这一类型吗?不能,没有名字怎么使用?
那么问题来了,在上面代码的基础上,下面的代码合法吗?
p = &x;
编译器会把上面的两个声明当成完全不同的两个类型。
所以是非法的。
结构变量的定义
结构声明只是告诉了编译器我要定义的结构的布局,需要定义变量后才会分配内存空间。
既可以在声明结构时创建变量(如n),又可以在结构声明以后再创建变量(如roy)。
struct any
{
int a;
char o[10];
int*p;
}n;
struct any roy;
int a;//struct any 与 int都是数据类型
结构体初始化与内部成员访问
类同数组,结构体的初始化也可以用花括号括起来,不同变量间用逗号隔开。
结构体通过 . 访问内部成员,如k.value。实质上与数组用下标访问数组元素是差不多的。
两个结构体类型中的内部成员名字相同也没关系,因为每个成员属于的是不同的结构体类型。
int a[] = {2, 1, 4};
struct e
{
char o[];
int r;
};
struct k
{
char o[];
struct e ad;
int r;
};
int main()
{
struct k wu = {"hello", {"world", 456}, 123};//嵌套初始化
printf("%d",wu.ad.r);
}
还可以这样写。
struct k wu = {.o = "hello",
.ad = {.o = "world",
.r = 5}
.r = 6 }
结构指针
结构体的指针,声明方式例如struct guy * him;
和数组不同的是,结构数组名并不是结构数组首元素的指针,需要在前面加上&,如him = &bay[0]。
用指针访问成员有两种方法:
1.使用->,比如,him = &bay[0],那么him->a即为bay[0].a,由指针指向对应结构的某成员。
2.使用*,比如,him = &bay[0],那么*him == bay[0]
,则有bay[0].a == (*him).a
,注意必须要用()因为。.
运算符比*运算符优先级要高。
结构体内存对齐
感觉起来内存好像应该是这样算的:
struct e
{
char o[6];//6
int r;//4
};
struct k
{
char o[6];//6
struct e ad;//10
int r;//4
};
int main()
{
struct k wu = {"hello", {"world", 456}, 123};//20
printf("%d",sizeof(wu));
return 0;
}
可结果却是24!
为什么呢?
因为结构体存储遵循一些原则。
对齐规则
1.第一个成员对齐到与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字 (对齐数)的整数倍的地址处。
每个成员的对齐数 = 编译器默认的一个对齐数与该成员内存大小的比较值(取较小值)。
(VS中默认为8,Linux没有默认对齐数概念)
3.结构体总大小为最大对齐数(每个成员都有一个自己的对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构的对齐数)的整数倍。
例1
struct S1
{
char c1;
int i;
char c2;
};
注意计算各成员的对齐数,除了第一个成员以外,其他成员要注意对齐数倍数的偏移量,最后算结构体总大小的时候别忘了对应为最大对齐数的整数倍。
下面各成员对应不同颜色,灰色的代表未被利用的空间。
例2
struct e
{
char o[6]; //6
int r; //4
};
要注意的是,对于数组,其对齐数为数组元素类型的对齐数,比如char o[6]的对齐数就是1。
下面各成员对应不同颜色,灰色的代表未被利用的空间。
例3
struct e
{
char o[6];
int r;
};
struct k
{
char o[6];
struct e ad;
int r;
};
struct k wu;
需要注意的是嵌套结构体的内存对齐,应当把内层结构体看成一个整体,对齐数就是它的成员中的最大对齐数, 然后从对应地址处开始对齐,其内部成员的对齐按照相对偏移量进行。
如图:
我们可以用一个宏offsetof来测试一下:
原型:
offsetof( structName, memberName)
第一个参数是结构体类型名,第二个参数是结构体成员名,用于计算该成员在对应的结构体中的偏移量大小。
用它来测一测例3:
#include<stddef.h>
#include<stdio.h>
struct e
{
char o[6];
int r;
};
struct k
{
char o[6];
struct e ad;
int r;
};
struct k wu;
int main()
{
printf("%d\n", offsetof(struct k, o));
printf("%d\n", offsetof(struct k, ad));
printf("%d\n", offsetof(struct k, r));
return 0;
}
发现都能对的上。
为什么存在内存对齐
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的,某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。
总而言之,结构体的内存对齐是拿空间来换取时间的做法。
例子:
有内存对齐时(图中上部分),读取成员t只需要访问一次,没有内存对齐时(图中下部分),读取成员t需要读两次。
那能不能 “站着把钱给挣了” ——设计结构体既满足对齐原则,又节省了空间?
那我们就让占用空间小的成员尽量集中在一起。
struct s1
{
char i;
char p;
int u;
};
struct s2
{
char i;
int u;
char p;
};
对比一下,明显地s1占用内存更少,假设再定义多两个char类型成员,像s1那样全部放在Int前面,就能减少结构体对齐产生的内存浪费。
还有什么办法没?
修改默认对齐数
#pragma pack(2)//设置默认对齐数为2,一般考虑设置偶数
struct s3
{
char a;
int b;
char c;
};
#prama pack()//取消用户设置,变回默认值8
如果认为对齐方式不合理,可以通过上面方法自己设置对齐方式。
以上就是本文的全部内容了,感谢观看,你的支持就是对我最大的鼓励~