文章目录
本文有些内容参考了相关博客,但是由于没有第一时间记录因此丢失了参考链接,有缘看到的博友可以提醒我一下我可以补上
0. 前言
0.1 使用智能指针的原因
- 资源释放,指针没有置空;
- 内存泄漏;
- 多次释放coredump
0.2 智能指针和普通指针的区别(什么是智能指针)
在于智能指针实际上是对普通指针加了一层封装机制,区别是它负责自动释放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。
新增了三种智能指针:unique_ptr、shared_ptr和weak_ptr。所有新增的智能指针都能与STL容器和移动语义协同工作。能够处理内存泄漏问题和空悬指针问题。
智能指针负责自动释放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。
指针类型 | 特定 |
---|---|
std::auto_ptr | 被C++11弃用。auto指针存在的问题是,两个智能指针同时指向一块内存,就会两次释放同一块资源,自然报错。 |
std::unique_ptr | (single ownership)unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。 **实现原理:**将拷贝构造函数和赋值拷贝构造函数申明为private或protected。不允许拷贝构造函数和赋值操作符,但是支持移动构造函数,通过std:move把一个对象指针变成右值之后可以移动给另一个unique_ptr。 |
std::shared_ptr | (shared ownership)多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。通过成员函数use_count()来查看资源的所有者个数。 |
std::weak_ptr | (temp/no ownership)是对share_ptr的补充 |
智能指针的三个常用函数:
- get() 获取智能指针托管的指针地址
- release() 取消智能指针对动态内存的托管。析构对象。
- reset() 重置智能指针托管的内存地址。构造新对象,析构原对象。如果地址不一致,原来的会被析构掉
1. auto_ptr
(C++98的方案,C++11已经弃用) 采用所有权模式。
1.1 基本说明
// 头文件
#include <memory>
// 用法
auto_ptr<类型> 变量名(new 类型)
1.2 例子🌰
可以自动析构
#include <iostream>
#include <memory>
using namespace std;
class Test {
public:
Test() {
cout << "Test的构造函数" << endl;
}
~Test() {
cout << "Test的析构函数..." << endl;
}
int getDebug() {
return this->debug;
}
private:
int debug = 20;
};
int main() {
// Test * t = new Test;
auto_ptr<Test> t(new Test);
return 0;
}
1.3 使用建议
- 不要将auto_ptr 变量定义为全局变量或指针;
- 除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个 智能指针;
auto_ptr<string> p1(new string("I reigned loney as a cloud."));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错
此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题。
3. unique_ptr
在某一时刻是能有一个unique_ptr指向它管理的动态内存上的对象。
3.1 实现原理
将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符,但是支持移动构造函数,通过std:move把一个对象指针变成右值之后可以移动给另一个unique_ptr。
3.2 unique_ptr特性
-
基于排他所有权模式:两个指针不能指向同一个资源
-
不支持copy,但支持move。无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
/* 当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:*/ unique_ptr<string> pu1(new string ("hello world")); unique_ptr<string> pu2; pu2 = pu1; // #1 not allowed unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You")); // #2 allowed /* 其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明unique_ptr 优于允许两种赋值的auto_ptr 。*/
-
保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
-
在容器中保存指针是安全的
3.3 例子🌰
#include <iostream>
#include <utility>
#include <memory>
using namespace std;
class Mystring {
public:
string _str;
Mystring(string str): _str(str) {
cout << "Mystring constructed " << _str << endl;
}
~Mystring() {
cout << "Mystring destructed " << _str << endl;
}
};
int main() {
unique_ptr<Mystring> uPtr1{new Mystring("A string")};
cout << uPtr1.get() << endl; // 返回一个地址0x55a5c5affeb0
cout << "---------------move--------------" << endl;
unique_ptr<Mystring> uPtr2{move(uPtr1)};
cout << uPtr1.get() << endl; // 0
cout << uPtr2.get() << endl; // 获得刚才对象的地址0x55a5c5affeb0
cout << endl;
cout << "---------------local ptr--------------" << endl;
// 会同时析构对象
{
unique_ptr<Mystring> uLocalPtr{new Mystring("A new string")};
}
cout << endl;
cout << "---------------reset--------------" << endl;
// reset重置指向的对象
uPtr2.reset(new Mystring("A new String"));
// 返回uPtr2指向的对象,同时释放uPtr2
Mystring* pstr = uPtr2.release();
delete pstr;
cout << endl;
// 互换资源
cout << "---------------swap--------------" << endl;
unique_ptr<Mystring> uPtr3{new Mystring("hello")};
unique_ptr<Mystring> uPtr4{new Mystring("world")};
cout << uPtr3.get() << endl;
cout << uPtr4.get() << endl;
swap(uPtr3, uPtr4);
cout << uPtr3.get() << endl;
cout << uPtr4.get() << endl;
cout << endl;
return 0;
}
4. shared_ptr
shared_ptr实现共享式拥有概念,多个 shared_ptr 智能指针可以共同使用同一块堆内存,采用的是引用计数机制,当计数等于0时,资源会被释放。
成员函数 | 说明 |
---|---|
use_count() | 返回引用计数的个数 |
unique() | 返回是否是独占所有权( use_count 为 1) |
reset() | 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少 |
swap() | 交换两个 shared_ptr 对象(即交换所拥有的对象) |
get | 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); sp 与 sp.get()是等价的 |
构造
shared_ptr<Person> sp1;
Person *person1 = new Person(1);
sp1.reset(person1); // 托管person1
shared_ptr<Person> sp2(new Person(2));
shared_ptr<Person> sp3(sp1);
引用计数的使用——use_count()
#include <iostream>
#include <utility>
#include <memory>
using namespace std;
class Person {
public:
int no;
Person(int v): no(v) {
cout << "constructed " << no << endl;
}
~Person() {
cout << "destructed " << no << endl;
}
};
class DestructPerson {
public:
void operator()(Person *pt) {
cout << "DestructPerson " << endl;
delete pt;
}
};
int main() {
shared_ptr<Person> sp1;
shared_ptr<Person> sp2(new Person(2));
cout << "sp1 use_count() = " << sp1.use_count() << endl; // 0
cout << "sp2 use_count() = " << sp2.use_count() << endl; // 1
sp1 = sp2;
cout << "sp1 use_count() = " << sp1.use_count() << endl; // 2
cout << "sp2 use_count() = " << sp2.use_count() << endl; // 2
return 0;
}
初始化——make_shared
shared_ptr<int> up1(new int(10)); // int(10) 的引用计数为1
shared_ptr<int> up2(up1); // 使用智能指针up1构造up2, 此时int(10) 引用计数为2
/* 使用make_shared 初始化对象,分配内存效率更高(推荐使用)
make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr; 用法:
make_shared<类型>(构造类型对象需要的参数列表); */
shared_ptr<int> up3 = make_shared<int>(2); // 多个参数以逗号','隔开,最多接受十个
shared_ptr<string> up4 = make_shared<string>("字符串");
shared_ptr<Person> up5 = make_shared<Person>(9);
赋值
shared_ptr<int> up1(new int(10)); // int(10) 的引用计数为1
shared_ptr<int> up2(new int(11)); // int(11) 的引用计数为1
up1 = up2; // int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2
释放
shared_ptrr<int> up1(new int(10));
up1 = nullptr ; // int(10) 的引用计数减1,计数归零内存释放
// 或
up1 = NULL; // 作用同上
重置——reset
p.reset(); // 将p重置为空指针,所管理对象引用计数 减1
p.reset(p1); // 将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
p.reset(p1,d); // 将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器, p1是一个指针!
交换——swap
std::swap(p1,p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
4.1 shared_ptr 线程安全性
源码还没有正确调试
多线程环境下,调用不同shared_ptr实例的成员函数是不需要额外的同步手段的,即使这些shared_ptr拥有的是同样的对象。但是如果多线程访问(有写操作)同一个shared_ptr,则需要同步,否则就会有race condition 发生。也可以使用 shared_ptr overloads of atomic functions来防止race condition的发生。
多个线程同时读同一个shared_ptr对象是线程安全的,但是如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。
多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。例子如下:
//程序实例
/* shared_ptr<long> global_instance = make_shared<long>(0);
std::mutex g_i_mutex;
void thread_fcn()
{
//std::lock_guard<std::mutex> lock(g_i_mutex);
//shared_ptr<long> local = global_instance;
for(int i = 0; i < 100000000; i++)
{
*global_instance = *global_instance + 1;
//*local = *local + 1;
}
}
int main(int argc, char** argv)
{
thread thread1(thread_fcn);
thread thread2(thread_fcn);
thread1.join();
thread2.join();
cout << "*global_instance is " << *global_instance << endl;
return 0;
} */
在线程函数thread_fcn的for循环中,2个线程同时对global_instance进行加1的操作。这就是典型的非线程安全的场景,最后的结果是未定的,运行结果如下:*global_instance is 197240539
如果使用的是每个线程的局部shared_ptr对象local,因为这些local指向相同的对象,因此结果也是未定的,运行结果如下:*global_instance is 160285803
因此,这种情况下必须加锁,将thread_fcn中的第一行代码的注释去掉之后,不管是使用global_instance,还是使用local,得到的结果都是: *global_instance is 200000000
4.2 使用陷阱——循环引用
shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!
#include <iostream>
#include <memory>
class CB;
class CA {
public:
CA() {
std::cout << "CA()" << std::endl;
}
~CA() {
std::cout << "~CA()" << std::endl;
}
void set_ptr(std::shared_ptr<CB>& ptr) {
m_ptr_b = ptr;
}
private:
std::shared_ptr<CB> m_ptr_b;
};
class CB {
public:
CB() {
std::cout << "CB()" << std::endl;
}
~CB() {
std::cout << "~CB()" << std::endl;
}
void set_ptr(std::shared_ptr<CA>& ptr) {
m_ptr_a = ptr;
}
private:
std::shared_ptr<CA> m_ptr_a;
};
int main()
{
std::shared_ptr<CA> ptr_a(new CA());
std::shared_ptr<CB> ptr_b(new CB());
ptr_a->set_ptr(ptr_b);
ptr_b->set_ptr(ptr_a);
std::cout << ptr_a.use_count() << " " << ptr_b.use_count() << std::endl;
return 0;
}
5. weak_ptr
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。
为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
5.1 使用
#include<iostream>
#include<memory>
using namespace std;
int main() {
shared_ptr<int> shp1{new int(0)};
weak_ptr<int> wp1;
weak_ptr<int> wp2(shp1);
wp1 = wp2;
cout << wp1.use_count() << endl; // 1
return 0;
}
// 弱指针不支持 * 和 -> 对指针的访问
5.2 如何解决循环引用问题
#include <iostream>
#include <memory>
class CB;
class CA {
public:
CA() {
std::cout << "CA()" << std::endl;
}
~CA() {
std::cout << "~CA()" << std::endl;
}
void set_ptr(std::shared_ptr<CB>& ptr) {
m_ptr_b = ptr;
}
private:
std::shared_ptr<CB> m_ptr_b;
};
class CB {
public:
CB() {
std::cout << "CB()" << std::endl;
}
~CB() {
std::cout << "~CB()" << std::endl;
}
void set_ptr(std::shared_ptr<CA>& ptr) {
m_ptr_a = ptr;
}
private:
std::weak_ptr<CA> m_ptr_a; // 修改了这里
};
int main()
{
std::shared_ptr<CA> ptr_a(new CA());
std::shared_ptr<CB> ptr_b(new CB());
ptr_a->set_ptr(ptr_b);
ptr_b->set_ptr(ptr_a);
std::cout << ptr_a.use_count() << " " << ptr_b.use_count() << std::endl;
return 0;
}
流程与上一例子大体相似,但是不同的是④这条引用是通过weak_ptr建立的,并不会增加引用计数。也就是说,CA的对象只有一个引用计数,而CB的对象只有2个引用计数,当main函数返回时,对象ptr_a和ptr_b被销毁,也就是①、③两条引用会被断开,此时CA对象的引用计数会减为0,对象被销毁,其内部的m_ptr_b成员变量也会被析构,导致CB对象的引用计数会减为0,对象被销毁,进而解决了引用成环的问题。