一、前言
我们知道,c/c++的内存,对程序员来说,是裸露的,所以可以拿到真实的地址,所以容易造成各种内存问题。比如:
野指针:未初始化或已经被释放的指针;
空指针:指向空地址的指针;
内存泄漏:在使用完动态分配的内存后没有释放(即delete或free);
悬挂指针:指向已经释放内存的指针;
等等......
二、RAII
简介
在认识智能指针前,我们先了解一下RAII,我简单介绍一下,RAII(Resource Acquisition Is Initialization)是由 C++ 之父Bjarne Stroustrup提出的,是一种编程技术,翻译为:资源获取即初始化。
它将在使用前获取的资源的生命周期,与对象的生命周期绑定到一起。(这些资源可以是:分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥量、磁盘空间、数据库连接等有限资源)
确保在控制对象的生命周期结束时,按照资源获取的相反顺序释放所有资源。同样的,如果资源获取失败,则按照初始化的相反顺序释放所有已完全构造的成员和基类子对象所获取的资源,它的生命周期由操作系统来管理,无需程序员介入。
这利用了核心语言特性(对象生命周期、作用域退出、初始化顺序和堆栈展开),以消除资源泄漏并确保异常安全。
原理
它的原理就是:利用栈上局部变量的自动析构,来保证资源一定会被释放。
我们知道,实例对象时会自动调用构造函数,销毁对象时会自动调用析构函数,这些都是通过编译器来执行的,所以如果资源的释放与对象的构造和析构在一起,就不会出现上述问题。
实现步骤
- 设计一个类来封装资源(可以是文件,内存,socket,锁等等);
- 在构造函数中执行资源的初始化,比如申请内存,打开文件,申请锁;
- 在析构函数中执行销毁操作,比如释放内存,关闭文件,释放锁;
- 使用时声明一个该类的对象,在函数开始位置,或者类的成员变量;
这里利用RAII思想,实现一个File类。
#include <fstream>
#include <string>
using namespace std;
//RALL思想
class File {
public:
File(const char *file) {
m_handle = ifstream(file);
cout << "文件已打开" << endl;
}
~File() {
if (m_handle.is_open()) {
m_handle.close();
cout << "文件已关闭" << endl;
}
}
ifstream& getHandle() {
return m_handle;
}
void readFile() { //输出文件内容...
string line;
while (getline(m_handle, line)) {
cout << line << endl;
}
}
private:
ifstream m_handle;
};
int main() {
File myfile("1.txt");
if (myfile.getHandle().is_open()) {
cout << "打开文件成功,输出文件内容:";
myfile.readFile();
}
else {
cout << "打开文件失败" << endl;
}
return 0;
}
输出:
文件已打开
打开文件成功,输出文件内容:hello,world!
文件已关闭
/*这样,在程序退出时,File类的析构函数会自动被调用,从而自动关闭文件,即使程序提前退出或者发生异常,也不会产生内存泄漏等问题*/
三、概念
智能指针:就是一种可以自动管理内存的指针,它可以在不需要手动释放内存的情况下,确保对象正确的被销毁,可以显著降低内存泄漏和悬挂指针的问题;
智能指针的核心思想就是RAII。
头文件是<memory>
;
#include <memory>
常用的智能指针:
std::unique_ptr
,唯一指针;
std::shared_ptr
,共享指针,(经常要与weak_ptr
配合使用);
唯一指针:
unique_ptr
是一个独占所有权的智能指针,它保证指向的内存只能由一个由它拥有,不能共享拥有权,离开作用域自动释放指向的内存;
#include <iostream>
#include <memory>
#include <string>
using namespace std;
int main() {
unique_ptr<int> u1 = make_unique<int>(10); // c++14标准新增
unique_ptr<int> u2(new int(20));
cout << *u1 << endl;
cout << *u2 << endl;
unique_ptr<string> u3 = make_unique<string>("hello ");
unique_ptr<string> u4(new string("world!"));
cout << *u3 << endl;
cout << *u4 << endl;
return 0;
}
共享指针:
shared_ptr
是一个共享所有权的智能指针,它允许多个shared_ptr指向同一对象,当最后一个shared_ptr
离开作用域, 自动释放指向的内存;
#include <memory>
int main() {
shared_ptr<int> s1 = make_shared<int>(10);
shared_ptr<int> s2(new int(20));
cout << "s1的值:" << *s1 << " 引用计数:" << s1.use_count() << " 地址:" << s1 << endl;
cout << "s2的值:" << *s2 << " 引用计数:" << s2.use_count() << " 地址:" << s2 << endl;
//调用 拷贝构造函数 和 重载=
shared_ptr<int> s3(s1);
shared_ptr<int> s4 = s2;
shared_ptr<int> s5 = s4;
cout << "s1引用计数:" << s1.use_count() << endl;
cout << "s2引用计数:" << s2.use_count() << endl;
cout <<"s3的值:" << *s3 << " 引用计数:" << s3.use_count() << " 地址:" << s3 << endl;
cout <<"s4的值:" << *s4 << " 引用计数:" << s4.use_count() << " 地址:" << s4 << endl;
cout <<"s5的值:" << *s5 << " 引用计数:" << s5.use_count() << " 地址:" << s5 << endl;
return 0;
}
s1的值:10 引用计数:1 地址:00CD058C
s2的值:20 引用计数:1 地址:00CDF0A8
s1引用计数:2
s2引用计数:3
s3的值:10 引用计数:2 地址:00CD058C
s4的值:20 引用计数:3 地址:00CDF0A8
s5的值:20 引用计数:3 地址:00CDF0A8
'会发现通过拷贝和=后,他们指向的地址是同一块,即它们共享同一个对象';
'use_count(),引用计数本身是使用指针实现的,也就是将计数变量存储在堆上,所以共享指针的shared_ptr 就存储一个指向堆内存的指针;
weak_ptr
解决共享指针循环引用的问题,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,即就是将一个weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数。具体实现可参考站内详细的文章。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int>a1(new int(100));
cout << "shared_ptr引用计数:" << a1.use_count() << endl;
weak_ptr<int>a2(a1);
cout << "weak_ptr引用计数:" << a2.use_count() << endl;
return 0;
}
输出:
shared_ptr引用计数:1
weak_ptr引用计数:1
//可以发现,在使用weak_ptr后,引用计数并没有增加。
四、智能指针的实现
之前介绍过,智能指针的核心思想是RAII,所以我们也可以根据这个思想来模拟实现一个共享指针。
共享指针shared_ptr的实现关键在于引用计数,设计思路如下:
- 在类中定义一个指针和引用计数指针;
- 在构造函数中为指针和引用计数指针分配内存;
- 在拷贝和赋值运算符中更新引用计数指针;
- 在析构函数中递减引用计数指针,并在引用计数指针为0时删除对象和引用计数指针;
实现代码:
//用于测试的Test类
class Test
{
public:
Test() {
cout << "Test()构造函数" << endl;
}
~Test() {
cout << "~Test()析构函数" << endl;
}
void Output() const {
cout << "Output()成员函数" << endl;
}
};
//具体实现
template <class T>
class Shared_Ptr
{
private:
T *m_ptr;
size_t *m_count;
//释放函数
void Free() {
if (m_count && --(*m_count) == 0) {
delete m_ptr;
delete m_count;
}
}
public:
//缺省的构造函数
Shared_Ptr(T *ptr = nullptr) {
this->m_ptr = ptr;
if (m_ptr) {
this->m_count = new size_t(1);
}
else {
this->m_count = nullptr;
}
}
//拷贝构造函数
Shared_Ptr(const Shared_Ptr &other) {
m_ptr = other.m_ptr;
m_count = other.m_count;
if (m_count) {
++(*m_count);
}
}
//重载=运算符
Shared_Ptr & operator = (const Shared_Ptr &other) {
if (this != &other) { //自赋值检查,如果是指向的同一对象,则返回当前对象的引用*this
Free();
}
m_ptr = other.m_ptr;
m_count = other.m_count;
if (m_count) {
++(*m_count);
}
return *this;
}
//析构函数,调用释放函数
~Shared_Ptr() {
Free();
}
//重载->运算符,返回被管理者的指针
T *operator->() const {
return m_ptr;
}
//返回被被管理者的指针
T *get() const {
return m_ptr;
}
//重载*运算符,返回被管理者
T &operator*() const {
return *m_ptr;
}
size_t Use_Count() {
return m_count ? *m_count : 0;
}
};
int main() {
Shared_Ptr<int> sp1(new int);
Shared_Ptr<int> sp2 = sp1;
{
Shared_Ptr<int> sp3;
sp3 = sp1;
cout << "引用计数:" << sp2.Use_Count() << endl;
}
cout << "引用计数:" << sp2.Use_Count() << endl;
Shared_Ptr<int> sp4(new int(100));
cout << "*sp4 = " << *sp4 << ", 引用计数:" << sp4.Use_Count() << endl;
Shared_Ptr<Test> sp5(new Test);
sp5->Output();
sp5.get()->Output(); //返回被管理者的指针,访问成员函数
return 0;
}
输出:
引用计数:3
引用计数:2
*sp4 = 100, 引用计数:1
Test()构造函数
Output()成员函数
Output()成员函数
~Test()析构函数
以上我们就实现了一个共享指针,它具有共享指针的基本功能,自动释放资源,引用计数等等;
五、总结
基于RAII思想,我们可以实现很多这种自动管理资源的功能,确保资源的获取和销毁都能被正确执行,使用这种编程技巧,可以提高程序的可靠性和可维护性。
本文为C++基础知识分享,本人水平有限,如有错误和不足,欢迎评论区留言!