首先声明:这里说的概念都是c语言中的,跟c++会有些许不一样。
预备知识
1.空指针常量(null pointer constant)
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
这里即是说明:值为0的整型常量表达式,或强制(转换)为 void * 类型的此类表达式,称为 空指针常量 。
如0、0L、3-3、'\0'、017、(void)0等都属于空指针常量。
至于系统会采用哪种形式来作为空指针常量使用,则是和具体实现相关。一般的C系统采用 (void *)0 或者 0 的居多,也有个别采用的 0L ;至于C++系统,由于存在严格的类型转换要求, void * 不能像C中自由转换成其他指针类型,所以通常选择 0 作为空指针常量。
把空指针常量赋给指针类型的变量p,p就成为了一个空指针。
2.NULL值
The macro NULL is defined in <stddef.h> (and other headers) as a null pointer constant.
即NULL是一个标准规定的宏定义,用来表示空指针常量。
我们找到 stddef.h 中的该宏定义:
#define NULL ((void *)0)
毫无疑问,NULL就是一种空指针常量。
那有个问题,我们可以自定义NULL值吗?
实际上NULL是标准库中的一个 reserved identifier (保留标识符) ,所以如果包含了相应的标准头文件引入了NULL的话,再在程序中重新定义NULL为其他值(比如把NULL定义为3)就是非法的。
一、空指针(null pointer)
1.空指针定义
If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
通过预备知识中对于空指针常量和NULL值的讲解,我们可以知道:
只要将空指针常量赋给指针类型变量,该指针变量就是空指针。
int *p;
p = 0;
p = 0L;
p = '\0';
p = 3 - 3;
p = 0 * 17;
p = (void*)0;
p = NULL;
如上所示代码,经过其中任何一种赋值操作后,p就是一个空指针。而且,由系统保证空指针不指向任何实际的对象或函数。反过来说就是:任何对象或者函数的地址都不可能是空指针。
2.空指针的内存指向
标准并没有对空指针指向内存中什么地方这一问题做出规定。也就是说,具体使用 0x0地址 还是其他地址来表示空指针,都依赖于具体系统的实现。两种实现如下:
(1)零空指针(zero null pointer)
这是我们常见的一种实现,即空指针的内部用全0来表示。
(2)非零空指针(nonzero null pointer)
也有一些系统用一些特殊的地址值或者特殊的方式来表示空指针。
我们在实际编程中不需要了解我们系统空指针的实现和内存指向,我们只需要了解一个指针是否是空指针就可以了——编译器会自动实现其中的转换,为我们屏蔽掉其中的实现细节。
3.空指针的使用
空指针的使用,主要就是防止野指针和防止悬垂指针。
防止野指针
int *p = NULL;
防止悬垂指针
int *p = malloc(sizeof(int));
free(p);
p = NULL; // 置空
详细见下文野指针和悬垂指针。
二、野指针(wild pointer)
1.野指针概念
野指针:没有初始化的指针
2.野指针产生原因
指针变量未初始化
如下程序:
int main()
{
int *p;
printf(%p", p);
return 0;
}
这里的p未被初始化,它的缺省值是随机的。
因此我们在声明一个指针变量的时候,为了防止出现野指针的问题,可以将其初始化为NULL,即设为空指针;也可以咋初始化时就将指针确定指向。
如下所示:
int a = 3;
int *p = &a;
// 或者
int *p = NULL;
int *p = 0;
其中int *p = 0和int *p = NULL都比较常用。
三、悬垂指针(dangling pointer)
1.悬垂指针概念
悬垂指针:指向已经被释放的自由区内存(free store)的指针。
它和野指针的区别就在于:悬垂指针曾经有效过,现在失效了;但是野指针从未有效过。
2.悬垂指针产生原因
(1)指针指向的内存释放之后未置空
指针指向的内存被free或者delete释放后,指针的值仍然为刚刚被释放的那块内存的首地址,但是此时指针已经失去了对那块内存的合法访问权限。
如下程序所示:
int main()
{
int *p = NULL;
p = malloc(sizeof(int));
*p = 3;
printf("Before free, p = %p, *p = %d\n", p, *p);
free(p);
/* 注意,此时p和p指向的内容并没有发生变化,但是free内存后已经失去了
对堆上那块内存的合法操作性 */
printf("After free, p = %p, *p = %d\n", p, *p);
return 0;
}
程序输出:
Before free, p = 0xe7a010, *p = 3
After free, p = 0xe7a010, *p = 0
在程序执行free(p)之后,p就是一个野指针。为了避免野指针可能引起的问题,我们应该在free(p)之后加上:
p = NULL;
这个操作就称为置空。
(2)指向同一块内存多个指针之一被释放
这种情况严格来讲跟第一种情况是一回事儿,示例代码如下:
int *p = malloc(sizeof(int));
*p = 3;
int *pd = p;
/* 当前p和pd指向的是同一块内存 */
free(p);
p = NULL;
/* 释放掉p所指向的内存,并将p置0 */
从上述代码可以看出,若有两个指针指向同一个内存,其中一个指针被free且被置空后,另一个指针却仍然指向那块被释放了的内存空间,这就成了一个悬垂指针。
这是悬垂指针产生的一个典型示例,常见于实际生产环境中。
(3)指针操作超出变量生命周期
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
示例代码如下:
// 定义一个函数
char *getString()
{
char *p = "Hello World!";
return p;
}
在函数内部定义的字符串指针p,其指向的内容存放在栈上,当这个函数执行完后退出后,这部分空间就会被释放,返回过去的p指针就成了悬垂指针。
四、void指针(void pointer)
1.void指针概念
void的意思是“无类型”,所以void指针又被称为“无类型指针”,void指针可以指向任何类型的数据,所以void指针一般被称为通用指针或者泛指针,也被叫做万能指针。
2.void指针的使用
(1)void指针变量p所指向的内容不能通过*p去访问
如果要通过void指针去获取它所指向的变量值时候,需要先将void指针强制类型转换成和变量名类型想匹配的数据类型指针后再进行操作,如下所示:
int a = 5;
void *p = &a;
printf("*p=%d\n", *p);
编译器会报错,提示你:
incomplete type is not allowed.
意思就是这个类型不完整,没有办法去获取它所指向的变量值。改成:
printf("*p = %d\n", *(int *)p);
就可以了。
(2)void指针赋给其他类型的指针
一个常见的使用场景就是:动态内存申请与释放。
首先要明确一件事,c语言和c++在一些语法实现上有区别,这里我们说的是c语言。
C语言中,void指针赋给其他任意类型的指针(除开函数指针,void指针赋给函数指针下面讨论),是天经地义的,无需手动强转;其他任意类型的指针赋给void指针,也是天经地义的。
例如:
typedef struct {
...
...
} complex_struct;
// c语言中的正确写法:
complex_struct = malloc(sizeof(complex_struct));
// c语言中多此一举的写法:
// complex_struct = (complex_struct)malloc(sizeof(complex_struct));
如果你发现不手动强转,编译器出现warning,请注意一下自己是不是忘了加malloc函数的头文件:
#include <stdlib.h>
因为在c语言中,编译器会把任何未定义的函数默认返回int,因此如果我们不加stdlib.h这个头文件,malloc函数默认返回int,所以我们看到编译器警告,如果看得不仔细的话会以为是因为没有强制类型转换导致两边类型不匹配,其实编译器提示的是int *和int类型不匹配,而不是int *和void *类型不匹配!(3)void指针赋给函数指针
void指针可以赋给函数指针以外的其他所有指针,malloc函数就是一个例子。至于函数指针的问题,这里来讨论。
linux中有个dlsym函数,函数声明如下:
void *dlsym(void *handle, const char *symbol);
man手册中有个示例代码:
int main()
{
...
void *handle;
double (*cosine)(double);
...
*(void **) (&cosine) = dlsym(handle, "cos");
...
}
这里面它想用函数指针去接收dlsym函数返回的void指针,却没有用以下两种更自然的、好理解的形式:
cosine = dlsym(handle, "cos");
cosine = (double (*)(double)) dlsym(handle, "cos");
这是因为C99标准的遗漏,导致了void指针无法直接转换成函数指针。所以它用了下面这种夸张的转换:
*(void **) (&cosine) = dlsym(handle, "cos");
先把consine取地址变成二级指针,然后将这个强转成(void **)这个void二级指针,再经过指针运算符*变成void一级指针,这样左右两边类型就匹配了。
(4)其他类型的指针赋给void指针
void指针可以用作泛型,接收任意数据类型指针。
void指针用于指向特定地址,而无需关心这个地址上存放着什么类型的数据。例如常见的memcpy等函数就用到void*,函数原型如下:
void *memcpy(void *des, void *src, size_t n);
此处的void *des和void *src可以接收任意类型的数据类型指针,既然是内存拷贝,入参就不应该限制传入什么类型的指针,逻辑上十分合理。