结构体是C语言中最重要的复合数据类型之一,它允许我们将不同类型的数据组合成一个有机整体。在实际开发中,结构体常用于表示复杂的数据实体,如学生信息、坐标点、商品属性等。
结构体在内存中的存储遵循成员声明顺序和内存对齐原则。假设我们定义如下结构体:
c复制struct Student {
int id; // 4字节
char name[20]; // 20字节
float score; // 4字节
};
在32位系统中,这个结构体通常占用28字节(4+20+4)。但实际占用可能因内存对齐而更大。理解这一点对优化内存使用和网络数据传输很重要。
内存对齐原则:结构体成员的起始地址通常是其自身大小的整数倍。编译器会在成员之间自动插入填充字节以满足对齐要求。
c复制// 先定义类型
struct Point {
int x;
int y;
};
// 后声明变量
struct Point p1, p2;
这种方式的优势在于类型定义与变量声明分离,提高了代码的可读性和复用性。
c复制struct {
char name[30];
int age;
} person1, person2;
匿名结构体适用于一次性使用的场景,但会限制代码的可维护性。我在实际项目中曾因滥用匿名结构体导致后期扩展困难,建议仅在内部作用域使用。
c复制typedef struct {
int x;
int y;
} Point;
Point p1, p2; // 无需再写struct关键字
typedef方式可以显著提升代码简洁性,特别是在需要频繁声明结构体变量的场景。
c复制struct Date {
int year;
int month;
int day;
};
struct Employee {
int id;
char name[50];
struct Date hire_date;
};
嵌套结构体适合表示具有层次关系的数据。访问时需要逐级引用:
c复制struct Employee emp;
emp.hire_date.year = 2023;
c复制struct Point p = {10, 20};
这种方式简单但不够灵活,必须严格按照成员声明顺序提供初始值。
c复制struct Point p = {.y = 20, .x = 10};
指定初始化器的优势:
c复制struct Point p = (struct Point){.x = 10, .y = 20};
特别适合作为函数参数临时创建结构体:
c复制drawPoint((struct Point){10, 20});
c复制struct Point p;
p.x = 10;
p.y = 20;
严格来说这不是初始化而是赋值,适用于需要动态确定成员值的场景。
c复制struct Point points[] = {
{1, 2},
{3, 4},
{.x = 5, .y = 6}
};
C语言的动态内存管理是程序员的必修课,也是许多bug的源头。理解malloc/free的工作原理对写出健壮的程序至关重要。
| 特性 | malloc | calloc |
|---|---|---|
| 函数原型 | void* malloc(size_t) | void* calloc(size_t, size_t) |
| 初始化内容 | 不初始化 | 初始化为0 |
| 参数含义 | 总字节数 | 元素个数和元素大小 |
| 适用场景 | 通用内存分配 | 数组分配 |
c复制int *arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
// 必须检查分配是否成功
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
常见错误:
c复制struct Student *class = (struct Student*)calloc(30, sizeof(struct Student));
// calloc会自动将所有成员初始化为0
calloc特别适合需要零初始化的场景,如哈希表、位图等数据结构。
c复制int *ptr = malloc(100 * sizeof(int));
// 使用ptr...
free(ptr);
ptr = NULL; // 重要:避免悬空指针
关键注意事项:
内存泄漏:分配后忘记释放
野指针:访问已释放的内存
越界访问:读写分配区域之外的内存
链表是动态数据结构的基石,理解链表的实现原理对掌握更复杂的数据结构至关重要。
c复制typedef struct Node {
int data; // 数据域
struct Node *next; // 指针域
} Node;
设计要点:
c复制Node *head = NULL; // 空链表
头指针的三种状态处理:
c复制Node* createNode(int data) {
Node *newNode = (Node*)malloc(sizeof(Node));
if (!newNode) {
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
c复制void insertAtHead(Node **head, int data) {
Node *newNode = createNode(data);
newNode->next = *head;
*head = newNode;
}
时间复杂度:O(1)
c复制void insertAtTail(Node **head, int data) {
Node *newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
return;
}
Node *current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
}
时间复杂度:O(n)
c复制void deleteNode(Node **head, int key) {
Node *temp = *head, *prev = NULL;
// 要删除的是头节点
if (temp != NULL && temp->data == key) {
*head = temp->next;
free(temp);
return;
}
// 查找要删除的节点
while (temp != NULL && temp->data != key) {
prev = temp;
temp = temp->next;
}
// 没找到
if (temp == NULL) return;
// 从链中解除
prev->next = temp->next;
free(temp);
}
c复制void printList(Node *head) {
Node *current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
c复制void sortedInsert(Node **head, int data) {
Node *newNode = createNode(data);
// 特殊情况:空链表或新节点值最小
if (*head == NULL || (*head)->data >= data) {
newNode->next = *head;
*head = newNode;
return;
}
// 查找插入位置
Node *current = *head;
while (current->next != NULL && current->next->data < data) {
current = current->next;
}
newNode->next = current->next;
current->next = newNode;
}
c复制void reverseList(Node **head) {
Node *prev = NULL;
Node *current = *head;
Node *next = NULL;
while (current != NULL) {
next = current->next; // 保存下一个节点
current->next = prev; // 反转指针
prev = current; // 移动prev
current = next; // 移动current
}
*head = prev;
}
c复制int hasCycle(Node *head) {
if (head == NULL) return 0;
Node *slow = head;
Node *fast = head->next;
while (fast != NULL && fast->next != NULL) {
if (slow == fast) return 1;
slow = slow->next;
fast = fast->next->next;
}
return 0;
}
段错误(Segmentation fault)
内存泄漏
链表断裂
完善的测试应该覆盖:
c复制void testLinkedList() {
Node *list = NULL;
// 测试空链表
assert(getLength(list) == 0);
// 测试插入
insertAtTail(&list, 10);
insertAtHead(&list, 5);
assert(getLength(list) == 2);
// 测试删除
deleteNode(&list, 10);
assert(getLength(list) == 1);
// 测试内存释放
freeList(&list);
assert(list == NULL);
}
在实际项目中,我曾遇到过因链表操作不当导致的内存泄漏问题。通过引入内存检测工具和严格的代码审查流程,最终将内存泄漏率降低了90%。这让我深刻认识到,对基础数据结构的扎实理解和规范操作是写出高质量C代码的关键。