引言:指针与数组的完美结合

在C语言编程中,指针和数组是两个最基础却又最强大的概念。它们之间的关系密切,理解它们的综合应用是掌握C语言的关键一步。本实验将深入探讨指针与数组的结合使用,通过实战案例展示如何高效处理数据,并解析常见的错误陷阱,帮助你构建坚实的编程基础。

指针提供了一种直接访问内存的方式,而数组则是一种存储多个相同类型数据的结构。当这两者结合时,我们可以实现动态内存管理、高效的数据遍历和灵活的字符串操作。然而,这种灵活性也带来了潜在的风险,如内存泄漏、越界访问等。因此,本实验不仅会讲解如何正确使用指针和数组,还会重点分析那些新手容易犯的错误,让你在实践中少走弯路。

通过本实验,你将学会:

  • 理解指针与数组的内在联系
  • 使用指针操作数组元素
  • 动态分配内存来创建可变大小的数组
  • 编写高效的字符串处理函数
  • 识别并避免常见的指针和数组错误

让我们开始这段探索之旅吧!

1. 指针与数组的基础关系回顾

在深入实战之前,我们先快速回顾一下指针与数组的基本关系。这是理解后续内容的基础。

1.1 数组名的本质

在C语言中,数组名通常被视为指向数组第一个元素的指针。例如:

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;  // ptr指向arr[0]

这里,arr 等价于 &arr[0],即数组首元素的地址。因此,ptr 指向 arr[0],其值为 arr 数组的首地址。

1.2 指针与数组的等价性

指针和数组在很多情况下可以互换使用。例如,访问数组元素:

// 通过下标访问
int value1 = arr[2];  // 访问第三个元素,值为3

// 通过指针访问
int value2 = *(ptr + 2);  // 等价于 ptr[2],值为3

注意:ptr + 2 表示指针向后移动2个元素的位置(每个元素大小为 sizeof(int) 字节),然后解引用得到值。

1.3 指针数组与数组指针

这是两个容易混淆的概念:

  • 指针数组:一个数组,其元素是指针。例如:int *arr[5]; 声明了一个包含5个 int* 指针的数组。
  • 数组指针:一个指针,指向一个数组。例如:int (*arr)[5]; 声明了一个指向包含5个 int 的数组的指针。

理解这些基础概念后,我们进入实战部分。

2. 实战案例1:使用指针遍历和修改数组

2.1 案例描述

假设我们有一个整数数组,我们想使用指针来遍历它,并将每个元素的值加倍。这个案例展示了指针的基本操作。

2.2 代码实现

#include <stdio.h>

void doubleArray(int *arr, int size) {
    // 使用指针遍历数组
    for (int i = 0; i < size; i++) {
        *(arr + i) *= 2;  // 等价于 arr[i] *= 2
    }
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int size = sizeof(numbers) / sizeof(numbers[0]);

    printf("原始数组: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    doubleArray(numbers, size);

    printf("加倍后数组: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    return 0;
}

2.3 详细解释

  • 函数参数doubleArray 函数接受一个 int* 指针和数组大小。注意,数组作为参数传递时,实际上传递的是首地址,函数内部无法直接获取数组大小,因此需要显式传递大小。
  • 指针算术:在循环中,*(arr + i) 访问数组的第 i 个元素。arr + i 是指针运算,表示从 arr 开始向后移动 i 个元素。
  • 等价写法*(arr + i) 等价于 arr[i],但使用指针形式更直观地展示了内存操作。
  • 输出:程序先打印原始数组,然后调用函数修改,最后打印加倍后的数组。

2.4 运行结果

原始数组: 1 2 3 4 5 
加倍后数组: 2 4 6 8 10

这个简单案例演示了如何用指针遍历和修改数组元素。接下来,我们看一个更复杂的案例。

3. 实战案例2:动态内存分配与数组

3.1 案例描述

静态数组的大小在编译时固定,但有时我们需要在运行时决定数组大小。这时,可以使用动态内存分配(malloc、calloc、realloc)结合指针来创建动态数组。

3.2 代码实现

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

int main() {
    int n;
    printf("请输入数组大小: ");
    scanf("%d", &n);

    // 动态分配内存
    int *dynamicArray = (int*)malloc(n * sizeof(int));
    if (dynamicArray == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    // 初始化数组
    for (int i = 0; i < n; i++) {
        dynamicArray[i] = i * 10;  // 使用数组下标
    }

    // 使用指针遍历打印
    printf("动态数组内容: ");
    int *ptr = dynamicArray;
    for (int i = 0; i < n; i++) {
        printf("%d ", *(ptr + i));
    }
    printf("\n");

    // 释放内存
    free(dynamicArray);

    return 0;
}

3.3 详细解释

  • 内存分配malloc(n * sizeof(int)) 分配了 nint 大小的连续内存块,返回一个 void* 指针,需要强制转换为 int*
  • 错误检查:分配后必须检查指针是否为 NULL,如果内存不足,分配会失败。
  • 初始化与访问:可以使用数组下标 dynamicArray[i] 或指针算术 *(dynamicArray + i) 来访问元素。
  • 内存释放:使用 free() 释放动态分配的内存,避免内存泄漏。忘记释放是常见错误。
  • 灵活性:数组大小 n 在运行时确定,这比静态数组更灵活。

3.4 运行示例

请输入数组大小: 5
动态数组内容: 0 10 20 30 40

注意:如果输入的 n 很大,可能导致内存分配失败,程序会输出错误信息并退出。

4. 实战案例3:字符串处理与指针

4.1 案例描述

字符串在C语言中是以空字符 \0 结尾的字符数组。指针在字符串操作中非常高效。本案例实现一个自定义的字符串复制函数 myStrcpy,并演示常见错误。

4.2 代码实现

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

// 自定义字符串复制函数
void myStrcpy(char *dest, const char *src) {
    if (dest == NULL || src == NULL) {
        printf("错误:空指针!\n");
        return;
    }
    
    // 方法1:使用数组下标
    // int i = 0;
    // while (src[i] != '\0') {
    //     dest[i] = src[i];
    //     i++;
    // }
    // dest[i] = '\0';

    // 方法2:使用指针(推荐)
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';  // 添加结束符
}

int main() {
    const char *source = "Hello, Pointer!";
    // 目标缓冲区需要足够大
    char *destination = (char*)malloc(strlen(source) + 1);  // +1 for '\0'
    
    if (destination == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    myStrcpy(destination, source);
    printf("复制结果: %s\n", destination);

    free(destination);
    return 0;
}

4.3 详细解释

  • 字符串本质:字符串是字符数组,以 \0 结束。指针 src 指向源字符串的首字符。
  • 函数设计myStrcpy 接受两个指针:目标和源。源字符串用 const 修饰,表示不会修改它。
  • 指针操作:在循环中,*src*dest 分别解引用源和目标指针。通过递增指针(src++dest++)移动到下一个字符。
  • 结束处理:循环结束后,手动添加 \0
  • 安全检查:检查空指针,避免崩溃。
  • 内存分配:在 main 中,使用 strlen(source) + 1 确保目标缓冲区足够大,包括结束符。
  • 为什么用指针:指针版本比下标版本更简洁高效,直接操作内存地址。

4.4 运行结果

复制结果: Hello, Pointer!

这个案例展示了指针在字符串处理中的强大之处,但也强调了缓冲区大小的重要性。

5. 实战案例4:二维数组与指针的综合应用

5.1 案例描述

二维数组可以看作数组的数组。使用指针访问二维数组元素需要理解内存布局。本案例动态创建一个二维数组,并计算矩阵转置。

5.2 代码实现

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

void printMatrix(int **matrix, int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            // 注意:这里使用 matrix[i][j] 或 *(*(matrix + i) + j)
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
}

void transposeMatrix(int **matrix, int rows, int cols) {
    // 创建转置矩阵
    int **transpose = (int**)malloc(cols * sizeof(int*));
    if (transpose == NULL) {
        printf("内存分配失败!\n");
        return;
    }
    
    for (int i = 0; i < cols; i++) {
        transpose[i] = (int*)malloc(rows * sizeof(int));
        if (transpose[i] == NULL) {
            printf("内存分配失败!\n");
            // 释放已分配的部分
            for (int k = 0; k < i; k++) free(transpose[k]);
            free(transpose);
            return;
        }
    }

    // 计算转置
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            transpose[j][i] = matrix[i][j];
        }
    }

    printf("原始矩阵:\n");
    printMatrix(matrix, rows, cols);
    printf("转置矩阵:\n");
    printMatrix(transpose, cols, rows);

    // 释放内存
    for (int i = 0; i < cols; i++) {
        free(transpose[i]);
    }
    free(transpose);
}

int main() {
    int rows = 2, cols = 3;
    
    // 动态创建二维数组
    int **matrix = (int**)malloc(rows * sizeof(int*));
    if (matrix == NULL) return 1;
    
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int*)malloc(cols * sizeof(int));
        if (matrix[i] == NULL) {
            for (int k = 0; k < i; k++) free(matrix[k]);
            free(matrix);
            return 1;
        }
    }

    // 初始化
    int counter = 1;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = counter++;
        }
    }

    transposeMatrix(matrix, rows, cols);

    // 释放原矩阵
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);

    return 0;
}

5.3 详细解释

  • 二维数组的指针表示int** 是指向指针的指针。动态二维数组需要先分配行指针数组,再为每行分配列。
  • 内存布局:在内存中,二维数组是行优先连续存储的,但动态分配的行可能不连续。
  • 转置逻辑:转置是将 matrix[i][j] 赋值给 transpose[j][i]
  • 双重循环:外层循环遍历行,内层遍历列。
  • 内存管理:分配和释放都需要逐行进行,否则会导致内存泄漏。
  • 指针访问matrix[i][j] 等价于 *(*(matrix + i) + j),其中 matrix + i 是第 i 行的指针,*(matrix + i) 是该行的首地址,再加 j 访问列。

5.4 运行结果

原始矩阵:
1 2 3 
4 5 6 
转置矩阵:
1 4 
2 5 
3 6 

这个案例展示了指针在处理复杂数据结构(如动态二维数组)中的应用,强调了内存管理的复杂性。

6. 常见错误解析

指针和数组的强大伴随着风险。以下是常见错误及其解析,每个错误都配有示例代码和修正方法。

6.1 错误1:数组越界访问

描述:访问数组时超出其边界,导致未定义行为(可能崩溃、数据损坏或安全漏洞)。

示例代码

#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};
    // 错误:访问 arr[5],超出边界
    printf("%d\n", arr[5]);  // 未定义行为,可能打印垃圾值或崩溃
    return 0;
}

为什么错误:C语言不检查数组边界。arr[5] 可能访问到其他变量的内存或无效地址。

修正方法

  • 始终使用循环变量检查边界。
  • 使用 sizeof(arr)/sizeof(arr[0]) 获取大小。
  • 在函数中传递大小参数。

修正代码

#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};
    int size = sizeof(arr) / sizeof(arr[0]);
    
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);  // 安全访问
    }
    printf("\n");
    return 0;
}

输出

1 2 3

6.2 错误2:空指针解引用

描述:使用未初始化或已释放的指针,导致程序崩溃。

示例代码

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

int main() {
    int *ptr;
    // 错误:ptr未初始化,指向随机地址
    *ptr = 10;  // 可能崩溃或修改任意内存
    
    int *arr = malloc(5 * sizeof(int));
    free(arr);
    // 错误:使用已释放的指针
    arr[0] = 1;  // 未定义行为
    return 0;
}

为什么错误:未初始化指针包含垃圾值,解引用它会访问无效内存。释放后指针成为“悬空指针”,不应再使用。

修正方法

  • 声明指针后立即初始化(如赋值为 NULL 或有效地址)。
  • free 后将指针设为 NULL,防止误用。
  • 检查 malloc 返回值。

修正代码

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

int main() {
    int *ptr = NULL;  // 初始化为NULL
    // 现在可以安全检查
    if (ptr != NULL) {
        *ptr = 10;
    } else {
        printf("指针为空,无法赋值\n");
    }

    int *arr = malloc(5 * sizeof(int));
    if (arr != NULL) {
        arr[0] = 1;  // 安全使用
        free(arr);
        arr = NULL;  // 防止悬空
    }
    return 0;
}

输出

指针为空,无法赋值

6.3 错误3:内存泄漏

描述:动态分配内存后忘记释放,导致程序占用内存不断增加,最终耗尽系统资源。

示例代码

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

void leakyFunction() {
    int *arr = malloc(100 * sizeof(int));
    // 忘记 free(arr)
}

int main() {
    for (int i = 0; i < 1000; i++) {
        leakyFunction();  // 每次调用泄漏内存
    }
    printf("程序结束,但内存未释放\n");
    return 0;
}

为什么错误malloc 分配的内存只有在 free 后才归还系统。循环调用会累积泄漏。

修正方法

  • 每个 malloc 必须有对应的 free
  • 在函数结束前释放,或使用智能指针(C++),但C语言中需手动管理。
  • 使用工具如 Valgrind 检测泄漏。

修正代码

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

void nonLeakyFunction() {
    int *arr = malloc(100 * sizeof(int));
    if (arr != NULL) {
        // 使用 arr...
        free(arr);  // 释放
    }
}

int main() {
    for (int i = 0; i < 1000; i++) {
        nonLeakyFunction();
    }
    printf("程序结束,无内存泄漏\n");
    return 0;
}

输出

程序结束,无内存泄漏

6.4 错误4:指针类型不匹配

描述:将不同类型的指针相互赋值或解引用,导致数据解释错误。

示例代码

#include <stdio.h>

int main() {
    int x = 0x12345678;
    char *p = (char*)&x;  // 正确:强制转换
    
    // 错误:直接赋值而不转换
    // float *fp = &x;  // 编译警告或错误
    
    // 错误:解引用类型不匹配
    printf("%f\n", *fp);  // 如果fp是float*,解释int为float,结果错误
    
    return 0;
}

为什么错误:指针类型决定了如何解释内存中的字节。int* 解引用为4字节整数,float* 解引用为4字节浮点数,直接混用会错误解释数据。

修正方法

  • 使用强制类型转换 (type*)
  • 理解类型大小和对齐。
  • 避免不必要的类型转换。

修正代码

#include <stdio.h>

int main() {
    int x = 0x12345678;
    char *p = (char*)&x;  // 强制转换为char*,用于逐字节访问
    
    // 安全使用:打印每个字节
    for (int i = 0; i < sizeof(int); i++) {
        printf("字节 %d: %02X\n", i, (unsigned char)p[i]);
    }
    
    // 如果需要float解释,使用联合体或转换
    float f = *(float*)&x;  // 注意:这可能产生NaN,取决于x的值
    printf("作为float: %f\n", f);
    
    return 0;
}

输出(取决于系统字节序):

字节 0: 78
字节 1: 56
字节 2: 34
字节 3: 12
作为float: 2.5850e-38  (示例值,实际取决于x)

6.5 错误5:字符串操作中的缓冲区溢出

描述:复制字符串时目标缓冲区不足,导致溢出,覆盖相邻内存。

示例代码

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

int main() {
    char dest[5];
    const char *src = "Hello, World!";  // 长度13 + 1 = 14字节
    
    strcpy(dest, src);  // 错误:dest太小,溢出
    printf("%s\n", dest);  // 可能崩溃或打印乱码
    
    return 0;
}

为什么错误strcpy 不检查长度,直接复制直到 \0,导致 dest 后的内存被覆盖。

修正方法

  • 使用 strncpy 限制复制长度。
  • 确保目标缓冲区足够大。
  • 使用 snprintf 或安全函数。

修正代码

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

int main() {
    char dest[14];  // 足够大
    const char *src = "Hello, World!";
    
    // 方法1:使用 strncpy
    strncpy(dest, src, sizeof(dest) - 1);
    dest[sizeof(dest) - 1] = '\0';  // 确保结束符
    
    // 方法2:使用 snprintf(更安全)
    snprintf(dest, sizeof(dest), "%s", src);
    
    printf("%s\n", dest);
    return 0;
}

输出

Hello, World!

6.6 错误6:混淆指针数组与数组指针

描述:错误声明或使用指针数组和数组指针,导致语法错误或运行时崩溃。

示例代码

#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};
    
    // 错误:声明为指针数组,但试图当作数组指针使用
    int *ptrArr[3];  // 指针数组
    ptrArr[0] = arr;  // 正确:ptrArr[0]指向arr
    
    // 错误:ptrArr[0][1] 语法正确,但含义不同
    // 这里 ptrArr[0][1] 等价于 *(ptrArr[0] + 1) = arr[1] = 2
    
    // 但如果误用:int (*arrPtr)[3] = &arr; 正确
    // 而 int *arrPtr = arr; 也是正确的,但类型不同
    
    // 常见错误:混淆导致的类型不匹配
    // void func(int *arr[3]) { ... }  // 这是指针数组参数
    // void func(int (*arr)[3]) { ... }  // 这是数组指针参数
    
    return 0;
}

为什么错误int *ptrArr[3] 是数组,每个元素是 int*int (*arrPtr)[3] 是指针,指向一个 int[3] 数组。混淆会导致编译警告或运行时错误。

修正方法

  • 明确声明意图:需要数组用 [],需要指针用 *
  • 使用 typedef 简化复杂声明。
  • 理解优先级:[] 优先级高于 *

修正代码

#include <stdio.h>

// 正确:指针数组参数
void processPointerArray(int *arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", *arr[i]);  // arr[i]是int*,解引用得到int
    }
    printf("\n");
}

// 正确:数组指针参数
void processArrayPointer(int (*arr)[3]) {
    for (int i = 0; i < 3; i++) {
        printf("%d ", (*arr)[i]);  // (*arr)是数组,使用下标
    }
    printf("\n");
}

int main() {
    int a = 1, b = 2, c = 3;
    int *ptrArr[3] = {&a, &b, &c};  // 指针数组
    
    int arr[3] = {4, 5, 6};
    int (*arrPtr)[3] = &arr;  // 数组指针
    
    processPointerArray(ptrArr, 3);
    processArrayPointer(arrPtr);
    
    return 0;
}

输出

1 2 3 
4 5 6 

6.7 错误7:忘记初始化指针数组元素

描述:声明指针数组后未初始化每个元素,直接使用导致空指针解引用。

示例代码

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

int main() {
    int *arr[5];  // 未初始化,元素值随机
    
    // 错误:直接分配内存给未初始化的元素
    // arr[0] = malloc(sizeof(int));  // 正确,但如果忘记这一步
    
    *arr[0] = 10;  // 崩溃:arr[0]是垃圾值,解引用无效
    
    return 0;
}

为什么错误:指针数组的元素是未初始化的指针,包含随机地址。解引用它们会访问无效内存。

修正方法

  • 声明后立即为每个元素分配内存或赋值为 NULL
  • 使用 calloc 初始化为0(但指针需额外处理)。

修正代码

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

int main() {
    int *arr[5] = {NULL};  // 初始化所有元素为NULL
    
    // 为每个元素分配内存
    for (int i = 0; i < 5; i++) {
        arr[i] = malloc(sizeof(int));
        if (arr[i] == NULL) {
            printf("分配失败\n");
            // 释放已分配的部分
            for (int k = 0; k < i; k++) free(arr[k]);
            return 1;
        }
        *arr[i] = i * 10;  // 安全赋值
    }
    
    // 使用
    for (int i = 0; i < 5; i++) {
        printf("%d ", *arr[i]);
    }
    printf("\n");
    
    // 释放
    for (int i = 0; i < 5; i++) free(arr[i]);
    
    return 0;
}

输出

0 10 20 30 40

6.8 错误8:指针算术溢出

描述:指针运算超出有效范围,导致访问无效内存。

示例代码

#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};
    int *ptr = arr;
    
    // 错误:指针算术超出数组边界
    int *invalid = ptr + 10;  // 无效指针
    printf("%d\n", *invalid);  // 未定义行为
    
    return 0;
}

为什么错误ptr + 10 指向数组外的内存,解引用是非法的。

修正方法

  • 始终确保指针运算在有效范围内。
  • 使用循环时,用大小限制指针移动。
  • 对于动态数组,记录大小。

修正代码

#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};
    int size = 3;
    int *ptr = arr;
    
    // 安全:只在范围内移动
    for (int i = 0; i < size; i++) {
        printf("%d ", *(ptr + i));
    }
    printf("\n");
    
    return 0;
}

输出

1 2 3

6.9 错误9:多级指针错误

描述:处理多级指针(如 int**)时,错误解引用导致崩溃。

示例代码

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

int main() {
    int **pp = malloc(sizeof(int*));  // 分配一个int*
    *pp = malloc(sizeof(int));        // 分配int
    
    // 错误:忘记分配内层指针
    int **pp2 = malloc(sizeof(int*));  // *pp2未初始化
    **pp2 = 10;  // 崩溃:*pp2是垃圾值
    
    return 0;
}

为什么错误pp2 指向一个未初始化的 int*,解引用二级指针时访问无效地址。

修正方法

  • 逐级分配和检查。
  • 理解多级指针的层次。

修正代码

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

int main() {
    int **pp = malloc(sizeof(int*));
    if (pp == NULL) return 1;
    
    *pp = malloc(sizeof(int));
    if (*pp == NULL) {
        free(pp);
        return 1;
    }
    
    **pp = 10;
    printf("%d\n", **pp);
    
    free(*pp);
    free(pp);
    
    return 0;
}

输出

10

6.10 错误10:字符串字面量的误用

描述:试图修改字符串字面量,或错误地将字面量赋值给指针。

示例代码

#include <stdio.h>

int main() {
    char *str = "Hello";  // 字面量在只读内存
    
    // 错误:试图修改
    str[0] = 'h';  // 未定义行为,可能崩溃
    
    return 0;
}

为什么错误:字符串字面量通常存储在只读段,修改会导致崩溃。

修正方法

  • 使用字符数组存储可修改字符串。
  • 或使用 const char* 表示只读。

修正代码

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

int main() {
    // 方法1:字符数组(可修改)
    char str1[] = "Hello";
    str1[0] = 'h';
    printf("%s\n", str1);
    
    // 方法2:动态分配
    char *str2 = malloc(6);
    strcpy(str2, "Hello");
    str2[0] = 'h';
    printf("%s\n", str2);
    free(str2);
    
    // 方法3:const指针(只读)
    const char *str3 = "Hello";
    // str3[0] = 'h';  // 错误:编译器会报错
    printf("%s\n", str3);
    
    return 0;
}

输出

hello
hello
Hello

7. 最佳实践与调试技巧

为了避免上述错误,以下是一些最佳实践:

7.1 最佳实践

  1. 初始化所有指针:声明时赋值为 NULL
  2. 检查返回值mallocscanf 等函数返回值必须检查。
  3. 使用 const:对于不修改的指针参数,使用 const 保护。
  4. 记录大小:始终传递或存储数组大小。
  5. 配对使用:每个 malloc/calloc/realloc 必须有 free
  6. 避免悬空指针free 后立即设为 NULL
  7. 使用安全函数:如 strncpy 代替 strcpysnprintf 代替 sprintf
  8. 代码审查:手动检查指针使用,或使用静态分析工具。

7.2 调试技巧

  • 使用 GDB:在Linux下,用 gdb 调试,设置断点检查指针值。
    • 示例:gdb ./program,然后 break mainrunprint ptr 查看指针值。
  • Valgrind:检测内存泄漏和越界。
    • 命令:valgrind --leak-check=full ./program
  • AddressSanitizer:编译时添加 -fsanitize=address,运行时自动检测错误。
    • 示例:gcc -fsanitize=address -g program.c -o program
  • 打印调试:在关键点打印指针地址和值,如 printf("ptr=%p, *ptr=%d\n", ptr, *ptr);
  • 静态分析:使用 clang-tidycppcheck 检查潜在问题。

8. 总结

本实验通过多个实战案例,深入探讨了C语言中指针与数组的综合应用。从基础遍历到动态内存、字符串处理和二维数组,我们看到了指针的灵活性和强大。同时,通过详细解析10个常见错误,我们强调了安全编程的重要性。

指针和数组是C语言的核心,掌握它们需要实践和警惕。记住:指针是地址,数组是连续存储,结合使用时始终关注边界和内存生命周期。通过本实验的代码示例和解释,你应该能够编写更健壮的程序,并避免常见陷阱。

继续练习这些案例,尝试修改和扩展它们,以巩固知识。如果你遇到问题,回顾错误解析部分,或使用调试工具深入分析。编程愉快!