目标
1、C语言中结构体和概念和使用
2、C语言中结构体数组和指针
3、C语言中结构体字节对齐和位域
4、C语言中共用体的概念和使用
5、C语言中枚举的概念和使用
6、C语言中的类型定义和typedef
结构体(Structure)是C语言中一种复合数据类型,它允许开发者将不同类型的数据项组织在一起。结构体中的每个数据项被称为“成员”,这些成员可以具有不同的数据类型,包括基本类型(如int,char,float等)和其他复合类型(包括其他结构体或数组)。结构体提供了一种方法,让开发者能够将相关数据项集中在一起并分配给一个变量。
结构体在以下场景中特别有用:
因此,结构体是C语言中一个非常重要的概念,掌握了结构体,就意味着你已经进入了C语言的一个更高级的阶段。
结构体的基本语法在C语言中如下:
struct struct_name {
data_type member1;
data_type member2;
};
其中:
struct
是一个关键字,表示这是一个结构体类型的定义。struct_name
是你为这个结构体类型取的名字,这个名字在后面声明结构体变量时会用到。data_type
是成员的数据类型,它可以是任何有效的C语言数据类型,包括基本数据类型(如int,float,char等)和其他复合类型(如数组,指针,甚至其他的结构体类型)。member1
,member2
等是成员的名字,你可以根据实际需要定义任意多个成员。例如,如果你想定义一个表示“学生”的结构体类型,你可以这么写:
struct Student {
char name[50];
int age;
float grade;
};
这个结构体类型名字叫做 “Student”,它有三个成员:一个是长度为50的字符数组 “name”,用来存储学生的名字;一个是整型 “age”,用来存储学生的年龄;一个是浮点型 “grade”,用来存储学生的成绩。
这只是定义了一个结构体类型,如果你想要创建一个具体的学生,你需要声明一个结构体变量,就像这样:
int main()
{
struct Student lile;
}
这样就创建了一个 “Student” 类型的变量 “tzp”,你可以通过 “.” 操作符来访问它的成员,比如:
strcpy(lile.name, "lile");
lile.age = 20;
lile.grade = 90.5;
声明结构体变量的方式和声明其他类型的变量类似。首先要写出结构体类型,然后跟上你要声明的变量名。如下面的示例:
struct Student {
char name[50];
int age;
float grade;
};
int main()
{
struct Student student1; // 声明了一个类型为struct Student的变量student1
}
此外,你也可以在定义结构体的同时声明变量,例如:
struct Student {
char name[50];
int age;
float grade;
} student1, student2; // 在定义struct Student类型的同时,声明了两个此类型的变量student1和student2
访问结构体变量的成员可以使用.
运算符。首先写出结构体变量的名字,然后写上.
,最后写上你要访问的成员的名字。例如:
struct Student {
char name[50];
int age;
float grade;
} student1, student2; // 在定义struct Student类型的同时,声明了两个此类型的变量student1和student2
int main()
{
strcpy(student1.name, "lican"); // 设置student1的名字为"John Doe"
student1.age = 20; // 设置student1的年龄为20
student1.grade = 90.5; // 设置student1的成绩为90.5
struct Student student3;
strcpy(student3.name, "lile"); // 设置student1的名字为"John Doe"
student3.age = 20; // 设置student1的年龄为20
student3.grade = 90.5; // 设置student1的成绩为90.5
}
当我们使用指向结构体的指针时,可以使用 ->
运算符来访问结构体的成员。下面是一个使用 ->
运算符访问结构体成员的示例:
#include
#include
struct Student {
char name[50];
int age;
float grade;
}student1;
int main()
{
strcpy(student1.name, "lican"); // 设置student1的名字为"John Doe"
student1.age = 20; // 设置student1的年龄为20
student1.grade = 90.5; // 设置student1的成绩为90.5
struct Student student3;
strcpy(student3.name, "lile"); // 设置student1的名字为"John Doe"
student3.age = 20; // 设置student1的年龄为20
student3.grade = 90.5; // 设置student1的成绩为90.5
struct Student *p = &student1; // p是一个指向student1的指针
// 使用->运算符访问student1的成员
printf("%s\n", p->name);
printf("%d\n", p->age);
printf("%f\n", p->grade);
p = NULL;
p = &student3; // p是一个指向student1的指针
// 使用->运算符访问student3的成员
printf("%s\n", p->name);
printf("%d\n", p->age);
printf("%f\n", p->grade);
}
在这个例子中,p
是一个指向 student1
的指针,p->name
就是访问 p
指向的结构体的 name
成员,p->age
和 p->grade
同理。所以,->
运算符是用于通过结构体指针访问结构体成员的。
初始化结构体变量可以在声明的同时进行。写出结构体类型和变量名,然后在等号右边用花括号包裹起来的值列表来初始化所有的成员。值的顺序应该和成员在结构体定义中的顺序一致。例如:
struct Student {
char name[50];
int age;
float grade;
};
struct Student student1 = {"tan", 20, 90.5}; // 初始化student1的所有成员
在这个例子中,"tan"
初始化了name
成员,20
初始化了age
成员,90.5
初始化了grade
成员。
也可以使用指定初始化器的方式来初始化结构体变量,这种方式可以不按照成员顺序进行,而是根据成员的名字来指定值。例如:
struct Student student1 = {.name = "John Doe", .age = 20, .grade = 90.5}; // 使用指定初始化器初始化student1的所有成员
我们可以将结构体作为函数的参数。在这种情况下,函数会接收结构体的一个副本,修改这个副本并不会影响原来的结构体。例如:
struct Student {
char name[50];
int age;
float grade;
};
void print_student(struct Student s) //参数列表为结构体
{
printf("%s\n", s.name);
printf("%d\n", s.age);
printf("%f\n", s.grade);
}
struct Student john = {"John Doe", 20, 90.5};
print_student(john); // 将john作为参数传递给print_student函数
如果要在函数内部修改原来的结构体,需要传递一个指向结构体的指针,然后在函数内部通过这个指针来访问和修改结构体。例如:
void birthday(struct Student *s) {
s->age++; // 通过指针访问并修改结构体的成员
}
birthday(&john); // 将指向john的指针作为参数传递给birthday函数
函数也可以返回一个结构体。 在这种情况下,函数会返回一个结构体的副本。例如:
struct Student make_student(char *name, int age, float grade) {
struct Student s;
strcpy(s.name, name);
s.age = age;
s.grade = grade;
return s;
}
struct Student john = make_student("peng", 20, 90.5); // 使用make_student函数的返回值来初始化john
需要注意的是,返回一个结构体会涉及到拷贝整个结构体,如果结构体很大,这可能会比较低效。在这种情况下,一种可能的解决方案是返回一个指向结构体的指针。但需要注意的是,不能返回指向局部变量的指针,因为当函数返回后,局部变量就不存在了。你可以返回一个指向动态分配的内存的指针,这个内存可以在函数返回后继续存在。例如:
struct Student *make_student(char *name, int age, float grade) {
struct Student *s = malloc(sizeof(struct Student));
if (s != NULL) {
strcpy(s->name, name);
s->age = age;
s->grade = grade;
}
return s;
}
struct Student *john = make_student("John Doe", 20, 90.5); // john是一个指向动态分配的结构体的指针
在这个例子中,make_student
函数使用 malloc
函数动态分配了一块内存,并返回了指向这块内存的指针。使用完这块内存后,你需要使用 free
函数来释放它,防止内存泄漏。
定义和初始化结构体数组的语法与普通数组非常类似。我们首先定义结构体类型,然后声明该类型的数组,并在声明时初始化数组。
以下是一个示例,我们定义了一个名为Student
的结构体,并声明了该类型的数组students
:
struct Student {
char name[50];
int age;
float grade;
};
// 定义并初始化一个结构体数组
struct Student students[3] = {
{"Alice", 20, 85.6},
{"Bob", 22, 90.8},
{"Charlie", 19, 88.5}
};
访问结构体数组的元素非常直观,我们可以通过索引来访问数组的元素,然后使用.
运算符来访问结构体的成员。
以下是一个示例,我们访问students
数组的元素并打印相关信息:
// 打印第一个学生的姓名
printf("Name: %s\n", students[0].name);
// 打印第二个学生的年龄
printf("Age: %d\n", students[1].age);
// 打印第三个学生的成绩
printf("Grade: %.2f\n", students[2].grade);
在这个示例中,students[0]
、students[1]
、students[2]
分别表示数组students
中的第一个、第二个和第三个元素,这些元素都是Student
类型的结构体。我们使用.
运算符来访问这些结构体的成员。
定义一个指向结构体的指针与定义普通指针类似。首先我们需要指定指针的类型,即指针指向的结构体的类型,然后指定指针的名称。例如,我们可以这样定义一个指向 struct Student
类型的指针 p
:
struct Student {
char name[50];
int age;
float grade;
};
struct Student student1 = {"John Doe", 20, 90.5};
struct Student *p; // 定义一个指向struct Student类型的指针p
p = &student1; // 将p指向student1
在上述代码中,我们首先定义了一个 struct Student
类型的变量 student1
,然后定义了一个指向 struct Student
类型的指针 p
,最后将 p
指向 student1
。
我们可以通过结构体指针来访问结构体的成员,这时我们需要使用 ->
运算符。例如:
printf("%s\n", p->name); // 使用->运算符访问p指向的结构体的name成员
printf("%d\n", p->age); // 使用->运算符访问p指向的结构体的age成员
printf("%f\n", p->grade); // 使用->运算符访问p指向的结构体的grade成员
在这个例子中,p
是一个指向 student1
的指针,p->name
就是访问 p
指向的结构体的 name
成员,p->age
和 p->grade
同理。所以,->
运算符是用于通过结构体指针访问结构体成员的。
字节对齐(Data Alignment)是计算机硬件为了提高内存读写效率所采取的一种措施。在C语言中,结构体的成员可能不会严格按照代码中的顺序在内存中排列,而会进行字节对齐。
结构体的字节对齐是指编译器在分配内存时,会保证每个成员的存储地址相对于结构体起始地址的偏移量是该成员类型大小的整数倍。例如,如果一个成员的类型为 int
,则其偏移量必须是 sizeof(int)
的整数倍。
在C语言中,可以通过编译指令 #pragma pack(n)
来设置字节对齐的规则。n
表示最大对齐字节数,必须是2的非负整数次幂,并且小于或等于平台最大支持的对齐字节数。
#pragma pack(2) // 设定字节对齐规则为2字节对齐
struct MyData {
char c;
int i;
};
#pragma pack() // 恢复编译器默认的对齐规则
在上面的例子中,使用 #pragma pack(2)
设定了2字节对齐,MyData
结构体的 c
成员和 i
成员之间可能就会有1个字节的填充。然后使用 #pragma pack()
恢复了编译器默认的对齐规则。
位域(Bit-field)是C语言中的一种特殊类型,它允许程序员对一个整型变量的位进行操作,从而节省内存空间。位域通常用于处理底层硬件或者协议中的数据,例如,一些硬件的寄存器可能只有几个位有实际意义,或者网络协议中的一些字段可能只有几位。
位域的定义在语法上类似于普通的结构体成员定义,但是需要在类型名和成员名之间加上一个冒号和一个数字,表示该成员的位数。
例如,下面的代码定义了一个包含两个位域成员的结构体 BitField
:
struct BitField {
unsigned int a : 3; // a是一个3位的无符号整型位域
unsigned int b : 4; // b是一个4位的无符号整型位域
};
使用位域的方式和普通的结构体成员类似,都是使用 .
运算符。例如:
struct BitField bf;
bf.a = 5; // 设置a为5
bf.b = 10; // 设置b为10
需要注意的是,位域的值不能超过它的位数所能表示的最大值。例如,在上面的代码中,a
最大只能表示到 2^3-1 = 7
,如果尝试设置一个更大的值,那么只有最低的3位会被保留,高位的值会被丢弃。
位域在内存中的布局取决于具体的编译器和平台。大部分情况下,同一类型的连续位域会被打包在一起。如果一组连续的位域的总位数超过了它们的类型的大小,那么编译器可能会将它们分配到两个或更多的字(word)中。如果位域之间有一个非位域成员,或者两个位域成员的类型不同,那么它们也可能被分配到不同的字中。
例如,下面的代码定义了一个包含三个位域成员的结构体 BitField2
:
struct BitField2 {
unsigned int a : 3;
unsigned int b : 4;
unsigned int c : 6; // c可能无法和a、b打包在一起,因为a、b、c的总位数超过了unsigned int的大小
};
在这个例子中,a
、b
和 c
可能无法全部打包在一个 unsigned int
中,因为它们的总位数(3+4+6 = 13)超过了 unsigned int
的大小(通常为8或16)。这时,c
可能会被分配到另一个 unsigned int
中。但是具体的内存布局取决于编译器和平台,可以通过编译器的文档或者实验来确定。
共用体(union)是C语言中的一种复合数据类型,类似于结构体。它允许在相同的内存位置存储不同的数据类型。也就是说,共用体的所有成员共享同一块内存空间,它的大小由最大的成员决定。因此,共用体可以被看作是一个可以存储多种数据类型的变量。
共用体的主要用途是节省内存,特别是当我们有一些组件会以多种方式使用的时候。但请注意,同一时间只能使用共用体的一个成员,因为所有成员都共享同一块内存。
共用体的定义与结构体类似,使用关键字 union
。例如:
union Data {
int i;
float f;
char str[20];
};
在这个例子中,我们定义了一个名为 Data
的共用体,它有三个成员:一个 int
,一个 float
和一个 char
数组。共用体 Data
的大小等于其最大成员的大小,即 char str[20]
的大小。
共用体的使用方法和结构体相似。我们可以定义一个共用体类型的变量,然后通过 .
运算符来访问它的成员。例如:
union Data data;
data.i = 10;
printf( "%d\n", data.i);
data.f = 220.5;
printf( "%f\n", data.f);
strcpy( data.str, "C Programming");
printf( "%s\n", data.str);
在上面的代码中,首先定义了一个 Data
类型的变量 data
,然后依次将其 int
成员 i
,float
成员 f
和 char
数组成员 str
赋值并打印。注意在赋值新的成员之后,之前的成员的值就不再保留了。
总结来说,共用体在C语言中是一个非常有用的工具,它可以帮助我们在不同情况下复用内存,但使用时需要注意其成员之间的覆盖关系。
枚举(enum)是C语言中的一种数据类型,它由程序员定义一组整数常量,并给这组常量赋予一个名字。枚举类型的变量只能被赋予枚举中的某个值,这样可以使程序更加清晰和易于理解。
枚举类型通常被用在需要一组固定值的场景,例如一周的七天、一个月的十二个月、棋盘的颜色(黑色和白色)、交通信号灯的颜色(红、黄、绿)等等。
枚举的定义使用关键字 enum
。例如,下面的代码定义了一个名为 Day
的枚举类型,它包含一周的七天:
enum Day {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
在这个例子中,SUNDAY
、MONDAY
等被称为枚举常量,它们默认对应的整数值从0开始,逐个加1。也就是说,SUNDAY
对应0,MONDAY
对应1,以此类推。你也可以显式地为它们赋予其他的整数值。例如:
enum Day {
SUNDAY = 1,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
在这个例子中,SUNDAY
被显式赋值为1,那么 MONDAY
对应的值就是2,TUESDAY
对应的值就是3,以此类推。
定义了枚举类型后,你就可以声明该类型的变量,并将枚举常量赋给它。例如:
enum Day day;
day = MONDAY;
你也可以直接使用枚举常量,因为它们本质上就是整数。例如,你可以在 printf
函数中打印它们:
printf("%d\n", MONDAY); // 打印1
或者在 switch
语句中使用它们:
switch(day) {
case MONDAY:
printf("Today is Monday.\n");
break;
case TUESDAY:
printf("Today is Tuesday.\n");
break;
// 其他情况
}
总结来说,枚举是一种非常有用的工具,它可以帮助我们创建一组命名的整数常量,使程序更加清晰和易于理解。
在 C 语言中,typedef
是一个关键字,它用于为复杂的数据类型定义新的名称,以方便在程序中使用。这通常被用来简化复杂的类型声明,或者为某种特定的类型定义更具可读性的名称。
typedef
的基本语法是这样的:
typedef existing_type new_type_name;
在这个语句中,existing_type
是一个已经存在的类型,可以是内置的类型(如 int
, float
等),也可以是用户定义的类型(如结构体,联合体等)。new_type_name
是你想要定义的新类型的名称。
例如,你可以为 unsigned int
类型定义一个新的名称 uint
:
typedef unsigned int uint;
之后,你就可以在程序中使用 uint
来代替 unsigned int
类型:
uint a = 10;
typedef
在结构体和联合体中的使用非常常见。例如,你可以为结构体定义一个新的类型名称,这样在声明结构体变量时就不用再写 struct
关键字了:
typedef struct {
int x;
int y;
} Point;
// 现在你可以直接使用 Point 来声明变量
Point p1, p2;
这种方式在定义复杂的类型,如指向结构体的指针或者结构体数组时,会让代码更加清晰易读。
总结来说,typedef
是一个非常有用的工具,它可以帮助我们简化复杂的类型声明,提高代码的可读性。