结构体内容介绍(内存对齐)

什么叫做结构体?

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

那么我们知道了什么叫做结构体了,又该怎样书写呢?请看下面这段代码:

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)又是多少呢?

显然,如果您将这段代码拿去测试一下,编译器很快就会提示错误,那么这又是为什么呢?

  1. 这意味着每个 struct Node 结构体会包含一个完整的、内嵌的 struct Node 成员。这种情况下,每个节点会无限递归地包含下一个节点,这样的定义通常是没有意义且无法正确初始化和使用的。
  2. 同时,在此结构体中,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;
}

既然都可以正确的输出,那为什么要有两种方式呢?

  • 传值调用
  1. print1(struct S s)函数接受一个结构体S的副本作为参数。这意味着当调用print1(s)时,会将结构体s的所有内容复制一份传递给函数。
  2. 缺点是如果结构体非常大(如本例中的数组data有1000个整数),复制过程可能会消耗更多时间和内存。
  3. 优点是在函数内部修改结构体不会影响到主调函数中的原始结构体。
  • 传址调用
  1. print2(struct S* ps)函数接受一个指向结构体S的指针作为参数。在调用print2(&s)时,实际传递的是结构体s在内存中的地址。
  2. 通过指针访问结构体时,并不复制整个结构体,因此在处理大型结构体时效率更高,且不需要额外的内存开销。
  3. 优点是可以通过指针直接修改原结构体的内容,但在这个例子中并没有进行修改操作,仅用于读取num字段。
  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值