C++ 跟野指针说bye bye!
C++ 跟野指针说bye bye!
野指针形成的原因
-
指针变量没有初始化
任何指针变量在刚创建时不会自动赋值为NULL指针(我们知道int整形变量没有初始化前也是随机的一个数,指针说简单点也是一个整形值,那自然它在为人为的初始化之前,自然也是一个随机值。某些编译器在debug模式下会赋值为NULL,但release下不会)。在VC会把未初始化的堆内存上的指针全部填成 0xcdcdcdcd。
-
指针delete或者free之后没有值为空
对于堆内存的操作,我们分配了一些内存空间(使用new或者malloc方法申请一片内存),使用完成之后进行了释放(使用delete 或者 free),但是并没有将起指针置为空,再次使用的时候就会出现崩溃现象。使用if(p==NULL)这种判断方式是不行的。
-
指针超过了变量的作用范围
即在变量的作用范围之外使用了指向变量地址的指针。这一般发生在将调用函数中的局部变量的地址传出来引起的。这个是很多在开始写C++代码时经常犯的错误
-
对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误,不可访问的数据(有一定的随机性,概率比较低)
//原因一,指针变量没有初始化,包括类成员变量的初始化;
int* p;//错误;
int* p=NULL;
class Sample
int *p;
public:
Sample(){} //没有在构造函数初始化,这个错误一个不小心就出现了;
int add(int x,int y) {return x+y;}
//原因二,指针delete或者free之后没有值为空
Sample* sample=new Sample();
Caller.setSample(sample);//这个指针变量赋值给一个Caller类对象的某个成员变量;
sample->add(4,5);
delete sample;//delete之后所指向的内容为空,但是自身地址还在,如果没有
if(sample!=NULL)
//执行代码;
//原因三,超过变量的作用范围
int* test()
int val=3;//局部变量,在函数执行完成之后,val变量会被析构,这个时候返回的指针就是野指针
return &val;
//原因四,内存被其他数据改动;
Sample* sample=new Sample();
//正常情况下基本没有这种操作模式,但是也有个别设计会用到(如通过偏移地址给私有类变量赋值)
char* temp_str="abcdefc";
memcpy(sample,temp_str,sizeof(temp_str);
常用解决办法
如果找到野指针出现的地方,解决野指针就比较简单了,然而大多数时候,我们是很难一下子发现问题点,这个时候我们需要的就是通过一些特殊的手段处理。
静态代码检查工具
很大程度上,野指针都是因为编码不善、习惯不好所产生的。要解决野指针,就是要养成好的编程习惯,当然也可以借助一些其他工具解决。
是指在不执行代码的情况下对其进行分析评估的过程,是软件质量和软件安全保障的重要一环。它通过词法分析、语义分析、控制流分析、数据流分析等技术对代码逐行解析暴露问题,从而协助我们将许多在运行时才会暴露的棘手麻烦扼杀于摇篮之中。
静态代码分析可以解决
缓冲区溢出
、
内存泄露
、
野指针
、
部分代码规范
等典型问题,代码分析工具可以解决的指针未初始化以及释放忘记delete的问题。 常用的代码检查工具有
pc-lint
、
cppcheck
、
TScanCode
、
Astree
等。另外某些工具也可以解决野指针问题如
vld
的windows下的内存泄露工具,以及linux下的
valgrind
工具等。
智能指针
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。 shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。
weak_ptr
share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段
野指针最好的解决办法就是通过智能指针来代替普通指针的实现(智能指针被封装为一个栈对象,当生命周期退出的时候就根据引用计数判断是否需要释放堆上的内存)
catch野指针内存
如果你的使用的win32/64 有一种办法可以判断野指针异常,尝试读取指针并捕获将在失败时抛出的结果SEH异常(普通的try catch不能普通指针异常问题)。 如果它没有抛出,那么它是一个有效的指针,这样你至少能够防止崩溃,对于某些未知问题引起的崩溃,如多线程共享内存,这种方式非常有效。
__try
int* p=NULL;
*P=3;//指针访问异常,如果使用普通的try-catch异常捕获是不能捕获到的,这个时候就可以使用这里面的结构化异常解决版本来处理;
__except(1)
//错误输出;
以上的方式在release下可以检测到这样的崩溃问题,但是能否用普通的try-catch截获到呢,当然是有的,就是在项目属性中
代码生成->启用C++异常:/EHsc
选择此项就可以了。
野指针判断
如果没有对野指针做任何特殊的处理,那么判断一个对象是否为野指针,基本上是没有办法的,但是在大多数情况下我们其实还可以通过一些取巧的办法来解决。
野指针内存有两种情况,一种是不可访问的内存(如某些操作系统特殊的内存段),还有一种是可访问的内存(正常释放之后内存值肯定会出现随机的一些值),基于以上两点,我们就可以实现一个野指针的判断方法。
不可访问内存
IsBadPtr
在windows下一个比较好用的函数,用于判断一个内存如0x00001234这样的内存,非常有效,在linux下可以使用以下代码判断一块内存是否可以访问
bool isBadPtr(void* p)
#ifdef WIN
return ISBadPtr(p,4);//在windows下判断内存的有效性函数;
#else
int fh = open( p, 0, 0 );
int e = errno;
if ( -1 == fh && e == EFAULT ) //无效内存;
return true;
else if ( fh != -1 )
close( fh );
return false;
#endif
可访问内存的内存错误
如果我们是做一个系统的话,我们可以定义一个基类,这个基类的第一个成员变量就是一个int值,这个值在基类构造函数的时候设置为一个常量值,其他的类继承这个基类,那么所有的类就可以同样的方法来判断是否为野指针了.
class IObject
const int _memory_check;//判断内存值的标记;
public:
IObject():_memory_chek(1){}
class Sample:public IObject
public:
int add(int x,int y){return x+y;}
void print(char* input_string){std::cout<<input_string;};
bool isVaildPtr(IObject* p)
auto offest = (int)(&((IObject*)0)->_memory_check); //获取类成员在类中的偏移地址;
long ptr=(long)ptr+offest;
int* memory_check_ptr=(int*)ptr;
if(memory_check_ptr==1)//说明是一个有效内存,如果被释放了,那么这个值应该是一个随机值;
return true;
return false;
结合不可访问内存和可访问内存的内存值错误我们就可以非常简单的判断一个指针是否为 野指针 了
bool isVaildPtr(IObject* p)
if(isBadPtr(p))
return false;
auto offest = (int)(&((IObject*)0)->_memory_check); //获取类成员在类中的偏移地址;
long ptr=(long)ptr+offest;
int* memory_check_ptr=(int*)ptr;
if(memory_check_ptr==1)//说明是一个有效内存,如果被释放了,那么这个值应该是一个随机值;
return true;
return false;
//测试代码;
int main()
Sample* new_simple=new Sample();
new_simple->add(3,4);
delete new_simple;