智能指针概述
在谈智能指针之前我们首先来回顾一下之前学过的有关指针的知识,指针指向一块内存区域也可以说指针就是地址。
知识回顾
指针需要通过取址符或者动态内存分配来赋值,在C语言中使用 malloc/free 函数来管理动态内存的开辟和释放;在C++中使用 new/delete 关键字来管理。我们看下面一个实例
#define _CRT_SECURE_NO_WARNINGS
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG
#define new new (_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
//上面都是启用VS内存泄露捕捉需要的头文件定义
#include <iostream>
using namespace std;
int main() {
int *p = (int *) malloc(sizeof(int));
int *q = p;
*q = 10;
cout << "*p=[" << *p << "]" << endl;
_CrtDumpMemoryLeaks(); //启用VS内存泄露捕捉使用的语句
return 0;
}
在上面的实例中如果我们不修改程序,程序虽然可以正常运行,但是运行结束会造成4字节的内存泄露
这就要我们时刻记得自己申请的内存要在使用完毕以后进行释放,但是这又会带来一个问题,q
指向的地址跟p
指向的地址一样;如果我们在使用完p
以后进行释放了,那么后续如果再使用q
也会引发程序错误。所以对于指针的使用要求我们要格外的小心翼翼。
资源管理
C语言在进行资源管理的时候,比如文件指针,由于分支较多,或者由于写代码的人与维护的人不一致,导致分支没有写的那么完善,从而导致文件指针没有释放。
void UseFile(char const *fn) {
FILE *f = fopen(fn, “r”); // 1. 获取资源
// …… //2.使用资源
// 回收资源有很多分支
if (!g()) {
fclose(f);
return;
}
// ...
if (!h()) {
fclose(f);
return;
}
// ...
fclose(f); // 释放资源
}
根据之前单例对象自动释放的经验,我们可以想到利用对象的生命周期去管理资源。那么就可以尝试实现一个安全回收文件的程序了。
class SafeFile {
public:
// 在构造函数中初始化资源(托管资源)
SafeFile(FILE *fp)
: _fp(fp) {
std::cout << "SafeFile(FILE*) " << std::endl;
}
// 提供方法访问资源
void write(const std::string &msg) {
fwrite(msg.c_str(), 1, msg.size(), _fp);
}
// 利用析构函数释放资源
~SafeFile() {
std::cout << "~SafeFile()" << std::endl;
if (_fp) {
fclose(_fp);
std::cout << "fclose(_fp)" << std::endl;
}
}
private:
FILE *_fp;
};
void test() {
std::string msg = "hello,world";
SafeFile sf(fopen("wd.txt", "a+"));
sf.write(msg);
}
RAII技术*
以上例子其实已经用到了RAII的技术。所谓RAII,是C++提出的资源管理的技术,全称为Resource Acquisition Is Initialization,由C++之父Bjarne Stroustrup提出。其本质是利用对象的生命周期来管理资源(内存资源、文件描述符、文件、锁等),因为当对象的生命周期结束时,会自动调用析构函数。
RAII技术,具备以下基本特征:
- 在构造函数中托管资源;(在给构造函数传参时初始化资源)
- 在析构函数中释放资源;
- 一般不允许进行复制或者赋值(对象语义);
- 提供若干访问资源的方法(如:读写文件)。
对象语义与值语义
与对象语义相反的就是值语义。
值语义:可以进行复制或赋值(两个变量的值可以相同)
int a = 10;
int b = a;
int c = 20;
c = a; // 赋值
int d = c; // 复制
对象语义:不允许复制或者赋值
(全世界不会有两个完全一样的人,程序世界中也不会有两个完全一样的对象)
常用手段:
- 将拷贝构造函数与赋值运算符函数设置为私有的
- 将拷贝构造函数与赋值运算符函数=delete
- 使用继承的思想,将基类的拷贝构造函数与赋值运算符函数删除(或设为私有),让派生类继承基类。
我们可以实现以下的一个类模板,模拟RAII的思想
template <class T>
class RAII {
public:
// 1.在构造函数中初始化资源(托管资源)
RAII(T *data)
: _data(data) {
cout << "RAII(T*)" << endl;
}
// 2.在析构函数中释放资源
~RAII() {
cout << "~RAII()" << endl;
if (_data) {
delete _data;
_data = nullptr;
}
}
// 3.提供若干访问资源的方法
T *operator->() {
return _data;
}
T &operator*() {
return *_data;
}
T *get() const {
return _data;
}
void set(T *data) {
if (_data) {
delete _data;
_data = nullptr;
}
_data = data;
}
// 4.不允许复制或赋值
RAII(const RAII &rhs) = delete;
RAII &operator=(const RAII &rhs) = delete;
private:
T *_data;
};
如下,raii不是一个指针,而是一个对象,但是它的使用已经和指针完全一致了。这个对象可以托管堆上的Point对象,而且不用考虑delete。
void test0() {
Point *pt = new Point(1, 2);
// 智能指针的雏形
RAII<Point> raii(pt);
raii->print();
(*raii).print();
}
RAII技术的本质: 利用 栈对象 的生命周期管理资源,因为栈对象在离开作用域时候,会执行析构函数。
引入智能指针
为了解决上面的困境,C++ 11中引入了新的管理指针的办法——智能指针,引入新的内存管理办法以后 以往的指针的形式就被称为是原始指针(或者说是裸指针)。智能指针是一种封装了原始指针的类,它通过重载运算符 *
和 ->
来表现得像一个普通指针一样。
智能指针可以帮助我们进行动态分配内存对象(new
出来的对象)的生命周期的管理,能过有效的防止内存泄漏,除了已经废弃的auto_ptr
其余三种指针都是类模板,我们可以将new
获得的地址赋值给他们。
Tips
你忘记delete
操作的时间智能指针可以帮助你delete
,你压根就不需要自己再进行delete
了,或者说智能指针的本份就是帮你进行delete
。
智能指针的分类
C++ 标准库中有几种不同类型的智能指针
unique_ptr
:独占式指针
同一时刻只能有一个指针指向该对象,当然该对象的所有权还是可以移交出去的。
- 不能被复制,只能移动(
std::move
) - 适用于独占资源管理(如文件、网络连接)
- 用
std::make_unique<T>(args...)
创建()
shared_ptr
:共享式指针
- 采用 引用计数,多个 shared_ptr 可共享同一对象,最后一个销毁时释放资源。
- 存在循环引用风险,可配合 std::weak_ptr 解决。
- 用
std::make_shared<T>(args...)
创建,减少内存分配开销。
weak_ptr
:弱引用指针
是辅助shared_ptr
工作的
- 依赖 shared_ptr,不会增加引用计数。
- 用于解决 shared_ptr 循环引用问题。
- 可通过 lock() 获取 shared_ptr,判断对象是否仍然有效。
auto_ptr
C++98中引入,C++11弃用并在C++17移除
它们各自有不同的用途和行为。例如,unique_ptr
只允许一个智能指针拥有某个对象,而 shared_ptr
则允许多个智能指针共享同一个对象。
总之,原始指针和智能指针都可以用来访问内存中的对象,但智能指针提供了更好的内存管理和安全性。