上一节主要讨论了C语言函数“返回”多个值的两个方法——使用全局变量,或者借助于函数的参数(当然了,需要指针型参数),为了兼顾多线程编程的安全性和方便性,更推荐的是后面那个方法。不过,如果C语言函数需要返回的值较多,再借助函数的指针型参数就麻烦了。
想象一下,某个C语言函数有十几个参数。
“返回”超多值
如果某个C语言函数能够计算出很多个结果,并且需要将这些结果都返回,该怎么办呢?总不能给函数定义十几个参数吧,何况就算不考虑效率这么做了,使用起来也不方便。
仔细想想应该能够发现,无论需要返回多少个值,这些值都是存在于内存中的,如果我们将这些值集中在一段连续的内存里,只需要知道这段连续内存的起始地址(一个参数就可以),其实就已经能够访问所有的返回值了。只不过,我们还需要知道如何访问这些返回值。例如一段 16 字节的内存,它既可以存放 16 个 char 值,也可以存放 4 个 int 值,还可以存放 2 个 long 型变量。
这里假设 int 和 long 分别占用 4 个字节和 8 个字节内存空间。
事实上,C语言中数据类型的作用之一就是告诉程序该如何访问内存,以及该如何解释该内存里存放的值。
借助数组
如果需要返回的多个值属于同一类型,则可以定义数组将其取出。例如:
void fun(int *arr)
{
...
arr[0] = res0;
arr[1] = res1;
...
arr[n] = resn;
}
上述C语言代码允许“返回”n 个 int 型的值,但是 fun() 函数并不需要定义 n 个参数,而是仅需一个参数就可以将所有值返回给函数调用者。这是因为数组的各个元素在内存中紧密排列,且每个元素占用内存空间相同,只需知道第一个元素的位置,就可以依次类推出其他所有元素。
借助结构体
如果C语言函数需要返回的多个值数据类型不同,例如 fun() 函数需要返回的值分别为 char 型、short 型、int 型、long 型、float 型、double 型,可能还会有一些指针等其他类型,显然数组无法满足需求。这时可以借助C语言中的结构体:
struct S{
char c;
short s;
int i;
...
void *p;
};
void fun(struct S *sret)
{
...
sret->c = cres;
sret->s = sres;
...
sres->p = pres;
}
从上面这段示例C语言代码可以看出,即使函数需要返回的多个值数据类型很杂时,借助于C语言结构体也能轻易将它们组合在一起,对于函数而言,仅需一个参数就可以将这些值返回,而无需定义很多个参数。
事实上,在C语言程序开发中,指针与结构体结合传递多个复杂参数的技巧也常被使用在其他地方,例如:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
pthread_create() 函数在 Linux 中常被用来创建线程,请看它的后面两个参数,分别是函数指针 start_routine 和 void 指针 arg。pthread_create() 函数会将 start_routine 指向的函数放在一个新线程里执行,要求 start_routine 指向的函数原型如下:
void *thread_fun(void *p);
参数 arg 负责传递参数给 thread_fun(),乍一看 thread_fun() 函数只能接收一个参数,可自由发挥的空间被限制,但了解了指针和结构体组合可以传递任意复杂的参数的技巧后,这种担忧应该就烟消云散了。
易跳进的“陷阱”
可以看出,借助于指针,C语言函数能够轻易通过参数返回任意多的值。不过到目前为止,本专栏介绍的返回多值的C语言函数调用方法都是使用外界变量提供的内存,例如:
int caller(int *res)
{
int a;
struct S param;
...
fun(&a);
sfun(¶m);
...
*res = a + p->i;
return 0;
}
这种风格的C语言代码要求 caller() 不能在 fun() 以及 sfun() 之前返回,否则就极有可能导致程序出现严重错误。因为 caller() 返回后,它的局部变量 a 和 param 就被系统回收了,这时 fun() 再使用它们就相当于使用野指针。具体分析,以及避免该问题的相应小技巧,下一节再说了。
小结
本节主要讨论了C语言函数返回超多值的方法,如果需要返回的值属于同一类型,则可以尝试将其组合成数组统一返回。要是需要返回的值数据类型比较杂,则应考虑使用结构体将其组合,并通过结构体指针返回。另外在最后,我们还一起简要的讨论了使用这两种方法做函数返回值的“陷阱”,如果读者感到迷惑,欢迎阅读下一节内容。