11. 常见指针陷阱.md 16 KB

11 常见指针陷阱

11.1 完整概念讲解

野指针(未初始化指针)

野指针是指向随机内存地址的未初始化指针,是最危险的指针错误之一。

int *p;      // 野指针:未初始化
*p = 10;     // 危险!可能修改任意内存

预防措施:

  • 声明时立即初始化为NULL或有效地址
  • 使用前检查指针有效性

悬空指针(指向已释放内存)

悬空指针指向已经释放或失效的内存,解引用会导致未定义行为。

int *p = malloc(sizeof(int));
*p = 100;
free(p);
*p = 200;    // 悬空指针:解引用已释放内存
p = NULL;    // 正确:释放后置为NULL

预防措施:

  • 释放内存后立即将指针设为NULL
  • 使用智能指针或内存管理库

数组越界访问

数组越界是C语言中最常见的错误之一,可能导致数据损坏或程序崩溃。

int arr[5] = {1, 2, 3, 4, 5};
arr[10] = 100;  // 越界:未定义行为
arr[-1] = 0;    // 越界:未定义行为

预防措施:

  • 使用循环时严格检查边界
  • 使用安全的数组操作函数(如strncpy代替strcpy)

空指针解引用

解引用NULL指针会导致段错误(Segmentation Fault)。

int *p = NULL;
*p = 10;  // 段错误

预防措施:

  • 使用指针前检查是否为NULL
  • 使用断言(assert)进行调试检查

指针类型混淆(强转后步长变化)

指针类型转换可能导致步长变化,引发意想不到的错误。

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
char *cp = (char*)p;
cp++;  // 只移动1字节,不是4字节
printf("%d\n", *cp);  // 可能输出错误值

预防措施:

  • 避免不必要的类型转换
  • 类型转换时明确步长变化

返回局部变量地址

函数返回局部变量的地址是未定义行为,因为局部变量在函数结束后被销毁。

int* wrong_func() {
    int local = 10;
    return &local;  // 危险!
}

int *p = wrong_func();
printf("%d\n", *p);  // 未定义行为

预防措施:

  • 返回全局变量或静态变量的指针
  • 返回动态分配的内存(调用者负责释放)
  • 返回结构体副本而非指针

内存泄漏

动态分配的内存未释放,导致内存泄漏。

void leak() {
    int *p = malloc(sizeof(int));
    *p = 10;
    // 忘记free(p)
}

// 多次调用leak()会导致内存泄漏

预防措施:

  • 每次malloc都有对应的free
  • 使用内存检测工具(如Valgrind)
  • 考虑使用自动内存管理

指针与数组混淆的常见场景

指针和数组在大多数情况下可以互换,但有重要区别。

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/语法

内存安全函数

// 安全字符串操作
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);

调试工具

#include <assert.h>
assert(p != NULL);  // 调试时检查

// Valgrind内存检查
// valgrind --leak-check=full ./program

指针安全操作

// 检查指针有效性
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:野指针和悬空指针

#include <stdio.h>
#include <stdlib.h>

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:数组越界和空指针

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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:指针类型混淆

#include <stdio.h>

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:返回局部变量地址

#include <stdio.h>
#include <stdlib.h>

// 危险:返回局部变量地址
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:内存泄漏检测

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 内存泄漏示例
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 完整示例程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 安全的内存分配
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

注意:此示例展示了如何安全地处理指针操作,避免常见的指针陷阱。实际项目中应考虑使用更高级的内存管理技术。