实验九概述:指针与数组的深入应用
实验九通常是C语言程序设计课程中的关键转折点,它标志着从基础语法向高级内存操作和数据结构处理的过渡。本实验的核心目标是帮助学生掌握指针与数组之间的紧密联系,理解动态内存分配的基本原理,并能够运用指针高效地处理字符串和数值数组。指针是C语言的灵魂,也是最难掌握的概念之一,因此本实验的详细解答和问题排查显得尤为重要。
实验目标与核心知识点
在开始解答具体题目之前,我们需要明确本次实验旨在巩固以下知识点:
- 指针与数组的关系:数组名本质上是指向数组首元素的常量指针,通过指针算术运算访问数组元素。
- 字符指针与字符串:理解
char*与char[]在存储字符串时的区别,掌握字符串的指针操作。 - 动态内存分配:使用
malloc和free在堆区申请和释放内存,解决数组长度不确定的问题。 - 指针作为函数参数:通过指针实现函数内部对调用者变量的修改(传地址)。
实验题目一:数组元素的指针访问与逆序输出
题目描述
编写一个函数 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--;
}
}
详细解析:
int *start = arr;:arr是数组首地址,赋值给start指针。int *end = arr + len - 1;:这是指针算术运算。arr + 1并不是地址加1字节,而是加一个int的大小(通常是4字节)。arr + len - 1精确地指向最后一个元素。while (start < end):这是循环的终止条件。当两个指针交叉或相等时,说明所有元素已交换完毕。如果数组长度为奇数,中间的元素不需要交换,逻辑依然正确。
实验题目二:字符串的复制与长度计算(不使用库函数)
题目描述
编写两个函数:
my_strlen:使用指针计算字符串的长度(不包含\0)。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');
}
详细解析:
my_strlen:p - str是指针减法,结果是两个指针之间相差的元素个数,即字符串长度。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;
}
}
}
}
详细解析:
malloc:在堆(Heap)上分配内存。栈(Stack)上的局部变量在函数结束时自动销毁,而堆内存必须手动释放。(int *)malloc(...):malloc返回void*(通用指针),必须强制类型转换为int*才能赋值给p。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 配对)。
总结与调试技巧
- 使用调试器:推荐使用 GDB (Linux) 或 Visual Studio 的断点调试功能。单步跟踪指针变量的值,观察
*p的变化。 - 打印地址:在调试时,多使用
printf("p = %p, *p = %d\n", p, *p);来观察指针指向哪里。 - 防御性编程:永远假设外部输入是错误的,对所有指针参数进行 NULL 检查,对
malloc结果进行检查。
通过以上详解和排查指南,你应该能够顺利通过C语言实验九,并深入理解指针的工作原理。指针虽然难,但它是通往C语言高级应用的必经之路。加油!
