C语言:枚举体、结构体、共用体
目录
第一章:枚举体
第二章:结构体
第三章:共用体
一、枚举体
1.1. 枚举体的定义
- 什么是枚举体?
枚举体是C语言中的一种数据类型,它允许程序员为一组整数值分配有意义的名称。通过使用枚举,代码的可读性和可维护性可以得到提高。 - 枚举体在C语言中的作用和意义。
枚举体在C语言中的主要作用是提供一种有效的方式来表示一组有限的、预定义的常量集合。通过为整数值分配描述性名称,枚举使得代码更易于理解和修改。
1.2. 枚举体的语法结构
enum
关键字的使用。
在C语言中,使用enum
关键字来定义枚举体。语法格式如下:
enum 枚举名 {
枚举常量1,
枚举常量2,
...
枚举常量n
};
- 枚举常量的定义和命名规则。
枚举常量是在枚举体中定义的,它们是一组整数值。命名规则通常遵循C语言的标识符命名规则,即以字母或下划线开头,后面可以跟着字母、数字或下划线。
1.3. 枚举体的使用
- 如何声明和初始化枚举变量?
声明枚举变量的方式与声明其他类型的变量类似。例如:
enum Day {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};
enum Day today = MONDAY;
- 枚举常量在程序中的应用场景。
枚举常量常用于表示一组固定的、互斥的值,如一周的七天、月份、颜色、状态码等。
1.4. 枚举体的注意事项
- 枚举常量的默认值和自定义值。
在枚举体中,如果枚举常量的值没有被显式指定,那么它们会被自动赋值。默认情况下,第一个枚举常量的值为0,后续枚举常量的值依次递增。也可以显式地为枚举常量指定整数值。 - 枚举类型的内存大小。
枚举类型的内存大小通常足够存储枚举体中定义的最大整数值。然而,具体的大小可能因编译器而异。可以使用sizeof
运算符来确定枚举类型的实际大小。
1.5. 应用实例
- 使用枚举体表示一周的七天。
#include <stdio.h>
enum Day {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
int main() {
enum Day today = WEDNESDAY;
printf("Today is %d\n", today); // 输出: Today is 3
return 0;
}
- 使用枚举体表示状态码(如成功、失败等)。
#include <stdio.h>
enum Status {
SUCCESS = 0,
FAILURE = -1,
PENDING = 1
};
int main() {
enum Status result = SUCCESS;
if (result == SUCCESS) {
printf("Operation was successful.\n");
} else if (result == FAILURE) {
printf("Operation failed.\n");
} else if (result == PENDING) {
printf("Operation is pending.\n");
}
return 0;
}
二、结构体
2.1. 结构体的定义
- 什么是结构体?
在C语言中,结构体是一种用户自定义的数据类型,它允许将多个不同类型的数据项组合成一个单一的类型。结构体可以包含任意数量的成员,每个成员可以是基本数据类型(如int、float、char等)或其他结构体类型。
结构体在C语言中的作用和意义。
结构体在C语言中非常重要,因为它们允许程序员创建能够更真实地表示现实世界实体的数据类型。例如,可以创建一个结构体来表示一个学生,其中包含学生的姓名、年龄、学号等信息。通过使用结构体,可以更方便地处理和管理这些数据。
2.2. 结构体的语法结构
struct
关键字的使用。
在C语言中,需要使用struct
关键字来定义一个结构体。下面是一个简单的例子:
struct Student {
char name[50];
int age;
float score;
};
在这个例子中,我们定义了一个名为Student
的结构体,它有三个成员:一个字符数组name
(用于存储学生的姓名),一个整数age
(用于存储学生的年龄),和一个浮点数score
(用于存储学生的分数)。
typedef
方法
为了避免在声明结构体变量时每次都使用struct
关键字,可以使用typedef
为结构体定义一个新的类型名。例如:
typedef struct Student {
char name[50];
int age;
float score;
} Student;
在这个例子中,我们使用typedef
定义了一个新的类型名Student
,这样我们就可以直接使用Student
来声明变量,而不需要再使用struct Student
。
- 结构体成员的声明和类型。
在结构体中,可以声明任意类型和数量的成员。成员的类型可以是基本数据类型(如int、float、char等),也可以是其他结构体类型。每个成员都有一个名称,用于在程序中访问和修改该成员的值。
2.3. 结构体的使用
- 如何声明和初始化结构体变量?
声明结构体变量的方法与声明其他类型的变量类似。例如,使用上面定义的Student
结构体,可以这样声明一个变量:
Student stu1; // 声明一个Student类型的变量stu1
也可以在声明的同时初始化结构体的成员:
Student stu2 = {"John Doe", 20, 90.5}; // 声明并初始化一个Student类型的变量stu2
- 结构体变量的访问和修改。
要访问或修改结构体变量的成员,需要使用点运算符(.
)。例如:
stu1.name = "Jane Smith"; // 修改stu1的name成员
stu1.age = 22; // 修改stu1的age成员
printf("Name: %s, Age: %d, Score: %.2f\n", stu2.name, stu2.age, stu2.score); // 访问stu2的成员并打印
在这个例子中,我们使用点运算符来访问和修改结构体变量的成员。通过stu1.name
和stu1.age
,我们可以分别访问和修改stu1
的name
和age
成员。同样地,stu2.name
、stu2.age
和stu2.score
用于访问stu2
的成员,并将它们的值打印出来。
2.4. 结构体的嵌套和数组
- 结构体中嵌套其他结构体
在C语言中,一个结构体可以包含另一个结构体作为其成员,这被称为结构体的嵌套。嵌套结构体允许创建更复杂的数据结构,以适应不同的编程需求。
示例:
typedef struct {
char name[50];
int age;
} Person;
typedef struct {
Person student;
float score;
} StudentRecord;
在这个例子中,Person
结构体被嵌套在 StudentRecord
结构体中。StudentRecord
结构体有一个 Person
类型的成员 student
和一个浮点数类型的成员 score
。
- 结构体数组的定义和使用
结构体数组是存储相同类型结构体的连续内存块。每个数组元素都是一个结构体变量,可以访问其成员。
示例:
#include <stdio.h>
typedef struct {
char name[50];
int age;
} Person;
int main() {
Person people[3]; // 定义一个结构体数组
// 初始化数组元素
strcpy(people[0].name, "Alice");
people[0].age = 25;
strcpy(people[1].name, "Bob");
people[1].age = 30;
strcpy(people[2].name, "Charlie");
people[2].age = 35;
// 打印数组内容
for (int i = 0; i < 3; i++) {
printf("Name: %s, Age: %d\n", people[i].name, people[i].age);
}
return 0;
}
这个程序定义了一个 Person
结构体和一个包含3个 Person
结构体的数组 people
。然后,它初始化了数组中的每个结构体元素,并打印了它们的内容。
2.5. 结构体与指针
- 指向结构体的指针
在C语言中,可以定义一个指针变量来存储结构体的地址。这样,就可以通过这个指针来访问结构体的成员。
示例:
typedef struct {
char name[50];
int age;
} Person;
int main() {
Person p1;
Person *ptr; // 定义一个指向Person结构体的指针
strcpy(p1.name, "Alice");
p1.age = 25;
ptr = &p1; // 将ptr指向p1的地址
printf("Name: %s, Age: %d\n", ptr->name, ptr->age);
return 0;
}
这个程序定义了一个 Person
结构体,一个 Person
类型的变量 p1
,和一个指向 Person
结构体的指针 ptr
。然后,它将 ptr
指向 p1
的地址,并通过 ptr
访问 p1
的成员。
- 结构体指针的运算和内存分配
可以使用结构体指针进行算术运算,但通常这种做法不常见,因为结构体的大小可能因成员而异。然而,可以使用指针算术来遍历结构体数组。
此外,可以使用 malloc
、calloc
和 realloc
函数为结构体动态分配内存。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[50];
int age;
} Person;
int main() {
Person *people;
int n = 3;
// 为结构体数组动态分配内存
people = (Person *)malloc(n * sizeof(Person));
// 初始化数组元素
for (int i = 0; i < n; i++) {
sprintf(people[i].name, "Person%d", i+1);
people[i].age = 20 + i;
}
// 打印数组内容
for (int i = 0; i < n; i++) {
printf("Name: %s, Age: %d\n", people[i].name, people[i].age);
}
// 释放内存
free(people);
return 0;
}
这个程序使用 malloc
函数为包含3个 Person
结构体的数组动态分配内存,并初始化和打印数组元素。最后,它使用 free
函数释放了分配的内存。
2.6. 结构体在函数中的应用
- 结构体作为函数参数传递
在C语言中,可以将结构体作为参数传递给函数。可以通过值传递或通过引用传递(使用指针)来实现。
示例(通过值传递):
typedef struct {
char name[50];
int age;
} Person;
void printPerson(Person p) {
printf("Name: %s, Age: %d\n", p.name, p.age);
}
int main() {
Person p1;
strcpy(p1.name, "Alice");
p1.age = 25;
printPerson(p1); // 通过值传递结构体
return 0;
}
示例(通过引用传递,使用指针):
typedef struct {
char name[50];
int age;
} Person;
void updatePerson(Person *p, int newAge) {
p->age = newAge;
}
int main() {
Person p1;
strcpy(p1.name, "Alice");
p1.age = 25;
printf("Before update: Name: %s, Age: %d\n", p1.name, p1.age);
updatePerson(&p1, 30); // 通过引用传递结构体(使用指针)
printf("After update: Name: %s, Age: %d\n", p1.name, p1.age);
return 0;
}
- 结构体作为函数返回值
可以让函数返回一个结构体。这通常用于从函数中检索多个值。
示例:
#include <stdio.h>
#include <string.h>
typedef struct {
char name[50];
int age;
} Person;
Person getPerson() {
Person p;
strcpy(p.name, "Alice");
p.age = 25;
return p; // 返回结构体
}
int main() {
Person p = getPerson(); // 接收返回的结构体
printf("Name: %s, Age: %d\n", p.name, p.age);
return 0;
}
请注意,当结构体较大时,通过值返回结构体可能会导致性能下降,因为这会涉及到整个结构体的复制。在这种情况下,使用指针传递结构体的地址可能更为高效。
2.7. 应用实例
- 使用结构体表示学生信息(学号、姓名、成绩等)。
#include <stdio.h>
#include <string.h>
struct Student {
int id;
char name[50];
float score;
};
int main() {
struct Student stu = {123, "John Doe", 85.0};
printf("Student ID: %d\n", stu.id);
printf("Student Name: %s\n", stu.name);
printf("Student Score: %.2f\n", stu.score);
return 0;
}
- 使用结构体和函数实现简单的学生信息管理系统。
#include <stdio.h>
#include <string.h>
#define MAX_STUDENTS 100
struct Student {
int id;
char name[50];
float score;
};
void addStudent(struct Student students[], int *numStudents, int id, const char *name, float score) {
students[*numStudents].id = id;
strcpy(students[*numStudents].name, name);
students[*numStudents].score = score;
(*numStudents)++;
}
void printStudents(const struct Student students[], int numStudents) {
for (int i = 0; i < numStudents; i++) {
printf("Student ID: %d, Name: %s, Score: %.2f\n", students[i].id, students[i].name, students[i].score);
}
}
int main() {
struct Student students[MAX_STUDENTS];
int numStudents = 0;
addStudent(students, &numStudents, 1, "Alice", 90.5);
addStudent(students, &numStudents, 2, "Bob", 85.0);
addStudent(students, &numStudents, 3, "Charlie", 92.5);
printStudents(students, numStudents);
return 0;
}
三、共用体(又称联合体)
3.1. 共用体的定义
- 什么是共用体?
共用体在C语言中是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型。可以认为共用体是一种“节省空间”的特殊结构,它的所有成员占用同一块内存地址,该地址是其最大的成员所需的空间。在任何给定时间,只有一个数据成员可以有值。共用体提供了一种有效的方式来使用相同的内存位置供多个用途。
-
共用体与结构体的区别和联系:
-
区别:结构体(Struct)中的成员都拥有自己的内存空间,它们同时存在。而共用体(Union)的成员共享同一块内存空间,它们不能同时存在。换句话说,对于共用体,一次只能使用一个成员,因为它们的起始地址都是相同的。
-
联系:结构体和共用体都是由多个不同的数据类型的成员组成,但在内存中的存储方式有所不同。它们都可以通过
.
或->
运算符来访问其成员。
3.2. 共用体的语法结构
union
关键字的使用:
在C语言中,共用体是通过union
关键字来定义的。
union UnionName {
data_type1 member1;
data_type2 member2;
...
data_typeN memberN;
};
其中,UnionName
是共用体的名称,data_type1
, data_type2
, …, data_typeN
是成员的数据类型,member1
, member2
, …, memberN
是成员的名称。
- 共用体成员的声明和类型:
共用体的成员可以是任何数据类型,包括基本数据类型、结构体、共用体等。但需要注意的是,由于共用体的所有成员共享内存,因此它们的地址都是相同的。
3.3. 共用体的使用
- 如何声明和初始化共用体变量?
声明共用体变量的方式与声明结构体变量类似。但需要注意的是,共用体不能像结构体那样直接进行初始化,因为共用体的所有成员都共享同一块内存空间。通常,我们可以通过给共用体的某个成员赋值来间接地初始化共用体。
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10; // 这里给共用体的成员i赋值,相当于初始化了共用体变量data
printf("%d\n", data.i); // 输出:10
return 0;
}
- 共用体变量的访问和修改:
访问和修改共用体变量的方式与访问和修改结构体变量的方式相同,都是通过.
或->
运算符来实现的。但由于共用体的所有成员共享同一块内存空间,因此修改一个成员的值可能会影响其他成员的值。
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10; // 给共用体的成员i赋值
printf("%d\n", data.i); // 输出:10
data.f = 220.5; // 给共用体的成员f赋值,这会导致成员i的值被覆盖
printf("%d\n", data.i); // 输出:不确定的值(因为float和int在内存中的表示方式不同)
return 0;
}
在上面的例子中,当我们给共用体的成员f
赋值后,成员i
的值就被覆盖了。这是因为i
和f
共享同一块内存空间,修改其中一个成员的值必然会影响其他成员的值。
3.4. 共用体的内存分配
- 共用体成员的内存重叠现象
在C语言中,共用体(union)是一种特殊的数据结构,它允许在相同的内存位置存储不同的数据类型。共用体的所有成员共享同一块内存区域,这意味着它们的内存地址是相同的。因此,当给共用体的一个成员赋值时,其他成员的值会被覆盖,因为它们占用的是相同的内存空间。这就是所谓的“内存重叠”现象。
例如,考虑以下共用体定义:
union Example {
int integer;
float floating;
};
union Example example;
example.integer = 10; // 整数成员被赋值为10
example.floating = 20.5; // 浮点数成员被赋值为20.5,此时整数成员的值将被覆盖
在这个例子中,integer
和floating
成员共享同一块内存。当给floating
成员赋值时,integer
成员的值就被覆盖了。
- 共用体的大小计算
共用体的大小取决于其最大成员的大小。这是因为共用体必须能够容纳其最大的成员,以便能够存储该成员的值。在内存分配时,系统会为共用体分配足够的空间来存储其最大成员。
例如,考虑以下共用体:
union Data {
char c; // 占用1字节
int i; // 假设占用4字节(这取决于系统和编译器)
double d; // 假设占用8字节(这取决于系统和编译器)
};
// 共用体Data的大小将是8字节(假设int是4字节,double是8字节),因为它需要足够的空间来存储最大的成员double d。
请注意,共用体的大小可能因系统和编译器的不同而有所差异。在某些系统中,可能存在内存对齐的要求,这可能会影响共用体的大小。
3.5. 共用体的应用场景
- 使用共用体节省内存空间
共用体的主要应用之一是节省内存空间。当有一个变量,它可以是几种不同的数据类型,但在任何给定时间只能是其中一种类型时,使用共用体可以非常有效地利用内存。这是因为共用体只为其最大的成员分配内存,而不是为每个成员分别分配内存。
- 共用体在底层编程和数据封装中的应用
共用体在底层编程中特别有用,尤其是在与硬件通信或处理系统级数据结构的场景下。例如,在处理网络协议或文件格式的二进制数据时,可能需要同时以不同的方式解释同一块内存中的数据。共用体允许程序员以不同的数据类型来访问同一块内存区域,从而简化了这类任务。
此外,在数据封装方面,共用体可以用于创建能够存储多种类型数据的自定义数据类型。这在创建灵活的数据结构或实现通用编程概念时非常有用,例如变体(variant)或任何类型(any type)的容器。
共用体还可以用于实现一些高级编程技巧,如类型擦除(type erasure),其中共用体用于在保持类型信息的同时提供对底层数据的通用访问。然而,这些应用通常需要更深入的C语言知识和编程经验。
3.6. 应用实例
- 使用共用体实现一个简单的日期转换器(日期格式转换)
共用体在C语言中常用于处理需要多种表示形式但只占用同一内存空间的数据。以下是一个使用共用体实现的简单日期转换器的例子,该转换器可以将日期从年-月-日
的格式转换为日/月/年
的格式,或者相反。
#include <stdio.h>
union Date {
struct {
unsigned int day : 5;// 位域,最大31天
unsigned int month : 4;// 位域,最大12个月
unsigned int year : 27;// 假设年份不会超过2^27 - 1
} parts;
unsigned int whole;// 整数表示的年月日
};
// 打印日期
void printDate(union Date date) {
printf("%02u/%02u/%08u\n", date.parts.day, date.parts.month, date.parts.year);
}
// 从整数构造日期
void setDateFromWhole(union Date* date, unsigned int whole) {
date->whole = whole;
// 这里其实不需要额外操作,因为whole和parts共享内存
}
// 从年月日构造日期(假设格式正确)
void setDateFromParts(union Date* date, unsigned int day, unsigned int month, unsigned int year) {
// 假设位域是按照它们在结构体中出现的顺序在内存中布局的
// 那么我们可以手动进行位移和位掩码操作来构造whole
// 但是由于parts和whole共享内存,我们只需要设置parts,whole就会自动更新
date->parts.day = day;
date->parts.month = month;
date->parts.year = year;
}
// 将年月日值转化为整数
unsigned int encodeDate(unsigned int day, unsigned int month, unsigned int year) {
return (year << 9) | (month << 5) | day;
}
int main() {
union Date date;
setDateFromParts(&date, 1, 4, 2023);
printDate(date); // 输出:01/04/2023
unsigned int wholeDate = encodeDate(1, 4, 2023); // 调用encodeDate函数将年月日(1,4,2023)转化为整数
setDateFromWhole(&date, wholeDate);
printDate(date); // 应该输出与上面相同的日期:01/04/2023
return 0;
}
实际上,上面的代码片段并不是一个完整的日期转换器,因为它没有包含从字符串解析日期或格式化日期为字符串的逻辑。通常,会使用strptime
函数来解析日期字符串,并使用strftime
函数来格式化日期。但是,为了演示共用体的用途,展示了一个简化的例子。
- 使用共用体处理不同数据类型的字节序问题
在网络编程和系统编程中,处理不同机器之间的字节序(endianness)差异是一个常见问题。一些机器使用大端字节序(big-endian),而另一些使用小端字节序(little-endian)。共用体可以用来简化字节序的转换。
#include <stdint.h>
#include <stdio.h>
union Int32 {
uint32_t i;
uint8_t b[4];
};
// 将32位整数从大端字节序转换为小端字节序,或者相反
uint32_t swapEndianness(uint32_t value) {
union Int32 input;
union Int32 output;
input.i = value;
// 假设机器是小端字节序,则input.b[0]是最低有效字节
// 为了转换为大端字节序,我们需要反转字节的顺序
output.b[0] = input.b[3];
output.b[1] = input.b[2];
output.b[2] = input.b[1];
output.b[3] = input.b[0];
return output.i;
}
int main() {
uint32_t bigEndianValue = 0x12345678;
uint32_t littleEndianValue = swapEndianness(bigEndianValue);
printf("Big-endian: 0x%08X\n", bigEndianValue);
printf("Little-endian: 0x%08X\n", littleEndianValue);
return 0;
}
在上面的代码中,swapEndianness
函数使用共用体来访问32位整数的各个字节,并将它们以相反的顺序重新排列,从而实现从大端到小端(或从小端到大端)的转换。这里假设了机器使用的是小端字节序;如果机器是大端的,那么转换函数就不需要做任何事情。在实际应用中,可能会使用预编译宏(如__BYTE_ORDER__
和__ORDER_LITTLE_ENDIAN__
)来确定机器的字节序,并据此编写条件编译代码。