# 11 常见指针陷阱 ## 11.1 完整概念讲解 ### 野指针(未初始化指针) 野指针是指向随机内存地址的未初始化指针,是最危险的指针错误之一。 ```c int *p; // 野指针:未初始化 *p = 10; // 危险!可能修改任意内存 ``` **预防措施:** - 声明时立即初始化为NULL或有效地址 - 使用前检查指针有效性 ### 悬空指针(指向已释放内存) 悬空指针指向已经释放或失效的内存,解引用会导致未定义行为。 ```c int *p = malloc(sizeof(int)); *p = 100; free(p); *p = 200; // 悬空指针:解引用已释放内存 p = NULL; // 正确:释放后置为NULL ``` **预防措施:** - 释放内存后立即将指针设为NULL - 使用智能指针或内存管理库 ### 数组越界访问 数组越界是C语言中最常见的错误之一,可能导致数据损坏或程序崩溃。 ```c int arr[5] = {1, 2, 3, 4, 5}; arr[10] = 100; // 越界:未定义行为 arr[-1] = 0; // 越界:未定义行为 ``` **预防措施:** - 使用循环时严格检查边界 - 使用安全的数组操作函数(如strncpy代替strcpy) ### 空指针解引用 解引用NULL指针会导致段错误(Segmentation Fault)。 ```c int *p = NULL; *p = 10; // 段错误 ``` **预防措施:** - 使用指针前检查是否为NULL - 使用断言(assert)进行调试检查 ### 指针类型混淆(强转后步长变化) 指针类型转换可能导致步长变化,引发意想不到的错误。 ```c int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; char *cp = (char*)p; cp++; // 只移动1字节,不是4字节 printf("%d\n", *cp); // 可能输出错误值 ``` **预防措施:** - 避免不必要的类型转换 - 类型转换时明确步长变化 ### 返回局部变量地址 函数返回局部变量的地址是未定义行为,因为局部变量在函数结束后被销毁。 ```c int* wrong_func() { int local = 10; return &local; // 危险! } int *p = wrong_func(); printf("%d\n", *p); // 未定义行为 ``` **预防措施:** - 返回全局变量或静态变量的指针 - 返回动态分配的内存(调用者负责释放) - 返回结构体副本而非指针 ### 内存泄漏 动态分配的内存未释放,导致内存泄漏。 ```c void leak() { int *p = malloc(sizeof(int)); *p = 10; // 忘记free(p) } // 多次调用leak()会导致内存泄漏 ``` **预防措施:** - 每次malloc都有对应的free - 使用内存检测工具(如Valgrind) - 考虑使用自动内存管理 ### 指针与数组混淆的常见场景 指针和数组在大多数情况下可以互换,但有重要区别。 ```c int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // sizeof不同 printf("sizeof(arr) = %zu\n", sizeof(arr)); // 20 printf("sizeof(p) = %zu\n", sizeof(p)); // 4或8 // &操作符不同 int (*arr_ptr)[5] = &arr; // 指向整个数组 int *p_ptr = p; // 指向首元素 ``` ## 11.2 核心API/语法 ### 内存安全函数 ```c // 安全字符串操作 size_t strncpy(char *dest, const char *src, size_t n); size_t strlcpy(char *dest, const char *src, size_t n); // 安全输入 char *fgets(char *str, int n, FILE *stream); // 内存检查 int memcmp(const void *s1, const void *s2, size_t n); ``` ### 调试工具 ```c #include assert(p != NULL); // 调试时检查 // Valgrind内存检查 // valgrind --leak-check=full ./program ``` ### 指针安全操作 ```c // 检查指针有效性 int is_valid_ptr(void *p) { return p != NULL; } // 安全释放 void safe_free(void **ptr) { if (ptr != NULL && *ptr != NULL) { free(*ptr); *ptr = NULL; } } ``` ## 11.3 代码示例(完整可编译,附gcc命令) ### 示例1:野指针和悬空指针 ```c #include #include void demonstrate_wild_pointer() { printf("=== 野指针演示 ===\n"); // 野指针:未初始化 int *wild_ptr; // *wild_ptr = 10; // 危险!可能崩溃 // 正确做法:初始化为NULL int *safe_ptr = NULL; if (safe_ptr != NULL) { printf("safe_ptr有效\n"); } else { printf("safe_ptr是空指针\n"); } // 分配内存后初始化 safe_ptr = (int*)malloc(sizeof(int)); if (safe_ptr != NULL) { *safe_ptr = 100; printf("safe_ptr = %d\n", *safe_ptr); free(safe_ptr); safe_ptr = NULL; // 释放后置为NULL } } void demonstrate_dangling_pointer() { printf("\n=== 悬空指针演示 ===\n"); int *p = (int*)malloc(sizeof(int)); *p = 200; printf("释放前: *p = %d\n", *p); free(p); // *p = 300; // 危险!悬空指针 // 正确做法:释放后置为NULL p = NULL; if (p == NULL) { printf("p已置为NULL\n"); } // 多次释放同一指针 // free(p); // 错误:重复释放 } int main() { demonstrate_wild_pointer(); demonstrate_dangling_pointer(); return 0; } ``` 编译命令:`gcc -o wild_dangling wild_dangling.c` ### 示例2:数组越界和空指针 ```c #include #include #include void demonstrate_out_of_bounds() { printf("=== 数组越界演示 ===\n"); int arr[5] = {1, 2, 3, 4, 5}; // 正确访问 for (int i = 0; i < 5; i++) { printf("arr[%d] = %d\n", i, arr[i]); } // 越界访问(未定义行为) // arr[10] = 100; // 可能崩溃或数据损坏 // arr[-1] = 0; // 可能崩溃或数据损坏 // 安全的数组操作 int index = 3; if (index >= 0 && index < 5) { printf("安全访问 arr[%d] = %d\n", index, arr[index]); } else { printf("索引越界\n"); } } void demonstrate_null_pointer() { printf("\n=== 空指针解引用演示 ===\n"); int *p = NULL; // 检查指针有效性 if (p != NULL) { printf("*p = %d\n", *p); } else { printf("p是空指针,不能解引用\n"); } // 动态分配后检查 p = (int*)malloc(sizeof(int)); if (p != NULL) { *p = 300; printf("分配后: *p = %d\n", *p); free(p); p = NULL; } } int main() { demonstrate_out_of_bounds(); demonstrate_null_pointer(); return 0; } ``` 编译命令:`gcc -o out_of_bounds_null out_of_bounds_null.c` ### 示例3:指针类型混淆 ```c #include void demonstrate_type_confusion() { printf("=== 指针类型混淆演示 ===\n"); int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // 不同类型的指针 char *cp = (char*)p; double *dp = (double*)p; // 指针算术步长不同 printf("int指针步长: %zu字节\n", sizeof(int)); printf("char指针步长: %zu字节\n", sizeof(char)); printf("double指针步长: %zu字节\n", sizeof(double)); // 演示步长差异 printf("\n地址变化:\n"); printf("p (int*) = %p\n", (void*)p); printf("p + 1 (int*) = %p\n", (void*)(p + 1)); printf("cp (char*) = %p\n", (void*)cp); printf("cp + 1 (char*) = %p\n", (void*)(cp + 1)); // 错误:通过char指针访问int数组 printf("\n错误访问:\n"); printf("*cp = %d (应该是1)\n", *cp); printf("*(cp + 1) = %d (应该是0)\n", *(cp + 1)); // 正确:使用正确类型 printf("\n正确访问:\n"); printf("*p = %d\n", *p); printf("*(p + 1) = %d\n", *(p + 1)); } int main() { demonstrate_type_confusion(); return 0; } ``` 编译命令:`gcc -o type_confusion type_confusion.c` ### 示例4:返回局部变量地址 ```c #include #include // 危险:返回局部变量地址 int* dangerous_func() { int local_var = 100; return &local_var; // 未定义行为 } // 正确:返回全局变量指针 int global_var = 200; int* get_global() { return &global_var; } // 正确:返回静态变量指针 int* get_static() { static int static_var = 300; return &static_var; } // 正确:返回动态分配内存 int* get_dynamic(int value) { int *p = (int*)malloc(sizeof(int)); if (p != NULL) { *p = value; } return p; } // 正确:返回结构体指针 struct Point { int x; int y; }; struct Point* create_point(int x, int y) { struct Point *p = (struct Point*)malloc(sizeof(struct Point)); if (p != NULL) { p->x = x; p->y = y; } return p; } int main() { printf("=== 返回局部变量地址演示 ===\n"); // 错误用法(未定义行为) // int *p1 = dangerous_func(); // printf("危险函数: %d\n", *p1); // 正确用法 int *p2 = get_global(); printf("全局变量: %d\n", *p2); int *p3 = get_static(); printf("静态变量: %d\n", *p3); int *p4 = get_dynamic(400); if (p4 != NULL) { printf("动态内存: %d\n", *p4); free(p4); } struct Point *point = create_point(10, 20); if (point != NULL) { printf("结构体: (%d, %d)\n", point->x, point->y); free(point); } return 0; } ``` 编译命令:`gcc -o return_local return_local.c` ### 示例5:内存泄漏检测 ```c #include #include #include // 内存泄漏示例 void leak_example() { int *p = (int*)malloc(sizeof(int)); *p = 10; // 忘记free(p) - 内存泄漏 } // 正确的内存管理 void correct_memory_management() { int *p = (int*)malloc(sizeof(int)); if (p != NULL) { *p = 20; free(p); // 正确释放 p = NULL; // 置为NULL } } // 多次分配和释放 void multiple_allocations() { int *arr[10]; for (int i = 0; i < 10; i++) { arr[i] = (int*)malloc(sizeof(int)); if (arr[i] != NULL) { *arr[i] = i * 10; } } // 使用内存 for (int i = 0; i < 10; i++) { if (arr[i] != NULL) { printf("arr[%d] = %d\n", i, *arr[i]); } } // 释放所有内存 for (int i = 0; i < 10; i++) { if (arr[i] != NULL) { free(arr[i]); arr[i] = NULL; } } } // 安全的字符串复制 void safe_string_copy() { char *src = "Hello, World!"; char *dest = (char*)malloc(strlen(src) + 1); if (dest != NULL) { // 安全复制 strncpy(dest, src, strlen(src) + 1); printf("复制的字符串: %s\n", dest); free(dest); dest = NULL; } } int main() { printf("=== 内存管理演示 ===\n"); // leak_example(); // 会导致内存泄漏 correct_memory_management(); multiple_allocations(); safe_string_copy(); printf("程序结束\n"); return 0; } ``` 编译命令:`gcc -o memory_leak memory_leak.c` ## 11.4 注意事项与易错点 ### 野指针 - 未初始化的指针指向随机地址 - 危害极大,可能导致任意内存修改 - 必须初始化后再使用 ### 悬空指针 - 释放内存后指针仍然存在 - 解引用导致未定义行为 - 释放后必须置为NULL ### 数组越界 - C语言不检查数组边界 - 越界访问可能损坏其他数据 - 使用循环时必须检查边界 ### 空指针解引用 - NULL指针不能解引用 - 函数返回指针时必须检查 - 使用断言进行调试检查 ### 类型混淆 - 指针类型转换改变步长 - 强制类型转换可能绕过类型系统 - 尽量避免不必要的类型转换 ### 内存泄漏 - 动态内存必须释放 - 释放后指针必须置为NULL - 使用内存检测工具 ## 11.5 面试要点(3-5个Q&A) **Q1: 什么是野指针?如何避免?** A: 野指针是未初始化的指针,指向随机内存地址。避免方法:1) 声明时立即初始化为NULL或有效地址;2) 使用前检查指针有效性;3) 使用内存检测工具。 **Q2: 什么是悬空指针?如何处理?** A: 悬空指针指向已释放的内存。处理方法:1) 释放内存后立即将指针设为NULL;2) 使用智能指针或内存管理库;3) 避免返回局部变量地址。 **Q3: 指针类型混淆会导致什么问题?** A: 指针类型转换会改变步长,导致指针算术错误。例如,将int*转换为char*后,p+1只移动1字节而不是4字节,可能导致数据访问错误。 **Q4: 如何避免内存泄漏?** A: 1) 每次malloc都有对应的free;2) 释放后将指针设为NULL;3) 使用内存检测工具如Valgrind;4) 考虑使用自动内存管理。 **Q5: 笔试中常见的指针陷阱有哪些?** A: 1) 指针和数组sizeof差异;2) 函数参数中数组退化;3) 返回局部变量地址;4) 指针算术越界;5) 多级指针误用;6) const指针与非常量指针的转换。 ## 11.6 完整示例程序 ```c #include #include #include // 安全的内存分配 void* safe_malloc(size_t size) { void *ptr = malloc(size); if (ptr == NULL) { fprintf(stderr, "内存分配失败\n"); exit(EXIT_FAILURE); } return ptr; } // 安全的字符串操作 char* safe_strdup(const char *s) { if (s == NULL) return NULL; size_t len = strlen(s) + 1; char *dup = (char*)safe_malloc(len); strcpy(dup, s); return dup; } // 链表节点(演示指针陷阱避免) struct Node { int data; struct Node *next; }; // 安全的链表插入 void safe_insert(struct Node **head, int data) { struct Node *new_node = (struct Node*)safe_malloc(sizeof(struct Node)); new_node->data = data; new_node->next = *head; *head = new_node; } // 安全的链表删除 void safe_delete(struct Node **head, int data) { struct Node *current = *head; struct Node *prev = NULL; while (current != NULL) { if (current->data == data) { if (prev == NULL) { *head = current->next; } else { prev->next = current->next; } free(current); return; } prev = current; current = current->next; } } // 安全的字符串连接 char* safe_strcat(const char *s1, const char *s2) { if (s1 == NULL || s2 == NULL) return NULL; size_t len1 = strlen(s1); size_t len2 = strlen(s2); char *result = (char*)safe_malloc(len1 + len2 + 1); strcpy(result, s1); strcat(result, s2); return result; } int main() { printf("=== 安全指针操作演示 ===\n"); // 安全内存分配 int *arr = (int*)safe_malloc(5 * sizeof(int)); for (int i = 0; i < 5; i++) { arr[i] = i * 10; } // 安全字符串操作 char *str1 = safe_strdup("Hello"); char *str2 = safe_strdup("World"); char *combined = safe_strcat(str1, " "); char *final = safe_strcat(combined, str2); printf("组合字符串: %s\n", final); // 清理内存 free(arr); free(str1); free(str2); free(combined); free(final); // 链表操作 struct Node *head = NULL; safe_insert(&head, 1); safe_insert(&head, 2); safe_insert(&head, 3); printf("链表: "); struct Node *current = head; while (current != NULL) { printf("%d ", current->data); current = current->next; } printf("\n"); // 删除节点 safe_delete(&head, 2); printf("删除2后: "); current = head; while (current != NULL) { printf("%d ", current->data); current = current->next; } printf("\n"); // 释放链表 current = head; while (current != NULL) { struct Node *temp = current; current = current->next; free(temp); } printf("程序安全结束\n"); return 0; } ``` 编译命令:`gcc -o safe_pointer safe_pointer.c` **注意**:此示例展示了如何安全地处理指针操作,避免常见的指针陷阱。实际项目中应考虑使用更高级的内存管理技术。