目录
一、结构体的概念
结构体是一些值的集合,这些值被称作成员变量,结构的每个成员可以是不同类型的变量,所以结构体常用来描述复杂对象。结构体类型不是又系统定义好的,需要我们自己去定义。C语言只是提供了关键字struct来标识所定义的结构体类型。
二、结构体的声明
一般声明
结构体的声明一般由结构体关键字+结构体名称+成员列表组成:
struct tag //struct:结构体关键字 tag:结构体标签
{
member - list; //成员列表
}variable - list; //变量列表(可以省略)
描述一个人如下:
struct Pepole
{
char name[20];//名字
int ID;//身份证号
char sex[10];//性别
int age;//年龄
};//注意这个分号不能丢
特殊声明
结构体声明的时候,可以不完全声明,即把结构体名称省略掉,这种结构体被称为匿名结构体:
//匿名结构体
struct
{
member-list;
}x;
声明一个匿名结构体:
struct
{
char name[20];//名字
int ID;//身份证号
char sex[10];//性别
int age;//年龄
}stu;
由于匿名结构体没有名字,所以不能在程序的其他位置使用该结构体创建的结构体变量,而只能在结构体声明的同时定义结构体,匿名结构体只能使用一次。
typedef的使用(结构体的别名)
我们如果在定义一个结构体变量的时候,觉得书写过于麻烦,我们可以给他定义一个新的名称,
//typedef的使用
typedef struct student
{
char name[20];
int age;
char id[20];
}stu;
int main()
{
stu s1 = { "jark", 30, "204071" };
printf("%s %d %s\n", s1.name, s1.age, s1.id);
这样我们就可以用stu来代替struct student,这样我们就方便结构体的创建,但是这样我们无法在结构体后面创建变量,取而代之的是结构体的别名。
二、结构体变量的定义和初始化
结构体的定义
struct student
{
int age;
char name[20];
}x1;
struct student x2;
struct student x3={18,"make"};
1、先定义一个学生结构类型,x1表示声明类型的同时定义变量x1。
2、x2表示定义结构体变量x2。
3、x3表示,初始化,定义变量的同时赋初值。
结构体的嵌套
struct Point
{
int x;
int y; }p1;
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
三、结构体成员的访问与成员变量的修改
//坐标类型
struct Point
{
int x;
int y;
};
//学生类型
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
int hight;//身高
};
int main()
{
//结构体初始化
struct Point p = { 10,20 };
struct Stu s = { "张三",20,"男",180 };
//结构体成员访问 点操作符
printf("x = % d y = % d\n", p.x, p.y);
printf("%s %d %s %d\n", s.name, s.age, s.sex, s.hight);
s.age=18;
//Stu.name="李四";//err
strcpy(Stu.name,"李四");//对字符串修改只能用strcpy
printf("%s %d %s %d\n", s.name, s.age, s.sex, s.hight);
//结构体成员访问 ->操作符
struct Point* ps = &p;
struct Stu* pt = &s;
printf("x = % d y = % d\n", ps->x, ps->y);
printf("%s %d %s %d\n", pt->name, pt->age, pt->sex, pt->hight);
return 0;
}
注意:1、访问结构体成员有两种方式,结构体变量.结构体成员名,结构体指针->结构体成员名。
2、对字符串的修改只能用strcpy,切勿使用赋值操作符。
四、结构体传参
struct S {
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s) {
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps) {
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0; }
一共有两种传参的方式,一个是传结构体,一个是传结构体的地址,不过要优先选择结构体地址进行传参,这是因为 :
函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。如果传递结构体地址,地址占用内存很小,时间空间都会节省,程序效率会更高。
五、结构体的自引用
错误的自引用方式
struct Node
{
int data;
struct Node next;
};
上面这种结构体声明是错误的,因为struct Node中包含了一个struct Node的 next,它是无限循环的,构成了一个死循环的,我们无法计算该结构体的大小,正确的结构体自引用应该是一个结构体中包含指向该结构体的指针
结构体自引用概念
结构体自引用是在结构体里面,创建一个指向自身类型结构体的指针。
正确的自引用方式
struct Node
{
int data;
struct Node* next;
};
一个结构体中包含了一个指向该结构体的指针,实现了结构体的自引用,同时,由于指针的大小是固定的(4/8个字节),所以该结构体的大小也是可计算的。
使用typedef时自引用方式
typedef struct
{
int data;
Node* next;
}Node;
以上是错误的,因为next虽然是一个指针,但这里的Node并没有定义。typedef是为结构体创建一个别名NODE,可是类型名的作用域是从语句的结尾开始的,在里面是无法使用的,因为没有定义。
正确的自引用方式:
typedef struct Node
{
int data;
struct Node* next;
}Node;
六、结构体的内存对齐(重点)
什么是内存对齐?
简单来说,结构体内存对齐是指我们创建一个结构体变量时,会向内存申请所需的空间,用来存储结构体成员的内容。我们需要计算结构体的大小时需要运用到该知识点。
结构体内存对齐的规则
1. 第一个成员在与结构体变量偏移量为 0 的地址处。2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值 。VS 中默认的值为 83. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
简单结构体求大小:
struct S1
{
char c1; //变量大小为1,默认对齐数为8 -> 对齐数为1
int i; //变量大小为4,默认对齐数为8 -> 对齐数为4
char c2; //变量大小为1,默认对齐数为8 -> 对齐数为1
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
运行结果为什么呢:
先看下图进行解析:
1、我们先求出各个成员的对齐数,char是1,int是4(和VS对齐数8比取最小值),最大对齐数是4。
2、按规则,第一个成员是char类型,对齐到相对于结构体变量起始位置为0的偏移处。
3、第二个成员是int类型占4个字节,但下一个地址偏移处是1,不是它的整数倍,继续往下找,找到4,就从4开始填4个字节,直到7处停止,最后一个是char类型c2,也占一个字节
4、结构体总大小为最大对齐数的整数倍:由于最大对齐数为4,所以总对齐数要为4的倍数,大于9的最小的4的倍数为12,所以整个结构体的大小为12个字节。
嵌套结构体求大小 :
struct S2
{
char c1;
int i;
char c2;
}s3;
struct S4
{
char c3;
struct S2 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
结果:
自己可以尝试画图分析一下,我就不展示了
1、求S4中各个成员的对齐数,char类型是1,因为是嵌套要求它的最大对齐数是4,d是8,所有最大对齐数是8。
2、我们从0开始,char占1字节,直接填。
3、第二个成员是嵌套结构体,要对齐到自己最大对齐数的整数倍处,从开始填12个字节,直到15停止,最后一个是double类型对齐数是8,并占8个字节,16符合条件,从它开始填到24。
4、最后一步是判断填完之后是否是所有最大对齐数的整数倍,24是8的倍数符合条件结束。
为什么存在内存对齐 ?
从上面的例子我们可以看到,结构体内存对齐会浪费一定的内存空间,但是计算机不是要尽可能的做到不浪费资源吗?那为什么还要存在内存对齐呢?关于内存对齐存在的原因,大部分的参考资料是这样说的:
1. 平台原因 ( 移植原因 ) :不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常2. 性能原因 :数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。总体来说:结构体的内存对齐是拿 空间 来换取 时间 的做法
struct S2
{
char c1;
char c2;
int i;
};
这个结构体只占8个空间。
七、C语言offsetof宏的使用
offsetof的介绍
offsetof是c语言中定义的一个用于求结构体成员在结构体中偏移量的一个宏,其对应的头文件是<stddef.h>,offsetof的使用方法跟函数一样,但是它不是函数。
offsetof的参数
#include <stddef.h>
size_t offsetof(type, member);
这个宏会返回一个结构体成员相对于结构体开头的字节偏移量(**经过结构对其之后**):
- type 结构体名称
- 结构体成员名称
这个宏非常有用,由于结构体对其的问题,整个结构体的大小并不是所有成员大小之和,往往要比他们的和大,(当然我们也可以执行结构体按一个字节进行对其),所以利用这个宏可以很好计算出每个结构体成员相对于结构体开头偏移的字节数。
offsetof的使用
#include <stdio.h>
#include <stddef.h> //offsetof对应头文件
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\t", offsetof(struct S1, c1));
printf("%d\t", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
printf("%d\t", offsetof(struct S2, c1));
printf("%d\t", offsetof(struct S2, c2));
printf("%d\n", offsetof(struct S2, i));
return 0;
}
我们观察后可以发现:结构体成员在结构体中的偏移量=结构体的地址-结构体的起始地址
8、修改默认对齐数
因为结构体字节的对齐方式在不同的编译器中不一样,我们可以使用“#pragma pack(num)”命令来修改VS中的默认对齐数。
代码如下:
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
结果是:
在struct S2中,我们通过“#pragma pack(num)”命令把VS默认对齐数设置为1,就是相当于不对齐,使得其大小变为6。