今天我们就来对自定义类型中的结构体进行较为深入的认识。
目录
自定义类型
结构体
对于结构体,我们在此之前已经学会了它的基本应用,那么我们就创建一个结构体进行简单的回顾。
创建结构体
我们来定义一位学生,那么含有的简单元素就有:名字,年龄,性别,成绩。我们对其进行创建:
struct Student { char name[20]; int age; char sex[5]; float score; }s1, s2, s3;//可以在这里来创建变量,为全局变量 int main() { struct Student s4, s5, s6;//也可以在这里创建变量,是局部变量 return 0; }
在此之上,我们还有一种特别的方法对结构体进行创建:创建匿名结构体。那么什么是匿名结构体呢?我们进行举例说明:
struct { char name[20]; char age; char sex[5]; }b1;
我们可以发现,匿名结构体只含有一个struct,因此,这种创建方式是在该结构体只使用一次的时候进行创建,它是无法进行第二次使用的。
结构体的自引用
结构体的自引用很好理解,就是在结构体里面存放另一个结构体的地址。可以通过该地址找到另一个结构体。那么下面我们进行创建并计算结构体的大小:
struct Node { int data; struct Node* n; }; int main() { printf("%zd\n", sizeof(struct Node)); return 0; }
这里如果嫌弃结构体引用名太长的话,我们就对其进行自定义:
typedef struct Node { int data; struct Node* n; }Node; int main() { printf("%zd\n", sizeof(Node)); return 0; }
运行结果如下:
结构体变量的定义和初始化
那么,我们应该如何对结构体进行初始化呢?我们首先先创建两个结构体:
typedef struct Stu //学生 { char name[20]; int age; }Stu; typedef struct Point //二维坐标 { int x; int y; }Point;
我们想对结构体进行初始化,只需要在创建的变量后面使用{}进行赋值就可以了:
int main() { Stu p1 = { "zhangsan",20 }; Point s1 = { 2,4 }; printf("%s %d\n", p1.name, p1.age); printf("(%d,%d)\n", s1.x, s1.y); return 0; }
当然,想不按顺序进行初始化也是可以的,我们只需要先引用再赋值就可以了:
int main() { Stu p1 = { .age = 20,.name = "zhangsan" }; Point s1 = { .y = 4,.x = 2 }; printf("%s %d\n", p1.name, p1.age); printf("(%d,%d)\n", s1.x, s1.y); return 0; }
运行结果如下:
那么如果结构体里面嵌套一个结构体,又应该如何进行初始化呢?其实还是较为简单的,我们只需要两次引用就可以了,就比如:
typedef struct Point //二维坐标 { int x; int y; }Point; typedef struct Node { int data; Point p; struct Node* s; }Node;
我们在对Point引用时,只需要让变量两次引用就可以了:
int main() { Node n = { 100,{2,4},NULL }; printf("%d (%d,%d)\n", n.data, n.p.x, n.p.y);//由于我们赋予了空指针,这里不进行打印 return 0; }
运行结果如下:
结构体内存对齐
所谓结构体内存对齐,就是对结构体大小进行判断。这里有些读者不免会疑惑,结构体的大小的计算不就是把所有类型的大小加起来吗?其实不是,下面我来举个例子:
我们创建两个结构体变量,结构体元素包含两个char类型和一个int类型 :
struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; };
这里的结构体大小是一样的吗?我们编写函数打印:
int main() { printf("%zd\n", sizeof(struct S1)); printf("%zd\n", sizeof(struct S2)); return 0; }
运行结果如下:
大小很明显不同,那么为什么两个结构体明明只是成员类型的顺序不同为什么大小不一样呢?
为了方便理解,这里我们使用需要offsetof,那么,什么是offsetof呢?它是来计算结构体成员相较与起始位置的偏移量的:
我们可以看到,offsetof所需要的参数是结构体和结构体成员,那我们就来进行使用(记得包含头文件哦):
#include<stddef.h> struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; }; int main() { printf("%zd\n", offsetof(struct S1, c1)); printf("%zd\n", offsetof(struct S1, i)); printf("%zd\n", offsetof(struct S1, c2)); return 0; }
运行结果如下:
我们可以发现,c1相较与起始位置的偏移量是0,i是4,c2是8,也就是说,在创建了char类型的c1之后,间隔了4个位置才创建了int类型的i,这又是为什么?我们画图进行分析:
我们可以明显的发现,在c1和i之间是创建了空间给S1的,但是并没有使用,S2同样也是如此:
那这自然与结构体内存对齐有关,那么,为什么C语言要内存对齐,它又是怎么来内存对齐的呢?
我们首先来了解内存是如何进行对齐的:
首先对编译器的规则进行了解:
1)在VS中默认的对齐数值为8
2)对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。我们再对对齐的规则进行了解:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
那么上面在S1中c1与i之间的浪费掉的空间我们就可以理解了,VS默认值是8,那么char和int的大小都是对齐数,所以int要从4开始进行创建
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
那么S1中c2浪费的空间也可以理解了,S2中int的大小4就是最大对齐数,但是在c2结束时才刚刚到9,所有还需要再开辟空间。4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
第4点又该如何理解呢,我们举例说明:
我们创建嵌套的结构体:struct S3 { double d; char c; int i; }; struct S4 { char c1; struct S3 s3; double d; };
我们写出打印S4大小的代码:
int main() { printf("%zd\n", sizeof(struct S4)); return 0; }
那么S4的大小又该是多少呢?我们结合规则4,进行计算:
我们可以计算出S3这个结构体的大小是16,嵌套结构体要对齐到自己成员的最大对齐数,就是double(8),所有在char c1创建之后,struct S3 s3,需要在8处进行创建,而它的大小为16,所以double d,可以直接在后面对齐,但是S3的整体大小是最大对齐数,所以最大对齐数是16,因此S4的大小是32。
运行结果如下:
那编译器为什么要进行内存对齐呢?
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。所以我们在进行使用的时候,我们就需要尽量的节省空间来进行创建,比如:
struct S1 { char c1; char c2; int i; };
相较于:
struct S2 { char c1; int i; char c2; };
更加节省空间一些。
那么这时就会有读者认为VS的默认对齐数也太大了,我可以把默认对齐数改小一点吗?当然是可以的,我们需要使用#pragma 这个预处理指令,来对默认对其数进行修改。
#include<stdio.h> #pragma pack(1)//将默认对齐数修改为1 struct S1 { char c1; int i; char c2; }; #pragma pack()//还原默认对齐数 struct S2 { char c1; int i; char c2; }; int main() { printf("%zd\n", sizeof(struct S1)); printf("%zd\n", sizeof(struct S2)); return 0; }
运行结果如下:
结构体传参
我们所知的传参无非就两种,传值和传地址。那么,哪一种更适合结构体传参呢?我们编写代码来进行分析:
我们首先创建一个结构体:
typedef struct S1 { int data[1000]; int num; }S1; int main() { S1 s = { {1,2,3},20 }; print1(s); print2(&s); return 0; }
接下来我们对打印函数进行编写,我们首先写传值的函数:
void print1(S1 p) { printf("%d %d %d %d\n", p.data[0], p.data[1], p.data[2], p.num); }
这时我们就会发现,我们在进行打印的时候,会再次创建,如果结构体很大的话,会占用很大的内存。接下来对传地址的函数进行编写:
void print2(const S1* ps) { printf("%d %d %d %d\n", ps->data[0], ps->data[1], ps->data[2], ps->num); }
相比起来,传地址就不会过多的占用内存,但是为了防止原数据被修改,我们还是得加上const进行修饰。当然,它们都可以实现打印。
运行结果如下:
小结
今天我们就先对结构体进行较为深入的了解吧, 国庆小编也是狠狠滴,好啦,今天就到这里,我们对C语言的学习还是任重而道远,我们下次再见!