建议18:正确区分void与void*
void及void指针类型对于许多C/C++(www.cppentry.com)语言初学者,甚至是部分有经验的程序员来说都是一个谜,它让人云里雾里,不甚清晰,因此在使用时也会出现一些这样那样的问题。也许在进入C/C++(www.cppentry.com)语言精彩世界的第一刻就认识了void和void*,可是它们的具体含义到底是什么呢?
void是“无类型”,所以它不是一种数据类型;void *则为“无类型指针”,即它是指向无类型数据的指针,也就是说它可以指向任何类型的数据。
从来没有人会定义一个void变量,如果真的这么做了,编译器会在编译阶段清晰地提示,“ illegal use of type 'void'”。void体现的是“有与无”的问题,要先“有”了,在非void的前提下才能去讨论这个变量是什么类型的,此哲学思想渗透于小小void的使用与设计中。
void发挥的真正作用是限制程序的参数与函数返回值。在C/C++(www.cppentry.com)语言中,对void关键字的使用做了如下规定:
(1)如果函数没有返回值,那么应将其声明为void类型。
在C语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是许多程序员却误以为其为void类型。例如:
- Add ( int a, int b );
- int main()
- {
- printf ( "1010 + 1001 = %d", Add ( 1010, 1001) );
- return 0;
- }
- Add ( int a, int b )
- {
- return a + b;
- }
程序运行的结果为:2 + 3 = 5。这个结果更加明确地说明了函数返回值为int类型,而非void。
在林锐博士的《高质量程序设计指南—C++(www.cppentry.com)/C语言(第3版)》一书中曾提到:“C++(www.cppentry.com)语言有很严格的类型安全检查,不允许上述情况(指函数不加类型声明)发生”。但是在一些较老的编译器(比如VC++(www.cppentry.com)6.0)中上述Add函数的编译无错也无警告且运行正确,所以不能将严格的类型检查这样的重任完全交给编译器。
为了避免出现混乱,在编写C/C++(www.cppentry.com)程序时,必须对任何函数都指定其返回值类型。如果函数没有返回值,则要声明为void。这既保证了程序良好的可读性,也满足了编程(www.cppentry.com)规范性的要求。
(2)如果函数无参数,那么声明函数参数为void。
正如我们原先遇到的情况一样,如果在调用一个无参数函数时,一不小心为其设定了参数:
- int TestFunction(void)
- {
- return 2012;
- }
- int main()
- {
- int thisYear = TestFunction(2011);
- // processing code
- return 0;
- }
那么在C++(www.cppentry.com)编译器中编译代码时则会出错,提示“' TestFunction ' : function does not take 1 parameters”。而在C语言中,据说它能编译通过且能正确执行。之所以说是据说,是因为本人没有在C环境下实验证实这种情况,请原谅我的懒惰,因为我真的不想去碰Turbo C,虽然那也曾经是我的入门开发环境。
所以,在C/C++(www.cppentry.com)中,若函数不接受任何参数,一定要指明参数为void。就算写的是C函数,为了将来的兼容性,请不要省略这个void。
接下来说说特殊指针类型void*。
众所周知,如果存在两个类型相同的指针pInt1和pInt2,那么我们可以直接在二者间互相赋值;如果是两个指向不同数据类型的指针pInt和pFloat,直接相互赋值则会编译出错,必须使用强制转型运算符把赋值运算符右侧的指针类型转换为左侧的指针类型,这一点在建议11中已经解释得很清晰,代码如下所示:
- int *pInt;
- float *pFloat;
- pInt = pFloat; //编译出错,提示“'=' : cannot convert from 'int *' to 'float *'”
- pInt = (float *)pFloat; //正确,需强制转型
而void *则不同,任何类型的指针都可以直接赋值给它,无须强制转型,如下所示:
- void *pVoid;
- float *pFloat;
- pVoid = pFloat; //正确,无需强制转型
但这种转换在C++(www.cppentry.com)中并不是双向的,在不使用强制转型的前提下,不允许将void *赋给其他类型的指针,如下所示:- void *pVoid;
- float *pFloat;
- pFloat = pVoid; //错误,编译失败,提示“'=' : cannot convert from 'void *' to ' float *'”
对于一般数据类型的指针,我们可以进行加减等算法操作,但是按照ANSI标准,对void指针进行算法操作是不合法的: - // 分别采用VC++(www.cppentry.com)编译器和Gcc编译器进行验证
- int * pInt;
- pInt ++; // 正确,pInt指针增大sizeof(int)
- pInt += 2; //正确, pInt指针增大2*sizeof(int)
-
- void * pVoid;
- pVoid ++; // 错误,error C2036: "pVoid*": 未知的大小
- pVoid += 1; // 错误
ANSI标准之所以这样认定,是因为只有在确定了指针指向数据类型的大小之后,才能进行算法操作。但是大名鼎鼎的GNU则有不同的规定,它指定void *的算法操作与char *一致。所以在上面代码片段中出现错误的代码在GNU编译器中能顺利通过编译,并且能正确执行。虽然GNU较ANSI更开放,提供了对更多语法的支持,但是ANSI标准更加通用,更加“标准”,所以在实际设计中,还是应该尽可能地迎合ANSI标准。在实际的程序设计中,为迎合ANSI标准,并提高程序的可移植性,可以采用以下方式进行代码设计:
- void * pVoid;
- (char *)pVoid ++; // ANSI:正确;GNU:正确
- (char *)pVoid += 2; // ANSI:错误;GNU:正确
如果函数的参数可以是任意类型指针,那么应声明其参数为void *,最典型的例子就是我们熟知的内存操作函数memcpy和memset的原型:- void * memcpy(void *dest, const void *src, size_t len);
- void * memset ( void * buffer, int c, size_t num );
仔细品味,就会发现这样的函数设计是多么富有学问,任何类型的指针都可以传入memcpy和memset中,传出的则是一块没有具体数据类型规定的内存,这也真实地体现了内存操作函数的意义。如果类型不是void *,而是char *,那么这样的memcpy和memset函数就会与数据类型产生明显联系,纠缠不清,这不是一个通用的、“纯粹的、脱离低级趣味”的函数设计!
请记住:
void与void*是一对极易混淆的双胞胎兄弟,但是它们在骨子里却存在着质的不同,区分它们,按照一定的规则使用它们,可以提高程序的可读性、可移植性。仔细体会,还会发现隐藏在它们背后的设计哲学。