在没有智能指针之前,我们使用普通指针管理堆区的内存是一件很令人头疼的事,因为我们在使用delete关键字释放指针指向的内存的时候,很难知道是否有其它指针指向这个内存区域。
如果我们忘记释放该被释放的内存,就会导致内存溢出,如果我们释放了还有其他指针指向的内存,就会产生引用非法内存的指针。
C++11标准库提供了两种智能指针:shared_ptr 和 unique_ptr 来方便我们管理堆内存空间。智能指针的用法与普通指针非常相似,但是它能帮我们自动的释放内存空间。weak_ptr类是一种伴随类,指向shared_ptr所管理的对象,它是一种弱引用。这三种智能指针都定义在头文件 memory中
一、 shared_prt类
与vector相似,智能指针也是模版,定义一个智能指针的时候必须指定它可以指向的类型
shared_ptr<string> sp1;//可以指向string
shared_ptr<vector<int>>;//可以指向int的vector
我们可以使用make_shared()标准库函数来创建一个shreed_ptr对象。该函数接受一系列参数在堆区分配一个对象并初始化它,返回指向该对象的shared_ptr。make_shared()函数也定义在头文件memory中。
class Dog{
public:
Dog() = default;
Dog(std::string name,int age):name(std::move(name)),age(age){
}
void print(){
std::cout << "name: " << name << " age: " << age << std::endl;
}
private:
std::string name;
int age;
};
int main(){
std::shared_ptr<Dog> p1 = std::make_shared<Dog>("泰迪",3);
std::shared_ptr<std::string> p2 = std::make_shared<std::string>("message");
p1->print();
return 0;
}
如上述代码,调用make_shared的参数必须可以用来构造给定类型,例如,调用make_shared<Dog>传递的参数必须与Dog类中某个构造函数匹配。如果我们不传递任何参数,默认进行值初始化。
每个shared_ptr都会记录有多少个shared_ptr指向相同的对象。可以认为每个shared_ptr都有一个关联的计数器,当我们拷贝一个shared_ptr时,计数器会递增。例如用一个shared_ptr初始化另一个shared_ptr,或将shared_ptr传递给一个函数以及作为返回值,它所关联的计数器都会递增。当我们给shared_ptr赋予一个新值或shared_ptr被销毁,它之前关联的计数器会递减。一旦一个shared_ptr的计数器变为0,他就会自动释放管理的对象。
int main(){
std::shared_ptr<std::string> p1 = std::make_shared<std::string>("string");
std::cout << "智能指针p1的引用计数值为: " << p1.use_count() << std::endl;
{
auto p2= p1;
std::cout << "智能指针p1的引用计数值为: " << p1.use_count() << std::endl;
std::cout << "智能指针p2的引用计数值为: " << p2.use_count() << std::endl;
}
std::cout << "智能指针p1的引用计数值为: " << p1.use_count() << std::endl;
return 0;
}
这段代码的运行结果是 1 2 2 1,首先定义并初始化了一个p1,它当前的引用计数为一,因为只有它一个人指向“string”字符串所在的堆内存,然后再一个作用域中将p1拷贝给p2,此时p1和p2关联在同一个引用计数器,p1和p2的引用计数都是2。离开{}后p2被销毁,p1的引用计数递减。
shared_ptr<T> sp | 空智能指针,可以指向类型为T的对象 |
if(p),while(p)... | 将p用于条件判断,若p指向一个对象,则为true |
*p | 解引用,获得它指向的对象 |
p-> | 等价于(*p).mem |
p.get() | 返回p中保存的常规指针 |
swap(p,q) | 交换p和q中的指针 UIOP[]\\\` Q23W4E5R6TY7U8IOP \`67ZC VBNM,./ |
p.swap(q) | 同上 |
make_shared<>(args) | 返回一个shared_ptr,指向一个动态分配的类型为T的对象,用args初始化此对象 |
shared_ptr<T>p(q) | p是 shared_ptr q的拷贝,此操作会递增q中的引用计数。q中的指针必须能转换为T* |
p = q | p和p都是shared_ptr,所保存的指针要可以相互转换,此操作会递增q的引用计数,递减p原来的关联的引用计数 |
p.unqiue() | 若p.use_count()为1,返回true,否则返回false |
p.use_count() | 返回p共享对象的指针数量 |
程序使用堆内存空间的原因之一就是需要在多个对象之间共享数据。
class Book{
public:
Book(int size):content(new char [size]){
}
Book(const Book& book){
content = book.content;
}
~Book(){
}
private:
char* content;
};
假设现在有一个Book类,我们希望同个一个Book类的实例拷贝出的对象共享同一个底层数据(content)。那么问题来了,假如通过一个Book对象拷贝了很多个实例,并且传递到了程序很多个地方,那么,怎么保证这被共享的底层数据被准确的被释放。显然,我们要使用智能指针。
class Book{
public:
Book(int size):content(std::shared_ptr<char>(new char[size],[](const char* p){
delete[] p;
})){
}
Book(const Book& book){
content = book.content;
}
~Book(){
}
private:
std::shared_ptr<char> content;
};
使用智能指针,当最后一份Book实例被销毁时,shared_ptr的引用计数变为0,指向的堆内存被释放。
shared_ptr不支持直接管理动态数组,如果希望使用shared_ptr管理动态数组,则必须提供自定义的删除器。
int main(){
shared_ptr<int> p = shaed_ptr(new int[1024],[](const int* d){delete[] d})
}
shared_ptr<T> p(q); | p管理内置指针所指向的对象,q必须是指向ew分配的内存,且能转换为T* 类型 |
shared_ptr<T> p(u) | 从unqiue_ptr u 对象上接管所有权,并将u置空 |
shared_ptr<T> p(q,d) | p接管内置指针q,q必须能转换成T*,d是一个可调用对象,用来替代默认的释放内存机制 |
shared_ptr<T> p(p2,d) | p是shaed_ptr p2的拷贝,p将使用d来进行释放 |
p.reset() | 若p是唯一指向其对象的shared_ptr,则reset会释放此对象 |
p.reset(q) | 令p指向内置指针q |
p.reset(q,d) | 令p指向内置指针q,使用d进行释放 |
使用智能指针的一些注意事项:
- 不要混合使用普通指针和智能指针
当将一个shared_ptr绑定到一个内置指针上时,我们就将内存管理的责任交给了shared_ptr。就不应该使用内置指针。避免对内置指针进行误操作导致智能指针指向的内存空间被释放
- 不要使用p.get()的返回值初始化另一个指针指针或为智能指针赋值
当我们使用get()的返回值为初始化另一个智能指针时,实际上就有两个独立的智能指针管理同一个指针,这样可会导致指针指向的内存被提起释放。
shared_ptr<int> p(new int(42));
int* p = p.get();
{
shared_ptr<int> tmp(p);
}//代码块结束,tmp被释放,同时p的内存也被释放
int val = *p; //err p指向的内存已经被释放
二、unique_ptr类
与shared_pet不同,某个时刻只能有一个unique_ptr指向一个给定的对象。当unique_ptr被销毁时,它所指向的对象也被销毁。没有标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到new返回的指针上。
unique_ptr<double> p1;
unique_ptr<int> p2(new int(42));
由于只能有一个unique绑定到一个特定的对象上,所以unique不支持普通的拷贝和赋值
unique_ptr<string> p1(new string("string"));
unique_ptr<string> p2(p1);//err:unique_ptr不支持拷贝操作
unique_ptr<string> p3;
p3 = p1; //err: unique_ptr不支持赋值操作;
unique_ptr<T> sp | 空智能指针,可以指向类型为T的对象 |
if(p),while(p)... | 将p用于条件判断,若p指向一个对象,则为true |
*p | 解引用,获得它指向的对象 |
p-> | 等价于(*p).mem |
p.get() | 返回p中保存的常规指针 |
swap(p,q) | 交换p和q中的指针 |
p.swap(q) | 同上 |
unqiue_ptr<T,D> u2 | 空unqiue_ptr,指向T类型的对象,使用类型为D的可调用对象来释放指针 |
unqiue_ptr<T<D> u(d) | 空unqiue_ptr,指向类型为T的对象,可以使用类型为D的对象d来释放它的指针 |
u = nullptr | 释放u指向的对象,将u置空 |
u.release() | u放弃对指针的控制,返回指针,并将u置空 |
u.reset() | 释放u指向的对象 |
u.reset(q) | 如果提供了内置指针q,令u指向这个对象 |
u.reset(nullptr) |
虽然不能拷贝或赋值unique_ptr,但可以调用release或reset将指针的所有从一个(非const)unique_ptr转移给另一个unique_ptr
//将所有权从p1转移给p2
unique_ptr<string> p2(p1.release);
//将所有权从p3转移给p2
unqiue_ptr<string> p3(new string("text"));
p2.reset(p3.release()); //reset释放了p2原来指向的内存
- 调用release返回的指针通常用来初始化另一个智能指针或给另一个智能指针赋值。
- 虽然unique_ptr不支持拷贝构造和移动构造,但是支持移动构造。
unique_ptr<int> create(int p){
return unqiue_ptr<int>(new int(p)); //返回一个右值,会优先调用移动构造
}
unique_ptr<int> create(int p){
unqiue_ptr<int> tmp(new int(p));
return tmp;
}
三、weak_ptr类
weak_ptr是一种弱智能指针。它不控制所指对象的生命周期,指向一个由shared_ptr管理的对象。将weak_ptr绑定到一个shared_ptr上不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被释放,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
weak_ptr<T> w | 空weak_ptr,可以指向类型为T的对象 |
weak_ptr<T> w (sp) | 与shared_ptr指向相同对象的weak_ptr |
w = p; | p可以是一个shared_ptr或一个weak_ptr,赋值后w与p共享对象 |
w.reset() | 将w置空 |
w.use_count() | 与w共享对象的shared_prt数量 |
w.expired() | 若w指向的对象存活返回false,否则返回true |
w.lock() | 如果expire()返回true,返回一个空shared_ptr,否则返回一个指向w对象的shared_ptr |
当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它。
auto p = make_shared<int>(23);
weak_ptr w(p);
w和p指向相同的对象,由于w是弱共享,创建w不会改变p的引用计数。
由于使用weak_ptr它指向的对象可能不存在,所以不能直接使用weak_ptr,必须调用lock锁定
if(shared_ptr<int> np = w.lock()){ //如果np不为空则条件成立
//使用np
}