实验九概述:指针与数组的深入应用

实验九通常是C语言程序设计课程中的关键转折点,它标志着从基础语法向高级内存操作和数据结构处理的过渡。本实验的核心目标是帮助学生掌握指针与数组之间的紧密联系,理解动态内存分配的基本原理,并能够运用指针高效地处理字符串和数值数组。指针是C语言的灵魂,也是最难掌握的概念之一,因此本实验的详细解答和问题排查显得尤为重要。

实验目标与核心知识点

在开始解答具体题目之前,我们需要明确本次实验旨在巩固以下知识点:

  1. 指针与数组的关系:数组名本质上是指向数组首元素的常量指针,通过指针算术运算访问数组元素。
  2. 字符指针与字符串:理解 char*char[] 在存储字符串时的区别,掌握字符串的指针操作。
  3. 动态内存分配:使用 mallocfree 在堆区申请和释放内存,解决数组长度不确定的问题。
  4. 指针作为函数参数:通过指针实现函数内部对调用者变量的修改(传地址)。

实验题目一:数组元素的指针访问与逆序输出

题目描述

编写一个函数 reverse_array,接收一个整型数组的首地址和数组长度,使用指针操作将数组元素进行逆序排列,并在主函数中输出结果。

答案详解

核心逻辑:利用两个指针,一个指向数组头(start),一个指向数组尾(end),交换它们指向的值,然后向中间靠拢,直到相遇。

代码实现

#include <stdio.h>

// 函数声明:使用指针接收数组首地址
void reverse_array(int *arr, int len);

int main() {
    int nums[] = {10, 20, 30, 40, 50};
    int len = sizeof(nums) / sizeof(nums[0]);

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

    // 调用函数,数组名即为首地址
    reverse_array(nums, len);

    printf("逆序数组: ");
    for (int i = 0; i < len; i++) {
        printf("%d ", nums[i]);
    }
    printf("\n");

    return 0;
}

/**
 * 使用指针逆序数组
 * @param arr 数组首地址
 * @param len 数组长度
 */
void reverse_array(int *arr, int len) {
    // 边界检查:防止空指针或长度小于等于0
    if (arr == NULL || len <= 0) return;

    int *start = arr;          // start指向数组第一个元素
    int *end = arr + len - 1;  // end指向数组最后一个元素

    // 当头指针小于尾指针时,继续交换
    while (start < end) {
        // 交换两个指针指向的值
        int temp = *start;
        *start = *end;
        *end = temp;

        // 移动指针
        start++;
        end--;
    }
}

详细解析

  1. int *start = arr;arr 是数组首地址,赋值给 start 指针。
  2. int *end = arr + len - 1;:这是指针算术运算。arr + 1 并不是地址加1字节,而是加一个 int 的大小(通常是4字节)。arr + len - 1 精确地指向最后一个元素。
  3. while (start < end):这是循环的终止条件。当两个指针交叉或相等时,说明所有元素已交换完毕。如果数组长度为奇数,中间的元素不需要交换,逻辑依然正确。

实验题目二:字符串的复制与长度计算(不使用库函数)

题目描述

编写两个函数:

  1. my_strlen:使用指针计算字符串的长度(不包含 \0)。
  2. my_strcpy:使用指针将一个字符串复制到另一个字符数组中。

答案详解

核心逻辑:字符串以 \0 结尾,指针遍历直到遇到 \0 为止。

代码实现

#include <stdio.h>

// 计算字符串长度
int my_strlen(const char *str);

// 复制字符串
void my_strcpy(char *dest, const char *src);

int main() {
    char source[] = "Hello, Pointer!";
    char destination[50]; // 确保目标空间足够

    // 1. 测试长度计算
    int length = my_strlen(source);
    printf("字符串 '%s' 的长度是: %d\n", source, length);

    // 2. 测试字符串复制
    my_strcpy(destination, source);
    printf("复制后的目标字符串: %s\n", destination);

    return 0;
}

// 使用指针计算长度
int my_strlen(const char *str) {
    if (str == NULL) return -1; // 安全检查

    const char *p = str; // 定义一个指针指向字符串首地址
    // 当 *p 不等于结束符 '\0' 时,指针后移
    while (*p != '\0') {
        p++;
    }
    
    // 返回首地址与当前地址的差值
    return (int)(p - str);
}

// 使用指针复制字符串
void my_strcpy(char *dest, const char *src) {
    if (dest == NULL || src == NULL) return;

    // 方法一:使用数组下标(虽然题目要求指针,但这是理解的基础)
    // while (*src != '\0') {
    //     *dest = *src;
    //     src++;
    //     dest++;
    // }
    // *dest = '\0';

    // 方法二:更简洁的指针写法(推荐)
    // 先解引用判断,再赋值,最后自增
    while ((*dest++ = *src++) != '\0');
}

详细解析

  1. my_strlenp - str 是指针减法,结果是两个指针之间相差的元素个数,即字符串长度。
  2. my_strcpy
    • (*dest++ = *src++):这是一个复合表达式。
    • 首先执行 *src 取值,赋值给 *dest
    • 赋值表达式的值是被赋的值。如果赋值的是 \0,则 while 条件为假,循环结束。
    • 然后执行 dest++src++,指针后移。
    • 这种写法非常精炼,是C语言高手常用的技巧。

实验题目三:动态内存分配与数组排序

题目描述

编写程序,让用户输入一个整数 N,然后动态申请内存存储 N 个整数,使用冒泡排序(或选择排序)对这 N 个数进行排序,最后输出并释放内存。

答案详解

核心逻辑:使用 malloc 申请空间,此时数组名变成了一个指针变量,可以像普通数组一样使用 [] 下标,或者指针算术。

代码实现

#include <stdio.h>
#include <stdlib.h> // 包含 malloc 和 free

void bubble_sort(int *arr, int len);

int main() {
    int n;
    int *p = NULL; // 定义指针并初始化为NULL

    printf("请输入要输入的整数个数 N: ");
    scanf("%d", &n);

    if (n <= 0) {
        printf("个数必须大于0。\n");
        return -1;
    }

    // 1. 动态内存分配
    // sizeof(int) * n 计算总字节数
    p = (int *)malloc(sizeof(int) * n);

    // 检查分配是否成功
    if (p == NULL) {
        printf("内存分配失败!程序退出。\n");
        return -1;
    }

    // 2. 输入数据
    printf("请输入 %d 个整数:\n", n);
    for (int i = 0; i < n; i++) {
        // 可以用 p[i],也可以用 *(p + i)
        scanf("%d", &p[i]); 
    }

    // 3. 排序
    bubble_sort(p, n);

    // 4. 输出结果
    printf("排序后的结果: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", p[i]);
    }
    printf("\n");

    // 5. 释放内存(非常重要!)
    free(p);
    p = NULL; // 养成好习惯,防止悬挂指针

    return 0;
}

// 冒泡排序
void bubble_sort(int *arr, int len) {
    for (int i = 0; i < len - 1; i++) {
        for (int j = 0; j < len - 1 - i; j++) {
            // 指针访问方式:*(arr + j) 等同于 arr[j]
            if (*(arr + j) > *(arr + j + 1)) {
                int temp = *(arr + j);
                *(arr + j) = *(arr + j + 1);
                *(arr + j + 1) = temp;
            }
        }
    }
}

详细解析

  1. malloc:在堆(Heap)上分配内存。栈(Stack)上的局部变量在函数结束时自动销毁,而堆内存必须手动释放。
  2. (int *)malloc(...)malloc 返回 void*(通用指针),必须强制类型转换为 int* 才能赋值给 p
  3. free(p):释放内存。如果不释放,程序结束时操作系统通常会回收,但在长时间运行的程序中会导致内存泄漏(Memory Leak)。

常见问题排查指南 (FAQ)

在实验九中,学生经常会遇到以下错误,导致程序崩溃或输出乱码。

1. 段错误 (Segmentation Fault / Segfault)

这是最严重的错误,通常发生在指针使用不当。

  • 原因 A:使用了未初始化的指针。
    • 错误代码:int *p; *p = 10;
    • 解释:p 指向一个随机的内存地址,写入数据会破坏系统内存。
    • 解决:始终初始化指针。如果是动态分配,检查 malloc 返回值;如果是数组,指向数组。
  • 原因 B:空指针解引用。
    • 错误代码:int *p = NULL; printf("%d", *p);
    • 解决:在使用指针前,务必检查 if (p != NULL)
  • 原因 C:数组越界访问。
    • 错误代码:int arr[5]; int *p = arr; p[100] = 5;
    • 解决:确保指针偏移量在合法范围内(0 到 len-1)。

2. 内存泄漏 (Memory Leak)

  • 现象:程序运行缓慢,或者申请大内存时失败。
  • 原因:使用 malloc 申请了内存,但忘记使用 free 释放。
  • 排查:检查每一个 malloc,确保在函数退出前有对应的 free。如果在函数内部分配内存并返回指针,要明确由谁负责释放。

3. 野指针 (Dangling Pointer)

  • 现象free 之后继续使用指针。
  • 错误代码
    
    int *p = (int*)malloc(sizeof(int));
    free(p);
    *p = 10; // 危险!p 变成了野指针
    
  • 解决free 之后,立即将指针赋值为 NULL。即 p = NULL;。这样再次访问会报错(容易发现),而不是悄无声息地破坏数据。

4. 字符串缺少结束符

  • 现象:输出字符串时出现烫烫烫烫烫(乱码),或者程序一直输出直到遇到内存中的 \0
  • 原因:字符数组未预留 \0 的空间,或者手动赋值时忘记加 \0
  • 代码对比
    • 错误:char s[5] = "Hello"; (需要6个字节,因为 ‘H’,‘e’,‘l’,‘l’,‘o’,‘\0’)
    • 错误:手动填充字符后未加 \0
  • 解决:确保字符数组长度至少为 字符串长度 + 1

5. 指针类型不匹配

  • 现象:计算地址偏移量错误。
  • 例子
    
    int arr[10];
    short *p = (short*)arr; // 强制转换
    // 此时 p+1 只跳过了 2 字节,而 arr+1 跳过 4 字节
    
  • 建议:除非有特殊目的(如处理二进制流),否则尽量保持指针类型与数据类型一致。

6. malloc 与 free 不匹配

  • 现象:程序崩溃。
  • 原因
    • free 了非堆内存(如 int a; free(&a);)。
    • free 了同一块内存两次。
    • 使用 new 分配却用 free 释放(C++中常见,C语言中主要是 malloc/free 配对)。

总结与调试技巧

  1. 使用调试器:推荐使用 GDB (Linux) 或 Visual Studio 的断点调试功能。单步跟踪指针变量的值,观察 *p 的变化。
  2. 打印地址:在调试时,多使用 printf("p = %p, *p = %d\n", p, *p); 来观察指针指向哪里。
  3. 防御性编程:永远假设外部输入是错误的,对所有指针参数进行 NULL 检查,对 malloc 结果进行检查。

通过以上详解和排查指南,你应该能够顺利通过C语言实验九,并深入理解指针的工作原理。指针虽然难,但它是通往C语言高级应用的必经之路。加油!