您的位置:首页 > 编程语言 > C语言/C++

c++智能指针的原理与简单实现

2017-03-06 22:53 489 查看
一、问题的引入

先看一段简单的代码如下:

#include<iostream>

using namespace std;

class Person {
private:
char *name ;
public:
Person() {
cout<<"Person()"<<endl ;
}

~Person() {
cout<<"~Person()"<<endl ;
}
} ;

void test(void)
{
Person *p = new Person() ;
}

int main(int argc , char** argv)
{
test();
return 0 ;
}

 在这段代码里面,定义了一个简单的类Person,它只有一个name成员和简单的构造函数和析构函数。在test函数里面,使用Person *p = new Person()构造了一个person对象,并在main函数里面调用了test()。很明显,在test()里面并没有去delete p ,将不会调用析构函数,因此可能在main函数退出前造成内存泄漏。

 我们当然可以在test()函数里面将“Person *p = new Person()  ” 替换成 “Person p ; ” ,对应为p分配的内存是在栈里面,因此在test()函数退出前会自动释放分配的内存(执行析构函数),但是这样就无法使用指针操作了(除非我们每次手工执行delete操作)。

 

二 、 智能指针的引入

 定义一个Sp类,如下:

#include<iostream>

using namespace std;

class Person {
private:

char *name ;

public:
Person() {
cout<<"Person()"<<endl ;
}

~Person() {
cout<<"~Person()"<<endl ;
}
} ;

class Sp {
private:
Person *p ;
public:
Sp(Person *per){
cout<<"Sp(Person *per)"<<endl ;
p = per ;
}

~Sp(){
cout<<"~Sp()"<<endl ;
if(p){
delete p ;
p = NULL ;
}
}
} ;

void test(void)
{
//Person *p = new Person() ;
Sp s = new Person() ;
}

int main(int argc , char** argv)
{
test();
return 0 ;
}

 

在test()函数里面,我们同样使用new来构造出来一个Person的实例化对象,与前面不同的是,这个对象指针作为一个参数传给Sp类的构造函数构造出来了一个实例化对象s。当test()函数执行完后,对象s的析构函数被调用,在析构函数里面再去delete Person的实例化对象。这是一种非常巧妙的方法,使用Sp s 代替了 Person *  , 由Sp为我们做好了delete操作,这样即使不手工执行delet
4000
e操作也不会造成内存泄漏的问题 。

因此,此时的Sp s 就可以替代 Person * 指针,这就是智能指针的智能之处,最终目的还是为了防止我们因为忘记delete操作而造成内存泄漏。

 

接下来我们在Person类里面添加一个普通的成员函数printInfo

void printInfo(void){
cout<<"just for test !"<<endl ;
}

在test()函数里面,我们要调用p的printInfo,即 s->printInfo( ) ,但是Sp 类里面并没有定义这个函数,我们可以在Sp类里面重载“->”:

Person * operator->(){
return p ;
}

同样,我们也应该重载“*”,它返回对象的引用:

Person& operator*(){
return *p ;
}
这样,就可以使用s->printInfo( ) 或者 (*s).printInfo() ;

 

三 、 智能指针的完善

1、为智能指针(Sp)添加拷贝构造函数

代码如下:

class Sp {
private:
Person *p ;

public:
/*使用Person *来构造*/
Sp(Person *per){
cout<<"Sp(Person *per)"<<endl ;
p = per ;
}

/*拷贝构造函数*/
Sp(const Sp &other){
cout<<"Sp(Sp &other)"<<endl ;
p = other.p;
}

~Sp(){
cout<<"~Sp()"<<endl ;
if(p){
delete p ;
p = NULL ;
}
}
Person * operator->(){ return p ; }} ;

在这个Sp类里面,我们实现了两个构造函数:使用Person * 来构造、使用拷贝构造函数构造。

这里要注意:拷贝构造函数中要将参数Sp & 这个引用声明为const,否则编译器会报错!!!(编译器不知道先使用Person * 来构造还是直接使用拷贝构造函数构造 s)。

 

2、添加引用计数

先看看如下代码:

#include<iostream>

using namespace std;

class Person {
private:
char *name ;
public:
Person() {
cout<<"Person()"<<endl ;
}

~Person() {
cout<<"~Person()"<<endl ;
}

void printInfo(void){ cout<<"just for test !"<<endl ; }
} ;

class Sp {
private:
Person *p ;
public:
/*使用Person *来构造*/
Sp(Person *per){
cout<<"Sp(Person *per)"<<endl ;
p = per ;
}

/*拷贝构造函数*/
Sp(const Sp &other){
cout<<"Sp(Sp &other)"<<endl ;
p = other.p;
}

~Sp(){
cout<<"~Sp()"<<endl ;
if(p){
delete p ;
p = NULL ;
}
}
} ;

int main(int argc , char** argv)
{
Sp s = new Person() ;
Sp s2 = s; return 0 ;
}

编译、运行该程序 ,发现程序崩溃。

分析原因:

(1)由前一点分析,在main函数里,它首先构造出来一个Person 的实例化对象(假设为p),并使s的p成员指向new出来的p;

(2)接着执行Sp s2 = s,将调用Sp的拷贝构造函数,在里面的s2 的p成员同样指向传入的s的p成员;

(3)main函数退出之前,将分别调用s、s2的析构函数,它们均delete p对应的内存;

因此,问题在于p对象被销毁了两次,这是明显不合理的。第一次释放了p对应的内存后,p = NULL;第二次来释放时,对象已经不存在了,main函数找不到p对象,会出现异常,程序自然崩溃了。

这里,通常的解决方法是给对象p添加一个引用计数,两次构造后p被引用了两次,引用计数为2;当第一次调用Sp的析构函数时,由于对象p仍然被引用,所以先不销毁它,仅仅减少它的引用计数为1;第二次调用Sp的析构函数时,先减少p的引用计数为0,即不被任何对象所引用了,再来销毁p。

(1)为Person添加引用计数,并增加相应接口,在第一次构造Person时初始化计数值为0:

class Person {
private:
char *name ;
/*添加引用计数*/
int count;
public:
void incStrong() {count ++ ;}
void decStrong() {count -- ;}
int getStrong() {return count ;}
Person(): count(0) {
cout<<"Person()"<<endl ;
}

~Person() {
cout<<"~Person()"<<endl ;
}

void printInfo(void){ cout<<"just for test !"<<endl ; }
} ;

(2)在Sp的构造函数里面,调用Person的incStrong增加引用;在析构函数里面,先调用decStrong减少引用,若此时引用值为0,则销毁它:

class Sp {
private:
Person *p ;
public:
/*使用Person *来构造*/
Sp(Person *per){
cout<<"Sp(Person *per)"<<endl ;
p = per ;
p->incStrong();
}
/*拷贝构造函数*/
Sp(const Sp &other){
cout<<"Sp(Sp &other)"<<endl ;
p = other.p;
p->incStrong() ;
}
~Sp(){
cout<<"~Sp()"<<endl ;
if(p){
p->decStrong() ;
if(p->getStrong() == 0)
{
delete p ;
p = NULL ;
}
}
}
} ;

3、使用类模板

我们可以将引用计数独立出来,实现一个名为RefBase的基类,并将Sp类定义为模板类:

#include<iostream>

using namespace std;

class RefBase {
private:
int count;
public:
RefBase() : count(0) {} ;
void incStrong() {count ++ ;}
void decStrong() {count -- ;}
int getStrong() {return count ;}
};

class Person :public RefBase{
private:
char *name ;

public:
Person() {
cout<<"Person()"<<endl ;
}

~Person() {
cout<<"~Person()"<<endl ;
}

void printInfo(void){ cout<<"just for test !"<<endl ; }
} ;

template <typename T>
class Sp {
private:
T *p ;

public:
/*使用Person *来构造*/
Sp(T *per){
cout<<"Sp(Person *per)"<<endl ;
p = per ;
p->incStrong();
}

/*拷贝构造函数*/
Sp(const Sp &other){
cout<<"Sp(Sp &other)"<<endl ;
p = other.p;
p->incStrong() ;
}

~Sp(){
cout<<"~Sp()"<<endl ;
if(p){
p->decStrong() ;
if(p->getStrong() == 0)
{
delete p ;
p = NULL ;
}
}
}
T * operator->(){
return p ;
}

T& operator*(){
return *p ;
}
} ;

int main(int argc , char** argv)
{
Sp<Person> s = new Person() ;
Sp<Person> s2 = s;

s->printInfo();
(*s).printInfo();
return 0 ;
}


总结:

我们可以使用智能指针代替平时的指针操作 ,使用智能指针来自动维护对象的生命周期。实际上,智能指针的目的就是防止在new操作后忘记delete操作从而造成内存泄漏,它仅仅是一种手段。Android源代码庞大且复杂,大量地采用智能指针来维护对象的生命周期,而在实际编程中,我们要时刻关注对象的生命周期,不应该依赖于这些手段而忽视对内存的重视,因为c/c++编程的本质就是在操作内存。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android c语言 c++