ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言指针返回多个结果:从底层原理到实战避坑

C语言指针返回多个结果:从底层原理到实战避坑 指针这个知识点很多学C语言的人都是绕过的不是不想学是真的被“指针就是地址”这句话给带偏了。尤其教材到了第八章开始讲“利用指针返回多个结果”的时候很多人会突然懵掉函数不是只能return一个值吗怎么就能返回多个结果了指针到底在这个过程中做了什么如果你正在被这个问题卡住这篇内容可以帮你把整条线理清楚。我会从底层原理讲到实际的代码写法再讲几个我能想到的、最常见的翻车场景尽量让你看完能直接上手写。我写这篇文章的背景是你手里大概率有《C语言程序设计第四版》何钦铭、颜晖主编这本书第八章讲指针。我尽量贴合书里的思路讲但我会用更生活化的方式把里面的逻辑拆开不会上来就甩术语。1. 为什么要舍近求远函数返回值到底卡在哪里先看最基本的问题C语言的函数为什么默认只能返回一个值有人说这是“语言规定”但这背后其实有一个很现实的硬件原因。C语言是贴近底层的语言函数的返回值通常是通过固定的寄存器比如EAX寄存器传给调用者的一个函数返回完这个寄存器里就只能存一份数据。也就是说你想一次性从这个“口子”里掏出两份以上的数据硬件层面就不支持。当然有人会说那我可以定义一个结构体把多个结果打包然后return这个结构体。确实可以但这是“把多个值塞进一个盒子里”的思路。对于结果数量固定、类型固定的场景这招很好用。问题在于很多场景的结果是动态的或者你压根就不想为了返回两三个值单独定义一个结构体或者你想返回结果的同时还要把某个已有的外部变量也改掉——这时候结构体就解决不了了。于是指针登场。指针的思路不是“改变返回通道”而是“改变数据流向”你把自己的变量的地址交给函数让函数顺着这个地址直接找到你变量的内存位置往里写数据。函数本身该返回什么还返回什么被“顺便”修改的数据就通过地址传出去了。这就是“利用指针返回多个结果”的本质不依赖return而是直接操作调用者变量的内存。书上第八章反复强调“实参和形参之间的单向值传递”核心就是想说明这件事——普通变量作为参数传进去函数里改的只是形参副本根本碰不到实参但如果你传的是实参的地址指针函数就能通过这个地址“隔空”修改实参。这才是指针参数存在的意义。2. 从0到1利用指针返回多个结果的最小案例先不看复杂例子看一个所有教材都会讲的小函数计算两个整数的商和余数。你想写一个函数一个数除以另一个数想同时知道商和余数用return顶多只能返回一个另一个怎么办用指针参数接。#include stdio.h void div_mod(int a, int b, int *quotient, int *remainder) { if (b 0) { *quotient 0; *remainder 0; return; } *quotient a / b; *remainder a % b; } int main() { int q, r; div_mod(17, 5, q, r); printf(商%d, 余数%d\n, q, r); return 0; }这代码很短但里面藏了几个初学者最容易忽略的细节。第一调用的时候必须用取地址把q和r的地址传给函数。如果你直接写div_mod(17, 5, q, r)编译器会报警告运行时大概率直接崩。因为你传给函数的是q和r里保存的值此时还没初始化可能是任意值函数拿这个值当内存地址去写写到哪里都不知道。第二函数内部要用*quotient a / b不是quotient a / b。quotient是地址指向main里q的地址*quotient才是这个地址里存的值。你赋值给quotient只是改变了指针的指向对q没有任何影响。第三函数里对*quotient的赋值等价于对main函数里q变量的赋值。这就是“通过地址间接修改外部变量”的原理你可以把它理解成你把自己的保险柜地址告诉别人别人不需要你把保险柜搬过去他直接去那个地址开柜取东西。再补一个知识点函数的形参int *quotient确实是个局部变量它接收的是地址值但这个局部变量存在的意义就是帮你“导航”。你在这个导航上输入目的地它带你去的是调用者的地盘而不是它自己的小房间。这个小例子的核心逻辑就是第八章里所有“利用指针返回多个结果”题目的最小原型。后面所有的变体不管是数组、字符串、还是链表操作本质都是这一个模式调用者准备变量取地址传进去函数用指针接收再通过星号运算改值。3. 实战拆解三个高频场景的指针返回方案3.1 一次算出一组统计结果很多练习题会让你写一个函数同时求数组的最大值、最小值和平均值。如果用return一次只能返回一个你可能会被迫把逻辑变成三个函数可那样数组要遍历三遍。用指针参数一次遍历全搞定。#include stdio.h void analyze(int arr[], int n, int *max, int *min, double *avg) { if (n 0) return; *max arr[0]; *min arr[0]; double sum 0; for (int i 0; i n; i) { if (arr[i] *max) *max arr[i]; if (arr[i] *min) *min arr[i]; sum arr[i]; } *avg sum / n; } int main() { int data[] {3, 7, 2, 9, 5}; int max_val, min_val; double avg_val; analyze(data, 5, max_val, min_val, avg_val); printf(max%d, min%d, avg%.2f\n, max_val, min_val, avg_val); return 0; }这个例子里有几个值得琢磨的地方。数组名传参时其实也是地址传递但这是另一套机制数组名本身就会退化成指向首元素的指针所以arr[]和*arr在形参列表中写法等价。但要注意数组传进来后你可以在函数里通过下标修改数组元素的值这是合法的因为你能通过地址找到原始内存。如果你想防止函数意外修改数组内容可以在形参上加const比如const int arr[]这是第八章习题里很常见的进阶要求。另外注意我用了double *avg而不是float *avg因为平均值会出现小数float精度不够容易出怪问题用double更稳妥。3.2 字符串处理中的指针返回字符串本质上是一种特殊的数组所以在“返回多个结果”的场景里它也经常被用来做试验田。比如写一个函数把字符串里的空格都去掉同时统计去掉的空格个数。去掉空格后的字符串需要写回原字符串空格计数需要传出去一个函数要同时完成两个动作。#include stdio.h void remove_spaces_and_count(char *str, int *count) { char *src str; char *dst str; *count 0; while (*src) { if (*src ) { (*count); } else { *dst *src; dst; } src; } *dst \0; } int main() { char s[] hello world, c lang; int space_count 0; remove_spaces_and_count(s, space_count); printf(处理结果: %s\n, s); printf(去除空格数: %d\n, space_count); return 0; }这里有个细节值得注意为什么dst和src都指向同一个str这是原地处理字符串的经典写法。一个指针负责读一个指针负责写两者速度不同所以读指针永远走在写指针前面。每次遇到非空格字符就把它搬到写指针的位置最后在写指针位置补上字符串结束符\0。处理完以后s的内容已经被改掉了空格数量通过count指针传回调用者。你可能会问这算“返回多个结果”吗严格来说只有一个结果是通过return返回的这里没有return另外两个结果都是通过参数内的指针返回的。实际工程里这种做法非常普遍因为很多场景不是“要返回值”而是“要在原地修改一个对象并告诉外部这个对象发生的增量变化”。3.3 链表遍历时的多结果返回书第八章往后会接触链表。链表的操作里特别容易出现“想同时拿到多个信息”的需求比如统计链表的节点数和总数值之和。#include stdio.h #include stdlib.h struct Node { int val; struct Node *next; }; void analyze_linked_list(struct Node *head, int *count, long *sum) { *count 0; *sum 0; struct Node *p head; while (p ! NULL) { (*count); *sum p-val; p p-next; } } int main() { struct Node n1 {10, NULL}; struct Node n2 {20, NULL}; struct Node n3 {30, NULL}; n1.next n2; n2.next n3; int node_cnt 0; long total 0; analyze_linked_list(n1, node_cnt, total); printf(节点数%d, 总和%ld\n, node_cnt, total); return 0; }链表场景里指针参数的效果比数组场景更明显。因为你遍历链表时没办法用数组下标的方式去定位某个节点只能靠一个指针一个指针地往下跳。如果你在函数里临时统计统计完又没带出去那这个统计就白做了。用指针参数把count和sum传出去配合return返回其他内容是链表函数设计的惯用招式。4. 新手必踩的坑指针返回结果时的常见问题速查我见过不少学第八章学到崩溃的不是理解不了原理而是写出代码之后各种报错、各种乱码、各种程序崩溃。这里整理几个出现频率极高的问题你可以对照排查。空指针和野指针。空指针是值为NULL的指针对它解引用会直接导致段错误。野指针是地址指向了一块你根本不该碰的内存这个更危险因为它往往不会立刻报错而是让你在某次看起来很正常的运行中突然翻车。有个很常见的新手操作定义一个指针变量就急着用它接收结果但根本没指向任何有效变量。int *ptr; *ptr 100; // 大错特错ptr不知指向哪里这题的解法是先把指针和某个实实在在的变量绑定在一起int value; int *ptr value;或者用malloc动态分配内存但malloc用完后必须记得free不然就内存泄漏。函数内部修改指针指向和修改指针指向的值是完全不同的两件事。这个坑几乎所有初学者都会踩。看这两段代码的区别void func1(int *p) { p p 1; // 只改变了形参的指向外面的指针不受影响 } void func2(int *p) { (*p) (*p) 1; // 修改了p指向的那个变量的值 }func1里改的是形参指针自己的值函数结束后形参销毁外面的变量毫发无损func2里改的是指针指向的内存空间值这个效果会保留。如果你想通过指针“返回多个结果”必须使用*p的形式去写值而不是让指针自己移动。传进去的是指针就一定是“返回多个结果”的指针参数吗不是。比如你要写的函数只是使用指针指向的内容并不需要修改外部变量那这个参数只是“传递数据”而不是“返回结果”。区分方法很简单函数里是否对*参数进行了赋值。赋值了才叫返回结果只读那只是个输入参数。这个区分在第八章的很多概念题里会被反复考到。别用未初始化指针做接收参数。这是我在习题里最常看到的问题。有人写int *p; func(p); // 咦为什么崩溃这里传的是p这个指针变量的地址也就是int **类型而func如果期望的是int *类型就对不上。当然如果你明确写了void func(int **pp)那传p是合法的但是你要清楚这个p自己还没绑定任何有效变量或内存你必须在func内部先让*pp指向一个有效区域才能用。这种场景不是用来处理简单结果的而是用来处理“指针本身需要被修改”的进阶场景我在下一节单独讲。我整理了一个问题速查表方便你对照着找自己的问题。症状可能原因解决方案程序编译通过运行崩溃对空指针或野指针解引用检查指针是否绑定有效变量用malloc后判空函数运行完外部变量没变化函数内只改形参指针自身没改*p用*p ...写值调用时不知道要不要加混淆了“传值”和“传地址”形参是int *实参就要变量编译警告“类型不兼容”传了int**给int*参数检查级数指针嵌套别弄错返回的指针指向的内容乱码返回了函数内局部变量的地址不要返回局部变量地址改用static或malloc打印指针指向的内容不对忘记解引用打印的是地址用printf(%d, *ptr)而不是printf(%d, ptr)5. 再进一步什么时候需要用到指针的指针5.1 为什么要引入二级指针上面讨论的所有例子都是调用者在自己的代码里预先准备好了变量然后取地址传给函数。函数拿到一级指针直接改指向的值。但有些场景你想让函数去修改调用者手里的那个指针本身。比如你在函数内部动态分配了一段内存希望调用者获得这段内存的起始地址同时返回分配是否成功。如果函数参数只用int *你传ptr和ptr的地址是两码事func(ptr)只是把ptr的值拷贝给形参你在函数里改形参指向哪外面的ptr根本不知道。func(ptr)形参是int **也就是“指向指针的指针”函数里写*pp malloc(...)才能把新分配的内存地址写进外面的ptr变量。这就是“指针的指针”最经典的使用场景。在第八章里一般不会讲太深但我发现很多练习题答案是直接用到这个技术的所以你要提前知道它为什么存在。举个例子写一个函数根据输入构造一个字符串并让调用者通过参数拿到这个字符串的地址。#include stdio.h #include stdlib.h #include string.h int create_message(char **msg_ptr) { char *tmp (char *)malloc(64 * sizeof(char)); if (tmp NULL) { return -1; } strcpy(tmp, hello from function); *msg_ptr tmp; return 0; } int main() { char *message NULL; int ret create_message(message); if (ret 0) { printf(%s\n, message); free(message); } return 0; }这里create_message接收的参数是char **你传进去的是message也就是char **。函数内部分配好内存后*msg_ptr tmp这行代码做了一件关键的事把message这个变量本身的值改成tmp指向的地址。message初始是NULL一次调用后它变成了一个指向合法字符串的指针后续你可以直接使用它。我之前见过很多人的写法是void create_message(char *msg) { msg (char*)malloc(64); }然后在main里char *msg NULL; create_message(msg); printf(%s, msg);结果程序直接崩溃或者打印出乱码。原因就是上面说的函数只修改了形参副本外面的msg依然是NULL。这种错误几乎每一个学二级指针的人都会踩到所以我在这里详细说一下你可能现在用不到但等学到后面动态内存管理你一定会感激这个知识点的。5.2 智能指针的概念C语言里应该怎样理解热搜词里出现了“智能指针实现”很多人会疑惑C语言并没有内置的智能指针那它到底是什么C里的std::unique_ptr和std::shared_ptr才是智能指针它们能在对象生命周期结束时自动释放内存。但C语言里没有析构函数所以你想实现类似效果只能通过结构体包装指针再在特定时机调用清理函数。struct SmartPtr { int *data; size_t refs; }; void smart_ptr_free(struct SmartPtr *sp) { if (sp sp-data) { free(sp-data); sp-data NULL; } }这个用法本身和“第八章指针”不是一个层级的但如果你在这门课之外对内存安全有点追求可以先了解一下概念C语言的所有“智能”都是靠约定和封装实现的裸指针没有任何自动保护机制越界写、重复释放、泄漏全都有可能发生。利用指针返回结果时最稳妥的习惯就是“谁分配谁释放谁接收谁负责”。5.3 和数组、文件指针的联动你留意到热词里有“指针数组”和“文件指针”这两点也顺带提一下因为它们跟“返回多个结果”经常混在一起考。指针数组是指一个数组里面的每个元素都是一个指针。它经常用来管理多个字符串比如char *names[3] {Alice, Bob, Cindy};这里names[0]是一个char*指向字符串常量。如果你要把这些字符串传给某个函数做处理比如替换某个字符并返回发生了多少次替换那你要传的实际上是char **类型因为数组名names会退化成char **。如果你传递数组首地址并接收“替换次数”这个函数签名可以是void replace_char(char **strings, int n, char old_char, char new_char, int *total) { *total 0; for (int i 0; i n; i) { char *p strings[i]; while (*p) { if (*p old_char) { *p new_char; (*total); } p; } } }调用时replace_char(names, 3, A, a, count);一层层指针嵌套会让人头晕但只要你记住一个原则每多一层指针就多一层“这个变量本身也是个需要被修改的地址”的含义。char **说的是“一个指向char指针的指针”它既可能是“指向数组首元素的指针”也可能是“指向某个指针变量的指针”。明确这一点面对任何复杂的指针类型都不会慌。文件指针的情况稍微特殊一点FILE *本身就是一个指针你可以在函数里打开文件、写入数据然后通过指针参数返回打开是否成功或文件内偏移量。它的思路和普通指针没有本质区别只是类型变成了FILE *。不少习题会让你写一个函数传入文件名把文件的字节数和行数都返回出来这时候函数签名往往是void count_file(const char *filename, long *size, int *lines)内部用fopen打开文件用fseek和ftell得到大小再逐行统计行号。做法还是那一套调用者准备两个变量取地址接收结果。6. 我自己的使用心得写完这么多最后说点题外的经验之谈。我刚开始教别人“利用指针返回多个结果”的时候喜欢画很多箭头图、内存示意图后来发现效果一般。真正有效的方法是让他们先写一个错得离谱的版本比如前面说的void func(int *p) { p ... }运行后看到外部变量没变再对比正确的*p ...版本这才真能理解“指针是值传递”这句话到底在说什么。所以你要是学这一章觉得绕不妨故意写错几次看看错误运行时到底发生了什么。还有个更实用的小技巧每次拿到一个带指针参数的函数你先别急着看参数名先用笔写出来“这个参数是输入还是输出”。输入型指针经常加const修饰输出型指针一定会在函数内部被*p ...赋值。养成这个习惯第八章做练习时你一眼就能看穿出题人的意图。后面真做了工程、接了大项目你会发现在C语言里“通过指针返回多个结果”几乎是无处不在的基础功学会它等于给你的C语言水平扩展了一个维度。
返回列表