C语言 结构体

一、结构体的声明和初始化

1. 结构体声明

程序清单:

#include <stdio.h>

struct Student {
	char name[20];		// 姓名
	int age;			// 年龄
	int studentID;		// 学号
}student1, student2;

int main() {

	struct Student student3;

	return 0;
}

2. 结构体初始化

程序清单:

#include <stdio.h>

struct Student {
	char name[20];			// 姓名
	int age;				// 年龄
	int studentID;			// 学号
}student1 = {"Rose", 19, 06}, student2={"小红", 17, 03};

int main() {

	struct Student student3 = {"Jack", 18, 02};
	
	return 0;
}

注意事项:

上面是结构体声明和初始化的标准格式,随着 Student 类型被创建出来,那么后面就可以根据其来创建相应的结构体变量:student1,student2,student3.

student1,student2 为全局变量,student3 为局部变量。

二、typedef 重定义结构体

程序清单1

#include <stdio.h>

typedef struct Student {
	char name[20];			// 姓名
	int age;				// 年龄
	int studentID;			// 学号
}Student;

int main() {

	Student student = { "Rose", 17, 03 };
	// struct Student student = ...

	return 0;
}

我们平时写类型的时候,都需要在自定义类型前加上 struct,才能够表示一个完整的结构体类型。而经 typedef 关键字重定义后, " struct Student " 和 " Student " 就是等价的了,这可以让我们后续更加简单地创建 Student 变量了。这里只需要注意写法即可。

程序清单2

typedef struct  {
	char name[20];  // 名字
	int age;		// 年龄
	int studentID;  // 学号
}Student;

int main() {
	Student student1 = { "Jack", 18, 32 };
	Student student2 = { "Bruce", 20, 05 };

	return 0;
}

对于程序清单2,typedef 也可以重命名匿名结构体,此时结构体的类型为 Student. 这是很多数据结构书本上的写法,同样也是正确的。

三、结构体成员的类型

结构的成员可以是标量、数组、指针,甚至是其他结构体。如下所示:

程序清单:

#include <stdio.h>

struct Grade {
	int chinese;				// 语文成绩
	int math;					// 数学成绩
	int english;				// 英语成绩
};

struct Student {
	char name[20];				// 姓名
	int age;					// 年龄
	int studentID;				// 学号
	struct Grade grade;			// 成绩
};

int main() {

	struct Student student = { "Rose", 17, 03, {90, 95, 93} };
	printf("学生信息:%s %d %d\n", student.name, student.age, student.studentID);
	printf("学生成绩:%d %d %d\n", student.grade.chinese, student.grade.math, student.grade.english);

	return 0;
}

输出结果:

1-1

四、结构体成员的访问

在访问结构体成员时,可以采用 . 或者 -> ,前者对应结构体变量,后者对应结构体指针变量。

程序清单:

#include <stdio.h>

struct Student {
	char name[20];  // 名字
	int age;		// 年龄
	int studentID;  // 学号
};

int main() {
	struct Student student1 = {"Jack", 18, 32};  
	struct Student student2 = {"Bruce", 20, 05};
	printf("%s %d %d\n", student1.name, student1.age, student1.studentID);

	struct Student* ps1 = &student1;
	printf("%s %d %d\n", (*ps1).name, (*ps1).age, (*ps1).studentID);
	printf("%s %d %d\n", ps1->name, ps1->age, ps1->studentID);
	return 0;
}

输出结果:

1-2

五、结构体传参

程序清单:

#include <stdio.h>

struct Student {
	char name[20];		// 姓名
	int age;			// 年龄
	int studentID;		// 学号
};

void print1(struct Student student) {

	printf("%s %d %d\n", student.name, student.age, student.studentID);
}

void print2(struct Student* ps) {

	printf("%s %d %d\n", ps->name, ps->age, ps->studentID);
}

int main() {

	struct Student student1 = {"Jack", 18, 02};

	print1(student1);	// 1. 传值
	print2(&student1);	// 2. 传地址

	return 0;
}

输出结果:

1-3

注意事项:

① 函数在定义时,应该写在结构体之后。由于 main 函数作为程序的入口,之后在 main 函数中调用 print1 和 print2 函数时,程序就需要自上到下扫描,如果两个打印函数都写在了结构体之前,就会产生找不到结构体类型的现象。

② 上面在使用结构体传参时,采用了两个传参类型。
第一,传值调用 print1 函数;第二,传地址调用 print2 函数。然而,从调用函数的效率角度看,后者更加高效。

前者在传值时,传的是整个结构体过去的,如果结构体过大,形参在接收实参传来的结构体时,也要同样开辟一个与原结构体一样大的内存空间,后续才能接收结构体中各种各样的成员变量。然而,后者在传整个结构体的地址时,形参只需要拿一个结构体指针变量接收即可,因为一个指针变量占内存的大小要么是 4,要么是 8 ,所以整体系统开销较小。

此外,这里结构体传参应该与数组传参区分开来,如果将一个结构体变量作为函数的参数,那么传递的就是整个结构体;然而,如果将一个数组作为函数的参数,那么传递的就是数组首元素的地址。

综上所述,结构体在传参时,应该尽可能传它的地址,而不是结构体变量本身。

六、结构体的自引用

结构体的自引用常用于表示数据结构中的链表,结构体中包含数据域和指针域。

#include <stdio.h>

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

int main() {

	struct Node node1;

	return 0;
}

七、结构体的内存对齐

对齐规则

1. 第一个结构体成员在处于偏移量为 0 的地址处。

2. 其他结构体成员需要从对齐数的整数倍的偏移量地址处开始继续偏移。
( 对齐数 = 编译器默认的对齐数与该结构体成员大小两者比较后的较小值 )
在 VS 编译器中,默认值为 8;在 Linux 环境下,没有默认对齐数,此时结构体成员的自身大小就是它的对齐数。

3. 结构体占用内存的总大小为最大对齐数的整数倍,单位字节。
( 最大对齐数 = 所有结构体成员对齐数中的最大值 )

4. 如果出现了结构体嵌套结构体的情况,那么内部的结构体对齐到自己的最大对齐数的整数倍处。最终,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

程序清单1

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

struct S1 
{
	char c1;		// 1
	int i;			// 4
	char c2;		// 1
};

int main() {

	printf("%d\n", sizeof(struct S1)); 			// 12

	printf("%u ", offsetof(struct S1, c1)); 	// 0
	printf("%u ", offsetof(struct S1, i)); 		// 4
	printf("%u \n", offsetof(struct S1, c2)); 	// 8

	return 0;
}

注意事项:

offsetof 是一个宏,它可以用来计算结构体成员的偏移量。头文件为 stddef.h.

S1 所占内存的计算过程:

1-4

程序清单2

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

struct S2
{
	char c1;		// 1
	char c2;		// 1
	int i;			// 4
};

int main() {

	printf("%d\n", sizeof(struct S2)); 

	return 0;
}

// 输出结果:8

S2 所占内存的计算过程:

1-5

程序清单3

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

struct S3
{
	double d;		// 8
	char c;			// 1
	int i;			// 4
};

int main() {

	printf("%d\n", sizeof(struct S3));

	return 0;
}

// 输出结果:16

S3 所占内存的计算过程:

1-6

程序清单4

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

struct S3			// 16
{
	double d;		// 8
	char c;			// 1
	int i;			// 4
};

struct S4
{
	char c1;		// 1
	struct S3 s3;	// 最大对齐数 8
	double d;		// 8
};

int main() {

	printf("%d\n", sizeof(struct S4)); 			// 32

	printf("%u ", offsetof(struct S4, c1)); 	// 0
	printf("%u ", offsetof(struct S4, s3)); 	// 8
	printf("%u \n", offsetof(struct S4, d)); 	// 24

	return 0;
}

S4 所占内存的计算过程:

1-7

修改默认对齐数

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

struct S1 {
	char c;			// 1
	double d;		// 8
};

#pragma pack(4)
struct S2 {
	char c;			// 1
	double d;		// 8 (取对齐数为 4)
};
#pragma pack()

#pragma pack(1)
struct S3 {
	char c;			// 1
	double d;		// 8 (取对齐数为 1)
};
#pragma pack()

int main() {

	printf("%d\n", sizeof(struct S1)); 	// 16
	printf("%d\n", sizeof(struct S2)); 	// 12
	printf("%d\n", sizeof(struct S3)); 	// 9

	return 0;
}

1-8

注意事项:

在 VS 编译器中,对齐数默认值为 8;在 Linux 环境下,没有默认对齐数,此时结构体成员的自身大小就是它的对齐数。当我们为程序设计对齐数时,也应该考虑到硬件的访问情况,通常应该将对齐数设置为 2 的次方。

此外,在对齐数相同的情况下,我们应该尽可能地将占用空间小的结构体成员集中在一起。对比上面程序清单1 和 程序清单2,同样的对齐数,同样的程序,后者将 char 类型的结构体成员放在了一起,结果既满足了对齐,又节省了空间。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十七ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值