基础知识
堆上开辟的内存(new),一定要人为地去进行释放(delete),使用裸指针去指向堆上开辟的内存,如果程序运行过程中发生异常情况,终止运行了,那么有可能出现,没有执行delete去释放堆上的内存,导致内存的泄漏,如果使用裸指针就需要考虑各种异常情况,并确保发生异常时能够顺利释放堆上的内存资源。
-
智能指针----->保证做到资源的自然释放,具体是如何实现的呢?
利用栈上的对象出作用域会自动调用析构函数来进行资源的自动释放
// 可以看到智能指针其实是对裸指针的一个封装
template<typename T>
class CSmartPtr
{
public:
CSmartPtr(T* ptr=nullptr) :mptr(ptr) { cout << "CSmartPtr(const T * ptr)" << endl; }
~CSmartPtr()
{
delete mptr;
}
// 为了保证智能指针使用起来和裸指针的表现是一致的,所以提供以下运算符重载函数
T& operator*() { return *mptr; }
T* operator->() { return mptr; }
private:
T* mptr;
};
class Test
{
public:
void test() { cout << "test()" << endl; }
};
int main()
{
CSmartPtr<int> ptr1(new int);
//栈上的对象出作用域会自动调用析构函数来进行资源的自动释放
*ptr1 = 20;
CSmartPtr<Test> ptr2(new Test());
//(ptr2.operator->())->test()
ptr2->test();
return 0;
}
不带引用计数的智能指针
- auto_ptr(C++库自带)
C++11标准提供:
-
scoped_ptr
-
unique_ptr
auto_ptr:转移资源的“所有权”
(不推荐使用)
auto_ptr<int> ptr1(new int);
auto_ptr<int> ptr2(ptr1); // 转移了资源的所有权,但是在代码层面没有体现出来,用户会认为现在ptr1和ptr2都指向一块堆内存
/*
拷贝构造时,会先调用ptr1的release方法,先记住堆内存的地址,然后将地址给ptr2,ptr1底层指针则置为nullptr;
一连串拷贝构造后只会让最后一个元素指向堆上的资源,之前的指针全部置空;
vector容器中如果使用了该智能指针 vector<auto_ptr<int>> vec1; vec2(vec1)如果发生容器的拷贝构造时vec1容器的所有底层指针将全部被置为nullptr
*/
scoped_ptr
// 直接删除拷贝构造和赋值运算符重载函数,这种方法更加暴力
// 如果没有删除,也没有自定义拷贝构造或者赋值运算符重载函数,就会生成默认的函数去执行浅拷贝,删除后如果进行赋值或者拷贝构造是会报错的
scoped_ptr(const scoped_ptr<T>& src) = delete;
scoped_ptr<T>& operator=(const scoped_ptr<T>& src) = delete;
// =delete是C++11提供的新语法
unique_ptr
一块内存只由一个指针所指向
// 删除了带左值引用参数的拷贝构造和赋值运算符重载函数
unique_ptr(const unique_ptr<T>& src) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& src) = delete;
// 定义了带右值引用的拷贝构造和赋值运算符重载函数(注意里面的操作都是关于指针指向的改变,即资源转移)
unique_ptr(unique_ptr<T>&& src);
unique_ptr<T>& operator=(unique_ptr<T>&& src);
unique_ptr<int> ptr1(new int);
// 使用时,用户可以直接明白代码用意是将ptr1的资源给ptr2,从而明白后续使用*ptr1也是没有意义的
unique_ptr<int> ptr2(std::move(ptr1)); //显式把ptr1资源转移给ptr2,ptr1不持有资源
带引用计数的智能指针
和不带引用计数的智能指针相比起来,带引用计数的智能指针支持多个智能指针可以管理同一个资源
带引用计数的智能指针:
- shared_ptr
- weak_ptr
(这两个指针都是线程安全的,可以直接使用在多线程环境中)
为每一个对象资源,匹配一个引用计数,当智能指针获取资源的时候,引用计数+1,当智能指针不使用资源的时候,引用计数-1,如果减1后引用计数为0了,那么说明这是最后一个使用该资源的指针,可以对资源进行释放了。
实现一个带引用计数的智能指针
// 定义引用计数类模板
template<typename T>
class RefCnt
{
public:
RefCnt(T* ptr = nullptr)
:mptr(ptr)
,_mount(0)
{
// 注意引用的资源要不为空,_mount才初始为1
if (nullptr != mptr)
_mount = 1;
}
void addRef() { ++_mount; }
int delRef() { return --_mount; }
private:
T* mptr; // 指向内存资源
int _mount; // shared_ptr和weak_ptr中引用计数都设置为atomic_int,原子整型,线程安全
};
// 定义带引用计数的智能指针类模板
template<typename T>
class CSmartPtr
{
public:
CSmartPtr(T* ptr = nullptr)
:mptr(ptr)
,mpRefCnt(new RefCnt<T>(mptr))
{}
CSmartPtr(const CSmartPtr<T>& src)
:mptr(src.mptr)
,mpRefCnt(src.mpRefCnt)
{
if (nullptr != mptr)
mpRefCnt->addRef();// 引用计数+1
}
CSmartPtr<T>& operator=(const CSmartPtr<T>& src)
{
if (this == &src)
return *this;
// 当前引用资源的引用计数要减一,然后判断当前资源引用计数是否为0
if (0 == mpRefCnt->delRef())
{
delete mptr;
}
mptr = src.mptr;
mpRefCnt = src.mpRefCnt;
mpRefCnt->addRef();
return *this;
}
T& operator*() { return *mptr; }
T* operator->() { return mptr; }
~CSmartPtr()
{
// 对原来资源的引用计数为0时,由最后一个指针进行释放
if (mpRefCnt->delRef() == 0)
{
delete mptr;
delete mpRefCnt;
mptr = nullptr;
mpRefCnt = nullptr;
}
}
private:
T* mptr;
RefCnt<T>* mpRefCnt;
};
int main()
{
// 智能指针要在栈上定义
CSmartPtr<int> ptr1(new int);
CSmartPtr<int> ptr2(ptr1);
CSmartPtr<int> ptr3;
ptr3 = ptr2;
// ptr1, ptr2, ptr3通过引用计数管理同一块资源
*ptr1 = 20;
cout << *ptr2 << " " << *ptr3 << endl;
return 0;
}
我们自定义的带引用计数的智能指针不是线程安全的,无法用于多线程环境下,而对于shared_ptr和weak_ptr这两个C++所提供的智能指针,它们的引用计数中mount是实现成atomic_int,即原子类型的,对于mount的加减操作具有原子性,所以这两个指针是线程安全的,可以用于多线程环境中。
shared_ptr的交叉引用问题
shared_ptr: 强智能指针 可以改变资源的引用计数
weak_ptr: 弱智能指针 不会改变资源的引用计数,只是观察者
只观察资源
不提供operator*(), operator->()方法强智能指针循环引用(交叉引用 )是什么问题? 什么结果? 怎么解决?
new出来的资源无法释放,造成资源泄漏问题
解决: 定义对象的时候用强智能指针,引用对象的时候用弱智能指针
强智能指针循环引用(交叉引用 )问题:
class B;
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void testA() { cout << "testA()" << endl; }
shared_ptr<B> _ptrb;
private:
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
shared_ptr<A> _ptra;
private:
};
int main()
{
// 栈定义对象的时候用强智能指针
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa->_ptrb = pb;
pb->_ptra = pa;
// 查看资源的引用计数
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;
return 0;
}
A,B对象的引用计数都为2,出作用域后,引用计数都减1后都为1,不为0,所以A,B对象无法析构,资源无法释放,导致内存的非法泄漏
(引用计数在资源那,表示一块资源被几个指针所指向)
在定义对象的时候用强智能指针,引用对象的时候用弱智能指针:
class B;
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void testA() { cout << "testA()" << endl; }
weak_ptr<B> _ptrb; // 引用对象的时候用弱智能指针,观察资源是否存在,不增加引用计数
private:
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
//shared_ptr<A> _ptra;
//void func()
//{
// _ptra->testA();//弱智能指针只是观察者
//}
// 如何使用弱智能指针
void func()
{// 判断观察的资源是否存在,如果存在就将弱智能指针提升为强智能指针
shared_ptr<A> ps = _ptra.lock(); // 提升方法
if (ps != nullptr)
{
ps->testA();
}
}
weak_ptr<A> _ptra;
private:
};
int main()
{
// 栈定义对象的时候用强智能指针
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa->_ptrb = pb;
pb->_ptra = pa;
// 查看资源的引用计数
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;
return 0;
}
weak_ptr只是一个观察者,观察资源是否存在,并不会增加资源的引用计数,并且***weak_ptr没有提供像裸指针一样的*和->运算符重载函数,也就是说并不能通过它访问资源的具体内容 **;
weak_ptr可以通过lock()方法提升成为强智能指针,如果资源已经被释放了,那么提升就会失败,如果提升成为shared_ptr,那么资源的引用计数也要加1,当然shared_ptr提供了*和->运算符重载函数。在上面的代码中,我们在B中定义的func方法中可以通过提升weak_ptr,如果提升成功,就可以通过其调用A中的testA()方法(shared_ptr和wea_ptr都是线程安全的,可以使用在多线程环境中)
多线程访问共享对象的线程安全问题
多线程访问共享对象的线程安全问题
class B;
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void testA() { cout << "testA()" << endl; }
weak_ptr<B> _ptrb; // 引用对象的时候用弱智能指针,观察资源是否存在,不增加引用计数
private:
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
// 如何使用弱智能指针
void func()
{// 判断观察的资源是否存在,如果存在就将弱智能指针提升为强智能指针
shared_ptr<A> ps = _ptra.lock(); // 提升方法
if (ps != nullptr)
{
ps->testA();
}
}
weak_ptr<A> _ptra;
private:
};
// 子线程
void handler01(A* q)
{
std::this_thread::sleep_for(std::chrono::seconds(2));
q->testA();
}
// 主线程
int main()
{
A* p = new A();
thread t1(handler01, p);
delete p;
t1.join();
return 0;
}
可以看到,主线程启动子线程后,马上去析构A对象,但是在子线程中依然调用了对象A的testA方法,这显然是不安全的,我们应该在调用前添加资源是否存在的判断!!
强弱智能指针的应用
void handler01(weak_ptr<A> ptr)
{
std::this_thread::sleep_for(std::chrono::seconds(2));
shared_ptr<A> q = ptr.lock();
if (q != nullptr)
{
q->testA();
}
else
{
cout << "对象已经析构了,无法访问...." << endl;
}
}
int main()
{
{
shared_ptr<A> ptr1(new A());
thread t1(handler01, weak_ptr<A>(ptr1));
t1.detach();//分离线程
}// 出作用域,ptr1自动执行析构函数,所引用的资源的引用计数-1
std::this_thread::sleep_for(std::chrono::seconds(20));
return 0;
}
可以看到通过使用强弱指针,在主线程中,出{}作用域后,ptr1要调用析构函数,先是对A资源的引用计数减1,由于资源A的引用计数减为0了,所以ptr1还要负责对资源A进行析构,然后在子线程中weak_ptr作为资源A的观察者,提升为强指针失败,说明资源已经不存在了,所以无法再调用A的方法。
多个线程访问共享对象,如果某个线程中由于对象的引用计数为0,对其进行析构了,通过强弱指针,可以避免其他线程在资源被释放后,再去访问对象的成员
自定义删除器
智能指针的删除器 deletor
智能指针: 保证资源的绝对释放
- 自定义删除器类模板,控制资源的释放
template<typename T>
class MyDeletor
{
public:
// 小括号运算符重载
void operator()(T* ptr)const
{
cout << "call MyDeletor::operator()(T* )" << endl;
delete[]ptr;
}
};
template<typename T>
class MyFileDeletor
{
public:
// 小括号运算符重载
void operator()(T* ptr)const
{
cout << "call MyFileDeletor::operator()(T* )" << endl;
fclose(ptr);
}
};
int main()
{
// 传入我们自定义的删除器类型
unique_ptr<int, MyDeletor<int>> ptr1(new int[100]);
// unique_ptr<FILE, MyFileDeletor<FILE>> ptr2(fopen("data.txt", "w"));
return 0;
}
但是这种自定义删除器类模板的方式比较复杂,不同的资源(比如数组,文件等)释放的方式不一样,这样就需要去定义多个类模板了
-
使用C++11提供的lambda表达式(lambda表达式是一个函数对象),并且使用c++11提供的function函数对象类,来实例化智能指针类
通过function函数对象以及lambda表达式自定义智能指针对资源的释放方式
int main()
{//function<返回值 参数类型> -->函数对象类型
unique_ptr<int, function<void (int*)>> ptr1(new int[100],
[](int* p)->void {
cout << "call lambda release" << endl;
delete[]p;
}
);
unique_ptr<FILE, function<void (FILE*)>> ptr2(fopen("data.txt", "w"),
[](FILE* p)->void {
cout << "call lambda FILE close" << endl;
fclose(p);
}
);
return 0;
}
既然自定义删除器,是为了自己实现对指针所指向的资源的释放方式,那么lambda形参列表的参数肯定是相应类型的指针变量