数组传参、指针函数、函数指针
C语言指针进阶:数组传参、指针函数、函数指针与const指针全解析
在C语言的学习路径中,指针始终是绕不开的核心难点,很多开发者写了多年代码,依然会在数组传参、函数指针这类场景里踩坑。今天我们就把最容易混淆的几个知识点串起来,从一维整型数组传参讲到const指针,结合可运行的代码示例帮你彻底理清逻辑。
一、一维整型数组的指针传参
很多新手第一次写数组传参时,会疑惑为什么明明定义的是int arr[],函数内部用sizeof(arr)得到的却不是数组总长度,而是指针的大小。这背后的核心原因是:C语言中数组作为函数参数传递时,会自动退化为指向数组首元素的指针。
我们先看一个错误写法的示例:
c
#include <stdio.h>
// 错误示例:无法在函数内获取数组真实长度
void print_arr(int arr[]) {
// 这里得到的是指针的大小,32位系统为4字节,64位系统为8字节
printf("sizeof(arr) in func: %d\n", sizeof(arr));
for(int i = 0; i < sizeof(arr)/sizeof(int); i++) {
printf("%d ", arr[i]);
}
}
int main() {
int arr[] = {1,2,3,4,5};
printf("sizeof(arr) in main: %d\n", sizeof(arr));
print_arr(arr);
return 0;
}
运行这段代码你会发现,main函数里sizeof(arr)得到的是20(5个int元素,每个4字节),但在print_arr函数里得到的却是8,最终循环只会打印前1-2个元素,完全不符合预期。
正确的指针传参写法,必须额外传入数组的长度参数:
c
#include <stdio.h>
// 标准写法:用指针接收数组首地址,同时传入数组长度
void print_arr(int *arr, int len) {
for(int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {1,2,3,4,5};
int len = sizeof(arr)/sizeof(arr);
print_arr(arr, len);
return 0;
}
这种写法的本质是把数组首元素的地址传递给函数,函数内部通过指针偏移来访问数组的每一个元素,既不会拷贝整个数组(节省栈空间),也能通过传入的长度参数准确遍历所有元素。
二、一维字符型数组的指针传参
字符数组的传参逻辑和整型数组基本一致,但因为C语言约定了字符串以\0作为结束标志,所以很多场景下我们不需要额外传入长度参数,直接通过指针遍历到\0停止即可。
最典型的例子就是标准库的strlen函数,它的核心实现逻辑就是基于字符指针传参:
c
#include <stdio.h>
// 模拟实现strlen,统计字符串长度
int my_strlen(char *str) {
int cnt = 0;
while(*str != '\0') {
cnt++;
str++;
}
return cnt;
}
int main() {
char str[] = "hello c pointer";
int len = my_strlen(str);
printf("string length: %d\n", len);
return 0;
}
这里要特别注意一个常见坑:如果传入的字符数组没有以\0结尾,指针遍历就会越界访问内存,直到遇到随机的\0才停止,最终得到一个完全错误的长度,甚至引发段错误。
如果我们只是要读取字符串内容、不希望函数内部修改原数组,就可以用const修饰指针指向的内容,这也是标准库函数的通用写法:
c
// 安全写法:禁止函数内部修改传入的字符串
int my_strlen(const char *str) {
int cnt = 0;
while(*str != '\0') {
cnt++;
str++; // 允许修改指针本身的指向,不允许修改*str的值
}
return cnt;
}
这种写法既保护了原字符串的内容不被意外修改,也支持直接传入字符串常量作为参数,兼容性更强。
三、指针函数:返回指针的函数
指针函数本质是一个普通函数,只是它的返回值类型是指针。它的声明格式很容易理解:返回值类型 *函数名(参数列表),这里的*是和返回值类型结合的,代表这个函数返回一个对应类型的指针。
比如我们写一个函数,拼接两个字符串的前半部分,就可以让它返回字符指针:
c
#include <stdio.h>
#include <string.h>
// 指针函数:返回拼接后的新字符串指针
char* concat_part(const char *s1, const char *s2) {
// 注意:这里用static修饰数组,避免函数返回后栈空间被释放
static char buf;
int len1 = strlen(s1);
strncpy(buf, s1, len1/2);
strcat(buf, s2);
return buf;
}
int main() {
char *res = concat_part("hello", "world");
printf("%s\n", res); // 输出"heworld"
return 0;
}
这里必须提醒一个高频坑:绝对不要返回局部变量的指针。因为局部变量是在栈上分配的,函数执行结束后栈空间就会被回收,返回的指针会变成野指针,后续访问会引发未定义行为。上面示例中用static修饰buf,就是把变量的生命周期延长到整个程序运行周期,避免了空间被释放的问题,更稳妥的写法也可以在堆上动态分配内存,由调用方负责释放。
很多新手会把指针函数和接下来要讲的函数指针搞混,核心区别就是看*有没有被括号包裹:没有括号的是指针函数,本质是函数,返回值是指针;被括号包裹的是函数指针,本质是指针,指向一个函数。
四、函数指针:指向函数的指针
函数指针的核心是指针,它存储的不是普通数据的地址,而是函数的入口地址。在C语言中,函数名本身就代表了函数的入口地址,这和数组名代表首元素地址是类似的逻辑。
声明函数指针的格式必须用括号把*和变量名包裹起来:返回值类型 (*指针名)(参数列表),括号不能省略,否则就变成了返回指针的指针函数。我们用一个简单的计算器示例来演示它的用法:
c
#include <stdio.h>
// 定义四个基础运算函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divi(int a, int b) { return a / b; }
int main() {
// 声明函数指针,指向"参数为两个int、返回值为int"的函数
int (*calc)(int, int);
calc = add;
printf("10 + 3 = %d\n", calc(10, 3));
calc = sub;
printf("10 - 3 = %d\n", calc(10, 3));
calc = mul;
printf("10 * 3 = %d\n", calc(10, 3));
calc = divi;
printf("10 / 3 = %d\n", calc(10, 3));
return 0;
}
你会发现同一个函数指针变量,可以先后指向不同的同类型函数,这就是函数指针最强大的地方:我们可以把同规则的函数统一管理,不需要写大量重复的分支判断代码。
实际开发中函数指针的应用非常广泛:POSIX标准里的pthread_create线程创建函数,就用函数指针接收线程执行函数的入口;信号处理函数signal也是通过函数指针,来指定收到对应信号后要执行的处理逻辑。我们还可以用typedef给函数指针类型起别名,让代码更简洁:
c
// 给函数指针类型起别名,后续声明变量更方便
typedef int (*CalcFunc)(int, int);
// 后续直接用别名声明即可
CalcFunc func_arr = {add, sub, mul, divi};
用函数指针数组,我们就可以把多个同类型函数放到数组里,通过下标直接调用,代码的可扩展性会大大提升。
五、const指针:理清"左定值、右定向"规则
const是C语言里用来限定不可修改的关键字,它和指针结合之后的几种组合,是很多人搞混的重灾区。这里我们用一个非常好记的口诀来总结:左定值,右定向,意思是const在*的左边,代表指针指向的值不能修改;const在*的右边,代表指针本身的指向不能修改。
我们把三种常见组合逐一拆解:
const int *p (常量指针)
const在*的左边,它修饰的是指针指向的数据。也就是说,你不能通过*p修改p指向的内存里的值,但p本身的指向可以改变:
c
int a = 10, b = 20;
const int *p = &a;
// *p = 100; // 错误,不能修改指向的值
p = &b; // 正确,可以修改指针的指向
int * const p (指针常量)
const在*的右边,它修饰的是指针变量本身。也就是说,p的指向一旦初始化就不能改变,但你可以通过*p修改指向的内存里的值:
c
int a = 10, b = 20;
int * const p = &a;
*p = 100; // 正确,可以修改指向的值
// p = &b; // 错误,不能修改指针的指向
这里要注意,指针常量必须在定义的时候就完成初始化,否则后续没有机会给它赋值,会直接报编译错误。
const int * const p (指向常量的指针常量)
*的左右两边都有const,相当于同时锁死了两个权限:既不能修改指针的指向,也不能通过*p修改指向的值,是完全只读的指针:
c
int a = 10;
const int * const p = &a;
// *p = 100; // 错误
// p = &b; // 错误
这个规则完全可以覆盖所有const和指针的组合场景,再也不用死记硬背了。
最后总结
今天我们梳理的这几个知识点,覆盖了C语言指针进阶里最核心的高频场景:数组传参要记住数组退化为指针的特性,永远记得给数组传长度;指针函数的核心坑点是不要返回局部变量指针;函数指针本质是存储函数入口地址,是实现回调、多态的基础;const指针用"左定值右定向"的口诀就能一秒判断权限。
