深入掌握C++智能指针_深入掌握c++智能指针 大秦坑王-优快云博客
C++11 - 右值引用_大秦坑王 右值引用+引用折叠原理-优快云博客
一、基本原理
智能指针的基本原理,就是利用栈上的对象出作用域会自动析构这么一个特点,把资源释放的代码全部放在这个析构函数中执行,就达到了所谓的智能指针。
template<typename T>
class CSmartPtr
{
public:
CSmartPtr(T *ptr = nullptr) :mptr(ptr) {}
~CSmartPtr() { delete mptr; }
private:
T *mptr;
};
int main()
{
CSmartPtr<int> ptr(new int);
/*其它的代码...*/
/*由于ptr是栈上的智能指针对象,不管是函数正常执行完,还是运行过程中出现
异常,栈上的对象都会自动调用析构函数,在析构函数中进行了delete
操作,保证释放资源*/
return 0;
}
上面这段代码就是一个非常简单的智能指针,主要用到了这两点:
1)智能指针体现在把裸指针进行了一次面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源
2)利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源
所以,智能指针一般都是定义在栈上的,曾经有一个面试问题,问“能不能在堆上定义智能指针?”,如这段代码CSmartPtr *p = new CSmartPtr(new int);大家应该能看出来,这里定义的p虽然是智能指针类型,但它实质上还是一个裸指针,因此p还是需要进行手动delete,又回到了最开始裸指针我们面临的问题。
二、智能指针要做到和裸指针相似,还得提供裸指针常见的*和->两种运算符的重载函数,使用起来才真正的和裸指针一样
template<typename T>
class CSmartPtr
{
public:
CSmartPtr(T *ptr = nullptr) :mptr(ptr) {}
~CSmartPtr() { delete mptr; }
T& operator*() { return *mptr; }
const T& operator*()const { return *mptr; }
T* operator->() { return mptr; }
const T* operator->()const { return mptr; }
private:
T *mptr;
};
int main()
{
CSmartPtr<int> ptr(new int);
*ptr = 20;
// -> 底层实际是 ( ptr.operator->() )->test(); test() 是方法
cout << *ptr << endl;
return 0;
}
三、智能指针的构造函数
int main()
{
CSmartPtr<int> ptr1(new int);
CSmartPtr<int> ptr2(ptr1);
return 0;
}
这个main函数运行,代码直接崩溃,问题出在默认的拷贝构造函数做的是浅拷贝,两个智能指针都持有一个new int资源,ptr2先析构释放了资源,到ptr1析构的时候,就成了delete野指针了,造成程序崩溃。所以这里引出来智能指针需要解决的两件事情:
怎么解决智能指针的浅拷贝问题
多个智能指针指向同一个资源的时候,怎么保证资源只释放一次,而不是每个智能指针都释放一次,造成代码运行不可预期的严重后果
之前的深拷贝构造函数,在智能指针中不行,因为是重新开辟了一块资源给ptr2,ptr1和ptr2是两块内存,可是用户人为是ptr1和ptr2指向同一块内存。
将怎么解决浅拷贝问题可以分为 不带引用计数的智能指针和带引用计数的智能指针
C++库中提供的不带引用计数的智能指针主要包括:auto_ptr,scoped_ptr,unique_ptr。
使用 explicit
关键字可以避免一些意外的隐式类型转换,提高代码的安全性和可读性。通常情况下,如果构造函数只接受一个参数,并且不希望它被隐式调用,就应该将其声明为 explicit
。
四、auto_ptr,scoped_ptr,unique_ptr
一、auto_ptr在拷贝构造中,将原有的智能指针置为nullptr,然后将对象赋值给新的智能指针。
二、scoped_ptr 将拷贝构造函数和赋值操作删除或私有化
template<class T> class scoped_ptr // noncopyable
{
private:
T * px;
/*
私有化拷贝构造函数和赋值函数,这样scoped_ptr的智能指针
对象就不支持这两种操作,从根本上杜绝浅拷贝的发生
*/
scoped_ptr(scoped_ptr const &);
scoped_ptr & operator=(scoped_ptr const &);
typedef scoped_ptr<T> this_type;
/*
私有化逻辑比较运算符重载函数,不支持scoped_ptr的智能指针
对象的比较操作
*/
void operator==( scoped_ptr const& ) const;
void operator!=( scoped_ptr const& ) const;
public:
typedef T element_type;
explicit scoped_ptr( T * p = 0 ): px( p ) // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_constructor_hook( px );
#endif
}
#ifndef BOOST_NO_AUTO_PTR
/*支持从auto_ptr构造一个scoped_ptr智能指针对象,
但是auto_ptr因为调用release()函数,导致其内部指
针为nullptr*/
explicit scoped_ptr( std::auto_ptr<T> p ) BOOST_NOEXCEPT : px( p.release() )
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_constructor_hook( px );
#endif
}
#endif
/*析构函数,释放智能指针持有的资源*/
~scoped_ptr() // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_destructor_hook( px );
#endif
boost::checked_delete( px );
}
};
从scoped_ptr的源码可以看到,该智能指针由于私有化了拷贝构造函数和operator=赋值函数,因此从根本上杜绝了智能指针浅拷贝的发生,所以scoped_ptr也是不能用在容器当中的,如果容器互相进行拷贝或者赋值,就会引起scoped_ptr对象的拷贝构造和赋值,这是不允许的,代码会提示编译错误。
auto_ptr和scoped_ptr这一点上的区别,有些资料上用所有权的概念来描述,道理是相同的,auto_ptr可以任意转移资源的所有权,而scoped_ptr不会转移所有权。
三、unique_ptr 将拷贝构造函数和赋值操作删除或私有化,提供右值引用的移动构造函数
template<class _Ty,
class _Dx> // = default_delete<_Ty>
class unique_ptr
: public _Unique_ptr_base<_Ty, _Dx>
{ // non-copyable pointer to an object
public:
typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
typedef typename _Mybase::pointer pointer;
typedef _Ty element_type;
typedef _Dx deleter_type;
/*提供了右值引用的拷贝构造函数*/
unique_ptr(unique_ptr&& _Right) noexcept
: _Mybase(_Right.release(),
_STD forward<_Dx>(_Right.get_deleter()))
{ // construct by moving _Right
}
/*提供了右值引用的operator=赋值重载函数*/
unique_ptr& operator=(unique_ptr&& _Right) noexcept
{ // assign by moving _Right
if (this != _STD addressof(_Right))
{ // different, do the move
reset(_Right.release());
this->get_deleter() = _STD forward<_Dx>(_Right.get_deleter());
}
return (*this);
}
/*
交换两个unique_ptr智能指针对象的底层指针
和删除器
*/
void swap(unique_ptr& _Right) noexcept
{ // swap elements
_Swap_adl(this->_Myptr(), _Right._Myptr());
_Swap_adl(this->get_deleter(), _Right.get_deleter());
}
/*通过自定义删除器释放资源*/
~unique_ptr() noexcept
{ // destroy the object
if (get() != pointer())
{
this->get_deleter()(get());
}
}
/*unique_ptr提供->运算符的重载函数*/
_NODISCARD pointer operator->() const noexcept
{ // return pointer to class object
return (this->_Myptr());
}
/*返回智能指针对象底层管理的指针*/
_NODISCARD pointer get() const noexcept
{ // return pointer to object
return (this->_Myptr());
}
/*提供bool类型的重载,使unique_ptr对象可以
直接使用在逻辑语句当中,比如if,for,while等*/
explicit operator bool() const noexcept
{ // test for non-null pointer
return (get() != pointer());
}
/*功能和auto_ptr的release函数功能相同,最终也是只有一个unique_ptr指针指向资源*/
pointer release() noexcept
{ // yield ownership of pointer
pointer _Ans = get();
this->_Myptr() = pointer();
return (_Ans);
}
/*把unique_ptr原来的旧资源释放,重置新的资源_Ptr*/
void reset(pointer _Ptr = pointer()) noexcept
{ // establish new pointer
pointer _Old = get();
this->_Myptr() = _Ptr;
if (_Old != pointer())
{
this->get_deleter()(_Old);
}
}
/*
删除了unique_ptr的拷贝构造和operator=赋值函数,
因此不能做unique_ptr智能指针对象的拷贝构造和
赋值,防止浅拷贝的发生
*/
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
};
// 示例2
unique_ptr<int> test_uniqueptr()
{
unique_ptr<int> ptr1(new int);
return ptr1;
}
int main()
{
/*
此处调用test_uniqueptr函数,在return ptr1代码
处,调用右值引用的拷贝构造函数,由ptr1拷贝构造ptr
*/
unique_ptr<int> ptr = test_uniqueptr();
return 0;
}
// 示例1
unique_ptr<int> ptr(new int);
unique_ptr<int> ptr2 = std::move(ptr); // 使用了右值引用的拷贝构造
ptr2 = std::move(ptr); // 使用了右值引用的operator=赋值重载函数
// 示例2
unique_ptr<int> test_uniqueptr()
{
unique_ptr<int> ptr1(new int);
return ptr1;
}
int main()
{
/*
此处调用test_uniqueptr函数,在return ptr1代码
处,调用右值引用的拷贝构造函数,由ptr1拷贝构造ptr
*/
unique_ptr<int> ptr = test_uniqueptr();
return 0;
}
// return 返回一个临时对象,然后通过调用右值引用的拷贝构造函数
五、带有引用技术 的智能指针 shared_ptr /weak ptr
多个智能指针可以管理同一个资源,shared_ptr /weak ptr都是线程安全的。
一、shared_ptr
这里主要介绍shared_ptr和weak_ptr两个智能指针,什么是带引用计数的智能指针?当允许多个智能指针指向同一个资源的时候,每一个智能指针都会给资源的引用计数加1,当一个智能指针析构时,同样会使资源的引用计数减1,这样最后一个智能指针把资源的引用计数从1减到0时,就说明该资源可以释放了,由最后一个智能指针的析构函数来处理资源的释放问题,这就是引用计数的概念。
要对资源的引用个数进行计数,那么大家知道,对于整数的++或者- -操作,它并不是线程安全的操作,因此shared_ptr和weak_ptr底层的引用计数已经通过CAS操作,保证了引用计数加减的原子特性,因此shared_ptr和weak_ptr本身就是线程安全的带引用计数的智能指针。
std::atomic_int计数器
#include <atomic>
template<typename T>
class RefCnt{
public:
RefCnt(T *ptr = nullptr): mptr(ptr), mcount(1) {}
void addRef() { mcount++; } // 增加资源引用计数
int delRef() { return --mcount; } // 减少资源引用计数
private:
T *mptr; // 资源指针
std::atomic_int mcount; // 引用计数
};
template<typename T>
class CSmartPtr{
public:
CSmartPtr(T *ptr = nullptr): mptr(ptr) {
mpRefCnt = new RefCnt<T>(mptr);
}
~CSmartPtr() {
if (mpRefCnt->delRef() == 0) { // 如果引用计数为0,释放资源
delete mptr;
delete mpRefCnt;
}
}
T& operator*() { return *mptr; }
T* operator->() { return mptr; }
CSmartPtr(const CSmartPtr<T>& src): mptr(src.mptr), mpRefCnt(src.mpRefCnt) {
if (mptr != nullptr) {
mpRefCnt->addRef();
}
}
CSmartPtr<T>& operator=(const CSmartPtr<T>& src) {
if (this == &src) {
return *this;
}
if (mpRefCnt->delRef() == 0) { // 如果引用计数为0,释放资源
delete mptr;
delete mpRefCnt;
}
mptr = src.mptr;
mpRefCnt = src.mpRefCnt;
mpRefCnt->addRef();
return *this;
}
private:
T *mptr; // 资源指针
RefCnt<T> *mpRefCnt; // 指向该资源引用计数对象的指针
};
int main() {
CSmartPtr<int> ptr1(new int);
CSmartPtr<int> ptr2(ptr1);
return 0;
}
在标准的 std::shared_ptr
实现中,引用计数器通常是在堆上分配的。这是因为需要保证多个 std::shared_ptr
实例共享相同的计数器,而计数器的生命周期必须与共享的资源一致,即在最后一个引用释放后销毁。
二、shared_ptr
循环引用
#include <iostream>
#include <memory>
using namespace std;
class B; // 前置声明类B
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
shared_ptr<B> _ptrb; // 指向B对象的智能指针
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
shared_ptr<A> _ptra; // 指向A对象的智能指针
};
int main()
{
shared_ptr<A> ptra(new A());// ptra指向A对象,A的引用计数为1
shared_ptr<B> ptrb(new B());// ptrb指向B对象,B的引用计数为1
ptra->_ptrb = ptrb;// A对象的成员变量_ptrb也指向B对象,B的引用计数为2
ptrb->_ptra = ptra;// B对象的成员变量_ptra也指向A对象,A的引用计数为2
cout << ptra.use_count() << endl; // 打印A的引用计数结果:2
cout << ptrb.use_count() << endl; // 打印B的引用计数结果:2
/*
出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
B对象的引用计数从2减到1,达不到释放A和B的条件(释放的条件是
A和B的引用计数为0),因此造成两个new出来的A和B对象无法释放,
导致内存泄露,这个问题就是“强智能指针的交叉引用(循环引用)问题”
*/
return 0;
}
可以看到,A和B对象并没有进行析构,通过上面的代码示例,能够看出来“交叉引用”的问题所在,就是对象无法析构,资源无法释放,那怎么解决这个问题呢?请注意强弱智能指针的一个重要应用规则:定义对象时,用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr。
弱智能指针weak_ptr区别于shared_ptr之处在于:
weak_ptr不会改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr来判定资源是否存在
weak_ptr持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数
weak_ptr没有提供常用的指针操作,无法直接访问资源,需要先通过lock方法提升为shared_ptr强智能指针,才能访问资源
#include <iostream>
#include <memory>
using namespace std;
class B; // 前置声明类B
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
weak_ptr<B> _ptrb; // 指向B对象的弱智能指针。引用对象时,用弱智能指针
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
weak_ptr<A> _ptra; // 指向A对象的弱智能指针。引用对象时,用弱智能指针
};
int main()
{
// 定义对象时,用强智能指针
shared_ptr<A> ptra(new A());// ptra指向A对象,A的引用计数为1
shared_ptr<B> ptrb(new B());// ptrb指向B对象,B的引用计数为1
// A对象的成员变量_ptrb也指向B对象,B的引用计数为1,因为是弱智能指针,引用计数没有改变
ptra->_ptrb = ptrb;
// B对象的成员变量_ptra也指向A对象,A的引用计数为1,因为是弱智能指针,引用计数没有改变
ptrb->_ptra = ptra;
cout << ptra.use_count() << endl; // 打印结果:1
cout << ptrb.use_count() << endl; // 打印结果:1
/*
出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
B对象的引用计数从1减到0,达到释放A和B的条件,因此new出来的A和B对象
被析构掉,解决了“强智能指针的交叉引用(循环引用)问题”
*/
return 0;
}
三、多线程访问共享对象
借助shared_ptr和weak_ptr解决了这样一个问题,多线程访问共享对象的线程安全问题,解释如下:线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。
#include <iostream>
#include <thread>
using namespace std;
class Test
{
public:
// 构造Test对象,_ptr指向一块int堆内存,初始值是20
Test() :_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析构Test对象,释放_ptr指向的堆内存
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 该show会在另外一个线程中被执行
void show()
{
cout << *_ptr << endl;
}
private:
int *volatile _ptr;
};
void threadProc(Test *p)
{
// 睡眠两秒,此时main主线程已经把Test对象给delete析构掉了
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
此时当前线程访问了main线程已经析构的共享对象,结果未知,隐含bug。
此时通过p指针想访问Test对象,需要判断Test对象是否存活,如果Test对象
存活,调用show方法没有问题;如果Test对象已经析构,调用show有问题!
*/
p->show();
}
int main()
{
// 在堆上定义共享对象
Test *p = new Test();
// 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p
std::thread t1(threadProc, p);
// 在main线程中析构Test共享对象
delete p;
// 等待子线程运行结束
t1.join();
return 0;
}
使用shared_ptr 和 weak_ptr解决多线程访问共享对象问题
#include <iostream>
#include <thread>
#include <memory>
using namespace std;
class Test
{
public:
// 构造Test对象,_ptr指向一块int堆内存,初始值是20
Test() :_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析构Test对象,释放_ptr指向的堆内存
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 该show会在另外一个线程中被执行
void show()
{
cout << *_ptr << endl;
}
private:
int *volatile _ptr;
};
void threadProc(weak_ptr<Test> pw) // 通过弱智能指针观察强智能指针
{
// 睡眠两秒
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
如果想访问对象的方法,先通过pw的lock方法进行提升操作,把weak_ptr提升
为shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
*/
shared_ptr<Test> ps = pw.lock();
if (ps != nullptr)
{
ps->show();
}
}
int main()
{
// 在堆上定义共享对象
shared_ptr<Test> p(new Test);
// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main线程中析构Test共享对象
// 等待子线程运行结束
t1.join();
return 0;
}
因为main线程调用了t1.join()方法等待子线程结束,此时pw通过lock提升为ps成功,见上面代码示例。
如果设置t1为分离线程,让main主线程结束,p智能指针析构,进而把Test对象析构,此时show方法已经不会被调用,因为在threadProc方法中,pw提升到ps时,lock方法判定Test对象已经析构,提升失败!main函数代码可以如下修改测试:
int main()
{
// 在堆上定义共享对象
shared_ptr<Test> p(new Test);
// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main线程中析构Test共享对象
// 设置子线程分离
t1.detach();
return 0;
}
四、自定义删除器
函数对象:
函数对象(Function Object),也称为仿函数(Functor),是 C++ 中的一种对象,可以像函数一样被调用。函数对象可以是一个类对象,重载了函数调用运算符 operator()
。通过重载 operator()
,函数对象可以像函数一样被调用,并且可以保存状态和行为。
函数对象的优点在于它可以作为参数传递给算法或者容器的成员函数,从而使得算法或者容器可以更加灵活地处理数据。它还可以实现一些特定的行为,比如在调用时执行额外的操作、记录调用次数等。
#include <iostream>
// 定义一个函数对象类
class MyFunctor {
public:
// 重载 () 运算符,使得该类的对象可以像函数一样被调用
void operator()(int x) const {
std::cout << "Value is: " << x << std::endl;
}
};
int main() {
// 创建一个函数对象的实例
MyFunctor functor;
// 调用函数对象
functor(42);
return 0;
}
unique_str 和shared_ptr 默认的删除器,重载()
template<typename T>
class MyDeletor{
publuc:
//const 关键字的使用是为了确保函数对象在执行 delete[] 操作时不会修改函数对象本身的状态。
void operator()(T * ptr) cosnt {
delete [] ptr;
}
}
int main(){
unique_ptr<int,MyDeletor<int>>ptr(new int[100]);
return 0;
}
从unique_ptr的析构函数可以看到,如果要实现一个自定义的删除器,实际上就是定义一个函数对象而已,示例代码如下:
class FileDeleter
{
public:
// 删除器负责删除资源的函数
void operator()(FILE *pf)
{
fclose(pf);
}
};
int main()
{
// 由于用智能指针管理文件资源,因此传入自定义的删除器类型FileDeleter
unique_ptr<FILE, FileDeleter> filePtr(fopen("data.txt", "w"));
return 0;
}
int main()
{
// 自定义智能指针删除器,关闭文件资源
unique_ptr<FILE, function<void(FILE*)>>
filePtr(fopen("data.txt", "w"), [](FILE *pf)->void{fclose(pf);});
// 自定义智能指针删除器,释放数组资源
unique_ptr<int, function<void(int*)>>
arrayPtr(new int[100], [](int *ptr)->void {delete[]ptr; });
return 0;
}