若问起 Java 与 C++ 在使用体验上的差别,许多人都会提到「垃圾回收」这个词,Java 自带垃圾回收机制,而 C++ 没有。同样是面向对象的程序语言,这一点上的设计理念却如此大相径庭。有个古老的段子说,Java 的设计者认为:“内存管理这么重要的事情怎么能交给愚蠢的程序员呢!”,而 C++ 设计者认为:“内存管理这么重要的事情怎么能交给愚蠢的机器呢!”二者好像都有些道理。
C/C++ 的指针把计算机底层暴露给了程序员,在使其变得灵活强大的同时也增加了程序员的负担,特别是对初学者来说,内存忘记释放、野指针满天飞都是常见问题,而且没那么容易克服。即使是对有经验的程序员来说,随着项目规模变大,资源管理也会成为一个令人头疼的问题。
为了解决内存泄漏的难题,C++ 11 标准引入了三种智能指针:std::shared_ptr
,std::unique_ptr
,std::weak_ptr
。但只会用是不足够的,这篇文章就来说说智能指针的基本思想与实现方法。注意,文章中的代码自然与 std 的实现有所不同,旨在抓住思想本质,免去为了适应特殊情况而引入的更多细节。若实际项目中真遇到那些所谓「特殊情况」,则仔细研读 std 的代码是很好的方案。
裸指针有什么问题?
首先要明确的是我们究竟想要解决什么问题。在 C++ 中通过 new
关键字在堆上申请空间后,除非使用 delete
关键字释放这块资源,否则这块资源对系统来说就是被占用的,直到整个程序结束才会被回收。即使当前程序已经不再需要这块资源,或者已经丧失了对这块资源的控制,也是如此。一个简单的例子:
for(int i=0; i<100; i++)
char* p = new char[100];
这段代码如果出现在程序中,运行完毕之后计算机可用内存会减少 10000 个字节,这 10000 个字节就像凭空蒸发了一样,既无法在本程序的其它地方使用(p
的作用域仅限 for
循环内),也无法被其它程序使用(系统不会回收这块内存),这就是内存泄漏。如果你的程序多次运行了上面的代码,只要次数够多,迟早耗尽物理内存导致崩溃。这是忘了写 delete
的情况,还有一种情况:
int* p = new int[100];
try {
// 这里抛出了异常
} catch (...) {
// 这里程序结束
}
delete []p;
上面的例子中,由于抛出了异常而进入了 catch 块,程序无法执行后面代码而导致了内存泄漏。这就是异常不安全的问题。
分析问题需抓住本质。内存泄漏的根本原因是:指针变量与其指向的资源不一定会同时被释放。比如上面的例子,指针变量 p
离开 for
语句块则立即被释放,但是指向的资源却没有释放,这就导致没有指针是指向那块资源的,也就是程序丧失了对资源的控制。
从指针的行为来理解,这是理所当然的:如果有多个指针指向同一块资源,不可能其中一个指针变量释放了就去把资源也释放掉,因为如果这样的话,下面这段代码就会变得莫名其妙了:
char* getArray(int n) {
char *p = new char[n];
return p;
}
char* pArray = getArray(100); // 申请 100 个字节的资源
若指针行为真变成前面所说,即指针变量总是与指向资源同时释放,那么上面这段代码显然是错误的:p
在离开 getArray
函数后就被释放了,对应的那 100 字节也被释放掉,那么 pArray
就根本没用了。
由此可见指针与其指向的资源需要保持一定的独立性,不能做过分严格的绑定。那么如何做内存管理?
聪明的程序员们发现,虽然指针不应该与其对应资源同生同死,但是反过来想有一点是明确的:任何一块 new
出来的内存资源,如果没有任何指针指向它,那么它肯定是没用的,应该被释放掉。这就是智能指针的设计基础。
秘技:引用计数
如果 C++ 足够聪明,每块 new
出来的资源都知道去数一数有多少个指针变量指向它,并且在没有指针指向它的时候释放资源就好了。可惜 C++ 没有那么聪明,此事需要程序员自己动手。
Idea 已经有了,也就是对每一块 new
出来的资源都维护一个计数,保存所有指向它的指针数量,这个数就叫做引用计数。新增指针指向这块资源,引用计数自增;指针释放或者改去指向别的地方,引用计数自减。当引用计数减到零,就去释放资源。具有这个性质的指针即可称为「智能指针」。
智能指针设计
智能指针肯定不是一个简单结构,它至少要有以下性质:
- 包含两个信息:引用计数与所指向资源的裸指针
- 表现出指针的行为,对程序员来说它是透明的
- 对所有的数据类型都适用
这些需求决定智能指针一定是类的对象,并且为了让它适用于所有类型,它还得是模板类的对象。基本的结构如下:
template<class T>
class SmartPointer
{
public:
SmartPointer();
~SmartPointer();
// other methods
// ...
private:
T* m_Pointer; // 指向资源的指针
int* m_RefPointer; // 指向引用计数的指针
}
由于每块资源都只对应一个引用计数,因此智能指针中的引用计数要存储为其指针,这样每个智能指针都能操作这内存中唯一的一个引用计数。
现在要做的是围绕这个结构,完善其成员函数。
构造函数
通常,我们至少需要实现这几种构造函数:默认构造函数;从对象指针构造;从另一智能指针构造。为了方便使用,可再实现一种静态方法,用于替代 C++ 的 new
关键字,使封装性更高。
默认构造函数
template<typename T>
SmartPointer<T>::SmartPointer()
: m_Pointer(nullptr), m_RefPointer(nullptr){}
默认的,不指向任何资源,不存在引用计数。
从对象指针构造
template<typename T>
SmartPointer<T>::SmartPointer(T* target)
:m_RefPointer(nullptr), m_Pointer(target)
{
addReference();
}
从对象指针构造时,初始化成员指针为对象指针,并初始化引用计数为 1。这里特别需要注意,由代码实现可见,引用计数实际上保存的是指向资源的智能指针数,并不是所有指针数。这个问题的原因以及带来的问题及解决方法后面会提到。
从另一智能指针构造
template<typename T>
SmartPointer<T>::SmartPointer(const SmartPointer<T>& from)
: m_RefPointer(from.m_RefPointer), m_Pointer(from.m_Pointer)
{
addReference();
}
从另一智能指针构造时,除了初始化成员变量,由于增加了新的智能指针指向对应资源,还应该增加引用计数。
静态 New 方法
这本身不属于构造函数之列,但是有必要单独说明。观察上文的「从对象指针构造」的构造函数,构造完成后引用计数是 1,而不是 2。这说明引用计数保存的是指向资源的智能指针数,并不包括裸指针。这是因为只有智能指针才能维护引用计数,因此要避免裸指针与智能指针的混用。因此一般推荐再多实现一个静态的 New()
方法,提高智能指针的封装程度,避免直接使用 C++ 的 new
关键字。
// 声明
static T* New();
// 定义
template<typename T>
T* SmartPointer<T>::New()
{
return new T();
}
// 使用(这里需要重载 = 操作符,后文会说)
SmartPointer<T> pointerT = SmartPointer<T>::New();
上面第 11 行执行时会调用「从对象指针构造」的构造函数,但是并没有出现指向所开辟资源的裸指针,并且封装了 new
关键字,提高安全性的同时使智能指针的使用体验更统一。
析构函数
析构函数需要完成两件事:引用计数自减,若减至零,则释放资源。
template<typename T>
SmartPointer<T>::~SmartPointer()
{
removeReference();
}
引用计数管理函数
至少需要包括两个部分:增加引用计数与减少引用计数。
增加引用计数
template<typename T>
void SmartPointer<T>::addReference()
{
if (m_RefPointer)
(*m_RefPointer)++;
else
m_RefPointer = new int(1);
}
减少引用计数
template<typename T>
void SmartPointer<T>::removeReference()
{
if (m_RefPointer)
{
(*m_RefPointer)--;
if (*m_RefPointer == 0) // 若引用计数归零
{
delete m_RefPointer; // 删除引用计数
delete m_Pointer; // 释放指向资源
m_RefPointer = nullptr;
m_Pointer = nullptr;
}
}
}
操作符重载
为了使智能指针表现如同普通指针,需要对某些操作符进行重载。
相等判断
判断两个智能指针是否相等,也即是否指向相同的资源。
template<typename T>
bool SmartPointer<T>::operator == (const SmartPointer<T>& other) const
{
return m_Pointer == other.m_Pointer;
}
对应的有不等判断:
template<typename T>
bool SmartPointer<T>::operator != (const SmartPointer<T>& other) const
{
return !operator==(other);
}
作为赋值语句的左值
此时代表本智能指针不再指向原来的资源了,需要将原本资源的引用计数减一,并对新指向资源的引用计数加一。
template<typename T>
SmartPointer<T>& SmartPointer<T>::operator = (const SmartPointer<T>& that)
{
if (this != &that) // 避免自己给自己赋值
{
removeReference();
this->m_Pointer = that.m_Pointer;
this->m_RefPointer = that.m_RefPointer;
addReference();
}
return *this;
}
解引用
重载 *
操作符,实现裸指针的 *
方法。
template<typename T>
T& SmartPointer<T>::operator*() const
{
return *m_Pointer;
}
公共成员调用方法
重载 ->
操作符。
template<typename T>
T* SmartPointer<T>::operator->() const
{
return m_Pointer;
}
测试
以上就是最基本的一个智能指针实现了,完整的代码可以查看:
SmartPointer.h
#pragma once
/* 声明 */
template<typename T>
class SmartPointer
{
public:
SmartPointer(); // 默认构造函数
SmartPointer(T* target); // 从对象创建
SmartPointer(const SmartPointer<T>& from); // 从智能指针创建
~SmartPointer();
static T* New(); // 静态方法
// 操作符重载
bool operator==(const SmartPointer<T>& other) const;
bool operator!=(const SmartPointer<T>& other) const;
SmartPointer<T>& operator=(const SmartPointer<T>& that);
T& operator*() const;
T* operator->() const;
// 获取引用计数
int getRefCount();
protected:
void addReference();
void removeReference();
private:
T* m_Pointer;
int* m_RefPointer;
};
/* 实现 */
template<typename T>
SmartPointer<T>::SmartPointer()
: m_Pointer(nullptr), m_RefPointer(nullptr){}
template<typename T>
SmartPointer<T>::SmartPointer(const SmartPointer<T>& from)
: m_RefPointer(from.m_RefPointer), m_Pointer(from.m_Pointer)
{
addReference();
}
template<typename T>
SmartPointer<T>::SmartPointer(T* target)
:m_RefPointer(nullptr), m_Pointer(target)
{
addReference();
}
template<typename T>
SmartPointer<T>::~SmartPointer()
{
removeReference();
}
template<typename T>
T* SmartPointer<T>::New()
{
return new T();
}
template<typename T>
void SmartPointer<T>::addReference()
{
if (m_RefPointer)
(*m_RefPointer)++;
else
m_RefPointer = new int(1);
}
template<typename T>
void SmartPointer<T>::removeReference()
{
if (m_RefPointer)
{
(*m_RefPointer)--;
if (*m_RefPointer == 0) // 若引用计数归零
{
delete m_RefPointer; // 删除引用计数
delete m_Pointer; // 释放指向资源
m_RefPointer = nullptr;
m_Pointer = nullptr;
}
}
}
template<typename T>
bool SmartPointer<T>::operator == (const SmartPointer<T>& other) const
{
return m_Pointer == other.m_Pointer;
}
template<typename T>
bool SmartPointer<T>::operator != (const SmartPointer<T>& other) const
{
return !operator==(other);
}
template<typename T>
SmartPointer<T>& SmartPointer<T>::operator = (const SmartPointer<T>& that)
{
if (this != &that) // 避免自己给自己赋值
{
removeReference();
this->m_Pointer = that.m_Pointer;
this->m_RefPointer = that.m_RefPointer;
addReference();
}
return *this;
}
template<typename T>
T& SmartPointer<T>::operator*() const
{
return *m_Pointer;
}
template<typename T>
T* SmartPointer<T>::operator->() const
{
return m_Pointer;
}
template<typename T>
int SmartPointer<T>::getRefCount()
{
if (m_RefPointer)
return *m_RefPointer;
return -1;
}
我们编写代码测试一下:
main.cpp
#include <iostream>
#include "SmartPointer.h"
using namespace std;
class A // a class for test
{
public:
A() { cout << "创建对象:" << this << endl; };
~A() { cout << "释放对象:" << this << endl; };
void Print()
{
cout << "智能指针对程序员是透明的,当前对象地址:" << this << endl;
}
};
int main()
{
SmartPointer<A> sp_outer;
{
cout << "开始一个作用域\n";
SmartPointer<A> sp_1 = SmartPointer<A>::New();
cout << "从对象新建智能指针后,引用计数:" << sp_1.getRefCount() << endl;
SmartPointer<A> sp_2 = sp_1;
cout << "从智能指针复制后,引用计数:" << sp_2.getRefCount() << endl;
sp_1 = SmartPointer<A>::New();
cout << "智能指针作为左值被赋值,原对象引用计数:" << sp_2.getRefCount()
<< ",新对象引用计数:" << sp_1.getRefCount() << endl;
sp_1->Print();
sp_outer = sp_1;
cout << "\n********\n走出作用域,自动释放对象:\n";
}
cout << "\n********\n保存在外部作用域的资源在程序结束时才会释放:\n";
}
编译运行后输出:
开始一个作用域
创建对象:013F07B0
从对象新建智能指针后,引用计数:1
从智能指针复制后,引用计数:2
创建对象:013F0900
智能指针作为左值被赋值,原对象引用计数:1,新对象引用计数:1
智能指针对程序员是透明的,当前对象地址:013F0900
********
走出作用域,自动释放对象:
释放对象:013F07B0
********
保存在外部作用域的资源在程序结束时才会释放:
释放对象:013F0900
请按任意键继续. . .
可见目的已经达到。
其它讨论
上文所编写的 SmartPointer
类即是一个智能指针雏形,与之起相似作用的是 std::shared_ptr
。但是 C++11 标准还引入了两种指针:std::unique_ptr
、std::weak_ptr
,它们是干嘛的?
weak_ptr
智能指针有一个不能忽略的、很恼人的问题,那就是环状引用。看这个例子:
class ClassB;
class ClassA
{
public:
ClassA() { cout << "ClassA Constructor..." << endl; }
~ClassA() { cout << "ClassA Destructor..." << endl; }
SmartPointer<ClassB> pb; // 在A中引用B
};
class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
SmartPointer<ClassA> pa; // 在B中引用A
};
int main()
{
{
// 创建对象,spa 指向对象引用计数为 1
SmartPointer<ClassA> spa = SmartPointer<ClassA>::New();
// 创建对象,spb 指向对象引用计数为1
SmartPointer<ClassB> spb = SmartPointer<ClassB>::New();
spa->pb = spb; // 复制指针,spb 与 spa->pb 指向相同对象,引用计数为 2
spb->pa = spa; // 复制指针,spa 与 spb->pa 指向相同对象,引用计数为 2
}
// 作用域结束
// spa 销毁,spa 指向对象引用计数为 1
// spb 销毁,spb 指向对象引用计数为 1
// 引用计数都没有归零,因此不会释放资源
cout<< "作用域结\n" << endl;
}
运行一下就知道,在作用域结束后,资源并不会被释放。这就是环形引用,由于环的存在,引用计数始终不能清零,导致无法释放资源,造成内存泄漏。
环形引用是不能通过程序语言的设计来解决的,我们只能打破这个环,来避免这个情况,这就是 weak_ptr
的作用。
严格来说 weak_ptr
并不能算作是一个智能指针,因为它不能影响对象的生命周期,也就是它的创建与释放不会影响引用计数,它只是一个观测者而已。一个环形引用,若其中一个节点被换成了 weak_ptr
,那么这个环就不能称之为环,问题便解决了。
weak_ptr
并没有重载 *
和 ->
操作符,程序员是不能用它直接访问对象方法的。这是合理的,因为既然 weak_ptr
不能影响引用计数,就有可能存在一种情况:weak_ptr
指向的对象被销毁了,但自身还存在。此时需要特别地实现一种方法,来检测其指向的对象是否存在,若存在,返回一个真正的智能指针。std 的做法是 weak_ptr::lock()
方法。
unique_ptr
unique_ptr
是一种更严格的、更自私的智能指针,当它自身被析构时,它所指向的对象也一并被析构。 并且还有一个性质:它不允许除它以外的智能指针指向同一对象(也就是没有实现 copy 方法),不过可以通过 move 来实现转移所有权,也就是转移到另一个unique_ptr
来管理对象。unique_ptr
有利于异常安全,但是似乎用的稍微少一些。
写在后面
最近重新梳理 C++ 中的一些基础知识,总结出一个规律:越是安全越是高级的语言,越是不信任程序员。这些语言的设计理念就是要把可能出错的地方都自动处理好,而不是指望程序员处理。
所谓程序语言是否安全,难道是指机器在执行代码时出错的可能性吗?这种理解是错误的。一旦代码写定,机器执行二进制指令都是一样的,没有 C++ 编译出的 binary 比 Java 的 binary 更容易出错的说法。机器几乎不会出错,不论是在跑什么语言。
所谓安全性,是指程序员把事情搞砸的可能性。高级语言的从语言特性、语法规则设计的角度,尽可能地去把程序员瞎写带来的隐患降低,这是提高安全性的本质。这一点在 C++ 的 const 关键字上体现得尤为明显。降低了自由度是肯定的,但是带来了更强的鲁棒性,一切也变得和平起来。
这么一想,其中的政治意味竟然是如此浓厚。