Skip to content

C 结构体

数组将相同数据类型的项目组合在一起,而结构体(struct)是用户自定义的类型,允许您将不同数据类型的项目组合到单个名称下。它们对于组织相关数据至关重要。

想象一下,您需要表示二维平面上一个点的信息。您需要一个 x 坐标和一个 y 坐标,它们可能具有不同的类型(例如,double)。结构体非常适合这种情况:

  • X 坐标(例如,double x;)
  • Y 坐标(例如,double y;)

您使用 struct 关键字定义结构体,后面跟着一个可选的标签名称和一个包含成员定义的 {} 块。

struct [structure_tag] {
type member1_name;
type member2_name;
// ... other members
} [optional_variable_declarations];

structure_tag 是结构体类型的名称。每个 member_definition 都是一个标准的变量声明。您可以在闭合花括号后立即声明结构体类型的变量,但更常见的做法是使用 struct structure_tag variable_name; 单独声明它们。

示例:定义一个 Point 结构体。

// 定义名为 'Point' 的结构体
struct Point {
double x;
double y;
};
// 定义一个 Book 结构体(使用 typedef 方便后续使用)
typedef struct {
char title[100];
char author[100];
int year_published;
double price;
} Book;

要访问结构体变量的成员,使用成员访问运算符,即点号(.)。

语法:structure_variable.member_name

#include <stdio.h>
#include <string.h> // For strncpy
// Define the Point structure
struct Point {
double x;
double y;
};
// Use the Book typedef from the previous example
typedef struct {
char title[100];
char author[100];
int year_published;
double price;
} Book;
int main(void) {
// Declare structure variables
struct Point p1, p2;
Book book1;
// --- Initialize p1 ---
p1.x = 10.5;
p1.y = -3.7;
// --- Initialize p2 using designated initializers (C99+) ---
// 使用指定初始化器初始化 p2 (C99+)
p2 = (struct Point){ .x = 0.0, .y = 5.0 }; // Compound literal initialization
// --- Initialize book1 ---
strncpy(book1.title, "The C Programming Language", sizeof(book1.title) - 1);
book1.title[sizeof(book1.title) - 1] = '\0'; // Ensure null termination
// 确保字符串以空字符结束
strncpy(book1.author, "Kernighan & Ritchie", sizeof(book1.author) - 1);
book1.author[sizeof(book1.author) - 1] = '\0';
book1.year_published = 1978;
book1.price = 25.99;
// --- Access and print members ---
// 访问并打印成员
printf("Point p1: (%.2f, %.2f)\n", p1.x, p1.y);
printf("Point p2: (%.2f, %.2f)\n", p2.x, p2.y);
printf("\nBook 1 Details:\n");
printf(" Title: %s\n", book1.title);
printf(" Author: %s\n", book1.author);
printf(" Year: %d\n", book1.year_published);
printf(" Price: $%.2f\n", book1.price);
return 0;
}

输出:

Point p1: (10.50, -3.70)
Point p2: (0.00, 5.00)
Book 1 Details:
Title: The C Programming Language
Author: Kernighan & Ritchie
Year: 1978
Price: $25.99

结构体可以通过两种方式传递给函数:

  • 按值传递(Pass by Value): 结构体的整个副本被传递给函数。函数内部的更改不会影响原始结构体。对于大型结构体来说,由于复制开销,这种方式可能效率较低。
  • 按指针传递(Pass by Pointer): 结构体的地址被传递。函数通过指针直接操作原始结构体。对于大型结构体来说,这种方式更有效率,并且允许函数修改原始结构体。

示例:按值传递与按指针传递

#include <stdio.h>
// Define the Point structure
struct Point {
double x;
double y;
};
// Function taking Point by value (makes a copy)
// 函数按值传递 Point (创建副本)
void print_point_value(struct Point p) {
printf("Value: (%.2f, %.2f)\n", p.x, p.y);
// p.x = 0; // Change here would NOT affect the original p1
// 在此处更改不会影响原始 p1
}
// Function taking Point by pointer (accesses original)
// 函数按指针传递 Point (访问原始结构体)
void print_point_pointer(const struct Point *p_ptr) { // Use const if not modifying
// 如果不修改,使用 const
// Access members using the arrow operator (->)
// 使用箭头运算符 (->) 访问成员
printf("Pointer: (%.2f, %.2f)\n", p_ptr->x, p_ptr->y);
// p_ptr->x = 0; // Change here WOULD affect the original p1 (if not const)
// 在此处更改会影响原始 p1 (如果不使用 const)
}
// Function to modify Point via pointer
// 通过指针修改 Point 的函数
void move_point(struct Point *p_ptr, double dx, double dy) {
p_ptr->x += dx;
p_ptr->y += dy;
}
int main(void) {
struct Point p1 = { 3.0, 4.0 };
printf("Original p1: ");
print_point_value(p1); // Pass by value
// 按值传递
printf("Original p1 via pointer: ");
print_point_pointer(&p1); // Pass address (pointer)
// 传递地址 (指针)
move_point(&p1, 1.0, -2.0); // Modify original p1
// 修改原始 p1
printf("Moved p1: ");
print_point_pointer(&p1); // Print modified p1
// 打印修改后的 p1
return 0;
}

输出:

Original p1: Value: (3.00, 4.00)
Original p1 via pointer: Pointer: (3.00, 4.00)
Moved p1: Pointer: (4.00, 2.00)

您可以声明持有结构体变量内存地址的指针。

struct Point *ptr_p1; // Declares a pointer to a Point structure
// 声明一个指向 Point 结构体的指针

使用地址运算符(&)分配结构体变量的地址。

struct Point p1 = {1.0, 2.0};
ptr_p1 = &p1; // ptr_p1 now holds the address of p1
// ptr_p1 现在持有 p1 的地址

要通过指针访问结构体的成员,使用箭头运算符(->)。

ptr_p1->x = 10.0; // Equivalent to (*ptr_p1).x = 10.0
// 等价于 (*ptr_p1).x = 10.0
ptr_p1->y = 20.0; // Equivalent to (*ptr_p1).y = 20.0
// 等价于 (*ptr_p1).y = 20.0
printf("Point via pointer: (%.2f, %.2f)\n", ptr_p1->x, ptr_p1->y);

表达式 (*ptr_p1).x 首先解引用指针(*ptr_p1)以获取结构体本身,然后使用点运算符(.)访问成员。箭头运算符(->)是这种常见操作的便捷简写形式。

位域允许您定义结构体成员,这些成员占据特定的位数,而不是完整的字节或字。这对于以下情况很有用:

  • 节省内存(Memory Saving): 将多个布尔标志或小型整数值打包到单个整数成员中。
  • 硬件接口(Hardware Interfacing): 使数据结构直接匹配硬件寄存器或特定二进制数据格式,其中字段不按字节对齐。

您通过指定成员类型(通常是 unsigned int 或 int)、成员名称、一个冒号 : 和位数来定义位域。

struct HardwareRegister {
unsigned int enable : 1; // 1 bit for enable flag
// 启用标志,占 1 位
unsigned int error_flag : 1; // 1 bit for error flag
// 错误标志,占 1 位
unsigned int mode : 3; // 3 bits for mode (0-7)
// 模式,占 3 位 (0-7)
unsigned int reserved : 3; // 3 unused bits (padding)
// 保留字段,占 3 位 (填充)
// Total: 1+1+3+3 = 8 bits (likely fits in 1 byte or int)
// 总计: 1+1+3+3 = 8 位 (可能存储在 1 字节或 int 中)
};
int main(void) {
struct HardwareRegister reg = {0}; // Initialize all bits to 0
// 初始化所有位为 0
reg.enable = 1; // Set the enable bit
// 设置启用位为 1
reg.mode = 5; // Set mode to 5 (binary 101)
// 设置模式为 5 (二进制 101)
if (reg.error_flag == 0) {
printf("No error detected.\n");
}
printf("Mode: %u\n", reg.mode);
printf("Size of register struct: %zu bytes\n", sizeof(reg));
return 0;
}

关于位域的重要注意事项:

  • 可移植性(Portability): 位域在内存中的确切布局(顺序、填充)是实现定义的(取决于编译器和架构)。依赖特定位域布局的代码可能不可移植。
  • 可寻址性(Addressability): 您不能获取位域成员的地址(&)。
  • 性能(Performance): 访问位域可能比访问常规成员慢,因为需要额外的位掩码和位移操作。
  • 类型(Type): 虽然 int 和 unsigned int 是常见的类型,但根据实现的不同,可能允许使用其他整数类型。

当内存节省或硬件映射是主要考虑因素时,请谨慎使用位域,但要了解其对可移植性的影响。