c++中的智能指针
前两天的电话面试中被问到智能指针的概念,完全答不上来。特意回来了解了一下,总结了一些智能指针的用法。之后再接着写C++的多线程。
为什么要使用智能指针?
一般来说在C++中新建指针都会使用new来新建一个被指向的对象。由于是程序员指定的内存分配,因此也必须要由程序员通过delete手动释放掉。但是总会有时候会忘记,或者遇到一些异常情况而没有仔细考虑程序中断。为了能够更加方便得管理指针,也更加智能得处理内存的分配和回收,于是产生了智能指针的概念。原因可以总结为以下三点:
- 能够帮我们处理资源泄漏问题。
- 帮我们处理空悬指针的问题。
- 帮我们处理比较隐晦的由异常造成的资源泄露。
一般来说使用智能指针的优先级高于普通指针。
智能指针
智能指针主要包含两大类:
- 共享式智能指针
- 独占式拥有
除此之外,还包括Weak_ptr等辅助性智能指针,末尾也会介绍。
共享式智能指针:Shared_ptr
共享指针,顾名思义,就是指多个指针同时共享一块内存。最后一个拥有者负责将该对象销毁,并清理与对象有关的所有资源。
- 初始化
shared_ptr<string> sp(new string(""));//正确的新建共享指针方法 shared_ptr<string> p1 = make_shared<string>("");//正确的新建共享指针方法 shared_ptr<string> p2 = new string("");//错误的方法
一般我们使用make_shared构造函数来新建共享式智能指针,这种方法比用new新建效率更高。
- 计数器
共享指针类中包括一个成员函数用来记住所管理的内存当前有多少个指针指向它。
shared_ptr<T> p(q):p是shared_ptr q的拷贝:此操作会递增q中的计数器。
p=q: p和q都是shared_ptr:此操作会递增q的引用计数,递减p原来的引用计数,若p原来的引用计数变为0,则销毁原来p指向的内存。
p.use_count(): 返回与p共享对象的共享智能指针数量。
#include<iostream> #include<memory> #include<string> using namespace std; int main() { cout<<"test shared_ptr basic usage:"<<endl; shared_ptr<string> p1(new string("hello")); //新建共享指针p1,此时p1的计数器为1 auto p2 = make_shared<string>("world"); //新建共享指针p2,此时p2的计数器为1 cout<<*p1<<" "<<*p2<<endl; cout<<"test shared_ptr use count"<<endl; //这一步实际包含了三步, //首先新建了共享指针p3,p3的计数器为1 //然后p3指向了p2,这时p2的计数器+1达到2 //再让p3的计数器 = p2的计数器,此时p3的计数器也达到2 auto p3 = p2; cout<<"p1 cnt = "<<p1.use_count()<<" p2 cnt = "<<p2.use_count()<<" p3 cnt = "<<p3.use_count()<<endl; p2 = p1;//此时p1的计数+1,p2原来指向的内存计数器-1 cout<<"p1 cnt = "<<p1.use_count()<<" p2 cnt = "<<p2.use_count()<<" p3 cnt = "<<p3.use_count()<<endl; }
输出:
具体过程为:
注意共享指针不仅共享了同一个内存地址,其实还共享了一个计数器的地址。这样在第3步(令p2 = p1)时,先让p2原来指向的计数器-1,这样p3的的计数器就成了1,再让p2新指向的p1的计数器+1成了2,再更新p2的计数器 = p1的计数器,这样p2的计数器也成为了2。
- 不能混用普通指针和共享智能指针
注意不能将一个new表达式的返回值赋给共享指针,因为new构造的是普通的指针,普通指针和智能指针不能混用。
举个例子:
#include<iostream> #include<memory> using namespace std; void process(shared_ptr<int> ptr) { cout<<"in process use_count = "<<ptr.use_count()<<endl; } int main() { cout<<"don't mix shared_ptr and normal pointer"<<endl; shared_ptr<int> p(new int(10)); process(p); //process函数中,将p复制了一份,并指向了p管理的内存,此时p的计数为2 cout<<*p<<endl; //离开process后,p的计数-1,变成1 int *p2 = new int(100); process(shared_ptr<int>(p2)); //将p2指向的内存新建了一个共享智能指针管理,计数器为1 cout<<*p2<<endl;//离开process后,共享指针的计数器为0,因此p2的指向的内存被销毁,于是p2本身称为了空悬指针。 }
输出:
所以,一旦将一个new表达式返回的指针交由shared_ptr管理之后,就不要再通过普通指针访问这块内存(除非shared_ptr指向的是数组)!
- 共享指针的get()
get返回的是共享指针所管理的指针。
- 共享指针的reset
shared_ptr可以通过reset方法重置指向另一个对象,此时原对象的引用计数-1。
#include<iostream> #include<memory> using namespace std; int main() { shared_ptr<int> p1(new int(10)); auto p2 = p1; auto p3 = p1; cout<<"此时p1的use_count = "<<p1.use_count()<<endl; p1.reset(new int(5)); cout<<"此时p1的use_count = "<<p1.use_count()<<endl; p1.reset(); cout<<"此时p1的use_count = "<<p1.use_count()<<endl; }
输出结果为:
- 共享指针的自定义deleter
可以定制一个deleter函数,用于在shared_ptr释放对象时调用。
shared_ptr本身提供默认内存释放器(default deleter),调用的是delete,不过只对“由new建立起来的单一对象”起作用。当然我们也可以自己定义内存释放器。不过值得注意的是,默认内存释放器并不能释放数组内存空间,而是要我们自己提供内存释放器,如:
shared_ptr<int> p(new int[10], // deleter (a lambda function) [](int *p) { delete[] p; cout<<"deleting p"<<endl; } );
当使用shared_ptr指向数组时,需要注意以下两点:
shared_ptr的数组智能指针,必须要自定义deleter!
shared_ptr的数组智能指针,有*和->操作,但不支持下标操作[],只能通过get()去访问数组的元素。
int main() { shared_ptr<int> p(new int[10], // deleter (a lambda function) [](int *p) { delete[] p; cout<<"deleting p"<<endl; } ); //p[1] = 4;//错误,无法通过智能指针去访问数组 int *ptr = p.get(); ptr[1] = 4; //正确 }
-
注意与多线程的结合时,使用了锁扔会保证线程安全,但是计数器会增加
在共享智能指针被多个线程调用且分别实例化的过程中,即使他们管理的是同一块内存,计数器也不会有特别的同步性操作。
#include <iostream> #include <memory> #include <thread> #include <chrono> #include <mutex> struct Base { Base() { std::cout << " Base::Base()\n"; } // Note: non-virtual destructor is OK here ~Base() { std::cout << " Base::~Base()\n"; } int num; }; struct Derived: public Base { Derived() { std::cout << " Derived::Derived()\n"; } ~Derived() { std::cout << " Derived::~Derived()\n"; } }; void thr(std::shared_ptr<Base> p, int a) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::shared_ptr<Base> lp = p; // thread-safe, even though the // shared use_count is incremented { static std::mutex io_mutex; std::lock_guard<std::mutex> lk(io_mutex); lp->num = a; std::cout << "local pointer in a thread:\n" << " lp.get() = " << lp.get() << ", lp.use_count() = " << lp.use_count() << '\n' << ", lp->num = " <<lp->num <<", p->num = "<<p->num<<std::endl; } } int main() { std::shared_ptr<Base> p = std::make_shared<Derived>(); std::cout << "Created a shared Derived (as a pointer to Base)\n" << " p.get() = " << p.get() << ", p.use_count() = " << p.use_count() << '\n'; std::thread t1(thr, p, 2),t2(thr, p, 3), t3(thr, p, 4); t1.join(); t2.join(); t3.join(); p.reset(); // release ownership from main std::cout << "Shared ownership between 3 threads and released\n" << "ownership from main:\n" << " p.get() = " << p.get() << ", p.use_count() = " << p.use_count() << '\n'; // t1.join(); t2.join(); t3.join(); std::cout << "All threads completed, the last one deleted Derived\n"; }
输出为:
独占式智能指针:unique_ptr
一般而言,这个智能指针实现了独占式拥有概念,意味着它可确保一个对象和其相应资源同一时间只被一个指针拥有。一旦拥有者被销毁或变成空,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源也会被释放。
-
unique_ptr
无类似make_shared
函数。定义时,需要将其绑定到new
返回的指针上 -
类似
shared_ptr
,初始化unique_ptr
需采用直接初始化形式 -
unique_ptr
独占其指向的对象,故不支持普通的拷贝或赋值操作
int main()
{
unique_ptr<string> p1(new string("hao")); // p2 指向一个值为 hao 的 string
unique_ptr<string> p2(p1); // 错误:unique_ptr 不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误:unique_ptr 不支持赋值
}
unique_ptr
虽然不能赋值或拷贝,但可以调用release
或reset
将所有权从非const
的unique_ptr
转移到另一个unique_ptr
int main()
{
unique_ptr<string> p1(new string("hao")); // p2 指向一个值为 hao 的 string
unique_ptr<string> p2(p1.release()); // release 将 p1 置为空,并将所有权转移给 p2
cout<<*p2<<endl;
}
输出为:
unique_ptr
在将被销毁时可以拷贝或赋值, 比如从函数返回unique_ptr
unique_ptr<int> clone(int p)
{
unique_ptr<int> up(new int(p));
cout<<"in local function"<<" up = "<<up<<endl;
return up;
}
int main()
{
unique_ptr<int> p1 = clone(5);
cout<<"in main function"<<" p1 = "<<p1<<endl;
cout<<*p1<<endl;
}
输出为:
unique_ptr的数组智能指针,没有*和->操作,但支持下标[]
using namespace std; int main() { unique_ptr<int[]> up(new int[10]); up[1] = 5; cout<<up[1]<<endl; //输出5 }
Weak_ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
#include <iostream>
#include <memory>
int main() {
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl;
std::weak_ptr<int> wp(sh_ptr);
std::cout << wp.use_count() << std::endl;
if(!wp.expired()){
std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl;
}
}
//delete memory
}
输出为:
参考:
https://www.cnblogs.com/xiehongfeng100/p/4645555.html
https://www.cnblogs.com/johngu/p/8427064.html
https://en.cppreference.com/w/cpp/memory/shared_ptr
https://blog.csdn.net/u011221820/article/details/80157524
https://blog.csdn.net/runner668/article/details/80539221