5. 结构体
5.1 基本概念
1. 什么是结构体?
编程最重要步骤之一就是选择合适的数据表示方式。C语言提供了许多内置基本变量类型(譬如:char、short、int、long、float、double)和数组等。实际应用中仅仅这些类型难以表示复杂的对象,例如一个班集体、一个公司等包含多种数据类型。
C语言提供了自定义数据类型——结构变量(structure variable),用于组织和存储不同类型数据。总结:
- 结构体是一种构造的数据类型,可以把不同类型的数据组合成一个整体进行处理;类似于类
class
- 多个成员组成: 结构体由多个成员组成,每个成员可以是不同的数据类型。
2. 创建结构体
创建结构体包括:创建结构体模版(声明)、定义结构体变量和初始化结构体变量。
① 创建结构体模版(声明)
struct 结构体名
{
menber_list;
//结构体的成员列表(基本的数据类型,指针,数组等);
}veriable_list;
// 结构体声明(结构体模板)
struct Person
{
char name[50];
int age;
float height;
};
printf("Size of struct Person is %d bytes.\n:",sizeof(struct Person));
//Size of struct Person is 60 bytes.
- 上述声明了一个由一个字符数组
name[50]
、一个float
和一个int
变量组成的结构; - 描述了结构体如何存储数据(注意还没有真正的存储在内存,即还没真正的分配内存空间)。
- 结构体的
sizeof
只是在编译时计算结构体的大小,而不涉及实际的内存分配。在运行时,只有创建结构体实例时,系统才会为其分配内存。 - 为什么不是58bytes?因为编译器进行了内存对齐。
② 定义结构体变量
此时,定义变量即会分配内存!
//创建结构体模板,再声明变量
struct Person
{
char name[50];
int age;
float height;
};
struct Person person;
struct Person *Ptperson;//指向结构体Person类型的指针变量
//创建结构体模板,接着声明变量
struct Person
{
char name[50];
int age;
float height;
}person, *Ptperson;
printf("Size of struct person is: %d bytes.\n", sizeof(person));
printf("Size of struct Ptperson is: %d bytes.\n", sizeof(Ptperson));//指向结构体Person类型的指针变量
//Size of struct person is: 60 bytes.
//Size of struct Ptperson is: 4 bytes.
③ 初始化结构体变量
//创建结构体模板,实例化普通变量
struct Person
{
char name[50];
int age;
float height;
}person = {"Andrew", 18, 178};
初始化指向结构体的指针变量:
//创建结构体模板,实例化指针变量
struct Person
{
char name[50];
int age;
float height;
};
/********方案1********/
struct Person *Ptperson;
Ptperson = (struct Person*)malloc(sizeof(struct Person)); // 分配内存
// 检查内存分配是否成功
if (Ptperson == NULL)
{
printf("Memory allocation failed.\n");
return 1;
}
strcpy(Ptperson->name, "Andrew");
(*Ptperson).age = 18;
Ptperson->height = 1.78;
printf("Name: %s\n", Ptperson->name);
printf("Age: %d\n", Ptperson->age);
printf("Height: %.2f meters\n", Ptperson->height);
// 释放分配的内存
free(Ptperson);
Ptperson = NULL;
/*
Name: Andrew
Age: 18
Height: 1.78 meters
*/
/********方案2********/
struct Person *Ptperson;
struct Person temp = {"Andrew", 18, 178};
Ptperson = &temp; // 将结构体变量的地址赋值给指针
④ 结构体成员为字符指针
上述代码修改如下:
struct Person
{
char *name;
int age;
float height;
};
// 创建结构体变量
struct Person person1;
// 分配内存并为指针成员赋值
person1.name = (char*)malloc(50 * sizeof(char));
strcpy(person1.name, "WANG");
person1.age = 20;
person1.height = 1.76;
//打印结果
printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("Height: %.2f meters\n", person1.height);
printf("\n");
//计算大小
printf("Size of struct person is: %d bytes.\n", sizeof(person1));
//释放内存
free(person1.name);
/*
Name: WANG
Age: 20
Height: 1.76 meters
Size of struct person1 is: 12 bytes.
*/
结构体大小由60 bytes
变为12 bytes
,字符串使用指针效率更加高效和节约空间;
3. 访问结构体成员
访问结构体成员可以使用成员访问运算符(.
)或指针运算符(->
),具体的使用取决于是直接操作结构体变量还是结构体指针;
①. 使用.
操作符(结构体变量)
struct Person {
……
};
struct Person person1 = {"John", 25, 1.75};
// 访问结构体成员
printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("Height: %.2f meters\n", person1.height);
②. 使用->
操作符(结构体指针)
有指向结构体的指针,应该使用->
操作符来访问结构体成员。
struct Person {
……
};
struct Person *Ptperson;
//初始化……
// 访问结构体成员
printf("Name: %s\n", personPtr->name);
printf("Age: %d\n", personPtr->age);
printf("Height: %.2f meters\n", personPtr->height);
4. 匿名结构体
即不完全声明结构体,只能使用一次:
struct
{
char name[50];
int age;
float height;
};
5. 使用typedef
定义创建结构体
typedef struct Person
{
char name[50];
int age;
float height;
}Person, *Ptperson;
Person person1 = {"John", 25, 1.75};
Ptperson ptperson1 = &person1;
//or
typedef struct
{
char name[50];
int age;
float height;
}Person, *Ptperson;//匿名结构体
注意:Person
并不是创建的变量,而是对于struct Person
的重命名,
同样:Ptperson
并不是创建的变量,而是对于struct Person *
的别名,是类型名。后续可按照需求使用:Person
来创建变量也可以用struct Person
来创建。
使用
typedef
方式的好处在于:
- 更简洁:不需要在每次声明结构体变量时都使用
struct
关键字。- 更具可读性:通过
typedef
为结构体定义一个新的名称,可以使代码更清晰易读。
6. 嵌套结构体
在一个结构体内部包含另一个结构体,这样一个结构体中组织和存储更为复杂的数据结构;
#include <stdio.h>
// 定义日期结构体
struct Date
{
int day;
int month;
int year;
};
// 定义学生结构体,包含日期结构体作为成员
struct Student
{
char name[50];
int age;
struct Date birthdate; // 嵌套结构体
};
int main()
{
// 声明并初始化嵌套结构体
struct Student student1 = {"Alice", 20, {15, 5, 2002}};
// 访问嵌套结构体成员
printf("Name: %s\n", student1.name);
printf("Age: %d\n", student1.age);
printf("Birthdate: %d/%d/%d\n", student1.birthdate.day, student1.birthdate.month, student1.birthdate.year);
return 0;
}
注意:
使用多次
.
操作符,用以访问嵌套结构体成员。例如
student1.birthdate.day
表示学生的出生日期中的天数,student1.address.street
表示学生的地址中的街道名。
5.2 结构体和数组
结构数组提供了一种有效的方式来组织和管理相似数据,使得代码更加模块化和易于维护。
1. 声明结构数组
#define MAXPEOPLE
// 定义一个简单的结构体
struct Person {
char name[50];
int age;
float height;
};
// 声明结构体数组
struct Person people[MAXPEOPLE];
注意1:
struct Person people[MAXPEOPLE]
是在栈上静态分配的,编译器在编译时就知道要为它分配多少内存。这种方式适用于相对较小的数组,且内存的分配和释放是隐式的,由编译器自动处理。
// 动态分配内存来存储结构体数组
struct Person *people = (struct Person *)malloc(3 * sizeof(struct Person));
if (people == NULL)
{
printf("Memory allocation failed.\n");
return 1;
}
free(people);
注意2:
用了
malloc
函数动态分配了足够的内存来存储结构体数组。这种方式适用于需要在运行时确定数组大小,或者需要较大内存空间的情况。
动态分配的内存需要手动释放,以避免内存泄漏。
2. 初始化结构数组
// 静态分配内存,初始化结构体数组
struct Person people[MAXPEOPLE] =
{
{"Alice", 25, 1.65},
{"Bob", 30, 1.75},
{"LaoW", 20, 1.78},
{"ZhangS", 50, 1.95},
// ... 继续初始化其他元素
};
动态分配如下:
struct Person *people = (struct Person *)malloc(MAXPEOPLE * sizeof(struct Person));
if (people == NULL)
{
fprintf(stderr, "Memory allocation failed.\n");
return 1;
}
// 初始化结构体数组
for (int i = 0; i < MAXPEOPLE; ++i)
{
sprintf(people[i].name, "Person %d", i + 1);
people[i].age = 20 + i;
people[i].height = 1.70 + i * 0.01;
}
free(people);
3. 使用结构体数组
动态分配内存:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Person
{
char name[50];
int age;
float height;
};
int main() {
int num_people;
// 获取人数
printf("Enter the number of people: ");
scanf("%d", &num_people);
// 动态分配内存
struct Person* people = (struct Person*)malloc(num_people * sizeof(struct Person));
if (people == NULL)
{
fprintf(stderr, "Memory allocation failed.\n");
return 1;
}
// 获取每个人的信息
for (int i = 0; i < num_people; ++i)
{
printf("Enter details for Person %d:\n", i + 1);
printf("Name: ");
scanf("%s", people[i].name);
printf("Age: ");
scanf("%d", &people[i].age);
printf("Height: ");
scanf("%f", &people[i].height);
}
// 打印结果
for (int i = 0; i < num_people; ++i)
{
printf("Person %d:\n", i + 1);
printf("Name: %s\n", people[i].name);
printf("Age: %d\n", people[i].age);
printf("Height: %.2f meters\n", people[i].height);
printf("\n");
}
// 释放动态分配的内存
free(people);
return 0;
}
非动态分配内存:
#include <stdio.h>
#include <string.h>
struct Person
{
char name[50];
int age;
float height;
};
int main()
{
// 初始化结构体数组并直接赋值
struct Person people[5] =
{
{"Alice", 25, 1.65},
{"Bob", 30, 1.75},
{"LaoW", 20, 1.78},
{"ZhangS", 50, 1.95},
{"Eve", 28, 1.78}
};
// 打印结果
for (int i = 0; i < 5; ++i)
{
printf("Person %d:\n", i + 1);
printf("Name: %s\n", people[i].name);
printf("Age: %d\n", people[i].age);
printf("Height: %.2f meters\n", people[i].height);
printf("\n");
}
return 0;
}
5.3 结构体和指针
指向结构体的指针一般比结构体本身,更容易操控,效率更高。
1. 声明和初始化结构体指针
结构体指针可以指向已有的结构体变量;
typedef struct Person
{
char name[50];
int age;
float height;
}Person, *Ptperson;
Person person1 = {"John", 25, 1.75};
Ptperson ptperson1 = &person1; //指向结构体变量
也可以指向新的结构体变量;
ptperson1 = (struct Person *)malloc(sizeof(struct Person));
或//Ptperson ptperson1 = (Ptperson)malloc(sizeof(struct Person));
//使用后
free(ptperson1);
2. 使用结构体指针
typedef struct Person
{
char name[50];
int age;
float height;
}Person, *Ptperson;
int main()
{
Person people[5] =
{
{"Alice", 25, 1.65},
{"Bob", 30, 1.75},
{"LaoW", 20, 1.78},
{"ZhangS", 50, 1.95},
{"Eve", 28, 1.78}
};
//创建结构体指针
Ptperson ptperson1;
//指向&people[0]
ptperson1 = &people[0];
// 打印结果
for (int i = 0; i < 5; ++i)
{
printf("Person %d:\n", i + 1);
printf("Address of people %d is: %p\n", i + 1, &people[i]);
//指针访问符
printf("Name: %s\n", ptperson1->name);
printf("Age: %d\n", (*ptperson1).age);
printf("Height: %.2f meters\n", ptperson1->height);
printf("\n");
//指向下一个结构体
ptperson1++;
}
return 0;
}
/*运行结果
Person 1:
Address of people 1 is: 012FF90C
Name: Alice
Age: 25
Height: 1.65 meters
Person 2:
Address of people 2 is: 012FF948
Name: Bob
Age: 30
Height: 1.75 meters
Person 3:
Address of people 3 is: 012FF984
Name: LaoW
Age: 20
Height: 1.78 meters
Person 4:
Address of people 4 is: 012FF9C0
Name: ZhangS
Age: 50
Height: 1.95 meters
Person 5:
Address of people 5 is: 012FF9FC
Name: Eve
Age: 28
Height: 1.78 meters
*/
5.4 结构体和函数
函数的参数把值传给函数,这个值可以是:基本的数据类型(int、float型等)、一个地址等;
1. 结构体值传递
值传递会拷贝副本,形参改不了实参;
① 结构体
struct Person
{
char name[50];
int age;
float height;
};
void printPerson(struct Person person)
{
printf("Name: %s\n", person.name);
printf("Age: %d\n", person.age);
printf("Height: %.2f\n", person.height);
}
int main()
{
struct Person person1 = {"Chris", 25, 1.75};
printPerson(person1);
return 0;
}
② 结构体成员
struct Person
{
char name[50];
int age;
float height;
};
int addPersonAge(int a, int b)
{
return a + b;
}
int main()
{
struct Person person1 = { "Chris", 25, 1.75 };
struct Person person2 = { "WANG", 24, 1.78 };
int addAge = addPersonAge(person1.age, person2.age);
printf("sum of age: %d", addAge);
return 0;
}
2. 结构体作函数返回值
函数也可以返回结构体,这样可以将多个相关的数据打包在一起返回。
struct Person
{
char name[50];
int age;
float height;
};
struct Person createPerson(char name[], int age, float height)
{
struct Person newPerson;
strcpy(newPerson.name, name);
newPerson.age = age;
newPerson.height = height;
return newPerson;
}
int main()
{
// 调用函数,获取返回的结构体
struct Person person2 = createPerson("Bob", 30, 1.75);
// 打印结构体成员的值
printf("Name: %s\n", person2.name);
printf("Age: %d\n", person2.age);
printf("Height: %.2f\n", person2.height);
return 0;
}
3. 结构体地址传递
如果结构体很大,传递结构体的副本可能会导致性能问题。这时候可以传递结构体的指针。
但是注意:使用值传递可以保护原数据不被修改;
使用指针传递参数,若要防止意外被修改,可以加上
const
关键字;
struct Person
{
char name[50];
int age;
float height;
};
void printPersonPtr(const struct Person *personPtr)
{
printf("Name: %s\n", personPtr->name);
printf("Age: %d\n", personPtr->age);
printf("Height: %.2f\n", personPtr->height);
}
int main() {
struct Person person3 = {"Charlie", 28, 1.80};
const struct Person *ptrPerson3 = &person3;
printPersonPtr(ptrPerson3);
return 0;
}
5.5 结构体内存对齐
1. 为什么有结构体内存对齐?
- 结构体内存对齐是指编译器为了提高存取效率,按照一定规则在结构体中插入一些填充字节(padding bytes)的过程。
- 在计算机中,内存访问通常是按照特定的规则进行的,比如字节对齐(byte alignment)。字节对齐要求数据类型的起始地址必须是其大小的整数倍。例如,一个4字节的整型变量需要以4字节的边界对齐,而一个8字节的双精度浮点数需要以8字节的边界对齐。
- 结构体是由多个成员变量组成的复合数据类型。如果结构体的成员变量没有按照规定的对齐方式排列,可能会导致内存空间的浪费和访问速度变慢。当成员变量没有对齐时,CPU 需要额外的操作来读取或写入这些数据,这增加内存访问的时间和开销。
- 通过进行结构体内存对齐,可以将结构体的各个成员变量按照对齐规则排列,避免内存浪费,并使得 CPU 在读取和写入结构体数据时更加高效。
2. 对齐规则
- 结构体的第1个成员,存放在结构体变量开始位置的0偏移量(第一个字节处)处;
- 【关于偏移量】从第2个成员开始,都要对齐到**对齐数的整数倍的地址(偏移量)**处;
- 【关于结构体总字节大小】结构体的总大小,必须是最大对齐数的整数倍;
- 【如果存在嵌套结构体】嵌套的结构体(结构体中的那个结构体)对齐到自己的最大对齐数的整数倍处。
- 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
- 对齐数 = 编译器默认的对齐数与结构体成员大小的较小值。
- 不是所有的编译器都有默认对齐数,VS 中的默认对齐数为 8,Linux 的GCC编译器则没有默认对齐数,当编译器没默认对齐数时,成员变量的大小就是该成员的对齐数。