首页 > 编程语言 >【C++】关于智能指针的简单学习

【C++】关于智能指针的简单学习

时间:2022-09-28 15:46:55浏览次数:51  
标签:String 计数 C++ 智能 引用 shared ptr 指针

智能指针

示例类:

class String {
private:
	string m_value;
public:
	String(string str) :m_value(str) {
		cout << "构造" << m_value << "\n";
	}
	friend ostream& operator<<(ostream& os,const String& str) {
		os << str.m_value;
		return os;
	}
	~String() {
		cout << "析构" << m_value << "\n";
	}
};

智能指针是用于管理指针类型内存的一类对象类型,它指向的对象指针被回收的条件如下(对于auto_ptr和unique_ptr):

1.智能指针对象自然析构,即在一个语句块中智能指针为局部变量,离开这个语句块智能指针就被析构了,自然它指向的对象指针的那部分内存也要释放;

例:

{
    auto_ptr<String> aptr(new String("hello"));
}
//离开语句块就回收

2.智能指针对象调用了reset,这个函数会直接把智能指针指向的对象清空掉或者指向了别的对象指针:

{
    auto_ptr<String> uptr( new String("hello") );
    uptr.reset(new String("hi")); //指向新的指针,此时就会释放掉原来那个指针
    //或者uptr.reset();清空,不过下面的代码就会报异常了
    cout << uptr.get() << ":" << *uptr.get() << "\n";
}

3.智能指针对象进行release,把指针的控制权交出去,可以由普通的指针去接release的返回值,然后普通指针再手动delete,这里的release指的不是释放内存,而是释放指针的控制权,当然如果不去手动delete的话,进程结束时该指针也不会调用析构函数。

例:

unique_ptr<String> uptr(new String("hello"));
cout << uptr.get() << ":" << *uptr.get() << "\n";
unique_ptr<String> uptr2(std::move(uptr));
cout << uptr2.get() << ":" << *uptr2.get() << "\n";
String* pstr = uptr2.release(); //放指针自由
delete pstr; //真正释放了指针

shared_ptr的回收思想与上述的auto_ptr、unique_ptr是类似的,当离开了语句块的时候shared_ptr就会被回收,但是不一样的地方在于shared_ptr引入了引用计数,只有当引用计数为0的时候才会进行对象指针的内存回收。

下面就看一下这三种指针:

auto_ptr

使用auto_ptr进行指针的管理,就可以不用手动去delete指针,例:

String* str = new String("string1");
auto_ptr<String> aptr(str);

当aptr被回收时,就会顺便把str回收了。但是这是有问题的,如果多个auto_ptr指向同一个对象指针,就会导致重复释放,例如:

String* str = new String("string1");
auto_ptr<String> aptr1(str);
auto_ptr<String> aptr2(str);

正确的做法应该是把指针的控制权交出去,而不是共享:

String* str = new String("string1");
auto_ptr<String> aptr1(str);
auto_ptr<String> aptr2(std::move(aptr1));
//又或者赋值运算符
auto_ptr<String> aptr2 = aptr1;

move或者赋值运算符都会把原先的auto_ptr中指向的对象指针交给新的auto_ptr,这样就不会出现上面两个智能指针指向同一个对象指针然后重复释放内存的问题,但是又有了一个新的问题,使用了赋值运算符将对象指针控制权转移了之后,可能会带来一个认知上的错误,就是原来那个智能指针可能还可以用,这时就会报异常了。

unique_ptr

unique_ptr对象之间无法使用赋值运算符去转移对象指针的控制权,不过使用move还是可以这样做,只能说使用的时候多长点心。unique_ptr和auto_ptr是很像的,我比较菜我感觉它们就是一样的,但是既然unique_ptr比较新,那就直接用这个unique_ptr。

下面附一个可能会踩的坑:

String GetString() {
    return String("哼哼哼");
}
{
    unique_ptr<String> uptr(&GetString());
    cout << uptr.get() << ":" << *uptr.get() << "\n";//解引用时发生异常
}

这个函数返回的是一个临时的String对象,然后我对函数返回结果进行取地址,然后在语法上这是过得去的,但是要知道函数返回的临时对象只会存在一行,在下一行就会被回收,所以在下一行中虽然get能够获得地址,但是进行解引用的时候就会发生异常。

所以使用的时候要长点心,因为这个坑是适用于所有智能指针的,不能用智能指针去接函数返回的临时对象的地址。

shared_ptr

unique_ptr和auto_ptr不支持多个智能指针指向同一个对象,但是shared_ptr中是可以的,它使用了引用计数,只有当引用计数为0时才会释放shared_ptr指向的对象指针。

例:

String* pstr = new String("hello");
shared_ptr<String> sptr(pstr);
cout << sptr.use_count() << "\n";
sptr.reset(); //这里reset后就直接释放了对象指针
cout << sptr.use_count() << "\n";

下面测试多个shared_ptr指向同一个对象指针:

String* pstr = new String("hello");
shared_ptr<String> sptr(pstr);
cout << sptr.use_count() << "\n"; //1
shared_ptr<String> sptr2( pstr ); //无效,引用计数不会增加,相当于sptr和sptr2为管理上独立的两个shared_ptr
cout << sptr.use_count() << "\n";
shared_ptr<String> sptr3( sptr ); //引用计数+1
shared_ptr<String> sptr4( sptr ); //引用计数+1
cout << sptr.use_count() << "\n"; //3
sptr3.reset(); //sptr3释放了,但是,sptr的引用计数不为0,不会调用析构
sptr2.reset(); //sptr2释放了,并且因为引用计数为0,调用了析构
cout << sptr.use_count() << "\n"; //这里虽然引用计数为2,但是sptr指向的那个指针其实已经被回收了,只是现在引用计数为2,还没有被回收没发生异常

从这个例子中可以看出,如果要进行引用计数的话,多个shared_ptr必须是通过拷贝构造产生,不能够直接使用普通指针去构造,否则多个shared_ptr的引用计数就不是同一个引用计数,此时释放内存就会出现一些问题,因为即便引用计数不为0,它指向的指针也是有可能已经被释放掉了。

再看一个例子:

String* pstr = new String("hello");
shared_ptr<String> sptr1(pstr);
cout << sptr1.use_count() << "\n"; //1
shared_ptr<String> sptr2(sptr1); //引用计数+1
shared_ptr<String> sptr3( sptr1 ); //引用计数+1
cout << sptr1.use_count() << "\n"; //3
cout << sptr2.use_count() << "\n"; //3
sptr1.reset();
cout << sptr1.use_count() << "\n"; //0
cout << sptr2.use_count() << "\n"; //2

这个例子可以看出来,当多个shared_ptr在引用计数时,如果其中一个shared_ptr释放掉了,其实是把引用计数减一,然后自身退出了引用计数,而对于其他的shared_ptr没有产生影响。

weak_ptr

循环引用的问题:

假设两个类型内部都对对方有shared_ptr的内聚,就会造成循环引用。

class Soul;
class Human {
private:
	shared_ptr<Soul> m_soul;
public:
	Human() {
		cout<< "构造了个人" << "\n" ;
	}
	void SetSoul(shared_ptr<Soul> soul) {
		m_soul = soul;
	}
	~Human() {
		cout << "析构了个人" << "\n";
	}
};
class Soul {
private:
	shared_ptr<Human> m_human;
public:
	Soul() {
		cout << "构造了个灵魂" << "\n";
	}
	void SetHuman(shared_ptr<Human> human) {
		m_human = human;
	}
	~Soul() {
		cout << "析构了个灵魂" << "\n";
	}
};
shared_ptr<Human> human(new Human);
shared_ptr<Soul> soul(new Soul);
human->SetSoul(soul);
soul->SetHuman(human);

会发现这种情况下析构函数不调用,也就是内存不会释放,发生了内存泄漏,原因很简单,在外面引用计数为1,进行Set之后发生了shared_ptr拷贝构造,引用计数加1,就变成了2,当进程结束时,human要回收了,于是它要回收human指向的Human指针,但是发现这玩意在Soul里面有引用计数,然后它要回收soul,发现soul的引用计数在Human里面,于是谁都不释放。

解决方法也很简单,就是把类内部内聚的shared_ptr直接改为weak_ptr,weak_ptr可以接shared_ptr,并且不会增加shared_ptr的引用计数,并且weak_ptr没有重载*和->,所以无法对实际的对象做什么。

weak_ptr也有弱引用计数,但是这个似乎没啥用,因为对象指针回不回收是看shared_ptr的引用计数。

weak_ptr最重要的有两个函数,一个是lock,另一个是expired,因为weak_ptr不增加shared_ptr的引用次数,所以使用weak_ptr时,可能对象指针已经释放了,所以需要使用expired去检查指针的有效性(返回false为有效);如果需要访问对象的话,就需要使用lock去创建一个shared_ptr,shared_ptr的引用计数+1,当然shared_ptr只在当前的语句块中起作用,离开当前语句块后shared_ptr的引用计数-1。

看例子:

//给Human类增加一个Say的成员函数
void Say() {
    cout << "我要献祭我的灵魂" << "\n";
}
//给Soul类增加一个listen的成员函数
void listen() {
    if (!m_human.expired()) {
        m_human.lock()->Say();
    }
    else {
        cout << "人没了" << "\n";
    }
}
shared_ptr<Human> human(new Human);
shared_ptr<Soul> soul(new Soul);
human->SetSoul(soul);
soul->SetHuman(human);
soul->listen();
human.reset();
soul->listen();

可以看到结果为:

构造了个人
构造了个灵魂
我要献祭我的灵魂
析构了个人
人没了

标签:String,计数,C++,智能,引用,shared,ptr,指针
From: https://www.cnblogs.com/thankvincisdaily/p/16738286.html

相关文章