欢迎光临
我们一直在努力

函数与扩容 | C指针基础(三)

函数与扩容 | C指针基础(三)

  • 指针作为函数参数
  • 数组扩容
    • 错误示例:数组整体赋值
    • malloc 与 free
  • 注意点:函数无法直接返回数组
    • 错误示例:局部变量的限制
  • 将扩容封装为函数

指针作为函数参数

将数组名(即数组首元素的地址)作为函数参数传入时,函数内部对数组元素的修改会直接作用于外部的原始数组。

需要特别说明的是:当数组名作为实参传入函数时,它会退化为指向首元素的指针。因此,形参写成 int arr[] 实际上等价于 int *arr。下面通过一个例子来验证:

#include <stdio.h>

// 形参 int arr[] 实际等价于 int *arr
void printArr(int arr[]) {
for (int i = 0; i < 10; i++) {
arr[i] = arr[i] + 1; // 通过指针修改元素值
printf("%d,", arr[i]);
}
printf("\\n");
}

int main() {
int arr[10] = {0};
printArr(arr); // 传入数组名,即数组首元素的地址
for (int i = 0; i < 10; i++) {
printf("%d,", arr[i]); // 主函数中的数组同步被修改
}
return 0;
}

运行后会发现:函数内部对 arr 的修改,会同步反映到 main 函数的数组中。原因是传入函数的正是数组首元素的指针,形参和实参指向的是同一块内存;对指针所指向内存的修改,自然会影响原始数组。这也正是指针作为函数参数的重要用途——在函数中直接修改外部数据,而无需返回整个数组。

数组扩容

在实际开发或竞赛题中,我们有时无法预先确定数组需要多大的长度,于是自然希望数组能够自动扩容。不过 C 语言原生数组一旦定义,长度就是固定的,通常需要借助动态内存分配来模拟“扩容”。

#include <stdio.h>
#include <stdlib.h>
int main() {
int len = 10;
int *arr1 = (int *)malloc(len * 4); // 字节单位

for (int i = 0; i < len; i++) {
//*(arr1 + 1) = i * 5;
arr1[i] = i * 5;
}
// 扩容
len *= 2;
int *arr2 = (int *)malloc(len * 4); // 字节单位
for (int i = 0; i < len; i++) {
arr2[i] = arr1[i];
}
// 替换地址
arr1 = arr2;
return 0;
}

在这段代码中,len 表示数组的长度,malloc 用于动态分配内存。len * 4 中的 4 代表 int 类型所占的字节数,更推荐使用 sizeof(int);(int *) 则是强制类型转换。向动态内存写入数据时,既可以使用指针访问,也可以使用数组下标访问。

所谓“扩容”,本质是重新动态分配一块大小为 len * 2 的新空间,把旧空间里的数据复制进去,再将地址替换成新空间的地址。这样一来,虽然 arr1 的变量名没变,但它保存的指针已经变成了新空间的地址,后续再通过 arr1 访问时,实际访问的是 arr2 指向的新空间,从而完成扩容。

先展示正确方法,下面看一些错误方法:

错误示例:数组整体赋值

#include <stdio.h>
#include <stdlib.h>
int main() {
int len = 10;
int arr1[len]={0}; // 字节单位

for (int i = 0; i < len; i++) {
//*(arr1 + 1) = i * 5;
arr1[i] = i * 5;
}
// 扩容
len *= 2;
int arr2[len] = {0}; // 字节单位
for (int i = 0; i < len; i++) {
arr2[i] = arr1[i];
}
arr1 = arr2;
return 0;
}

乍一看似乎没问题:先创建新数组 arr2,逐个元素复制后,再把 arr1 指过去,好像就完成了扩容。但在 C 语言中,这段代码存在多处无法通过编译的问题。

首先,int arr2[len] = arr1; 不是合法的数组初始化方式,C 语言不允许用一个数组去初始化另一个数组。其次,arr1 = arr2; 也是非法的,因为数组名在表达式中会退化为指向首元素的指针,但它本身并不是一个可以修改的指针变量,而是一个编译期确定的常量地址,不能作为左值被赋值。因此,所谓的“旧数组 = 新数组”无法通过修改数组名来实现。

另外,在 C 语言中,数组只能在定义或声明时完成一次初始化,之后不能对整体再次赋值。

malloc 与 free

malloc 用于在程序运行期间从堆上动态申请一块连续内存,并返回这块内存的首地址;申请失败时会返回 NULL。这块内存不会随函数结束自动释放,因此适合用来构造需要跨函数使用的数组。

malloc 的语法结构如下:

指针变量 = (类型 *)malloc(字节数);

例如:int *p = (int *)malloc(n * sizeof(int)); 表示申请一块能存放 n 个 int 的连续内存,并把首地址赋给 int * 类型的指针 p。

动态申请的内存在使用完毕后必须调用 free 主动释放,否则会造成内存泄漏。释放后最好及时把指针置为 NULL,避免之后误用已经失效的地址,形成“悬空指针”。同时要注意:只能释放由 malloc 等动态分配函数返回的地址,不能对同一块内存重复释放。

free 的语法结构如下:

free(指针变量);

例如:free(p); 表示释放指针 p 所指向的、由 malloc 等函数动态申请的内存。释放后建议把指针置为 NULL,例如:p = NULL;,避免形成悬空指针。

一个示例如下:

#include <stdlib.h>

int main() {
int *p = (int *)malloc(sizeof(int));
if (p == NULL) return 1;
*p = 10;
free(p);
p = NULL;
return 0;
}

这个示例完整演示了 malloc 和 free 的基本使用规范:

  • int *p = (int *)malloc(sizeof(int));:从堆上申请一块能存放一个 int 的内存,并把这块内存的首地址赋给指针 p。这里建议写 sizeof(int),而不是写死 4,因为不同平台上 int 占用的字节数可能不同。
  • if (p == NULL) return 1;:malloc 有可能申请失败,比如系统内存不足。申请失败时它会返回 NULL,此时 p 不指向任何有效内存,不能再执行 *p = 10,否则可能让程序崩溃。判断 p == NULL 就是为了提前处理这种情况。return 1 表示 main 函数以非 0 状态结束;在 C 语言惯例中,main 返回 0 表示程序正常结束,返回非 0 表示程序执行失败。
  • *p = 10;:通过解引用指针 p,把 10 写入刚申请到的内存中。
  • free(p);:这块内存使用完毕后,必须调用 free 主动释放,否则会造成内存泄漏。
  • p = NULL;:释放内存后把指针清空,避免之后误用已经失效的地址,形成悬空指针。
  • return 0;:最后返回 0,表示程序正常结束。

注意点:函数无法直接返回数组

在 C 语言中,函数无法直接将整个数组作为返回值输出。因为数组名在表达式中会退化为指向首元素的指针,所以函数真正能返回的是数组首元素的指针,而不是数组本身。

虽然函数不能直接返回一个完整的数组,但可以返回由 malloc 动态分配的数组首地址。下面是一个正确示例:

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

int *createArray(int n) {
int *arr = (int *)malloc(n * sizeof(int));
return arr;
}

int main() {
int n;
scanf("%d", &n);
int *arr = createArray(n);
return 0;
}

在上述代码中,createArray 根据传入的 n,使用 malloc(n * sizeof(int)) 申请一块能够存放 n 个整数的连续内存空间,并返回这块内存的首地址。main 函数通过 int *arr = createArray(n); 接收这个地址后,就可以像普通数组一样使用 arr[i] 访问其中的元素。

错误示例:局部变量的限制

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

int *createArray(int n) {
int arr[n];
return arr;
}

int main() {
int n;
scanf("%d", &n);
int *arr = createArray(n);
return 0;
}

这段代码看似把数组返回给了主函数,但其实 arr 只是函数内部临时创建的数组。函数一结束,这块内存就不再属于当前程序,里面原来保存的数据也可能被其他内容覆盖。此时返回的地址已经“失效”,主函数再照着这个地址去读取数据,结果就无法保证:可能读到错误的值,也可能直接让程序崩掉。

正确做法还是像前面那样,用 malloc 申请内存。malloc 申请到的内存不会因为函数结束而自动消失,因此返回它的地址才是安全的。

将扩容封装为函数

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

int *expand(int *length,int arr[]){
int newLength = *length * 2;
int *newArr = (int *) malloc(newLength * sizeof(int));
for (int i = 0; i < *length; i++) {
newArr[i] = arr[i];
}
*length = newLength;
return newArr;
}

int main() {
int length = 5, num;
int *arr = (int *)malloc(length*sizeof(int));
printf("Please enter a number: ");
scanf("%d", &num);
arr = expand(&length,arr);
return 0;
}

把扩容逻辑单独抽成一个 expand 函数后,主函数里的调用更清晰了:arr = expand(&length, arr); 一次调用就完成了“申请新空间、复制旧数据、更新长度、返回新地址”。

下面结合代码梳理几个容易忽略的注意点:

  • 为什么传 &length 而不是 length?

    函数只能返回一个值,这里已经用返回值返回了新数组的首地址,没法再同时返回新的数组长度。于是我们把 length 的地址传进去,让函数通过指针直接修改 main 里的 length,这样函数结束后长度变化仍然保留。这也是前面“指针作为函数参数”一节思路的延续。

  • 函数内部使用 length 时为什么加 *?

    形参 int *length 是一个指针,保存的是外部 length 变量的地址。要读取它指向的值或修改它,需要先解引用,也就是写 *length;如果直接写 length,拿到的只是地址本身。

  • 形参 int arr[] 其实是语法糖。

    在函数形参中,int arr[] 等价于 int *arr,它接收的是数组首元素地址,而不是把整个数组复制一份。所以函数内部通过 arr[i] 操作元素时,实际上是在操作外部数组。

  • 还需要注意:动态申请的内存使用完毕后,应及时用 free 释放。本示例为保持简短,没有展示释放步骤。

    赞(0)
    未经允许不得转载:171主机测评 » 函数与扩容 | C指针基础(三)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址