文章目录
前言
一般来说,我们想要在堆上开辟内存空间需要使用关键字new,但由于使用new之后我们还是需要进行手动释放,我们却常常忘记delete,或者在不该delete的地方delete了,这就会导致程序出错。
因此C++11推出了智能指针,让程序管理内存,让程序对new出来的内存进行自动释放(在析构智能指针的时候同时delete对象),这样就能避免很多因为粗心产生的bug。
正文
(1) 三种智能指针
这三种智能指针都在头文件memory中:
- unsigned_ptr:可以有多个指针指向同一个对象,其中包含了一个计数器
- unique_ptr:独占指向的对象
- weak_ptr:指向shared_ptr所管理的对象
(2) 智能指针的设计思路
- 智能指针是类模板,在栈上创建智能指针对象
- 将普通指针交给智能指针对象
- 智能指针对象过期时,调用析构函数释放普通指针的内存
(其实还有个auto_ptr,但是在C++17中移除了,这里也就不说了)
(3) unique_ptr
它的意思是独享,也就是一个unique_ptr对象只对一个资源负责,当它析构的时候,指向的对象所分配的内存也随之释放。
因此,它在定义的时候禁用了拷贝构造函数和复制构造函数。
unique_ptr& operator=( const unique_ptr& ) = delete;
constexpr unique_ptr( std::nullptr_t ) noexcept = delete;
但是我们需要注意:指针和智能指针是两种类型!!!因此,我们不能直接使用智能指针接收new出来的对象:
#include<iostream>
#include<memory>
int main(){
// 正确
std::unique_ptr<int> ptr(new int(5));
// 正确,C++14的写法
std::unique_ptr<int> ptr2 = std::make_ptr<int>(int(5));
// 错误,不能直接使用智能指针接收直接new出来的对象
std::unique_ptr<int> ptr3 = new int(10);
// 错误,拷贝构造函数是已被删除的函数
// 在VS中会显示该函数已被删除
std::unique_ptr<int> temp_ptr(ptr);
}
它的底层原理其实也很简单:他的内部只有一个指针,这个指针指向的是它初始化时所指定的对象。
这里写一段示例代码:
#include <iostream>
#include <ostream>
#include <memory>
class AA{
public:
// 利用友元进行运算符重载
friend std::ostream& operator<<(std::ostream& os, const AA& temp);
AA(){
this->val = 0;
std::cout << "调用了AA的构造函数" << std::endl;
}
AA(const int&& value){
std::cout << "调用了AA的初始化构造函数" << std::endl;
this->val = std::move(value);
}
~AA(){
std::cout << "调用了AA的析构函数" << std::endl;
}
int showVal() const{
return this->val;
}
private:
int val;
};
// 需要注意ostream是命名空间std的成员
// 并且我上面没有手动使用std::
// 需要注意的点:被const声明的变量只能使用const声明的成员函数
std::ostream& operator<<(std::ostream& os, const AA& temp){
os << temp.showVal();
return os;
}
int main(){
AA* temp = new AA(100);
std::cout << *temp << std::endl;
std::unique_ptr<AA> auto_ptr(temp);
std::cout << "main函数运行中" << std::endl;
}
通过输出语句的顺序,我们就能够理解它的原理:
调用了AA的初始化构造函数
100
main函数运行中
调用了AA的析构函数
它其实就是在智能指针进行析构的时候,其析构函数中再调用了其所指向对象的析构函,以此来释放所分配的内存。
智能指针重载了输入输出运算符,因此我们可以像使用普通指针一样去使用智能指针。
这里需要注意一个问题:不要用同一个指针(也叫裸指针或者原始指针)去初始化不同的unique_ptr对象。这个原因也很简单,就是重复释放内存的,跟深浅拷贝一样。
unique_ptr的几种初始化方法
这里我就直接给出代码吧:
#include <memory>
using namespace std;
int main(){
unique_ptr<int> ptr1(new int(100)); // 分配内存并初始化
unique_ptr<int> ptr2 = make_unique<int>(100); // C++14标准
int temp = 100;
unique_ptr<int> ptr3(temp); // 用已存在的地址初始化
}
获取unique_ptr的地址
在C++20之前,unique_ptr没有重载输出运算符,所以我们不能像使用普通指针那样输出智能指针所指向的地址:
#include <memory>
#include <iostream>
int main(){
std::unique_ptr<int> ptr(new int(5));
// 错误:unique_ptr不支持输出运算符
std::cout << ptr;
}
想要获取其指向的地址,需要使用其成员函数get():
#include <memory>
#include <iostream>
int main(){
std::unique_ptr<int> ptr(new int(5));
// 错误:unique_ptr不支持输出运算符
std::cout << ptr.get() << std::endl;
std::cout << *ptr << std::endl;
}
由于智能指针为了模拟原始指针,而重载了"*“运算符和”->"运算符,因此我们能够像原始指针一样去使用它。
unique_ptr的使用
这里需要重点强调的是unique_ptr作为参数进行函数传递的时候,我们都知道函数传参有两种方式:值传递和引用传递。
前面我们说到:由于unique_ptr是独享的,因此它禁用了拷贝构造函数和复制构造函数,正是因此,unique_ptr在作为参数传递的时候只能使用引用传递:
#include <memory>
#include <iostream>
void func(std::unique_ptr<int> ptr){
std::cout << ptr.get() << "中,存储的数据是:" << *ptr << std::endl;
}
int main(){
std::unique_ptr<int> ptr = std::make_unique<int>(100);
func(ptr);
}
上面这段函数会报错,正式因为使用的是值传递,只需要将函数传参变为传引用即可:
void func(std::unique_ptr<int>& ptr){}
(4) shared_ptr
std::shared_ptr底层和std::unique_ptr差不多,但它底层增加了一个计数器,用于计算共有多少个shared_ptr共享同一个对象:
#include <memory>
#include <iostream>
int main(){
std::shared_ptr<int> ptr = std::make_shared<int>(5);
std::cout << "ptr = " << ptr.get() << std::endl;
std::cout << "共有" << ptr.use_count() << "个shared_ptr指向" << ptr << std::endl;
std::shared_ptr<int> ptr2 = ptr1;
std::cout << "共有" << ptr.use_count() << "个shared_ptr指向" << ptr << std::endl;
}
知道了unique_ptr,shared_ptr就很好理解了。
现在我们来看看shared_ptr的常用方法:
- get():和unique_ptr中的一样,获取指向底部管理着的对象的地址
- use_count():获取当前shared_ptr中的计数器