一、智能指针介绍
C++11标准引入了智能指针,该指针可以减少程序的内存泄漏,提高安全性。智能指针被定义在<memory>头文件的 std 命名空间中。
C++相比C的重要特性就是类概念的引入;C++还有诸如泛型编程的性质,可以把类和函数模板化,提高代码的复用率。因而,智能指针被设计成了一个类模板。使用者可以参照类模板的方式使用智能指针。智能指针模板类的成员变量包括具体类型的指针(可以是int型指针、float指针等,也可以是指向自定义类的指针,在现代C++中,这些区别于智能指针的原始指针被统称为裸指针)以及其他变量。智能指针模板类也提供了一些成员方法,进行类似delete关键字那样清除内存空间的操作;同时,由于可以进行运算符重载,用 -> 和 * 对智能指针进行操作也具备了可能性。
C++11提供了三种类型的智能指针,分别是 unique_ptr 、shared_ptr 、 weak_ptr ,分别具有不同的功能和应用场景。在之后的章节中会详细介绍。
下面,以unique_ptr为例介绍一下智能指针的基本使用方法。
#include <memory>
#include <iostream>
class robot
{
private:
int id;
public:
void Showid()
{
std::cout<<id<<std::endl;
}
robot(int param_id)
{
id=param_id;
}
};
int main(void)
{
std::unique_ptr<robot> robotptr0(new robot(0));
robotptr0->Showid();
robot* rawptr0=robotptr0.get();
rawptr0->Showid();
robotptr0.reset();
robotptr0->Showid(); //非法
}
1、如何声明创建指针
一般而言,利用智能指针这一模板类自己的构造函数,构造函数的参数是特定类型的裸指针。一旦智能指针被创建,它便会开始管理指针所指向的变量。对于unique_ptr,一旦指针自身被销毁,那么它会自动销毁其所指向的变量。(这也是智能指针最具价值的性质)
std::unique_ptr<robot> robotptr0(new robot(0));
在C++14及以上的版本中,允许使用make_unique来代替这个过程。
std::unique_ptr<robot> robotptr1=std::make_unique<robot>(1);
robotptr1->Showid();
(有人研究源码发现,这个make_unique只是把第一种方法封装了一下)
2、使用 -> 和 * 指针运算符
robotptr0->Showid();
如上所示,智能指针也可以用 -> 这一的指针运算符调用类方法。* 操作符也可以使用。
3、智能指针类的方法
get方法可以返回同类型的裸指针,reset方法可以提前释放智能指针所指向空间的内存,提前销毁智能指针所指向的变量。
robot* rawptr0=robotptr0.get();
rawptr0->Showid();
robotptr0.reset();
robotptr0->Showid(); //非法
二、介绍 unique_ptr
众所周知,任何变量都有自己的内存空间。指针类变量也应当有自己的内存空间。对于裸指针,我们能做到一下操作:
#include <iostream>
int main(void)
{
int* ptra=new int;
*ptra=2024;
int* ptrb=ptra;
std::cout<<*ptrb;
}
输出:
2024
虽然ptra和ptrb是两个变量,分别占用不同的内存空间,但由于ptrb被ptra赋值,它同样能访问ptra所指向的变量。
这就带来一个问题:如果释放ptra的内存空间,并销毁了ptra,那么通过ptrb访问空间就是非法的了。unique_ptr 就解决了这样一个问题。它通过自身的规定,让程序在任何时候都只允许有一个智能指针指向特定变量,否则就会发生错误。
因此, 它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何 C++ 标准库算法。
1、创建unique_ptr
std::unique_ptr<robot> robotptr0(new robot(0));
robotptr0->Showid();
std::unique_ptr<robot> robotptr1=std::make_unique<robot>(1);
robotptr1->Showid();
如上一节介绍,可以使用这两种方法来创建unique_ptr。
2、移动unique_ptr
尽管unique_ptr无法复制,但是可以将其移动。这便是说,原来是unique_ptr a 指向某变量,现在unique_ptr a 放弃指向a,转而让unique_ptr b 指向a 。这并不违反程序在任何时候都只允许有一个智能指针指向特定变量的规则。
利用std::move函数可以实现此操作。
std::unique_ptr<robot> robotptr2=std::move(robotptr1);
#include <memory>
#include <iostream>
class robot
{
private:
int id;
public:
void Showid()
{
std::cout<<id<<std::endl;
}
robot(int param_id)
{
id=param_id;
}
};
int main(void)
{
std::unique_ptr<robot> robotptr0(new robot(0));
robotptr0->Showid();
std::unique_ptr<robot> robotptr1=std::make_unique<robot>(1);
robotptr1->Showid();
std::unique_ptr<robot> robotptr2=std::move(robotptr1);
robotptr2->Showid();
}
输出:
0
1
1
此时,再使用robotptr1去访问就非法了!
3、unique_ptr 作为类成员变量可以被列表初始化
class MyClass
{
private:
// unique_ptr factory 作为类成员变量
unique_ptr<ClassFactory> factory;
public:
// 使用列表初始化对factory这一智能指针进行操作
MyClass() : factory (make_unique<ClassFactory>())
{
}
void MakeClass()
{
factory->DoSomething();
}
};
当然,直接在构造函数内部初始化也是可以的!
三、介绍 shared_ptr
unique_ptr智能指针虽然通过强制的规则保障了安全性,但很大程度上丧失了指针的灵活性。因此,引入了shared_ptr。shared_ptr 就允许同一时间有多个智能指针指向同一变量。同时,对于每个被shared_ptr智能指针指向的变量,创建一个控制块,该控制块可以记录有多少个shared_ptr正在指向该资源。如果它检测到没有shared_ptr指向自身关联的资源,则它会自我销毁并释放变量占用的内存空间。
1、首次创建shared_ptr
初次创建shared_ptr时,显然程序是要准备控制块的。所以,需要使用如unqiue_ptr的那两种方法创建。推荐使用make_shared函数,也可以使用shared_ptr的构造函数。
int main(void)
{
/*方法一*/
std::shared_ptr<robot> robotptr0=std::make_shared<robot>(0);
robotptr0->Showid();
/*方法二*/
std::shared_ptr<robot> robotptr1(new robot(0));
robotptr1->Showid();
}
2、复制shared_ptr
shared_ptr 就允许同一时间有多个智能指针指向同一变量。对于第二个、第三个······指向同一变量的智能指针,可以用第一个shared_ptr的复制构造函数进行赋值,也可以直接用 = 进行赋值(应该是已经重载运算符了)。
int main(void)
{
std::shared_ptr<robot> robotptr0=std::make_shared<robot>(0);
robotptr0->Showid();
std::shared_ptr<robot> robotptr1(robotptr0);
std::shared_ptr<robot> robotptr2=robotptr0;
auto robotptr3(robotptr1);
auto robotptr4=robotptr1;
robotptr0.reset();
robotptr3->Showid();
}
输出:
0
0
上面的代码展示了两种方法。这里还展示了auto关键字的作用,可以用于自动推断类型,增加代码可读性。
可以看到,即使我们对robotptr0对象使用了reset方法,但其指向的robot类对象仍然没有被销毁。
3、shared_ptr作为函数参数
按值传递shared_ptr。 这将调用复制构造函数,增加引用计数,并使被调用方成为所有者。
按引用或常量引用传递shared_ptr。 在这种情况下,引用计数不会增加,只要调用方不超出范围,被调用方就可以访问指针。 或者,被调用方可以决定基于引用创建一个shared_ptr,并成为一个共享所有者。
四、介绍weak_ptr
weak_ptr-CSDN博客一文详细介绍了一种需要weak_ptr的情况。由于在shared_ptr实例之间有循环引用,会造成无法释放内存的情况。
weak_ptr可以像shared_ptr一样指向同一个对象,但增加引用计数。也即如果指向某一内存空间的最后一个shared_ptr被销毁了,那么指向同一内存空间的weak_ptr就无法访问对象了。
智能的地方在于,如果所有shared_ptr被销毁,那么由该种shared_ptr创建的weak_ptr会自动变为nullptr。
1、创建weak_ptr
使用复制构造函数或者 = 运算符创建
int main(void)
{
std::shared_ptr<robot> robotptr0=std::make_shared<robot>(0);
std::weak_ptr<robot> weak0(robotptr0);
std::weak_ptr<robot> weak1=robotptr0;
}
2、weak_ptr不能使用->和*运算符
由于weak_ptr是不稳定的,不一定能指向正确的内存空间,这两个重载的运算符就被禁用了。
3、weak_ptr的lock()方法
对于指向内存空间未过期的weak_ptr,调用lock()方法会自动创建一个指向同一内存空间对应类型的shared_ptr,并作为返回值。对于指向内存空间过期的weak_ptr,调用lock()方法会返回nullptr。
int main(void)
{
std::shared_ptr<robot> robotptr0=std::make_shared<robot>(0);
std::weak_ptr<robot> weak0(robotptr0);
std::shared_ptr<robot> robotptr1=weak0.lock();
}
4、use_count()方法
返回一个整数,是当前所指变量的share_ptr指针的数量。
#include <memory>
#include <iostream>
class robot
{
private:
int id;
public:
void Showid()
{
std::cout<<id<<std::endl;
}
robot(int param_id)
{
id=param_id;
}
};
int main(void)
{
std::shared_ptr<robot> robotptr0=std::make_shared<robot>(0);
std::weak_ptr<robot> weak0(robotptr0);
std::shared_ptr<robot> robotptr1=weak0.lock();
std::cout<<weak0.use_count();
}
输出:
2
5、expired() 方法
返回一个bool值,查看所指向的内存空间是否被释放。如果被释放,则返回true。
此函数与 use_count() == 0 意义相同。