C语言 结构体详解


正文开始

1. 结构体类型的声明

1.1 什么是结构体?

C语言为我们提供了基本的数据类型,例如intcharfloat等,但我们在实际生活中的对象都是复杂的,不能仅靠一种数据简单的描述。
我们回顾一下数组,数组是一种自定义类型,比如int arr[10],它的类型就为int [10],自定义类型使我们能够更加灵活的解决问题。
而结构体同样是一种自定义类型。而结构体就实现了对一个对象进行多方面描述的功能。

1.2 结构体的声明

struct tag
{
	member-list;//成员列表
};

举个栗子,例如要描述一个学生,需要他的名字、年龄、性别、学号,就可以这样定义

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

注:

  • struct 代表着这是一个结构体类型
  • Student 代表着这个结构体的名字
  • 结构体内部的就是成员列表
  • 最后分号不能丢

对于结构体的理解:结构体就是一个自定义的类型,也就是说,当我们创建一个结构体后,它就可以类似于intchar这些数据类型一样拿来用。

1.3 结构体的特殊声明

在声明结构体时,可以不完全的声明,例如:

//匿名结构体类型
struct
{
	int a;
	char b;
	float c;
}x;

匿名结构体类型定义时,只能在定义结构体时声明结构体变量,否则是不能声明结构体变量的。

//匿名结构体类型
struct
{
	int a;
	char b;
	float c;
}x;

struct
{
	int a;
	char b;
	float c;
}a[20], *p;

(p == &x)成立吗?答案是不成立,因为这是匿名结构体类型,就算里面的成员变量完全一样,编译器仍会把它们当成完全不同的类型。匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。

1.4 结构体的自引用

在结构体内部包含一个该结构体本身的成员是否可行呢?
比如这样:

struct Node
{
	int data;
	struct Node next;
}

这样写显然是不行的,因为一个结构体内在包含一个同类型的结构体变量,这样就会形成一个无限的套娃,结构体变量的大小就会变成无穷大,是不合理的。我们可以通过使用结构体指针变量来自引用

例如这样就是合法的:

struct Node
{
	int data;
	struct Node* next;
	//包含了下一个节点的地址
	//而不是下一个节点的內容
	//这样就避免了无限套娃的情况
}

在结构体自引用使用的过程中,若使用了typedef对匿名结构体类型重命名,也容易产生问题,例如:

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

上述代码是将一个匿名结构体类型重命名为了Node,并且在匿名结构体类型中包含了该结构体类型的变量next,但是这是不合法的,因为在匿名结构体内部提前使用了 Node 类型来创建成员变量。所以,强烈建议定义结构体不要使用匿名结构体

2. 结构体变量的创建和初始化

2.1 结构体变量的创建

既然结构体是一种类型,那么我们就可以使用这种类型来创建变量,创建变量有两种方式:

  • 先创建结构体类型,再创建结构体变量
//结构体类型创建
struct Student
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};

int main()
{
	//结构体变量的创建
	struct Student a;
	//创建了类型为struct Student的变量a
	return 0;
}
  • 创建结构体类型同时创建结构体变量
//结构体类型创建
struct Student
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}a;
//结构体变量创建
//这时创建的结构体变量是一次性的

int main()
{
	return 0;
}

2.2 结构体变量的初始化

结构体变量的初始化有两种情况,一种是按照结构体成员列表顺序初始化,一种是指定顺序初始化。例如:

struct Student
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
int main()
{
	//按照结构体成员列表顺序初始化
	struct Student a = { "张三", 20, "男", "20232022" };
	//指定顺序初始化
	struct Student b = {.age = 18, .name = "李四", .sex = "女", .id = "20235600"}
	return 0;
}

注:

  • 在定义结构体变量时可以将其一起全部初始化,但如果在定义结构体变量的时候没有初始化,那么后面就不能全部一起初始化了,只能单独对其中的成员初始化
struct Student
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
int main()
{
	struct Student a;
	
	//err 这样是不允许的
	//a = { "张三", 20, "男", "20232022" };

	//只能单独初始化
	a.name = "张三";
	a.age = 20;
	return 0;
}
  • 结构体也可以像数组一样,全部将內容初始化为0,例如:
struct Student
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
int main()
{
	struct Student a = { 0 };
	return 0;
}

3. 结构体成员的访问

3.1 结构体成员的直接访问

结构体变量通过操作符.来访问成员:

结构体变量.成员名

例如:

#include <stdio.h>
struct Text
{
	int x;
	int y;
};

int main()
{
	struct Text p = { 1,2 };
	printf("x:%d\ny:%d\n", p.x, p.y);
	//p.x访问成员x
	//p.y访问成员y
	return 0;
}

运行结果:
在这里插入图片描述

3.2 结构体成员的间接访问

指针可以指向结构体类型,我们可以通过一个结构体指针变量来间接访问结构体成员

(*结构体指针).成员名
结构体指针->成员名

#include <stdio.h>
struct Text
{
	int x;
	int y;
};

int main()
{
	struct Text p = { 1,2 };
	struct Text * pp = &p;
	pp->x = 4;
	(*pp).y = 5;
	printf("x=%d\ny=%d\n", pp->x, pp->y);
	return 0;
}

运行结果:
在这里插入图片描述
注:当使用 (*结构体指针).成员名时,括号不能省去,因为.的优先级要大于 *

4. 结构体内存对齐

结构体在内存中的存储方式同一般的类型颇有不同,它遵循结构体内存对齐规则

4.1 对齐规则

  1. 结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
    - 对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值
    - VS 中默认值为8
    - Linux 中 gcc 没有默认对齐数,对齐数就是成员自身的大小
  3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大值)的整数倍
  4. 如果嵌套了结构体,嵌套的结构体成员到自己的成员中最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(包含嵌套结构体中成员的对齐数)的整数倍

相信大家现在都是一脸懵吧,别急,我来举个例子:

#include <stdio.h>

struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}

我们来探讨一下在VS中运行结果是什么:

  1. 首先,根据对齐规则第一条,将char c1安置好
    在这里插入图片描述

  2. 然后安置第二个结构体成员变量int i,它的大小为4个字节,编译器默认对齐数为8,两者较小值为4。也就是说,它的对齐数为4,需要对齐到4的整数倍的地址处,也就是这样:
    在这里插入图片描述

  3. 随后安置第三个结构体成员变量char c2,它的大小为1个字节,编译器默认对齐数为8,两者较小值为1。也就是说,它的对齐数为1,需要对齐到1的整数倍的地址处,也就是这样:
    在这里插入图片描述

  4. 最后根据对齐规则第3条,三个成员变量中最大的对齐值为4,所以总大小为四的整数倍,所以整个结构体的内存情况就为:
    在这里插入图片描述
    结构体类型struct S1的大小就为12个字节,我们运行以下代码验证一下:
    在这里插入图片描述
    来几个例子练习一下:

#include <stdio.h>
//练习1
struct S2
{
	char c1;
	char c2;
	int i;
};
//练习2
struct S3
{
	double d;
	char c;
	int i;
};
int main()
{
	printf("%d\n", sizeof(struct S2));
	printf("%d\n", sizeof(struct S3));
	return 0;
}

在这里插入图片描述
在这里插入图片描述

运行验证一下:
在这里插入图片描述
当结构体内部嵌套结构体变量时,嵌套的结构体对齐数为其内部的最大对齐数,整个结构体的对齐数为所有最大对齐数(包含嵌套结构体中成员的对齐数)的整数倍。

例如:

#include <stdio.h>

struct S3
{
	double d;
	char c;
	int i;
};

//练习4-结构体嵌套问题
struct S4
{
	char c1;
	struct S3 s3;
	double d1;
};
int main()
{
	printf("%d\n", sizeof(struct S4));
	return 0;
}

在这里插入图片描述
运行验证一下:
在这里插入图片描述

4.2 为什么需要内存对齐

  1. 平台原因(移植原因)
    • 不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会产生硬件异常的错误
  2. 性能原因
    • 数据结构(尤其是栈)应该尽可能地在自然边界上对齐,这样在访问数据的时候可以减少读取的次数。假设一个机器每次访问八个字节,若没有对齐规则的限制,访问对象可能被分在了两个8字节内存块中,这样就需要读取两次才能完整的读取访问对象;而有了对齐规则,就能够保证访问对象都放在同一块内存块中,减少了访问次数。但弊端也很明显,就是为了对齐,而造成的空间浪费。

总的来说,结构体的内存对齐就是拿空间来换取时间的做法。我们在设计结构体的时候,应该尽可能地将占用空间小的成员集中在一起,这样可以在一定程度上减少空间的使用。

4.3 修改默认对齐数

使用#pragma pack()这个预处理指令,可以修改编译器默认的对齐数

例如:

#pramg pack(1)//设置默认对齐数为1

5. 结构体传参

将结构体变量作为参数传递进函数,同样有传址调用和传值调用:

#include <stdio.h>

struct S
{
	int data[100];
	int num;
};
//传值调用
void print1(struct S s)
{
	printf("%d\n", s.num);
}
//传址调用
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	struct S s = { { 1,2,3,4 }, 30};
	print1(s);
	print2(&s);
}

运行结果:
在这里插入图片描述

在我们使用结构体传参时,更推荐使用传址调用,因为函数在传参时,参数需要压栈(即在栈区申请空间来存储参数),会有时间和空间上的系统开销,如果传入的结构体对象过大,那么系统开销就大,就会导致性能的下降。

6. 结构体实现位段

6.1 什么是位段?

在前面我们学习了结构体的对齐规则,实现了用空间换取时间的效果,但如果我们想要节省空间,那该怎么办呢?C语言为我们提供了位段的概念,来实现节省空间的效果。

例如:

struct A
{
	int _a:2;
	int _b:5;
	int _c:10;
	int _d:30;
};

其中:

  • 位段的成员必须是intunsigned intsigned intchar类型,在C99中位段成员的类型也可以是其他类型
  • 位段的成员名后边有一个冒号和一个数字,数字代表着这个成员所占的比特位

6.2 位段的内存分配

  1. 位段的成员后面的数字就代表着这个成员在内存中所占的比特位
  2. 位段的空间以4个字节(int)或者1个字节(char)的方式开辟,会首先开辟一块空间,若不够用则继续开辟,直到将所有成员存储起来
  3. 位段中有很多C语言没有统一的因素,所以位段是比较依赖环境的,这就意味着位段的移植性是很差的,是不跨平台的
    • 位段申请到一块内存中,是从左向右使用,还是从右向左使用,是不确定的
    • 一块内存中剩余的空间,不足下一个成员使用的时候,是浪费还是继续使用是不确定的

例如:

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

int main()
{
	struct S s = { 0 };
	s.a = 10;//二进制:1010
	s.b = 12;//二进制:1100
	s.c = 3;//二进制:11
	s.d = 4;//二进制:100
	return 0;
}

这里我们假设位段申请到一块内存中,是从右向左使用;并且剩余的空间,不足下一个成员使用的时候浪费掉,那么内存分配如下:

  1. 首先,成员都是char类型,所以每次开辟一个字节的空间,开辟了第一个字节后,存放进成员 a,它共占3bit
    在这里插入图片描述

  2. 然后存放成员 b,它共占4bit
    在这里插入图片描述

  3. 随后要放成员 c,它共占5bit,但开辟的第一个字节已经不够用了,所以就会开辟第二块内存
    在这里插入图片描述

  4. 最后要存放成员 d,它共占4个字节,但开辟的第二个字节已经不够用了,所以就会开辟第三块内存
    在这里插入图片描述

  5. 这样,结构体成员的内存就都开辟出来了,共三个字节,接下来是给成员初始化;成员 a 二进制表示为1010、成员 b 二进制表示为1100、成员 c 二进制表示为 11、成员 d 二进制表示为100,按位存储进内存,多余位置为0,超出部分舍去
    在这里插入图片描述
    在对应环境下运行检验一下:
    请添加图片描述

6.3 位段的跨平台问题

跟结构体相比,位段可以达到相同的效果,并且可以很好的节省空间,但是有跨平台的问题存在,原因如下:

  • int 位段被当成有符号数还是无符号数没有统一
  • 位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,若写成26,在16位机器会出现问题)
  • 位段中成员在内存中从左向右分配,还是从右向左分配尚未统一
  • 当一个结构包含两个位段,第二个位段成员较大,无法容纳于第一个位段剩余位的时候,是舍弃还是利用剩余的位,尚未统一

6.4 位段使用的注意事项

位段的几个成员可能公用一个字节,由于地址是以字节为单位分配的,一个字节内部的 bit 位是没有地址的,而有些成员的起始位置并不是某个字节的起始位置,所以这些成员是没有地址的
所以不能对位段成员使用&操作符,也就不能使用 scanf 直接给位段成员输入值,只能先输入一个放在变量里,然后赋值给位段成员

例如:

#include <stdio.h>
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

int main()
{
	struct S s = { 0 };
	//scanf("%d", &s.a)//这是错误的

	//正确用法
	int i = 0;
	scanf("%d", &i);
	s.a = i;
	printf("%d", s.a);
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值