【C语言】结构体篇
目录
- 结构体的定义
- 结构体变量的声明和初始化
- 声明结构体变量
- 初始化结构体变量
- 访问结构体成员
- 结构体数组
- 结构体指针
- 结构体嵌套
- 结构体作为函数参数
- 值传递
- 指针传递
- 结构体的内存对齐
- 位域
结构体的定义
结构体是一种自定义的数据类型,它把不同类型的数据组合成一个整体,方便管理和操作相关的数据。在定义结构体时,使用 struct 关键字,后面跟着结构体的名称,再用花括号 {} 包含结构体的成员列表,每个成员由数据类型和成员名组成,成员之间用分号 ; 分隔。
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
// 可以有更多成员
};
例如,定义一个描述图书信息的结构体:
struct Book {
char title[100]; // 书名,用字符数组存储
char author[50]; // 作者,用字符数组存储
int year; // 出版年份,用整数存储
float price; // 价格,用浮点数存储
};
注意事项和细节
- 结构体名的命名规则:遵循标识符的命名规则,不能与关键字重名,尽量使用有意义的名称,以提高代码的可读性。
- 结构体定义结束的分号:结构体定义的末尾必须有分号 ;,这是 C 语言语法的要求。
- 结构体只是一种类型定义:定义结构体本身并不分配内存,只是告诉编译器这种新的数据类型的组成结构。
结构体变量的声明和初始化
声明结构体变量
先定义结构体,再声明变量:这是最常见的方式,先定义好结构体类型,然后在需要使用的地方声明该类型的变量。
struct Book {
char title[100];
char author[50];
int year;
float price;
};
struct Book book1; // 声明一个 Book 类型的变量 book1
在定义结构体的同时声明变量:这种方式在定义结构体的同时就声明了变量,适用于只在当前代码块使用该结构体类型的情况。
struct Movie {
char name[80];
int duration;
} movie1, movie2; // 定义 Movie 结构体的同时声明了两个变量 movie1 和 movie2
使用匿名结构体声明变量:匿名结构体没有结构体名,只能在定义时声明变量,之后无法再声明该类型的其他变量,使用场景较少。
struct {
int x;
int y;
} point; // 声明一个匿名结构体类型的变量 point
初始化结构体变量
按成员顺序初始化:按照结构体定义中成员的顺序,依次为每个成员赋值。
struct Book book2 = {"C Programming", "John Doe", 2020, 29.99};
指定成员名初始化(C99 及以后支持):可以不按照成员的顺序,通过指定成员名来初始化,提高代码的可读性和可维护性。
struct Book book3 = {.author = "Jane Smith", .title = "Data Structures", .year = 2021, .price = 39.99};
注意事项和细节
- 成员顺序初始化时的匹配:按成员顺序初始化时,提供的值的类型和顺序必须与结构体成员的类型和顺序一致。
- 部分初始化:如果只初始化部分成员,未初始化的成员将被自动初始化为 0(对于数值类型)或空字符(对于字符类型)。
struct Book book4 = {"Python Basics"}; // 只初始化了 title 成员,其他成员自动初始化为 0 或空字符
- 匿名结构体的局限性:匿名结构体无法在其他地方再次使用,因为没有结构体名,不利于代码的复用。
访问结构体成员
使用点运算符 . 来访问结构体变量的成员。点运算符的左边是结构体变量名,右边是结构体的成员名。
#include <stdio.h>
#include <string.h>
struct Book {
char title[100];
char author[50];
int year;
float price;
};
int main() {
struct Book book;
strcpy(book.title, "Java Programming"); // 给 title 成员赋值
strcpy(book.author, "Mark Johnson"); // 给 author 成员赋值
book.year = 2019; // 给 year 成员赋值
book.price = 49.99; // 给 price 成员赋值
printf("Title: %s\n", book.title);
printf("Author: %s\n", book.author);
printf("Year: %d\n", book.year);
printf("Price: %.2f\n", book.price);
return 0;
}
注意事项和细节
- 成员访问的合法性:只能访问结构体中已经定义的成员,访问不存在的成员会导致编译错误。
- 字符数组成员的赋值:对于字符数组类型的成员,不能直接使用赋值运算符 = 进行赋值,需要使用 strcpy 等字符串处理函数。
结构体数组
结构体数组是由多个相同结构体类型的元素组成的数组。可以将多个相关的结构体变量组织在一起,方便进行批量处理。
#include <stdio.h>
struct Book {
char title[100];
char author[50];
int year;
float price;
};
int main() {
struct Book books[3] = {
{"C++ Primer", "Stanley Lippman", 2012, 79.99},
{"Effective Java", "Joshua Bloch", 2018, 59.99},
{"The Pragmatic Programmer", "Andrew Hunt", 2019, 49.99}
};
for (int i = 0; i < 3; i++) {
printf("Book %d:\n", i + 1);
printf("Title: %s\n", books[i].title);
printf("Author: %s\n", books[i].author);
printf("Year: %d\n", books[i].year);
printf("Price: %.2f\n", books[i].price);
printf("\n");
}
return 0;
}
注意事项和细节
- 数组大小的确定:在声明结构体数组时,需要明确指定数组的大小,或者在初始化时由编译器根据初始化列表的元素个数自动确定。
- 数组元素的访问:通过数组下标访问结构体数组的元素,再使用点运算符访问元素的成员。
内存连续性:结构体数组的元素在内存中是连续存储的,这有利于提高访问效率。
结构体指针
可以定义指向结构体的指针,通过指针来访问结构体的成员。使用 -> 运算符来访问指针所指向结构体的成员,-> 运算符是 (*指针).成员 的简写形式。
#include <stdio.h>
#include <string.h>
struct Book {
char title[100];
char author[50];
int year;
float price;
};
int main() {
struct Book book = {"JavaScript: The Definitive Guide", "David Flanagan", 2020, 69.99};
struct Book *p = &book; // 定义一个指向 Book 结构体的指针 p,并指向 book 变量
printf("Title: %s\n", p->title);
printf("Author: %s\n", p->author);
printf("Year: %d\n", p->year);
printf("Price: %.2f\n", p->price);
return 0;
}
注意事项和细节
- 指针的初始化:在使用结构体指针之前,必须先将其初始化为一个有效的结构体变量的地址,否则会导致未定义行为。
- -> 运算符的使用:-> 运算符用于通过指针访问结构体成员,避免了使用 (*指针).成员 这种较为繁琐的写法。
- 动态内存分配:可以使用 malloc、calloc 等函数动态分配结构体的内存,并使用指针来管理。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Book {
char title[100];
char author[50];
int year;
float price;
};
int main() {
struct Book *p = (struct Book *)malloc(sizeof(struct Book));
if (p == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
strcpy(p->title, "Python Crash Course");
strcpy(p->author, "Eric Matthes");
p->year = 2015;
p->price = 39.99;
printf("Title: %s\n", p->title);
printf("Author: %s\n", p->author);
printf("Year: %d\n", p->year);
printf("Price: %.2f\n", p->price);
free(p); // 释放动态分配的内存
return 0;
}
结构体嵌套
结构体可以嵌套,即一个结构体的成员可以是另一个结构体类型。通过结构体嵌套,可以更复杂地组织数据。
#include <stdio.h>
struct Date {
int year;
int month;
int day;
};
struct Book {
char title[100];
char author[50];
struct Date publishDate; // 嵌套 Date 结构体
float price;
};
int main() {
struct Book book = {
"The C Programming Language",
"Brian Kernighan",
{1978, 2, 22}, // 初始化嵌套的 Date 结构体
29.99
};
printf("Title: %s\n", book.title);
printf("Author: %s\n", book.author);
printf("Publish Date: %d-%d-%d\n", book.publishDate.year, book.publishDate.month, book.publishDate.day);
printf("Price: %.2f\n", book.price);
return 0;
}
注意事项和细节
- 嵌套结构体的初始化:在初始化包含嵌套结构体的结构体变量时,需要按照嵌套的层次依次初始化。
- 成员访问的层次:访问嵌套结构体的成员时,需要使用多个点运算符,从外层结构体逐步访问到内层结构体的成员。
- 内存布局:嵌套结构体的内存布局是按照成员的定义顺序依次分配的,嵌套结构体的成员也遵循内存对齐的规则。
结构体作为函数参数
值传递
将结构体变量的值复制一份传递给函数,函数内部对参数的修改不会影响原结构体变量。
#include <stdio.h>
struct Point {
int x;
int y;
};
// 函数接受一个 Point 结构体作为参数
void printPoint(struct Point p) {
printf("Point: (%d, %d)\n", p.x, p.y);
}
int main() {
struct Point p = {3, 4};
printPoint(p); // 传递结构体变量 p 的值给函数
return 0;
}
注意事项和细节
- 内存开销:值传递会复制整个结构体变量,当结构体较大时,会占用较多的内存和时间。
- 数据的独立性:函数内部对参数的修改不会影响原结构体变量,保证了数据的独立性。
指针传递
将结构体变量的地址传递给函数,函数内部可以通过指针修改原结构体变量的值。
#include <stdio.h>
struct Point {
int x;
int y;
};
// 函数接受一个指向 Point 结构体的指针作为参数
void movePoint(struct Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
int main() {
struct Point p = {3, 4};
movePoint(&p, 1, 2); // 传递结构体变量 p 的地址给函数
printf("New Point: (%d, %d)\n", p.x, p.y);
return 0;
}
注意事项和细节
- 内存开销小:指针传递只传递结构体变量的地址,不复制整个结构体,内存开销小。
- 数据的修改:函数内部可以通过指针修改原结构体变量的值,需要注意避免意外修改数据。
- 指针的有效性:在函数内部使用指针时,需要确保指针指向的是有效的结构体变量,避免空指针引用。
结构体的内存对齐
结构体的成员在内存中并不是连续存储的,编译器会根据成员的类型和平台的要求进行内存对齐,以提高访问效率。内存对齐的规则如下:
- 结构体的第一个成员的偏移量为 0。
- 每个成员的偏移量必须是该成员类型大小的整数倍。
- 结构体的总大小必须是最大成员类型大小的整数倍。
#include <stdio.h>
struct Example {
char c; // 1 字节
int i; // 4 字节
char d; // 1 字节
};
int main() {
printf("Size of struct Example: %zu\n", sizeof(struct Example));
return 0;
}
在这个例子中,由于内存对齐的原因,struct Example 的大小可能不是 6 字节(1 + 4 + 1),而是 12 字节。char c 从偏移量 0 开始存储,占 1 个字节;int i 由于偏移量必须是 4 的整数倍,所以从偏移量 4 开始存储,占 4 个字节;char d 从偏移量 8 开始存储,占 1 个字节;为了满足结构体总大小是最大成员类型大小(4 字节)的整数倍,结构体的总大小为 12 字节。
注意事项和细节
- 内存浪费:内存对齐会导致一定的内存浪费,特别是当结构体中包含不同大小的成员时。
- 平台相关性:不同的平台和编译器可能有不同的内存对齐规则,因此结构体的大小可能会有所不同。
- 调整对齐方式:可以使用 #pragma pack 指令来调整结构体的对齐方式,但这可能会影响访问效率。
位域
位域允许在一个结构体中以位为单位来指定成员所占的存储空间,用于节省内存。
#include <stdio.h>
struct Flags {
unsigned int flag1 : 1; // 占 1 位
unsigned int flag2 : 1; // 占 1 位
unsigned int flag3 : 1; // 占 1 位
unsigned int flag4 : 1; // 占 1 位
};
int main() {
struct Flags f;
f.flag1 = 1;
f.flag2 = 0;
f.flag3 = 1;
f.flag4 = 0;
printf("Flag1: %d\n", f.flag1);
printf("Flag2: %d\n", f.flag2);
printf("Flag3: %d\n", f.flag3);
printf("Flag4: %d\n", f.flag4);
return 0;
}
在这个例子中,struct Flags 的四个成员 flag1、flag2、flag3 和 flag4 各占 1 位,总共只占 1 个字节的存储空间。
注意事项和细节
- 位域的类型:位域的类型必须是 int、unsigned int 或 signed int,有些编译器也支持 char 类型。
- 位域的宽度:位域的宽度不能超过其类型的大小,例如 unsigned int 类型的位域宽度不能超过 32 位。
- 位域的可移植性:位域的实现可能因编译器和平台而异,因此位域的使用可能会影响代码的可移植性。
- 未命名位域:可以使用未命名的位域来填充字节,以达到特定的对齐要求。
struct Example {
unsigned int flag1 : 1;
unsigned int : 3; // 未命名位域,占 3 位
unsigned int flag2 : 1;
};