您的位置:首页 > 汽车 > 新车 > C语言之结构体

C语言之结构体

2024/9/16 3:46:42 来源:https://blog.csdn.net/x_p96484685433/article/details/141866856  浏览:    关键词:C语言之结构体

目录

前言

一、结构体类型的声明

二、结构体变量的创建和初始化

三、结构体的自引用

四、结构体的内存对齐

五、结构体传参

六、结构体实现位段

总结



前言

        本文包括结构体的声明、初始化、自引用等基础知识,另外包括结构体的内存对齐,结构体实现位段。


一、结构体类型的声明

1. 声明:

struct tag

{

        member-list;

}variable-list;

  • struct:结构体关键字
  • tag:结构体标签,也就是该结构体类型的名字
  • member-list:成员列表,结构体可以有多个成员,并且每个成员变量可以是不同的类型
  • variable-list:变量列表,可以在声明时实例出多个该结构体类型的变量,也可省略。

例如描述一个学生:

#include <stdio.h>struct Stu
{char name[20]; //姓名int age; //年龄char sex[5]; //性别char id[20]; //学号
}a, b, c;int main()
{return 0;
}

2.结构体的特殊声明

在声明结构体时,可以不起类型名,也叫匿名结构体

#include <stdio.h>//匿名结构体
struct
{char name[20];int age;char sex[5];char id[20];
}a, b, c;int main()
{return 0;
}

注意:

  1. 匿名结构体只能在声明时取变量名,也就是a,b,c,后续再无法使用该类型实例出变量
  2. 匿名结构体适合只需要用一次,后续不再使用的场景


二、结构体变量的创建和初始化

结构体变量的创建方式:

  1. 第一种创建方式就是上述在声明结构体类型时创建
  2. 第二种就是使用 struct+结构体类型名+变量名

结构体初始化方式:

  1. 按照结构体成员的顺序初始化
  2. 按照指定的顺序初始化

如:

#include <stdio.h>struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号
};int main()
{//按照结构体成员的顺序初始化struct Stu s = { "张三", 20, "男", "20230818001" };printf("name: %s\n", s.name);printf("age : %d\n", s.age);printf("sex : %s\n", s.sex);printf("id  : %s\n", s.id);//按照指定的顺序初始化	struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女" };printf("name: %s\n", s2.name);printf("age : %d\n", s2.age);printf("sex : %s\n", s2.sex);printf("id  : %s\n", s2.id);return 0;
}


三、结构体的自引用

首先,带着这样的一个疑问:结构体能不能将自己作为一个成员变量?

答案:不能直接嵌套一个相同的结构体,但是可以使用指针间接包含一个结构体

以下为错误写法:

//结构体中有相同的结构体,无限递归,无法计算内存
struct Node
{int data;struct Node next;
};

以下为正确写法:

struct Node
{int data;struct Node* next;
};

另外,使用typedef重命名结构体时,如果结构体中自引用了结构体指针,不能以重命名的名字声明

以下为错误写法:

//因为重命名是在程序编译后生效,提前使用是不行的
typedef struct Node
{int data;Node* next;
}Node;

以下为正确写法

typedef struct Node
{int data;struct Node* next;
}Node;

另外匿名结构体是不支持自引用的


四、结构体的内存对齐

结构体内存对齐,也就是结构体中的成员变量在内存中是如何存储的,我们都知道数组在内存中是一块连续的内存区域,结构体呢?

对齐规则:

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
  2. 其他成员变量要对齐到对齐数的整数倍的地址处。
  3. 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

偏移量:结构体在内存中的起始地址,各个成员变量的地址相对于这个地址的距离,通常第一个成员变量的地址就是该结构体的起始地址,所以第一个成员变量的偏移量为0。

  • 扩展:offsetof宏可计算结构体各成员的偏移量,详细可自行查询

对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值。

编译器默认对齐数:

  • VS中默认的是8
  • Linux中gcc没有默认对齐数,对齐数就是成员自身的大小

引例:计算两个结构体大小

#include <stdio.h>struct S1
{char c1;char c2;int n;
};struct S2
{char c1;int n;char c2;
};int main()
{struct S1 a = { 0 };struct S2 b = { 0 };printf("S1: %zu\n", sizeof(a));printf("S2: %zu\n", sizeof(b));return 0;
}

运行结果:

仅仅因为两结构体成员变量的排布顺序不一样,就导致占用内存不同。

现在我们就可以使用上面的对齐规则来手动计算上面两个结构体的大小,验证结果

只需要记住,除第一个成员变量外,其余成员变量的偏移量为对齐数的整数倍,而中间空的是不储存数据的

另外需要注意的是结构体的最终大小是由最大对齐数决定的,即使浪费了内存它的最终大小也是这么多,S1储存全部数据后占了8字节,刚好为最大对齐数的整数倍,因此S1为8,而S2存储完后为9,所以S2为12。

由此我们也能看出,结构体中良好的成员排布能省下不少内存,所以我们声明变量时可以将小的类型放前面,大的数据类型放后面,能较大节省空间。

练习:

#include <stdio.h>struct S3
{double d;char c;int i;
};struct S4
{char c;struct S3 s3;double d;
};int main()
{struct S3 a = { 0 };struct S4 b = { 0 };printf("S3: %zu\n", sizeof(a));printf("S4: %zu\n", sizeof(b));return 0;
}

运行结果:

解:

1. 为什么存在内存对齐?

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

总体来说:结构体的内存对齐是拿空间来换取时间的做法。

2.默认对齐数是可以修改的

#pragma 这个预处理指令,可以改变编译器的默认对齐数。

#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1struct S
{char c1;int i;char c2;
};#pragma pack()//取消设置的对⻬数,还原为默认int main()
{//输出的结果是什么?printf("%d\n", sizeof(struct S));return 0;
}

运行结果:

解:因为对齐数是默认对齐数与数据自身大小的较小值,默认对数数被修改为1,因此对齐数就为1,所以结果为6.


五、结构体传参

结构体传参实际也可以和数组一样分为:传值传参传址传参

和数组不一样的是:

  • 传值传参:访问结构体成员使用操作符 .
  • 传址传参:访问结构体成员使用操作符 ->

示例:

#include <stdio.h>struct S
{int data[1000];int num;
};
struct S s = { {1,2,3,4}, 1000 };//结构体传参
void print1(struct S s)
{printf("%d\n", s.data[3]);printf("%d\n", s.num);
}//结构体地址传参
void print2(struct S* ps)
{printf("%d\n", ps->data[3]);printf("%d\n", ps->num);
}int main()
{print1(s);  //传结构体print2(&s); //传地址return 0;
}

运行结果:


六、结构体实现位段

1.什么是位段

位段的声明和结构体是类似的,但是有两个不同:

  1. 位段的成员必须是 int 、 unsigned int 或 signed int ,在C99中位段成员的类型也可以选择其他类型。
  2. 位段的成员名后边有⼀个冒号和⼀个数字。

位段其实就是为了节省内存的一种结构体

示例:

#include <stdio.h>struct A
{char _a : 3;char _b : 4;char _c : 5;char _d : 4;
};int main()
{struct A a = { 0 };a._a = 10;a._b = 12;a._c = 3;a._d = 4;printf("A: %zu\n", sizeof(struct A));return 0;
}

运行结果:

  1. 成员冒号后的数字:代表着占有几个比特位(bit)
  2. 成员前面的_符号:只是与普通结构体成员做一个区分
  3. 需要注意的是:一个字节=8个比特位,因此冒号后的数字不能超过变量本身大小

为什么位段A占3个字节?

解:位段的内存分配

  1. 位段的成员可以是int 、unsigned int 、signed int或者是 char 等类型
  2. 位段的空间上是按照需要一个字节(char)或者4个字节(int)的方式来开辟

如上,成员变量是char类型,因此以一个字节为单位开辟:

如上图假设我们得到二进制数据:0110 0010 0000 0011 0000 1000,我们将其翻译为16进制:62 03 04

使用VS调试观察内存,发现与上述一致,因此该假设成立

2.使用位段需要注意的问题:

  1. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
  2. int 位段被当成有符号数还是无符号数是不确定的。
  3. 位段中最大位的数目不能确定(16位机器最大16,32位机器最大32),写成27,在16位机器会出问题。
  4. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
  5. 当⼀个结构包含两个位段,第⼆个位段成员比较大,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
  6. 位段的几个成员共有同⼀个字节,所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。

总结:跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。位段在网络协议中,减少数据报大小,提高网络畅通性方面有很大作用


总结

        以上就是本文的全部内容,感谢支持

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com