观察现象
我几年来陆续在不同公司的不同人写出来的不同程式码里见到一种使用void *
传递参数的现象。
合理状况下,我们在函式介面里加上void *
参数多半是因为不知道使用者欲传入的资料型态为何?
例如标准函式库里的memset、memcpy等,但让我看到的程式码都不是这种!
下面我用一个简单的范例来描述我所看到的状况:
//这是函式的宣告,位在某个h档。
//虽然我不喜欢匈牙利命名法,但看到这样的程式码通常都爱用匈牙利命名,不知两者间是否有何种关联?
//为了保持原程式的「味道」,所以我也很无聊的保留了匈牙利命名的变数。
void * SomeFunction( void *pData);
//这是函式实体,位在某个c档。
void * SomeFunction( void *pData)
{
//这里有很多的变数宣告…
SOME_STRUCT_TYPE *pstData = (SOME_STRUCT_TYPE *) pData;
//这里还有另外很多从匈牙利来的变数们…
//以下是函式工作内容,可能有数百至千行!
//但可以肯定的是,再也见不到pData参数。
// ...
}
这个函式的参数并不存在前述「不知使用者会传入何种参数」或「希望函式能支援任何种类参数」的需求,
事实上以函式内容来看,函式根本就预设它的参数是SOME_STRUCT_TYPE
型态,这个函式根本就应该被直接宣告为:
void SomeFunction(SOME_STRUCT_TYPE *Data);
虽然原来的写法一样可以编译、甚至可以正确执行,但却存在几个大缺点:
1.不直观,光看函式宣告无法看出参数型态,在不知型态的情况下有些时候甚至不能直觉的知道参数的意义!
2.无法依赖编译器的型态检查把关,若有一天因故需要修改参数型态,此时编译器连吭都不会都不会吭一声!
只能祈祷可以手动找出所有关联的地方并完成同步修改,然后希望它在执行时不会出错!
分析解法对策
那么为何这些函式要这样宣告呢?
说来好玩,大部份的原作者已找不到人;而在作者还在的情况下,问他,他也答不出个所以然!
我对这种奇怪写法是否具有某种历史意义已经没有多大的兴趣,所以直接猜测会这样宣告函式的可能原因:
1.这是要传入某个程式库的回呼函式,
因为程式库API要求的是长这样的回呼函式,所以只好把要传入的函式定成这样,
不然编译器会抱怨说函式参数不符!
2.这函式就是个普通的函式,也没有被回呼的需求。
但因为程式的结构如同义大利面条一样、程式档案的引用关系毫无顺序章法,
如果明确定义函式参数型态,则该型态必须在函式标头处定义,或引用相关标头;
然而一旦这么做,就会引发一些莫名其妙的编译错误,如无限循环引用、或型态冲突等!
因此最后只好这么恶搞,好来逃避错误的发生!
以上原因不是在唬烂,都是在真正经手过的程式码里出现的状况!
对于第二种原因我不再去探究,因为这种状况根本就应该要对整坨程式重新分层、封装、解偶、重构,
而不是去研究语法上有什么可避免问题的技巧!
对于第一种原因,以下将阐述我的这种情况的分析和采行策略。
为了避免有些人说我写的东西太理论,下面我就拿真正的程式库函式来做范例,比方说pthread。
当使用pthread欲建立一个执行绪时,我们会呼叫下面这个函式:
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) ( void *),
void *arg);
由于这不是执行绪教学,就不说明函式的用法行为等细节,
这里只探讨函式的最后两个参数,即start_routine与arg。
start_routine是一个回呼函式,会在执行绪建立起来后被呼叫;
而pthread基本上不会去处理arg参数,只会把它直接pass给start_routine函式。
假设我有一个想被新执行绪执行的函式(以及它的资料参数)定义如下:
struct my_data
{
int value;
};
void * my_thread_proc( struct my_data *data)
{
printf("My thread has started!\ n");
printf("The argument value is: %d\n", data->value);
return NULL;
}
然后我在主程式执行下面程式码来建立执行绪并执行我的函式:
struct my_data mydata = { .value=7 };
pthread_t thread
pthread_create(&thread, NULL, my_thread_proc, &mydata);
这样,程式可以被编译并正常执行。
我的回呼函式会收到data参数,而这个参数就是我在主程式所传入的mydata变数,
因此回呼函式会印出数字7,没什么意外。
唯一美中不足的就是在编译时,编译器会发出警告,告诉我pthread_create的参数不匹配!
原因很明显,因为我的回呼函式长相是void *(*)( struct my_data*)
,
而pthread_create要的却是void *(*)( void *)
。
其实指标是相通的,不同型态的指标都一样是记忆体位址,所以即使型态不匹配,执行结果仍然正确。
而pthread_create的回呼参数定义为void *
型态原因也很合理,
因为它不知道使用者实际需要的参数型态为何,所以使用了万用指标。
唯一让人不开心的地方就是关于型态匹配的编译警告!
除了直接关闭编译警告这种做法外(极度不推荐),在某个地方强制转型大概是免不了的了!
只是该在什么地方执行怎么样的转型?
当然这文章最初提到的处理方法是其中一种解法,好处坏处都分析过了,但极不建议使用!
使用这种方法来处理的话,我需要将回呼函式修改成下面这样:
void * my_thread_proc( void *data)
{
struct my_data *data = arg;
// ...
}
第二种方法就是不动我的函式宣告,然后在呼叫pthread_create的时候进行参数转型。
那么我的函式长相将维持最初定义的:
void * my_thread_proc( struct my_data *data)
而在建立执行绪时就需要一些转型修饰:
pthread_create(&thread, NULL, ( void *(*)( void *)) my_thread_proc, &mydata);
这种做法的好处是维持了自订回呼函式的参数明确性,也解决了传递函式给别人时的参数不匹配问题;
缺点就是每次写这个转型叙述有点烦,占版面又容易让人眼花,特别是在有些回呼型态比较复杂的时候。
第三种做法是在别处使用typedef定义回呼函式的类型,这样在做转型的时候,画面会比较好看,
但原理与第二种做法类似。
使用这种做法,要先定义回呼函式的型态:
typedef void *(*pthread_start_routine_t)( void *);
那么pthread_create的函式原型可能可以改成这样:
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
pthread_start_routine_t start_routine,
void *arg);
在建立执行绪时的呼叫就会变成:
pthread_create(&thread, NULL, (pthread_start_routine_t) my_thread_proc, &mydata);
这种做法看似简洁、画面也不凌乱,但却存在致命缺点:
当pthread_create的回呼介面、也就是pthread_start_routine_t做了更动时,
编译器无法给予任何的检查警告!
有时还会见到一种做法与第三种方法类似,但副作用更剧烈,就是把所有的回呼传入通通转型成void *
:
pthread_create(&thread, NULL, ( void *) my_thread_proc, &mydata);
或者它可能会长成这样:
#define PTHREAD_CALLBACK(f) (( void *)f)
pthread_create(&thread, NULL, PTHREAD_CALLBACK(my_thread_proc), &mydata);
但不管哪一种,效果是一样的,也都不推荐。
最优做法
绕了一大圈后,我觉得最好的方法,也是我通常再用的写法是上面提的第二种写法,
也就是每次不厌其烦重写一次回呼介面转型的写法:
pthread_create(&thread, NULL, ( void *(*)( void *)) my_thread_proc, &mydata);
虽然写起来比较繁复,但却是最保险的写法!
当pthread_create的回呼函式型态发生变化,比如说变成这样时:
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)( void *, int, float),
void *arg);
此时在所有呼叫到pthread_create的地方都会发生函式参数不匹配的警告,提醒程式师来一一修改。
当然,你可以说会不会有人只修改了转型宣告就了事:
pthread_create(&thread, NULL, ( void *(*)( void *,int,float)) my_thread_proc, &mydata);
//然而my_thread_proc仍然是原样而无任何更动!
void * my_thread_proc( struct my_data *data);
当然这是有可能的!
只能说我们已经尽力做好防止警告,若真有人这样只改了转型宣告后却不知道要检查修改后面的函式,
只能说是纪律问题!
还有另外一种状况也会让这种写法无法产生警告:那就是当my_thread_proc介面变动的时候,
因为我们在呼叫pthread_create时都进行了强制转型,所以编译器不会检查出任和异状!
针对这种状况,只能说尽力了!毕竟C语言有它的语法限制,这已经是在限制条件下想到的最佳做法,
若需要语法上完美的回呼机制的话,只能改用具有物件导向多型特性的其它高阶程式语言了!
翻译自http://www.programmer-club.com.tw/showsametitleN/c/46934.html