指针陷阱知多少...

指针,对于C/C++的程序员来说是永远不能回避的,用得好,会让你的代码更有效率和更简洁。
但是稍有不慎,你就会进入它的各种陷阱中,而变得茫然不知所措。
本文就简要说说在编程过程中容易遇到的几种指针陷阱。

指针定义
指针是一种特殊类型的变量,它存储的是"变量在计算机内存中的存放位置",也即内存地址。
我们通常说,指针指向一块内存地址,和指针存放的是一块内存地址表达的是同样的意思。

陷阱1——“偷天换日”
描述:指针最经常用的一种用途是,用来存放(指向)我们在堆中动态分配的内存地址,我们都知道,从堆中动态分配的内存地址,在使用完毕以后,需要我们自己手动去释放,这就是我们经常所说的 new —— delete    malloc —— free 要成对出现的原因,因为如果我们没有主动去释放那些不再使用的动态分配的内存地址,那么在程序运行过程中会造成内存泄漏,造成资源浪费。当然,当整个程序结束以后,系统会去回收那些没有释放掉的内存。
就这个简单的过程,很多时候,我们不小心就会造成“野指针”的出现,然后那些动态分配的内存无法释放,其中一个很重要的原因,就是当初用来指向动态分配的内存的指针,它的指向改变了,也就是说它指向了其他的内存块,造成原来指向的内存丢失,我们称它为"偷天换日"
如例1所示:
 1#include <iostream>
 2using namespace std;
 3
 4int main()
 5{
 6    int ia[5= {1,2,3,4,5};
 7
 8    /* 指针pi指向在堆中分配的内存的首地址,该内存大小为5*4=20字节. */
 9    int* pi = new int[5];   
10
11    /* 这里pi指向栈中分配的数组ia的首地址,上面动态分配的20字节内存地址,
12    *  成为野指针,丢失无法释放,造成程序内存泄漏. 
13    */

14    pi = ia;  
15
16    for ( int i=0;i<5;++i)
17    {
18        cout << *(pi+i) << " ";
19    }

20    cout << endl;
21
22    delete pi;  // 这里释放的并不是最先的堆中动态分配的内存,程序运行出错.
23
24    return 0;
25}

在例1中该程序运行会出错,具体原因已经在代码注释中有标注。运行例1会出现图1所示的错误

图1 出错提示
从图1的出错提示中我们也可以看到,他告诉我们我们在试图释放一个无效的指向堆中内存地址的指针。
改正方法:很明显,我们可以用一个临时指针把动态分配的内存保存一份,这样在释放的时候,我们可以利用临时指针来释放动态分配的内存地址,从而就算原来指向动态申请的内存的指针已经改变指向,我们仍然可以正确地把它释放掉,修正代码如下: 



陷阱2——"暗度陈仓"
描述:是的,有时候我也会犯这样的错误,像这样,我们在堆中分配一块指定大小的内存地址,并用一个指针去存储该地址,然后,我们很自然地想为这些地址赋值,或许,我们会想到用一个for循环再加上指针自增,如此赋值,我们觉得这看起来没什么错误,但是事实却有点出人意料,我们有可能不经意间该变了该指针的指向,但这次并不是明显的直接把它指向另一个内存地址,而是指针自己指向发生变化,如例2所示:

#include <iostream>
using namespace std;

int main()
{
    
int* pi = new int[5];

    
for ( int i = 0;i<5;++i )
    
{
        
*pi = i;
        pi
++;     // 注意这里指针pi在自增,这是一个隐藏的错误,pi已经不再是指向上面动态分配的内存的首地址了。
    }


    
for ( int j = 0;j<5;j++ )
    
{
        cout 
<< *pi << " ";   // 会像我们想象的那样输出么?这里只会输出不可意料的数..
        pi++;                 // 注意这里pi继续自增,现在它已经指向内存中一个未知的内存地址块了。
    }


    delete pi;   
// 不出所料,这里释放的已经不是动态申请的内存块了所以,出错是必然的.
    return 0;
}

这里的错误提示跟例1是一样的,因为都是去释放一块并不是从堆中动态分配的内存而引起的问题。
改正方法:
方法1,跟例1一样,用一个临时指针保存动态分配的内存地址,然后释放的时候用临时指针确保释放正确。
方法2,赋值和取数的时候,在这里,使用数组的下标运算符的形式。代码如下:

#include <iostream>
using namespace std;

int main()
{
    
int* pi = new int[5];

    
for ( int i = 0;i<5;++i )
    
{
        pi[i] 
= i;
    }


    
for ( int j = 0;j<5;j++ )
    
{
        cout 
<< *(pi+j) << " ";   // 这里正确输出,跟我们想得一样
        
// cout << p[j] << " ";   // 这个跟上式是等价的。
    }

    cout 
<< endl;

    delete pi;   
// 此时pi 仍然指向动态分配的内存的首地址,所以释放没有问题。
    return 0;
}


陷阱3——“有名无实”
描述:顾名思义“有名无实”就是空有名字,其实并没有实际意义。指针也是一样,想过这样的事么,我们去动态申请一个内存,我们有没有考虑过,申请是否成功呢?或许,我们想应该是成功的,然后我们去解除一个为NULL的指针的引用(pi==NULL;,*pi),很不幸这会发生不可预知的错误,而且,很难检查出来,但只能怪我们自己没有养成好的习惯,因为如果,我们每一次在申请内存的时候去判断一下是否申请成功,就可以避免这个问题了。你看,习惯也是很危险的~ 
 

int* pi = new int[65540]; // 能成功么?或许能

// 我们应该像以下这样做,防范重于一切~~
if( NULL == pi )
{
  printf(
"new a malloc fail!\n");
  
return
}


以上...暂时这么多,以后有遇到新的再继续补充上去.

posted on 2011-04-26 21:45 梦五 阅读(454) 评论(0)  编辑 收藏 引用 所属分类: C/C++


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

导航

统计

常用链接

留言簿

随笔分类

随笔档案

文章分类

文章档案

搜索

最新评论

阅读排行榜

评论排行榜