【C语言】结构体———超详解

目录

前言

1.什么是结构体?

2.结构体的声明

3.结构体的特殊声明

4.结构体的自引用及类型重定义

 (1)结构体的自引用

(2)结构体的类型重定义

5.结构体变量的定义与初始化

(1)结构体变量的定义

(2)结构体变量的初始化

(3)结构体变量的嵌套初始化

6.结构体的内存对齐

(1)结构体大小的计算

(2)内存对齐存在的原因

7.修改默认对齐数

(1)更改默认对齐数

(2)更改默认对齐数的好处

8.结构体传参

刹国(结束)!!!


前言

在初识C语言中,我们已经学习了int(整形),long(长整形),long long(长长整形),char(字符型),float(单精度浮点型),double(双精度浮点型)等数据类型,但是有这些数据类型就足够了吗?我们知道描述一个整形数字可以用整形,描述一个小数可以用浮点型,描述一个字符可以用字符型,但是如果让你描述一个人呢?一本书呢?又或是一件物品呢?例如描述一个学生,我们可以通过名字,年龄,性别,学号来做描述。这个时候就需要用到我们今天要探讨的结构体了!

1.什么是结构体?

结构体就是不同数据类型的一些数据的集合,这些数据称为结构体成员。结构体的每个成员可以是不同类型的变量,而这些变量又被称作结构体的成员变量。

2.结构体的声明

 

 struct是结构体的关键字,tag是结构体名,member-list是结构体成员,variable-list是结构体变量。

例如描述一个学生:

struct Student
{
	char name[20];//姓名
	int age;//年龄
	char sex[5];//性别
	char ID[20];//学号
};//分号不能省

上述结构体中,姓名,年龄,性别,学号都是结构体的成员,年龄age又被称作结构体的成员变量。注意:定义结构体时,结构体变量不一定要在结构体末尾的分号前定义,也可以在结构体外部定义,不是非要写在结构体末尾的分号前面,这里后续会介绍。

3.结构体的特殊声明

在声明结构体的时候,可以不完全声明。比如

//匿名结构体
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}arr[20], *p;

 结构体在定义时也可以省略结构体名,此代码中,x是定义的一个结构体变量;a[20]是定义的一个结构数组,该数组可以存放20个包括整形,字符型,浮点型数据在内的结构体类型的信息;p是定义的一个结构体指针。

细心一点你会发现,上面两个结构体的成员都一样,那么问题来了,既然两个结构体是一样的,那么p = &x吗?来看下面这个代码:

//匿名结构体
#include<stdio.h>
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}arr[20], *p;

int main()
{
	p = &x;
	return 0;
}

运行结构如图:

 可以看见这样写会出现类型不兼容的问题,说明编译器会把上面两个声明当做两个完全不同的两个类型。所以p = &x是不成立的

4.结构体的自引用及类型重定义

 (1)结构体的自引用

下面用一个实例来说明结构体的自引用,再说这个实例之前需要先引入几个概念。在数据结构中,我们在定义和存储数据时,如果按照顺序依次存储的这种方式或者这种结构叫做顺序表,而不按照顺序在内存空间内任意位置随意存储的的方式叫做链表。例如我要在内存中存储12345这几个数字,如图:

 如图,当用链表存放数据时,数据在内存中时是杂乱无章的,而想要不丢失对各个数据的访问,就需要保存好每个数据的地址,因此用链表的方式存放数据时,会将其分为两个部分,上半部分用于存储数据,我们称之为数据域,下半部分用于存放数据地址,我们称之为指针域(指针域中存放的是下一个数据的地址)。而这种既包含数据域又包含指针域的结构,称之为结点。回到正题,现在让你定义一个结点,需要怎么做呢?没错,我们既需要包含一个数据,又需要包含一个地址(即指针),是不是正好可以考虑用结构体,而链表中每个数据都是以这样的形式存放数据,我们就需要考虑用一个和它自身相同的结构体定义指针,这样就可以完美的访问到下一个结点中的数据和地址。看代码:

struct Node
{
	int data;//数据域
	struct Node* next;//指针域
};

这就是结构体的自引用,很多小伙伴认为自引用不就是自己引用自己吗?所以很多人就把结构体的自引用写成了这样:

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

这是一个错误的代码,结构体的自引用不能这么写,一定要切记!!!

结构体的自引用(self reference),就是在结构的内部,包含指向自身类型结构体的指针。

上述实例只是为了方便引出结构体的自引用及其使用的场景,若是初学C语言,可以不用掌握,初学者只需知道结构体的自引用是什么,如何使用即可。

(2)结构体的类型重定义

说到类型重定义,你首先想到的是不是typedef呢?没错,结构体的类型重定义同样也是用其进行更改。有时我们在翻看代码,特别是关于结构体的代码时,时常会看到这样的代码:

typedef struct Contact
{
	char name[20];
	int sz;
}contact;

这里typedef将struct Contact的这个结构体类型重命名为了contact,可千万不能以为contact是结构体变量,一定要区分开来!!!

现在我们再来看这个代码:

typedef struct Contact
{
	char name[20];
	int sz;
}contact;

int main()
{
	struct Contact n1;
	contact n2;
	return 0;
}

思考一个问题:代码中n1和n2的数据类型是否相同?

我们将结构体类型进行了重定义,所以这里struct Contact与contact是一个类型,因此n1和n2的数据类型是一样的。

5.结构体变量的定义与初始化

(1)结构体变量的定义

有了结构体类型,结构体变量的定义就变得尤为简单了,我们知道,定义一个数据只需要用类型名+变量名即可,例如:

#include<stdio.h>
int main()
{
	int i;//定义一个整形
	long f;//定义一个长整形
	long long m;//定义一个长长整形
	char c;//定义一个字符型
	float a;//定义一个单精度浮点型
	double d;//定义一个双精度浮点型
	short n;//定义一个短整型
	return 0;
}

结构体变量的定义同样如此,也是类型名+变量名,下面我们来尝试定义一个结构体变量:

#include<stdio.h>
struct point
{
	int x;
	int y;
}p1;      //声明类型的同时定义结构体变量p1
struct point p2;//定义结构体变量p2
int main()
{
	struct point p3;//定义结构体变量p3
    return 0;
}
	

p1,p2都是定义的结构体变量,不同之处在于,p1与p2都是全局的结构体变量,只是两者的变量定义方式不同,而p3定义在函数体内部,是局部变量。

(2)结构体变量的初始化

初始化就是在定义变量的同时进行赋初值,看例子:

//初始化:定义变量的同时赋初值
#include<stdio.h>
struct point
{
	int x;
	int y;
}p1;
struct Student
{
	char name[20];
	int age;
};
int main()
{
	struct point p3 = {3,4};
	struct Student s = { "zhangsan",20 };
	return 0;
}

初始化赋值时需要对结构体的每个成员进行赋值,中间用逗号隔开。再来看下面这个代码:

#include<stdio.h>
struct point
{
	int x;
	int y;
}p1;
int main()
{
	struct point p3;
	p3 = { 3,4 };
	printf("%d %d\n", p1.x, p1.y);
	printf("%d %d\n", p3.x, p3.y);
	return 0;
}

这是一个 错误代码,运行时你会发现编译器会报错,如图:

 所以,一定要切记,变量的定义和初始化是一起的,不能分开进行,因为初始化的定义就是在定义变量的同时赋初值

(3)结构体变量的嵌套初始化

#include<stdio.h>
struct point
{
	int x;
	int y;
}p1;
struct Student
{
	struct point p;
	char name[20];
	int age;
}s1 = { {1,2},"lisi",23 };//结构体嵌套初始化
int main()
{
	struct Student s2 = { {3,4},"zhangsan",20 };//结构体嵌套初始化
	return 0;
}

定义一个结构体时,在结构体内定义另一个结构体的的变量作为该结构体的成员,再用该结构体定义一个变量对齐进行初始化,就叫做结构体变量的嵌套初始化。其中在初始化时,作为结构体成员的那个结构体变量需要用大括号括起来对其初始化。

6.结构体的内存对齐

(1)结构体大小的计算

经过上面的讲解,相信各位也已经掌握了结构体的基本使用。我们知道整形数据是4个字节,字符型数据是1个字节,短整型是2个字节,那么结构体作为一种类型,肯定也有其大小一说。现在我们来深入探讨一下结构体的大小这一问题。

温馨提示:结构体的内存对齐是一个特别热门的考点

先来看一个代码:

#include<stdio.h>
struct S1
{
	char a;
	int b;
	char c;
};
int main()
{
	struct S1 s;
	printf("%d\n", sizeof(s));
	return 0;
}

问:求一下这个代码的最终打印出多少?

相信各位在心中已经有了自己的答案,你心中的答案是否是6呢?现在我们来看一下运行结果:

答案是12,为什么呢?此时相信各位小伙伴已经是满脸问号,其实结构体大小的计算并不是简单的将各成员的大小加在一起。而是有其一套单独的计算规则。 要想知道结构体的大小如何计算,必须先明白结构体的对齐规则。

结构体的对齐规则:
1.第一个变量在结构体偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值
 VS编译器中的默认对齐数为8
 //Linux环境下没有对齐数(相当于对齐数为0)
3.结构体大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,
结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

下面我就用上面举例的代码对其进行解释:

 一个小格子是一个字节,绿色区域表示变量a在内存中的存放空间,红色区域表示b在内存中的存放空间,蓝色区域表示c在内存中的存放空间,叉表示被浪费掉的空间

 a是结构体的第一个变量,数据类型为字符型,在内存中占一个字节,放置于结构偏移量为0的地址处;第二个变量是b,数据类型为整形,在内存中占4个字节,其存放位置为b的对齐数的整数倍处,在VS环境下,默认对齐数为8,而结构体成员的对齐数为默认对齐数与其成员大小的较小值,默认对齐数为8,b的大小为4,所以b的对齐数为4,因此变量b的存放位置为4的整数倍处,显然1,2,3都不是4的倍数,所以b从4的位置依次往下存放,1,2,3的位置被浪费掉;第三个变量c,数据类型为字符型,数据大小为1个字节,所以其对齐数为1,而8是1的倍数,所以变量c存放在8位置处;这还没完,由结构体的对齐规则我们知道,结构体的大小为最大对齐数的整数倍。a的对齐数为1,b的对齐数为4,c的对齐数为1,所以最大对齐数为4。0~8总共是9个字节,而结构体的大小应该为4的整数倍,因此存放完c后还要再浪费三个字节到11的位置,所以该结构体的大小为12个字节!

(2)内存对齐存在的原因

1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能
在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的
内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。

 

那在设计结构体时,如何做到既满足对齐,又节省空间?

让占用空间小的成员尽量集中在一起

下面来看一个例子:

#include<stdio.h>
struct S1
{
	char a;
	int b;
	char c;
};
struct S2
{
	char a;
	char c;
	int b;
};
int main()
{
	struct S1 s1;
	struct S2 s2;
	printf("%d\n", sizeof(s1));
	printf("%d\n", sizeof(s2));
	return 0;
}

 程序运行结果:

可以发现,S1和S2类型的结构体成员一模一样,但是S1和S2所占空间的大小有了一些区别,S2相较S1浪费的空间更少,在一定程度上节省了空间。 

7.修改默认对齐数

经过上面的介绍,我们知道在VS环境下,默认对齐数为8,其实默认对齐数是可以更改的。那么默认对齐数如何更改?为什么要更改默认对齐数?

(1)更改默认对齐数

对于默认对齐数的更改,我们只需要用#pragma这个预处理指令就可对其更改。

#pragma pack(4)//修改默认对齐数为4
#pragma pack(1)//修改默认对齐数为1->相当于无对齐,数据紧挨着存放,但是不便于数据读取
#pragma pack()//恢复默认对齐数

当然,默认对齐数还可以更改为其他的值,可以根据自己的需求来进行更改。

(2)更改默认对齐数的好处

我们先来看一个代码案列:

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

S1默认对齐数为8,最大对齐数为4,结构体大小为4的整数倍;
S1默认对齐数为1,最大对齐数为1,结构体大小为1的整数倍;
 

可以发现,修改默认对齐数,同样也可以节省内存空间,所以在默认对齐数不合适的时候,我们可以自行对其进行更改。

8.结构体传参

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函数

原因:函数在传参时,参数需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象时,结构体过大,那么参数压栈时的系统开销也就比较大,所以所导致系统性能下降。

结论:结构体传参时,首选传址传参。


本篇以结构体为主题的博客到此就要结束了,希望本篇博客可以帮助到各位小伙伴,博主码字不易,如果本篇博客对你有所帮助,还望各位小伙伴点赞👍,收藏⭐+关注,感谢各位的支持!如果有什么不明白的地方或是另有其他的高见,也可以在评论区留言,博主也会及时回复或进行更改,欢迎指正,谢谢!
 

刹国(结束)!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值