函数与扩容 | C指针基础(三)
- 指针作为函数参数
- 数组扩容
- 错误示例:数组整体赋值
- malloc 与 free
- 注意点:函数无法直接返回数组
- 错误示例:局部变量的限制
- 将扩容封装为函数
指针作为函数参数
将数组名(即数组首元素的地址)作为函数参数传入时,函数内部对数组元素的修改会直接作用于外部的原始数组。
需要特别说明的是:当数组名作为实参传入函数时,它会退化为指向首元素的指针。因此,形参写成int arr[]实际上等价于int *arr。下面通过一个例子来验证:
#include<stdio.h>// 形参 int arr[] 实际等价于 int *arrvoidprintArr(intarr[]){for(inti=0;i<10;i++){arr[i]=arr[i]+1;// 通过指针修改元素值printf("%d,",arr[i]);}printf("\n");}intmain(){intarr[10]={0};printArr(arr);// 传入数组名,即数组首元素的地址for(inti=0;i<10;i++){printf("%d,",arr[i]);// 主函数中的数组同步被修改}return0;}运行后会发现:函数内部对arr的修改,会同步反映到main函数的数组中。原因是传入函数的正是数组首元素的指针,形参和实参指向的是同一块内存;对指针所指向内存的修改,自然会影响原始数组。这也正是指针作为函数参数的重要用途——在函数中直接修改外部数据,而无需返回整个数组。
数组扩容
在实际开发或竞赛题中,我们有时无法预先确定数组需要多大的长度,于是自然希望数组能够自动扩容。不过 C 语言原生数组一旦定义,长度就是固定的,通常需要借助动态内存分配来模拟“扩容”。
#include<stdio.h>#include<stdlib.h>intmain(){intlen=10;int*arr1=(int*)malloc(len*4);// 字节单位for(inti=0;i<len;i++){// *(arr1 + 1) = i * 5;arr1[i]=i*5;}// 扩容len*=2;int*arr2=(int*)malloc(len*4);// 字节单位for(inti=0;i<len;i++){arr2[i]=arr1[i];}// 替换地址arr1=arr2;return0;}在这段代码中,len表示数组的长度,malloc用于动态分配内存。len * 4中的4代表int类型所占的字节数,更推荐使用sizeof(int);(int *)则是强制类型转换。向动态内存写入数据时,既可以使用指针访问,也可以使用数组下标访问。
所谓“扩容”,本质是重新动态分配一块大小为len * 2的新空间,把旧空间里的数据复制进去,再将地址替换成新空间的地址。这样一来,虽然arr1的变量名没变,但它保存的指针已经变成了新空间的地址,后续再通过arr1访问时,实际访问的是arr2指向的新空间,从而完成扩容。
先展示正确方法,下面看一些错误方法:
错误示例:数组整体赋值
#include<stdio.h>#include<stdlib.h>intmain(){intlen=10;intarr1[len]={0};// 字节单位for(inti=0;i<len;i++){// *(arr1 + 1) = i * 5;arr1[i]=i*5;}// 扩容len*=2;intarr2[len]={0};// 字节单位for(inti=0;i<len;i++){arr2[i]=arr1[i];}arr1=arr2;return0;}乍一看似乎没问题:先创建新数组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>intmain(){int*p=(int*)malloc(sizeof(int));if(p==NULL)return1;*p=10;free(p);p=NULL;return0;}这个示例完整演示了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(intn){int*arr=(int*)malloc(n*sizeof(int));returnarr;}intmain(){intn;scanf("%d",&n);int*arr=createArray(n);return0;}在上述代码中,createArray根据传入的n,使用malloc(n * sizeof(int))申请一块能够存放n个整数的连续内存空间,并返回这块内存的首地址。main函数通过int *arr = createArray(n);接收这个地址后,就可以像普通数组一样使用arr[i]访问其中的元素。
错误示例:局部变量的限制
#include<stdio.h>#include<stdlib.h>int*createArray(intn){intarr[n];returnarr;}intmain(){intn;scanf("%d",&n);int*arr=createArray(n);return0;}这段代码看似把数组返回给了主函数,但其实arr只是函数内部临时创建的数组。函数一结束,这块内存就不再属于当前程序,里面原来保存的数据也可能被其他内容覆盖。此时返回的地址已经“失效”,主函数再照着这个地址去读取数据,结果就无法保证:可能读到错误的值,也可能直接让程序崩掉。
正确做法还是像前面那样,用malloc申请内存。malloc申请到的内存不会因为函数结束而自动消失,因此返回它的地址才是安全的。
将扩容封装为函数
#include<stdio.h>#include<stdlib.h>int*expand(int*length,intarr[]){intnewLength=*length*2;int*newArr=(int*)malloc(newLength*sizeof(int));for(inti=0;i<*length;i++){newArr[i]=arr[i];}*length=newLength;returnnewArr;}intmain(){intlength=5,num;int*arr=(int*)malloc(length*sizeof(int));printf("Please enter a number: ");scanf("%d",&num);arr=expand(&length,arr);return0;}把扩容逻辑单独抽成一个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释放。本示例为保持简短,没有展示释放步骤。