首页 > 代码库 > Dangling pointer(悬垂指针、迷途指针)和 Wild pointer(野指针)
Dangling pointer(悬垂指针、迷途指针)和 Wild pointer(野指针)
一、迷途指针(悬垂指针)
迷途指针经常出现在混杂使用malloc() 和 free() 库调用: 当指针指向的内存释放了,这时该指针就是迷途的。和前面的例子一样,一个避免这个错误的方法是在释放它的引用后将该指针的值重置为NULL,如下所示:
在计算机编程领域中,迷途指针与野指针指的是不指向任何合法的对象的指针。
当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称迷途指针(悬垂指针)。若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的迷途指针,则将产生无法预料的后果。因为此时迷途指针所指向的内存现在包含的已经完全是不同的数据。通常来说,若原来的程序继续往迷途指针所指向的内存地址写入数据,这些和原来程序不相关的数据将被损坏,进而导致不可预料的程序错误。这种类型的程序错误,不容易找到问题的原因,通常会导致段错误(Linux系统中)和一般保护错误(Windows系统中)。如果操作系统的内存分配器将已经被覆盖的数据区域再分配,就可能会影响系统的稳定性。
当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称迷途指针(悬垂指针)。若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的迷途指针,则将产生无法预料的后果。因为此时迷途指针所指向的内存现在包含的已经完全是不同的数据。通常来说,若原来的程序继续往迷途指针所指向的内存地址写入数据,这些和原来程序不相关的数据将被损坏,进而导致不可预料的程序错误。这种类型的程序错误,不容易找到问题的原因,通常会导致段错误(Linux系统中)和一般保护错误(Windows系统中)。如果操作系统的内存分配器将已经被覆盖的数据区域再分配,就可能会影响系统的稳定性。
二、迷途指针的成因
在很多编程语言中(如C语言)从内存中删除一个对象或者返回时删除栈帧后,并不会改变相关的指针的值。该指针仍然指向原来的内存地址,即使引用已经删除,现在也可能已经被其它进程使用了。
一个直接的例子,如下所示:
在很多编程语言中(如C语言)从内存中删除一个对象或者返回时删除栈帧后,并不会改变相关的指针的值。该指针仍然指向原来的内存地址,即使引用已经删除,现在也可能已经被其它进程使用了。
一个直接的例子,如下所示:
{
char *cp = NULL;
/* ... */
{
char c;
cp = &c;
} /* c falls out of scope */
/* cp is now a dangling pointer */
}
上述问题的解决方法是在该部分程序退出之前立即给CP赋0值(NULL)。另一个办法是保证CP在没有初始化之前,将不再被使用。char *cp = NULL;
/* ... */
{
char c;
cp = &c;
} /* c falls out of scope */
/* cp is now a dangling pointer */
}
迷途指针经常出现在混杂使用malloc() 和 free() 库调用: 当指针指向的内存释放了,这时该指针就是迷途的。和前面的例子一样,一个避免这个错误的方法是在释放它的引用后将该指针的值重置为NULL,如下所示:
#include <stdlib.h>
{
char *cp = malloc ( A_CONST );
/* ... */
free ( cp ); /* cp now becomes a dangling pointer */
cp = NULL; /* cp is no longer dangling */
/* ... */
}
{
char *cp = malloc ( A_CONST );
/* ... */
free ( cp ); /* cp now becomes a dangling pointer */
cp = NULL; /* cp is no longer dangling */
/* ... */
}
有个常见的错误是当返回一个基于栈分配的局部变量的地址时,一旦调用的函数返回,分配给这些变量的空间将被回收,此时它们拥有的是"垃圾值"。
int * func ( void )
{
int num = 1234;
/* ... */
return #
}
在调用func之后一段时间,尝试从该指针中读取num的值,可能仍然能够返回正确的值(1234),但是任何接下来的函数调用会覆盖原来的栈为num分配的空间。这时,再从该指针读取num的值就不正确了。如果要使一个指向num的指针都返回正确的num值,则需要将该变量声明为static。{
int num = 1234;
/* ... */
return #
}
三、迷途指针导致的安全漏洞
如同缓存溢出错误,迷途指针/野指针这类错误经常会导致安全漏洞。 例如,如果一个指针用来调用一个虚函数,由于vtable指针被覆盖了,因此可能会访问一个不同的地址(指向被利用的代码)。或者,如果该指针用来写入内存,其它的数据结构就有可能损坏了。一旦该指针成为迷途指针,即使这段内存是只读的,仍然会导致信息的泄露(如果感兴趣的数据放在下一个数据结构里面,恰好分配在这段内存之中)或者访问权限的增加(如果现在不可使用的内存恰恰被用来安全检测).
四、避免迷途指针的错误
避免迷途指针,有一种受欢迎的方法——即使用智能指针(Smart pointer)。智能指针使用引用计数来回收对象。
像Java语言,迷途指针这样的错误是不会发生的,因为Java中没有明确地重新分配内存的机制。而且垃圾回收器只会在对象的引用数为零时重新分配内存。
避免迷途指针,有一种受欢迎的方法——即使用智能指针(Smart pointer)。智能指针使用引用计数来回收对象。
像Java语言,迷途指针这样的错误是不会发生的,因为Java中没有明确地重新分配内存的机制。而且垃圾回收器只会在对象的引用数为零时重新分配内存。
五、野指针
某些编程语言允许未初始化的指针的存在,而这类指针即为野指针。野指针所导致的错误和迷途指针非常相似,但野指针的问题更容易被发现。
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
六、野指针的产生
野指针指的是还没有初始化的指针。严格地说,编程语言中每个指针在初始化前都是野指针。
一般于未初始化时便使用指针就会产生问题。大多数的编译器都能检测到这一问题并警告用户。
奇怪吗?不要急,还有更奇怪的呢,你可以把主函数中加上一句更不可理喻的:
((CTestClass*)NULL)->Function();
这仍然没有问题!!
我这还有呢,哈哈。现在你在主函数中这么写,倘说上一句不可理喻,那么以下可以叫做无法无天了:
你也许要问为什么,但是在我解答你之前,请你在主函数中加入如下代码:
printf( "%d, %d", sizeof( CTestClass ), sizeof( int ) );
这时你就会看到真相了:输出结果是——得到的两个十进制数相等。对,由sizeof得到的CTestClass的大小其实就是它的成员m_nInteger的大小。亦即是说,对于CTestClass的一个实例化的对象(设为a)而言,只有a.m_nInteger是属于a这个对象的,而a.Function()却是属于CTestClass这个类的。所以以上看似危险的操作其实都是可行且无误的。
关于成员函数CTestClass::Function的补充说明
这个函数是一个普通的成员函数,它在编译器的处理下,会成为类似如下的代码:
那么p->Function();一句将被编译器解释为:
Function( p );
这就是说,普通的成员函数必须经由一个对象来调用(经由this指针激活②)。那么由上例的delete之后,p指针将会指向一个无效的地址,然而p本身是一个有效的变量,因此编译能够通过。并且在编译通过之后,由于CTestClass::Function的函数体内并未对这个传入的this指针进行任何的操作,所以在这里,“野指针”便成了一个看似安全的东西。
至于virtual虚函数,如果在类定义之中将CTestClass声明为虚函数:
那么C++在构建CTestClass类的对象模型时,将会为之分配一个虚表指针vptr。vptr是一个指针,它指向一个函数指针的数组(虚函数表vtbl),数组中的成员即是在CTestClass中声明的所有虚函数。在调用虚函数的时候,必须经由这个vptr,这也就是为什么虚函数较之普通成员函数要消耗一些成本的缘故。以本例而言,p->Function();一句将被编译器解释为:
(*p->vptr[1])( p ); // 调用vptr表中索引号为1的函数(即Function)③
上面的代码已经说明了,如果p指向一个无效的地址,那么必然会有非法操作。
备注:
①关于函数的命名,我采用了原名而没有变化。事实上编译器为了避免函数重载造成的重名情况,会对函数的名字进行处理,使之成为独一无二的名称。
②将成员函数声明为static,可以使成员函数不经由this指针便可调用。
③vptr表中,索引号0为类的type_info。
(*p->vptr[1])( p ); // 调用vptr表中索引号为1的函数(即Function)③
上面的代码已经说明了,如果p指向一个无效的地址,那么必然会有非法操作。
一般于未初始化时便使用指针就会产生问题。大多数的编译器都能检测到这一问题并警告用户。
int f(int i)
{
char* cp; //cp is a wild pointer
static char* scp; //scp is not a wild pointer: static variables are initialized to 0
//at start and retain their values from the last call afterwards.
//Using this feature may be considered bad style if not commented
}
{
char* cp; //cp is a wild pointer
static char* scp; //scp is not a wild pointer: static variables are initialized to 0
//at start and retain their values from the last call afterwards.
//Using this feature may be considered bad style if not commented
}
补充:安全的野指针
看看下面的例子:
#include <stdio.h>
class CTestClass
{
public:
CTestClass( void );
int m_nInteger;
void Function( void );
};
CTestClass::CTestClass( void )
{
m_nInteger = 0;
}
void CTestClass::Function( void )
{
printf( "This is a test function." );
}
int main()
{
CTestClass* p = new CTestClass;
delete p;
p->Function();
return 0;
}
class CTestClass
{
public:
CTestClass( void );
int m_nInteger;
void Function( void );
};
CTestClass::CTestClass( void )
{
m_nInteger = 0;
}
void CTestClass::Function( void )
{
printf( "This is a test function." );
}
int main()
{
CTestClass* p = new CTestClass;
delete p;
p->Function();
return 0;
}
OK,程序到此为止,诸位可以编译运行一下看看结果如何。你也许会惊异地发现:没有任何的出错信息,屏幕上竟然乖乖地出现了这么一行字符串:
This is a test function.
奇怪吗?不要急,还有更奇怪的呢,你可以把主函数中加上一句更不可理喻的:
((CTestClass*)NULL)->Function();
这仍然没有问题!!
我这还有呢,哈哈。现在你在主函数中这么写,倘说上一句不可理喻,那么以下可以叫做无法无天了:
int i = 888;
CTestClass* p2 = (CTestClass*)&i;
p2->Function();
你看到了什么?是的,“This is a test function.”如约而至,没有任何的错误。CTestClass* p2 = (CTestClass*)&i;
p2->Function();
你也许要问为什么,但是在我解答你之前,请你在主函数中加入如下代码:
printf( "%d, %d", sizeof( CTestClass ), sizeof( int ) );
这时你就会看到真相了:输出结果是——得到的两个十进制数相等。对,由sizeof得到的CTestClass的大小其实就是它的成员m_nInteger的大小。亦即是说,对于CTestClass的一个实例化的对象(设为a)而言,只有a.m_nInteger是属于a这个对象的,而a.Function()却是属于CTestClass这个类的。所以以上看似危险的操作其实都是可行且无误的。
现在你明白为什么我的“野指针”是安全的了,那么以下我所列出的,就是在什么情况下,我的“野指针”不安全:
- 在成员函数Function中对成员变量m_nInteger进行操作;
- 将成员函数Function声明为虚函数(virtual)。
以上的两种情况,目的就是强迫野指针使用属于自己的东西导致不安全,比如第一种情况中操作本身的m_nInteger,第二种情况中变为虚函数的Function成为了属于对象的函数(这一点可以从sizeof看出来)。
其实,安全的野指针在实际的程序设计中是几乎毫无用处的。我写这一篇文章,意图并不是像孔乙己一样去琢磨回字有几种写法,而是想通过这个小例子向诸位写明白C++的对象实例化本质,希望大家不但要明白what和how,更要明白why。
其实,安全的野指针在实际的程序设计中是几乎毫无用处的。我写这一篇文章,意图并不是像孔乙己一样去琢磨回字有几种写法,而是想通过这个小例子向诸位写明白C++的对象实例化本质,希望大家不但要明白what和how,更要明白why。
关于成员函数CTestClass::Function的补充说明
这个函数是一个普通的成员函数,它在编译器的处理下,会成为类似如下的代码:
void Function( const CTestClass * this ) // ①
{
printf("This is a test function./n");
}
{
printf("This is a test function./n");
}
那么p->Function();一句将被编译器解释为:
Function( p );
这就是说,普通的成员函数必须经由一个对象来调用(经由this指针激活②)。那么由上例的delete之后,p指针将会指向一个无效的地址,然而p本身是一个有效的变量,因此编译能够通过。并且在编译通过之后,由于CTestClass::Function的函数体内并未对这个传入的this指针进行任何的操作,所以在这里,“野指针”便成了一个看似安全的东西。
然而若这样改写CTestClass::Function:
那么它将会被编译器解释为:
你看到了,在p->Function();的时候,系统将会尝试在传入的这个无效地址中寻找m_nInteger成员并将其赋值为0,剩下的我不用说了——非法操作出现了。
void CTestClass::Function( void )
{
m_nInteger = 0;
}
{
m_nInteger = 0;
}
那么它将会被编译器解释为:
void Function( const CTestClass * this )
{
this->m_nInteger = 0;
}
{
this->m_nInteger = 0;
}
你看到了,在p->Function();的时候,系统将会尝试在传入的这个无效地址中寻找m_nInteger成员并将其赋值为0,剩下的我不用说了——非法操作出现了。
至于virtual虚函数,如果在类定义之中将CTestClass声明为虚函数:
class CTestClass
{
public:
// ...
virtual void Function( void );
};
{
public:
// ...
virtual void Function( void );
};
那么C++在构建CTestClass类的对象模型时,将会为之分配一个虚表指针vptr。vptr是一个指针,它指向一个函数指针的数组(虚函数表vtbl),数组中的成员即是在CTestClass中声明的所有虚函数。在调用虚函数的时候,必须经由这个vptr,这也就是为什么虚函数较之普通成员函数要消耗一些成本的缘故。以本例而言,p->Function();一句将被编译器解释为:
(*p->vptr[1])( p ); // 调用vptr表中索引号为1的函数(即Function)③
上面的代码已经说明了,如果p指向一个无效的地址,那么必然会有非法操作。
备注:
①关于函数的命名,我采用了原名而没有变化。事实上编译器为了避免函数重载造成的重名情况,会对函数的名字进行处理,使之成为独一无二的名称。
②将成员函数声明为static,可以使成员函数不经由this指针便可调用。
③vptr表中,索引号0为类的type_info。
(*p->vptr[1])( p ); // 调用vptr表中索引号为1的函数(即Function)③
上面的代码已经说明了,如果p指向一个无效的地址,那么必然会有非法操作。
参考wiki:http://zh.wikipedia.org/wiki/%E8%BF%B7%E9%80%94%E6%8C%87%E9%92%88
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。