指针,对于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的出错提示中我们也可以看到,他告诉我们我们在试图释放一个无效的指向堆中内存地址的指针。
改正方法:很明显,我们可以用一个临时指针把动态分配的内存保存一份,这样在释放的时候,我们可以利用临时指针来释放动态分配的内存地址,从而就算原来指向动态申请的内存的指针已经改变指向,我们仍然可以正确地把它释放掉,修正代码如下:
1#include <iostream>
2using namespace std;
3
4int main()
5{
6 int ia[5] = {1,2,3,4,5};
7 int* pi = new int[5];
8 int* ptmp = pi; // 用一个临时指针保存,动态分配的内存地址,以方便我们以后正确释放。
9
10 pi = ia; // pi指向该变,但这次我们用ptmp把原来动态分配的内存地址保存起来了.
11
12 for ( int i=0;i<5;++i)
13 {
14 cout << *(pi+i) << " ";
15 }
16 cout << endl;
17
18 delete ptmp; // 现在用临时指针把动态申请的内存地址释放,没有问题.
19 ptmp = NULL; // 这里可有可无,如果不是程序即将结束,把释放掉的指针赋值为NULL是一个好习惯.
20
21 return 0;
22}
陷阱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;
}
以上...暂时这么多,以后有遇到新的再继续补充上去.