前言
- 静态内存:用于保存局部静态变量、类内的静态数据成员以及全局变量
- 栈:用于保存函数内部的非static变量
- 堆:存储动态分配的对象(程序运行时分配的对象)
静态内存和栈内存的对象由编译器自动创建和销毁
而堆区的动态内存,其生命周期由程序控制
虽然动态内存是必要的,但是管理动态内存是非常棘手的一件事 。
在c++中,动态内存的管理由new/delete这对运算符完成,其中:
- new负责为对象分配一块内存空间,并返回指向该空间的指针
- delete负责销毁由new指向的内存空间
虽然c++提供了动态内存的管理机制,但是程序员在使用时很容易忘记释放内存,这样就很容易造成内存泄漏的问题,并且在合适的时间释放内存也是及其困难的问题。
因此,为了更加容易和安全的使用动态内存,c++提供了两种智能指针类管理动态内存对象——shared_ptr与unique_ptr,而weak_ptr是shared_ptr的一种伴随类。
shared_ptr
简介
shared_ptr即共享指针,顾名思义,其允许多个指针指向同一个对象。每当有一个指针指向该共享对象时,该共享对象的引用计数就加1,只有当最后一个指向该对象的 shared_ptr
被销毁时,对象的内存才会被释放。
其实现机制可以简单理解为:普通指针+引用计数,如下图所示,有两个shared_ptr类型的智能指针q和p指向了同一个共享内存对象,则此时p.use_count()与q.use_count()都为2(use_count()方法用于获取该智能指针对所引用对象的引用计数)
初始化
shared_ptr的使用与普通指针的使用基本相同,可以使用*对指针进行解引用
make_shared函数
功能:标准的分配和初始化动态内存
头文件:memory
原型和使用方法:
make_shared<T>(args)
- T:动态对象的类
- args:动态对象的初始化值
shared_ptr<string> q = make_shared<string>(10, '.'); auto p = make_shared<int>(42); cout << *q << endl; cout << *p << endl;
常用操作
获取shared_ptr中的指针
get()函数
功能:获取shared_ptr的指针
shared_ptr<int> p = make_shared<int>(42);
int *pp = p.get();
注意,永远不要用
get
初始化另一个智能指针或者为另一个智能指针赋值。
shared_ptr<int> p(new int(42)); // 引用计数为1
// 新程序块
{
// 两个独立的shared_ptr指向相同的内存
shared_ptr<int> q(p.get()); // 错误:将get用来初始化指针指针
} // 程序块结束,q被销毁,它指向的内存被释放
int foo = *p; // 未定义:p指向的内存已经被释放了
另外,也不要delete get()返回的指针,这可能会导致以下问题:
多个智能指针共享同一块内存时,如果其中一个智能指针调用了
delete
操作符,会导致其他智能指针持有的原始指针成为悬挂指针,从而引发未定义行为或内存访问错误。如果智能指针已经释放了其管理的资源,而你仍然尝试通过
get
返回的指针进行delete
操作,这将导致二次释放内存,同样会引发未定义行为。
获取shared_ptr所指对象的引用计数
use_count()
功能:获取共享对象的引用计数
//创建一个动态对象,其值被初始化为42,并使用shared_ptr类型的共享指针p指向该动态内存
shared_ptr<int> p = make_shared<int>(42);
//由于此时只有一个shared_ptr类型的指针p指向值为42的动态内存,故该动态对象的引用计数为1
cout << "指针p所指向动态对象的引用计数:" << p.use_count() << endl;
auto q(p);//拷贝动态指针,使得shared_ptr指针q也指向与p所指向的动态内存
cout<<"指针q所指向动态对象的引用计数:" << q.use_count() << endl;
cout << "指针p所指向动态对象的引用计数:" << p.use_count() << endl;
reset()函数
功能:修改shared_ptr所管理的资源,或将shared_ptr置空
shared_ptr<int> p = make_shared<int>(42);
cout << "p1:" << *p << endl;
//修改共享指针p的指向为值为24的动态内存,原来的p指向的值为42的动态内存被释放
p.reset(new int(24));
cout << "p2:" << *p << endl;
p.reset();//若参数为空,则表示将共享指针置空
一般情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因此智能指针默认使用 delete
释放它所关联的对象。但是也可以将智能指针绑定到一个指向其他类型的资源的指针上,只是此时需要提供自己的释放操作来替代 delete
。
// 如果p是唯一指向其对象的shared_ptr,reset会释放此对象。
p.reset();
// 若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。
p.reset(q);
//若还传递了参数d,将会调用d而不是delete来释放q
p.reset(q, d);
拷贝与赋值
shared_ptr<int> p = make_shared<int>(42);
auto q(p);//拷贝指针p,让指针p与指针q指向同一块内存
int* p(new int(1024)); // p指向动态内存
auto q = p; // p和q指向同一块内存
delete p; // p和q均变为无效
p = nullptr; // 此时p不再绑定到任何对象
// 此时重置p对q没有任何作用,q仍然指向原来那块(已经被释放的)内存地址
// 在实际系统中,查找指向相同内存的所有指针是异常困难的!
shared_ptr与new的结合使用
接受指针参数的智能指针的初始化函数是explicit的,故而不能将一个普通指针隐式转换为智能指针,必须使用直接初始化方式来初始化一个智能指针,如下:
shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式
至此,我们已经知道了两种初始化shared_ptr类型指针的方式,分别是使用make_shared函数和使用关键字new进行直接初始化
同理,一个返回 shared_ptr
的函数也不能在其返回语句中隐式转换一个普通指针:
shared_ptr<int> clone(int p){
return new int(p); // 错误
return shared_ptr<int>(new int(p)); // 正确
}
注意:不要混合使用普通指针和智能指针,使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁
// ptr是传值方式传递,因此拷贝时会递增其引用次数。
void process(shared_ptr<int> ptr){
// 使用ptr
} // ptr离开作用域,ptr会被销毁。由于ptr销毁了,引用次数会递减
// process正确的使用方法:
shared_ptr<int> p(new int(42)); // 此时引用计数为1
process(p); // 拷贝p会递增其引用次数,此时在process中引用计数值为2
int i = *p; // 离开了process作用域,引用计数值为1
// process错误的使用方法:
int *x(new int(1024)); // 危险:x是普通指针,不是智能指针
process(x); // 错误:不能将int* 转换为shared_ptr<int>
process(shared_ptr<int>(x)); // 合法,但是由于传参时使用的是一个临时变量,
// 临时变量在传递完之后会被销毁,所以传完参
// 之后的process里的ptr引用计数为1
int j = *x; // 未定义:离开了process作用域,引用计数值为0,此时x为一个悬空指针
shared_ptr使用案例
#include<iostream>
#include<vector>
#include<string>
#include<memory>
#include<exception>
#include<initializer_list>
using namespace std;
typedef vector<string>::size_type size_type;
class StrBlob {
private:
shared_ptr<vector<string>> data;
void check(const size_type& i, const string& msg);
public:
//构造函数
StrBlob();
StrBlob(initializer_list<string> li);//使用初始化列表构造函数
size_type size();
bool empty();
void push_back(const string& str);
void pop_back();
string front();
string back();
friend ostream& operator<<(ostream& os, const StrBlob& data);
};
StrBlob::StrBlob():data(make_shared<vector<string>>()){}
StrBlob::StrBlob(initializer_list<string> li) :data(make_shared<vector<string>>(li)){}
void StrBlob::check(const size_type& i,const string& msg)
{
if (i >= data->size())
throw out_of_range(msg);
}
size_type StrBlob::size()
{
return data->size();
}
bool StrBlob::empty()
{
return data->empty();
}
void StrBlob::push_back(const string& str)
{
data->push_back(str);
}
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
string StrBlob::front()
{
check(0, "front on empty StrBlob");
return data->front();
}
string StrBlob::back()
{
check(0, "back on empty StrBlob");
return data->back();
}
ostream& operator<<(ostream& os, const StrBlob& str)
{
for (auto p = str.data->cbegin(); p != str.data->cend(); ++p)
{
os << *p << " ";
}
return os;
}
unique_ptr
- 与shared_ptr相反,unique_ptr智能指针独占其所指向动态对象的所有权
- 故unique_ptr不支持拷贝与赋值操作
- 但是我们可以拷贝一个右值类型unique_ptr,也就是一个即将被销毁的unique_ptr
-
定义一个
unique_ptr
时,需要将其绑定到一个new
返回的指针上。初始化unique_ptr
必须采用直接初始化形式。
unique_ptr<int> up(new int(42));
cout << *up << endl;
unique_ptr<int> uq(up);//错误,unique_ptr不支持拷贝操作
unique_ptr<int> uq=up;//错误,unique_ptr不支持赋值操作
//unique_ptr支持移动操作,也就是将一个即将销毁的对象赋给一个unique_ptr
unique_ptr<int> upp = std::move(up);
unique_ptr<int> clone(int s)
{
return unique_ptr<int>(new int(s));//return返回的值是一个右值,支持拷贝(移动)
}
weak_ptr
-
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr
管理的对象。 -
将一个
weak_ptr
绑定到一个shared_ptr
不会改变shared_ptr
的引用计数。一旦最后一个指向对象的shared_ptr
被销毁,对象就会被释放。即使有weak_ptr
指向对象,对象也还是会被释放。因此,weak_ptr
的名字抓住了这种智能指针 “弱” 共享对象的特点。 -
由于对象可能不存在,我们不能使用
weak_ptr
直接访问对象,而必须调用lock,
检查对象是否存在。如果存在,lock
返回一个指向共享对象的shared_ptr
,否则返回一个空shared_ptr。
这使得weak_ptr
可以用于检测所关联的shared_ptr
是否已被销毁,从而避免了循环引用问题。
shared_ptr<int> ptr1 = std::make_shared<int>(5);
weak_ptr<int> ptr2 = ptr1;//定义一个weak_ptr,使其指向shared_ptr类型的ptr1所指向的内存对象
cout << ptr1.use_count() << endl;//输出1,weak_ptr不占用shared_ptr的引用计数
cout << *ptr2.lock() << std::endl; // 输出5
ptr1.reset();//释放ptr1的内存对象
if (!ptr2.lock()) {
std::cout << "ptr1 has been destroyed." << std::endl; // 输出ptr1 has been destroyed.
}
weak_ptr的性质可以用来解决shared_ptr的循环引用问题,如下:
#include <iostream>
#include <memory>
class B; // 前置声明
class A {
public:
std::shared_ptr<B> bPtr;
~A() {
std::cout << "A destructor called." << std::endl;
}
};
class B {
public:
std::weak_ptr<A> aWeakPtr;
~B() {
std::cout << "B destructor called." << std::endl;
}
};
int main() {
std::shared_ptr<A> aPtr = std::make_shared<A>();
std::shared_ptr<B> bPtr = std::make_shared<B>();
aPtr->bPtr = bPtr;
bPtr->aWeakPtr = aPtr;
return 0;
}
在这个示例中,我们定义了两个类 A
和 B
,它们之间存在循环引用关系。A
类包含一个 shared_ptr<B>
成员变量,而 B
类包含一个 weak_ptr<A>
成员变量。
在 main()
函数中,我们创建了 A
类和 B
类的实例,并将它们互相引用。这样就形成了一个循环引用结构,如果不采取措施,这些对象将永远无法被销毁,导致内存泄漏。
但是,由于 B
类使用了 weak_ptr
来引用 A
类的对象,weak_ptr
不会增加引用计数。当 shared_ptr
指向 A
类的对象被销毁时,A
类的析构函数会被调用,并且与之关联的 B
类对象的 weak_ptr
将自动失效。
因此,在这个示例中,循环引用问题得到了解决,A
和 B
对象都能够正常销毁,并在其析构函数中输出相应的消息。
总之,通过使用 weak_ptr
,我们可以打破 shared_ptr
的循环引用,避免内存泄漏问题。weak_ptr
允许我们检测所关联的 shared_ptr
是否已经被销毁,从而解决了循环引用带来的资源管理问题。