悬垂指针(Dangling pointer) 注意事项

前言:

在C/C++里面, 指针的使用往往是比较容易出错的地方, 比如牵涉到delete, 就有可能会产生悬垂指针(Dangling pointer) , 牵涉到初始化问题, 那么就可能产生野指针(没有初始化的指针)等等, 程序里面一旦出现这些指针, 他们的行为往往是未定义的, 而且将导致严重错误的发生, 更令人头痛的是, 这类错误往往很难被发现. 所以牵涉到指针的操作的时候, 一定要非常小心, 这里主要讨论了悬垂指针(Dangling pointer) 的概念, 产生原因, 以及注意事项, 也给出了实验代码, 分析了使用未定义行为的悬垂指针(Dangling pointer) 会输出的可能结果.

悬垂指针(Dangling pointer) 概念:

悬垂指针(Dangling pointer) 是指这样一类指针, 他指向曾经存在的对象(现在当前已经消亡), 所以这类指针的行为是未定义的.

悬垂指针(Dangling pointer) 产生的原因:

基本上悬垂指针(Dangling pointer)都是由于delete操作之后, 指针没有清零(赋值NULL)导致的. 所以悬垂指针(Dangling pointer) 还保存着之前指向对象的相同地址, 可是那个地址上, 已经没有任何对象存在了, 要访问该地址上保存的内容, 那将是一个未定义的行为, 结果也往往是未定义的. 一旦程序的其他部分依赖于这个地址的内容, 那么程序的正确性将无法保证.

使用悬垂指针(Dangling pointer) 的可能实验结果:

既然上面说了, delete以后, 对象已经消亡, 悬垂指针(Dangling pointer)的使用行为和输出结果都像是未定义的, 那么到底会怎么样呢? 我们看一下下面这个代码的结果, 结果已经在代码注释中.

#include <cstdio>

struct Node {
    int val;
    Node *next;
    Node(int v): val(v), next(NULL) {}
    Node(): val(0), next(NULL) {}
};

// output result:
//
// before delete, val = 111
// after  delete, val = 0
// Segmentation fault

int main() {
    Node *pNode = new Node(111);
    printf("before delete, val = %d\n", pNode->val);
    delete pNode;
    printf("after  delete, val = %d\n", pNode->val);
    pNode = NULL;
    printf("after  delete, val = %d\n", pNode->val);
    return 0;
}

笔者在写这段测试代码的时候预测第二个输出应该是111, 或者说每次输出的结果都不一定, 比如运行两次结果可能是0, -10之类的, 理由就是, delete以后pNode变成了悬垂指针(Dangling pointer), 但是他依然指向{111, next}这段内容(当前一进消亡), 这段内存已经被回收, 可以被其他程序使用, 所以这段内存上的内容对于当前程序(使用了pNode这个悬垂指针(Dangling pointer)的程序)来说是未定义的, 所以我预测这个结果也是未定义的, 或者短时间内并不太可能会有其他程序修改这段内存的内容, 所以我又预测第二个输出的值很可能还是111. 但是事实上呢, 每次运行, 第二个结果一般都是0.

其实再仔细分析一下, 就不难明白其中的原因了, 原因就是delete操作有实际的清零作用, delete 一个对象是会调用对象的析构函数的, 对于一般基本类型, 也会自动清零. 这就是为什么基本上每次第二个输出结果都是0而不是一些未定义行为的值的原因了. (这个解释可能还有待商榷, 需要进一步查证, 实际可能还要取决于编译器的具体做法, 所以也不用太深究~)

另外感谢lincoln大神留言提醒>_< 上面这个代码只是为了测试用的, 并非写代码中的典型case. 一般最容易出现悬垂指针(Dangling pointer)的情况是这样的, 我们在作用域A里面定义了一个指针P, 然后再另一个作用域B里面把一个局部变量的地址赋给这个指针P了, 在作用域B里面我们做了我们想做的事情, 然后出了作用域B, 由于P是在作用域A里面定义了, 于是在作用域B里面我们很容易忘记清理P, 当跑出作用域B, P之前所指向的局部变量的内存被回收, P又没有被清理(P = NULL之类的操作), P就变成了悬垂指针(Dangling pointer), 仍旧指向已经被销毁的内存空间, 要是在作用域A里面的其他地方使用P, 那么程序的行为就未定义了, 这个就很危险了.  更加具体或者典型的的可以看下面的代码, A就是全局作用域, B就是函数里面的作用域, 出了函数func(), ptrValue之前指向的内存空间iLocalValue里面原本是101, 但是被销毁之后这块内存可能用作他用, 所以这块内存的内容就变成了一个随机数了, 1652605920.

#include <iostream>

int *ptrValue = NULL;

void  func()
{
    int iLocalValue = 101;
    ptrValue = &iLocalValue;
    std::cout << "in func(), *ptrValue = " << *ptrValue << std::endl;
}

int main() {
    func();
    std::cout << "in main(), *ptrValue = " << *ptrValue << std::endl;
    std::cout << "in main(), *ptrValue = " << *ptrValue << std::endl;
    std::cout << "in main(), *ptrValue = " << *ptrValue << std::endl;
    std::cout << "in main(), *ptrValue = " << *ptrValue << std::endl;
    return 0;
}
    /*
    in func(), *ptrValue = 101
    in main(), *ptrValue = 101
    in main(), *ptrValue = 1652605920
    in main(), *ptrValue = 1652605920
    in main(), *ptrValue = 1652605920
    */

避免悬垂指针(Dangling pointer) :

避免悬垂指针(Dangling pointer)的比较好的办法就是牢记, 一旦delete以后, 立刻赋予指针有意义的值, 可以是NULL, 也可以是其他没有销毁的对象(用作他用). 目的就是为了让该指针的行为有定义, 即使是NULl, 该指针的行为也是有定义的, 就是一个NULL指针.

另一个建议是可以使用智能指针. 这里不做详述.

(全文完,转载时请注明作者和出处)


(转载本站文章请注明作者和出处 烟客旅人 sigmainfy — http://www.sigmainfy.com,请勿用于任何商业用途)

Written on September 22, 2013