什么叫做结构体?
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
那么我们知道了什么叫做结构体了,又该怎样书写呢?请看下面这段代码:
struct Stu//struct表示这是个结构体
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
匿名结构体
在声明结构的时候,可以不完全的声明,下面这段代码就是一个匿名的结构体:
struct
{
int a;
char b;
float c;
}x;
上面的这个结构在声明的时候省略掉了结构体标签
结构体的自引用
上面我们介绍过结构体可以包含多个不同类型的成员,那么是否可以包含一个结构体呢?不妨让我们继续往下探讨:
struct Node
{
int data;
struct Node next;
};
这段代码包含了它本身,是否可行呢?如果可以,那sizeof(sructt Node)又是多少呢?
显然,如果您将这段代码拿去测试一下,编译器很快就会提示错误,那么这又是为什么呢?
- 这意味着每个
struct Node
结构体会包含一个完整的、内嵌的struct Node
成员。这种情况下,每个节点会无限递归地包含下一个节点,这样的定义通常是没有意义且无法正确初始化和使用的。 - 同时,在此结构体中,
sizeof(struct Node)
会计算包括一个整数值和一个完整嵌套的struct Node
的大小,即每个节点都会包含一个数据字段以及一个完全相同的、嵌套的节点。然后一直自我递归,最后导致编译错误或者栈溢出的情况
那么正确的写法应该是怎样的呢?如下:
struct Node
{
int data;
struct Node* next;// 这里是一个指向 struct Node 类型的指针成员
};
这里的 struct Node* next;
意味着 next
是一个指向 struct Node
类型变量的地址。在链表数据结构中,这种设计很常见,因为每个节点通常需要存储对下一个节点的引用(或称为链接)。通过使用指针,可以节省内存空间并实现动态长度的数据结构。
当一个 struct Node
变量被创建时,next
成员可以被初始化为 NULL
(表示链尾)或者另一个 struct Node
变量的地址,从而形成链表结构。
结构体变量的定义和初始化
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
struct Point p3 = {10,20};声明了一个名为 p3 的 struct Point 类型变量
,并同时赋予了它的 x 成员变量值为 10,y 成员变量值为 20。
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
结构体内存对齐
为什么要有结构体内存对齐这个东西?
答:简单来说结构体内存对齐是为了优化CPU处理数据的速度和保证程序在不同平台上的正确执行
是一种用空间来换取时间的做法。
那么结构体内存对齐又该怎样计算呢?首先我们得掌握结构体内存对齐的规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
Linux中没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面有两个小例子:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
上面这两段代码看似相同(换了个位置),实则在计算的时候有很大的差异,我们直接去计算可以看到结果是这样的:
让我们一起来研究一下为什么会是这样的吧!
这是两张内存对齐的图:
先来讨论S1:c1因为是char类型所以占一个字节,i因为是int类型所以占4个字节 ,但是我们根据上面说的规则二可知,i必须要对齐到某个数字(对齐数)的整数倍的地址处,我是用的vs测试的,所以我就拿vs举例了,在vs中默认对齐数为8,对齐数 = 编译器默认的一个对齐数与该成员大小的较小值,因为int是4个字节,而默认对齐数为8,所以较小值为4,我们则应该在4地址处增加4个字节,因为c2又是char类型占一个字节,而任何数都是1的倍数,且无论怎么比都是1最小,所以在8地址的后面增加一个字节,最后我们得到了9个字节,但是这并没有结束,细心的小伙伴可以看到我们还有规则3,结构体总大小为最大对齐数的整数倍,所以在这里面1 4 1,最大对齐数理所应当是4,可是刚才我们算出来的总大小是9,明显不是4的倍数,我们往后数可以发现12是4的倍数,所以最后我们得到的结果也就是12了!!!(文字版讲解教长,需结合图观看,S2就不在这里重复一遍啦,过程都是差不多的!!!)
结构体传参
结构体传参分为两种,一种是直接传,而另一种则是把地址传过去:
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;
}
既然都可以正确的输出,那为什么要有两种方式呢?
- 传值调用
print1(struct S s)
函数接受一个结构体S的副本作为参数。这意味着当调用print1(s)
时,会将结构体s
的所有内容复制一份传递给函数。- 缺点是如果结构体非常大(如本例中的数组data有1000个整数),复制过程可能会消耗更多时间和内存。
- 优点是在函数内部修改结构体不会影响到主调函数中的原始结构体。
- 传址调用
print2(struct S* ps)
函数接受一个指向结构体S的指针作为参数。在调用print2(&s)
时,实际传递的是结构体s
在内存中的地址。- 通过指针访问结构体时,并不复制整个结构体,因此在处理大型结构体时效率更高,且不需要额外的内存开销。
- 优点是可以通过指针直接修改原结构体的内容,但在这个例子中并没有进行修改操作,仅用于读取
num
字段。