C语言-结构体深度解析

本文详细介绍了C语言中结构体的声明、变量定义、初始化方法,包括结构体成员的访问方式,匿名结构体、自引用的处理,以及内存对齐规则和结构体传参的性能优化。重点讨论了内存对齐的原因和结构体地址传参的效率提升。
摘要由CSDN通过智能技术生成

结构体简介:结构体是一种复杂类型,C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类 型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要 名字、年龄、学号、⾝⾼、体重等;描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问 题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。

本节要点如下: 

1.结构体的声明  2.结构体变量定义及其初始化  3.结构体成员的访问

4.匿名结构体类型  5.结构体的自引用  6.结构体内存对齐 7.结构体传参

1.结构体的声明

基本格式
struct tag
{
 member-list;
};

举例:
struct stu
{
	char* name;
	int age;
	char* sex;
	double score;
};//分号不能丢(一般vs自动打出来)

2.结构体变量定义及其初始化

1.结构体变量的定义
struct person
{
	char* name;
	int age;
	char* sex;
}p1;//声明结构体变量的同时直接定义变量

struct person p2;//定义一个结构体变量

2.结构体变量的初始化
struct person p3 = { "zhangsan",20,"男" };
//按照顺序初始化
struct person p4 = { .age = 20,.name = "wangwu",.sex = "男" };
//不按照顺序初始化

3.嵌套结构体定义及其初始化
struct Node
{
	int data;
	struct person p1;
	struct Node* next;
}s = { 20,{"lihua",18,"女"},NULL };
//嵌套结构体初始化,记得嵌套的那个用大括号括起来
这里面还定义了一个结构体指针,这其实是数据结构链表的相关思路,后续再说吧

3.结构体成员的访问(两种方式)

01.用"  .  "操作符的直接访问(结构体变量)

举例
struct stu ps1 = { "liming",18,"女",91.5 };
printf("%s %d %s %.1lf", ps1.name, ps1.age, ps1.sex, ps1.score);
运行结果
liming 18 女 91.5

02.用" -> "操作符的间接访问(结构体指针)

举例
struct stu ps1 = { "liming",18,"女",91.5 };
struct stu* ps = &ps1;
printf("%s %d %s %.1lf", ps->name, ps->age, ps->sex, ps->score);
运行结果
liming 18 女 91.5

特别注意事项:

寻址操作符(->)与点操作符(.)优先级都要大于寻址操作,所以必要情况下

应该先转型,后寻址,例子如下:

int struct_cmp_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);
    return strcmp(*((struct stu*)p1)->name, *((struct stu*)p2)->name);
}
这里我们举出的例子是qsort函数比较结构体字符串大小传过去的那个函数
详见函数指针与qsort函数
前者是用->操作符间接访问,后者使用.操作符直接访问

4.匿名结构体类型

1.2 结构的特殊声明
在声明结构的时候,可以不完全的声明。
⽐如:
//匿名结构体类型
struct
{
 int a;
 char b;
 float c;
}x;
struct
{
 int a;
 char b;
 float c;
}a[20], *p;

编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。

所以我的建议是不要经常使用这种匿名结构体类型

5.结构体的自引用

例如当我们定义一个链表的节点的时候
struct Node
{
	int data;
	struct Node next;
};
思考这样合理么???,并思考下面代码的执行结果
int main()
{
  printf("%zd ",sizeof(struct Node));
  return 0;
}

当我们执行这段代码的时候,我们的编译器就会报错,其实也很好理解,如果可以这样定义,一个结构体内部嵌套自己接着又会嵌套自己,无穷无尽,所以本质上实际算不出来大小,也就是我们无法嵌套自己定义结构体

但是我们可以通过嵌套一个自身的结构体指针

struct Node
{
	int data;
	struct Node* next;
};这种就是可以的

这样子就是合法的,具体如何实现链表等到后续再说吧

6.结构体内存对齐(计算结构体的大小)

01.有四大结构体内存对齐的规则

1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处

2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。 对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。VS 中默认的值为 8 - Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍.

4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数( 而维持含嵌套结构体中成员的对⻬数)的整数倍。

理解了这几大规则,我们直接举一个例子

struct S1
{
 char c1;
 int i;
 char c2;
};
可得知该结构体大小是12,那为什么是12呢?

我先来解释以下这个图,左边的数字是相对于初始位置的偏移量,比如第一块空间就是0,也就是偏移量是0,黑色是未利用的空间

因为第一个成员为char类型大小也就是1, 自动跟起始位置对齐, 也就是图中的蓝格子, 第二个成员为int 类型,大小为4字节,而 vs默认的对齐数是8,取小的那个也就是4,填充在4的整数倍也就是从偏移量为4处开始填充4字节(红色), 接下来的类型是char 为1字节, 而系统默认是8, 取小的那个也就是1,所以填充1的倍数,所以填充8即可,但是我们最终的总大小是最大对齐数的倍数,填充到这里也就填充了9个字节,不是4(这里的最大对齐数),继续填充至11,此时填充了12个格子,也就是12bytes,就是最终大小

总结一下

第一个紧贴初始位置填充,剩下的看偏移量,但是最后算的是填充的格子

02.offsetof计算偏移量

#include<stdio.h>
#include<stddef.h>
//offsetof()这个宏列属于头文件#include<stddef.h>
struct S1
{
 char c1;
 int i;
 char c2;
};
int main(){
  printf("%d %d %d", offsetof(struct, c1), offsetof(struct, i), offsetof(struct, c2));
  return 0;
}
程序运行结果是 0 4 8 与上面的图保持一致

03.#pragema pack (number)

修改默认对齐数为number

#include<stdio.h>
#include<stddef.h>

#pragma pack(1)//括号里面是多少就把默认对齐数置为多少
struct S1
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//恢复默认对齐数

int main(){
  printf("%d %d %d", offsetof(struct, c1), offsetof(struct, i), offsetof(struct, c2));
  return 0;
}
运行结果为0 1 5(其实也就是按着连续存放就行)

04.为什么要内存对齐

以下是大部分文献的解释:

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

7.结构体传参

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 和 print2 函数哪个好些?

答案是:⾸选print2函数。

原因: 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

结论: 结构体传参的时候,要传结构体的地址。

C语言中的结构体是一种用户自定义的数据类型,用于将不同的数据类型组合在一起,形成一个新的数据类型。结构体由多个成员变量组成,每个成员变量可以是不同的数据类型,如整型、字符型、浮点型等。 定义结构体采用关键字"struct",后面跟结构体名称和成员变量列表,每个成员变量由数据类型和成员名组成。结构体的定义通常放在函数外部,全局可访问。 使用结构体需要先定义变量,也就是实例化结构体。定义变量时使用结构体名称,并在后面加上变量名。可以通过"."运算符访问结构体的成员变量,对结构体成员变量进行读写操作。 结构体可以作为函数参数传递,可以作为函数返回值返回,可以作为数组元素使用。在函数中传递结构体时,可以将结构体作为参数传递给函数,也可以将结构体指针传递给函数。 结构体还可以嵌套定义,也就是将一个结构体作为成员变量添加到另一个结构体中,形成嵌套结构体。可以通过"."运算符访问嵌套结构体的成员变量。 结构体在实际应用中有广泛的用途,可用于表示复杂的数据结构,如链表、树等,也可用于表示具有多个属性的实体,如学生、员工等。 总而言之,结构体C语言中一种强大的数据类型,通过结构体可以将多种不同类型的数据组织在一起形成新的数据类型,为程序提供了更大的灵活性和可扩展性。通过合理的设计和使用,结构体可以极大地简化程序的编写和维护过程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值