正确区分v o i d 与v o i d *

来源:互联网 发布:提高口语 知乎 编辑:程序博客网 时间:2024/06/06 03:21

正确区分v o i d 与v o i d *
v o i d 及v o i d 指针类型对于许多C / C + + 语言初学者,甚至是部分有经验的程序员来说都
是一个谜,它让人云里雾里,不甚清晰,因此在使用时也会出现一些这样那样的问题。也许
在进入C / C + + 语言精彩世界的第一刻就认识了v o i d 和v o i d * ,可是它们的具体含义到底是什
么呢?
v o i d 是“无类型”,所以它不是一种数据类型;v o i d   * 则为“无类型指针”,即它是指向
无类型数据的指针,也就是说它可以指向任何类型的数据。
从来没有人会定义一个v o i d 变量,如果真的这么做了,编译器会在编译阶段清晰地提
示,“  i l l e g a l   u s e   o f   t y p e   ' v o i d ' ”。v o i d 体现的是“有与无”的问题,要先“有”了,在非v o i d
的前提下才能去讨论这个变量是什么类型的,此哲学思想渗透于小小v o i d 的使用与设计中。
v o i d 发挥的真正作用是限制程序的参数与函数返回值。在C / C + + 语言中,对v o i d 关键
字的使用做了如下规定:
(1 )如果函数没有返回值,那么应将其声明为v o i d 类型。
在C 语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是
许多程序员却误以为其为v o i d 类型。例如:
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 。这个结果更加明确地说明了函数返回值为i n t 类型,而非
v o i d 。
在林锐博士的《高质量程序设计指南—C + + / C 语言(第3 版)》一书中曾提到:“C + +
语言有很严格的类型安全检查,不允许上述情况(指函数不加类型声明)发生”。但是在一
些较老的编译器(比如V C + + 6 . 0 )中上述A d d 函数的编译无错也无警告且运行正确,所以不
能将严格的类型检查这样的重任完全交给编译器。
为了避免出现混乱,在编写C/C++ 程序时,必须对任何函数都指定其返回值类型。如果函
数没有返回值,则要声明为void。这既保证了程序良好的可读性,也满足了编程规范性的要求。
(2 )如果函数无参数,那么声明函数参数为v o i d 。
正如我们原先遇到的情况一样,如果在调用一个无参数函数时,一不小心为其设定了参数:
第1 章 从C 继承而来的   4 3
int TestFunction(void)
{
   return 2012;
}
int main()
{
   int thisYear = TestFunction(2011);
   // processing code
   return 0;
}
那么在C + + 编译器中编译代码时则会出错,提示“'   Te s t F u n c t i o n   '   :   f u n c t i o n   d o e s   n o t 
t a k e   1   p a r a m e t e r s ”。而在C 语言中,据说它能编译通过且能正确执行。之所以说是据说,是
因为本人没有在C 环境下实验证实这种情况,请原谅我的懒惰,因为我真的不想去碰Tu r b o 
C ,虽然那也曾经是我的入门开发环境。
所以,在C / C + + 中,若函数不接受任何参数,一定要指明参数为v o i d 。就算写的是C
函数,为了将来的兼容性,请不要省略这个v o i d 。

接下来说说特殊指针类型v o i d * 。
众所周知,如果存在两个类型相同的指针p I n t 1 和p I n t 2 ,那么我们可以直接在二者间互
相赋值;如果是两个指向不同数据类型的指针p I n t 和p F l o a t ,直接相互赋值则会编译出错,
必须使用强制转型运算符把赋值运算符右侧的指针类型转换为左侧的指针类型,这一点在建
议1 1 中已经解释得很清晰,代码如下所示:
int *pInt;
float *pFloat;
pInt = pFloat;  // 编译出错,提示“'=' : cannot convert from 'int *' to 'float *'”
pInt = (int *)pFloat; // 正确,需强制转型
而v o i d   * 则不同,任何类型的指针都可以直接赋值给它,无须强制转型,如下所示:
void *pVoid;
float *pFloat;
pVoid = pFloat;  // 正确,无需强制转型
但这种转换在C + + 中并不是双向的,在不使用强制转型的前提下,不允许将v o i d   * 赋
给其他类型的指针,如下所示:
void *pVoid;
float *pFloat;
pFloat = pVoid;  // 错误,编译失败,提示“'=' : cannot convert from 'void *' to ' float *'”
对于一般数据类型的指针,我们可以进行加减等算法操作,但是按照A N S I 标准,对
v o i d 指针进行算法操作是不合法的:
//  分别采用VC++ 编译器和Gcc 编译器进行验证
int * pInt;
pInt ++;     //  正确,pInt 指针增大sizeof(int)
pInt += 2;    //正确, pInt 指针增大2*sizeof(int)
void * pVoid;
pVoid ++;    //  错误,error C2036: "pVoid*": 未知的大小
pVoid += 1;    //  错误
A N S I 标准之所以这样认定,是因为只有在确定了指针指向数据类型的大小之后,才能
进行算法操作。但是大名鼎鼎的G N U 则有不同的规定,它指定v o i d   * 的算法操作与c h a r   *
一致。所以在上面代码片段中出现错误的代码在G N U 编译器中能顺利通过编译,并且能正
确执行。虽然G N U 较A N S I 更开放,提供了对更多语法的支持,但是A N S I 标准更加通用,
更加“标准”,所以在实际设计中,还是应该尽可能地迎合A N S I 标准。在实际的程序设计
中,为迎合A N S I 标准,并提高程序的可移植性,可以采用以下方式进行代码设计:
void * pVoid;
(char *)pVoid ++;    // ANSI:正确;GNU :正确
(char *)pVoid += 2;   // ANSI:错误;GNU :正确
如果函数的参数可以是任意类型指针,那么应声明其参数为v o i d   * ,最典型的例子就是
我们熟知的内存操作函数m e m c p y 和m e m s e t 的原型:
void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );
仔细品味,就会发现这样的函数设计是多么富有学问,任何类型的指针都可以传入
m e m c p y 和m e m s e t 中,传出的则是一块没有具体数据类型规定的内存,这也真实地体现了
内存操作函数的意义。如果类型不是v o i d   * ,而是c h a r   * ,那么这样的m e m c p y 和m e m s e t 函
数就会与数据类型产生明显联系,纠缠不清,这不是一个通用的、“纯粹的、脱离低级趣味”
的函数设计!
请记住:
v o i d 与v o i d * 是一对极易混淆的双胞胎兄弟,但是它们在骨子里却存在着质的不同,区
分它们,按照一定的规则使用它们,可以提高程序的可读性、可移植性。仔细体会,还会发
现隐藏在它们背后的设计哲学。