指针未释放引起的内存泄漏
先定义一个Person类,在Person类中定义一个构造函数,一个析构函数,一个普通的成员函数。
Person类
class Person {
public:
Person()
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
void printInfo(void)
{
cout << "printInfo()" << endl;
}
};
然后编写一个测试函数test_func,从内存中申请了一块Person类大小的空间。
test_func函数
void test_func(void)
{
Person *p = new Person();
p->printInfo();
}
最后,在main函数中调用test_func函数。
main函数
int main(int argc, char **argv)
{
test_func();
return 0;
}
最终,函数源码如下:
#include <iostream>
#include <string.h>
#include <unistd.h>
using namespace std;
class Person {
public:
Person()
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
void printInfo(void)
{
cout << "printInfo()" << endl;
}
};
void test_func(void)
{
Person *p = new Person();
p->printInfo();
}
int main(int argc, char **argv)
{
test_func();
return 0;
}
这个代码有什么问题呢?
我们编译执行一下,可以看到Person类的构造函数被调用了,但是在程序运行完毕退出后,Person类的析构函数没有调用。
也就是说,在程序运行退出后,没有将之前申请的内存释放掉,这时候,就发生了内存泄漏。
增加内存释放
要想防止这个问题,可以这样做:
1、在函数退出时增加delete操作。
2、或者使用局部变量也可以防止这个问题。
今天讲第 3 种方法,那就是使用智能指针。
智能指针
定义一个sp类,在sp类中,有一个私有成员p,它是一个指向Person类的指针。
然后定义两个构造函数,一个构造函数的传参为一个指向Person类的指针,另一个构造函数的传参为空,主要是在没有申请Person类空间的时候将私有成员指空。
定义一个析构函数,在析构函数中,如果指针p不为空,则表示有申请过内存空间,此时需要将空间释放。
class sp {
private:
Person* p;
public:
sp () : p(0)
{
cout << "sp () : p(0)" << endl;
}
sp (Person *other)
{
cout << "sp (Person *other)" << endl;
this->p = other;
}
~sp ()
{
cout << "~sp ()" << endl;
if (p) {
cout << "delete this->p" << endl;
delete this->p;
}
}
};
修改test_func函数,在函数中创建了一个sp类的对象s。
void test_func(void)
{
sp s = new Person;
}
此时,会调用构造函数 sp (Person *other)。
编译测试,可以看到虽然没有delete,但是申请的内存空间在函数调用结束后,还是会自动释放。
这是因为创建的对象p实际上是一个局部变量,局部变量的生命周期随函数调用结束而终止。
重定向->
此时,如果想要使用Person类里面的成员要怎么做?
显然直接使用“->”是不行的,因为 s 实际上还是一个sp类型的对象。
此时需要重定向“->”,在sp类的定义中添加下面的重定向函数,返回sp类中指向Person类的指针成员。
Person* operator->(void)
{
return this->p;
}
修改test_func函数,增加调用Person类的成员函数printInfo。
编译测试,可以看到成员函数printInfo被成功调用。
增加传参为sp类本身的构造函数
继续修改代码,增加一个拷贝构造函数,传参为sp类本身。
修改测试函数,传入一个sp类对象,使用传入的对象,初始化函数内部定义的局部变量。
修改main函数,创建一个sp类对象,并且把创建的对象作为参数传给test_func函数。
编译测试,发现第71行有报错。
报错信息说:不可以把sp转换为sp引用。
先分析一下第71行,实际上,编译第71行的代码时,编译器实际上是通过三行代码实现的:
sp other = new Person();
相当于:
Person *p = new Person();
sp tmp(p); ==> 对应的构造函数:sp (Person *other)
sp other(tmp); ==> 对应的构造函数:sp(sp &other)
问题出在第三行:sp &other = tmp; 这是错误的语法,不能引用临时变量。
修改为:const sp &other = tmp; 正确的语法。
修改构造函数,增加const属性。
此时可以编译成功,但是执行时有报错。
这里其实有一个疑惑。
我的理解是sp other = new Person(); 这行代码其实传入的就是一个指向Person类的指针,调用的应该就是sp (Person *other),没有用到sp (const sp &other)才对;
用到sp (const sp &other)函数的,应该是 sp s = other;
从执行来看也是sp other = new Person(); 也是没有调用到 sp (const sp &other) 的,但是编译器确实在71行报了类型错误,费解...
显然,报错的原因是申请的空间被释放了两次:
- test_func函数,退出函数,清除临时变量时调用了一次;
- 程序执行完毕退出时,又调用了一次;
当空间已经释放后,不应该再对这个空间进行二次释放。
修改代码,在Person类中增加一个私有成员count,用来记录sp类的使用次数,当这个次数不为0时,就表示没有其他程序使用该对象,此时可以将申请的空间释放掉。
在Person类的构造函数中将计数值初始化为0。
在sp类的构造函数中对计数值加1。
在析构函数中对计数值减1,当减为0时,表示没有其他程序要操作这个类,此时才调用delete释放空间。
编译测试,程序可以正常执行。
重定向*
使用智能指针的目的是:少用"Person *"; 用"sp"来代替"Person *"。
防止出现内存未释放引起的内存泄漏。
Person *per,有2种操作:per->xxx, (*per).xxx;
那么,sp也应该有这两种操作:sp->xxx, (*sp).xxx;
其中,sp->xxx之前已经实现了,现在来增加 (*sp).xxx。
修改sp类,增加一个成员函数 Person& operator*(void),重定向*。
修改main函数,测试一下。
程序可以正常执行。
增加基类
前面为了解决空间重复释放的问题,我们在Person类中加了一个count变量,还有与这个变量相关的一系列操作函数。
这部分的内容,如果定义了一个Cat类,Dog类时,也有可能会使用。
那么,可以把这部分内容单独拿出来,单独定义一个基类,后续如果有其他类需要使用计数的功能时,就可以直接继承这个基类。
修改代码,增加一个基类RefBase。
需要注意的是,要将之前在Person类的构造函数初始化count值,改为在RefBase类的构造函数初始化count值。
Person类继承基类RefBase即可。
编译测试,程序执行成功。
使用模板
对于当前的代码,可以使用sp代替Person *p,但是如果有Cat类,或者Dog类,要使用sp代替Cat *c,Dog *d,还需要重新定义sp类。
但是,如果将sp类定义为一个模板,则可以免去重新定义的繁琐操作。
将sp类修改为模板,将之前sp类中的Person全部换成T。
template <typename T>
class sp {
private:
T* p;
public:
sp () : p(0)
{
cout << "sp () : p(0)" << endl;
}
sp (T *other)
{
cout << "sp (T *other)" << endl;
this->p = other;
p->incStrong();
}
sp (const sp &other)
{
cout << "sp (const sp &other)" << endl;
this->p = other.p;
p->incStrong();
}
~sp ()
{
cout << "~sp ()" << endl;
if (p) {
cout << "delete this->p" << endl;
p->decStrong();
if (0 == p->getStrongCount()) {
delete this->p;
this->p = NULL;
}
}
}
T* operator->(void)
{
return this->p;
}
T& operator*(void)
{
return *p;
}
};
测试函数也修改为模板函数。
template <typename T>
void test_func(sp<T> &other)
{
sp<T> s = other;
s->printInfo();
}
main函数中,创建sp对象时,将T声明为Person。
编译测试,执行成功。
这样,以后如果需要使用Person *时,就可以使用sp<Person>来代替。
此时,new出来了一个对象,但是不需要去delete它,由系统来自动delete这个对象。