1 序
1.1 智能指针
为了避免手动在堆中分配出的内存没有释放造成内存泄露的问题,C++11提供了智能指针。
智能指针将指针封装成一个栈对象。栈对象在生命周期结束自动销毁时会调用析构函数。智能指针基本上也是在析构函数中做文章,实现的堆上内存管理。
C++11推荐使用的智能指针有unique_ptr、shared_ptr和weak_ptr三种。
1.2 内容简介
本文为std::unique_ptr的实现篇,介绍实现一个UniquePtr类,实现与std::unique_ptr类似的功能。代码实现参考了std::unique_ptr的实现,当然仅仅是简单的实现,准确的说是实现了std::unique_ptr的一种特例。
实现UniquePtr的目的仅仅是为了更直观理解学习std::unique_ptr的用法和其实现中的亮点,并不是为了替代或者在工程中使用。代码都是需要时间去修改稳定的。大家公认的轮子,特别STL的轮子,直接学习使用就行了,不要自己搞。
1.3 测试环境
系统:Windows | 10 ;
编译环境:CLion | 2020.1.1 ;
编译工具:CMake | 3.16.5;
编译器:MinGW GCC | 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project) ;
默认情况下gcc编译器会自动优化临时对象构造新对象的行为。为了更好的了解对象的构造流程,需要在CMake中添加如下命令关闭该优化。
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-elide-constructors")
1.4 示例说明
为了便于查看UniquePtr所托管指针的申请和释放,示例使用下面的自定义Test类。
class Test {
public:
explicit Test(int a = 0) : a_(a), str_(new char[32]) { // explicit禁用隐式类型转换
sprintf(str_, "%d", a_);
printf("Test[%p].[%s] constructor\n", this, str_);
}
~Test() {
printf("~Test[%p].[%s]\n", this, str_);
delete[] str_;
}
// ...
public:
int a_;
char *str_;
};
2 std::unique_ptr自定义deleter
std::unique_ptr支持传入自定义deleter,在std::unique_ptr对象释放析构的时候调用会调用该deleter释放托管指针内存。在不传入deleter的情况下,默认的deleter使用delete释放托管指针内存。
当使用std::unique_ptr去托管new[]生成的指针时,需要使用delete[]去释放托管指针内存。此时,默认的deleter就不可用,需要传入自定义的deleter。有了自定义的deleter,std::unique_ptr甚至可以托管文件流、套接字(socket)等。
2.1 仿函数方式
托管new[]的自定义的deleter可以使用仿函数实现。实现一个仿函数,在其中调用delete[]。
template<typename T>
struct array_deleter {
void operator()(T *p) {
delete[] p;
}
};
然后,使用如下方式传入array_deleter。
std::unique_ptr<Test, array_deleter<Test>> t1(new Test[10]);
在t1析构的时候会自动调用array_deleter,释放托管指针内存。
2.2 匿名函数方式
托管new[]的自定义的deleter可以使用匿名(lambda)函数实现。实现一个匿名函数,在其中调用delete[]。为了让代码看起来简洁可以将匿名函数存储在一个lambda_deleter变量中。
auto lambda_deleter = [](Test *p) -> void {
delete[] p;
};
然后,使用如下方式传入lambda_deleter。
std::unique_ptr<Test, decltype(lambda_deleter)> t2(new Test[10], lambda_deleter);
在t2析构的时候会自动调用lambda_deleter,释放托管指针内存。
2.3 普通函数方式
下文自定义的deleter使用普通函数实现,示例分别实现文件流和套接字的托管。注意下,自定义的deleter传入方式,有些小复杂。
2.3.1 托管文件流
实现一个file_deleter函数,在其中调用fclose。
void file_deleter(std::FILE *fp) {
std::fclose(fp);
}
然后,使用如下方式传入file_deleter。
std::unique_ptr<std::FILE, decltype(&file_deleter)> fp(std::fopen("test.txt", "wb+"), &file_deleter);
在fp析构的时候会自动调用file_deleter,释放托管文件流。
2.3.2 托管套接字(socket)
可以使用类似方式实现套接字的托管。实现一个socket_deleter函数,在其中调用closesocket。
void socket_deleter(SOCKET *s) {
printf("socket_deleter\n");
closesocket(*s);
*s = 0;
}
由于套接字创建函数socket()返回值并不是套接字类型的指针,所以有些许麻烦。可以使用如下方式传入socket_deleter(第15行)。不过下文这种写法,代码中可以访问到socket()的返回值s。有多处访问s的风险并不完美。也没有查到相关的代码示例,先将就着看吧。
WSADATA wsa;
int ret = WSAStartup(MAKEWORD(2, 2), &wsa); // windows环境下需要
if (ret != 0 || LOBYTE(wsa.wVersion) != 2 || HIBYTE(wsa.wVersion) != 2) {
printf("WSAStartup failed with error:%d!\n", ret);
WSACleanup();
return -1;
}
SOCKET s = socket(AF_INET, SOCK_DGRAM, 0);
if (s == INVALID_SOCKET) {
printf("create socket failed with error:%d!\n", WSAGetLastError());
return -1;
}
printf("socket = %d\n", s); // s原值
{ // sp作用域
std::unique_ptr<SOCKET, decltype(&socket_deleter)> sp(&s, &socket_deleter);
printf("sp socket = %d\n", *sp); // 从sp中获取s的值
}
printf("socket = %d\n", s);// 释放后s的值
上面代码段的输出结果如下:
socket = 96
sp socket = 96
socket_deleter
socket = 0
2.4 总结
上面介绍的三种向std::unique_ptr传递自定义deleter的方法没啥技术难度,就是语法问题。在实际使用的时候可以根据自己的习惯选择使用。不过,实际中一般也用不到,至少我没有用过,也没有在工作代码中看到过,就随便看看知道有这么个知识点就行。
3 UniquePtr实现
本文示例代码为了简洁,取消了自定义deleter功能,也简化了std::unique_ptr的实现逻辑,仅实现UniquePtr对象托管new生成的指针,领会下std::unique_ptr托管指针的思想就行。
想了解自定义deleter实现或std::unique_ptr完整实现的,直接看STL的代码就行。STL的代码乍一看会让你有种这是不是C++代码的感觉。多看看,习惯就好。
3.1 模板类
定义一个如下的UniquePtr模板类,使得可以使用UniquePtr托管各种类型的指针。类中包含一个T *ptr_用于存储要托管的指针。
template<typename T>
class UniquePtr {
// ...
private:
T *ptr_;
};
3.2 构造函数
定义如下构造函数可以使得使用形如UniquePtr<Test> t1(new Test(111))和UniquePtr<Test> t2两种方式构造t1和t2对象。其中,t1对象托管了一个Test类型的指针,t2对象未托管任何指针。
explicit UniquePtr(T *ptr = nullptr) : ptr_(ptr) {
printf("UniquePtr[%p] constructor\n", this);
}
3.3 析构函数
当UniquePtr对象作用域消失,会调用析构函数。定义如下析构函数,释放所托管指针的内存。
~UniquePtr() {
printf("~UniquePtr[%p]\n", this);
if (ptr_) {
delete ptr_;
ptr_ = nullptr;
}
}
3.4 禁用拷贝构造和赋值运算符
由于UniquePtr对象托管的指针是独享的,拷贝构造和赋值操作会有歧义,因此使用如下方式禁用。
// Disable copy from lvalue.
UniquePtr(const UniquePtr &p) = delete;
UniquePtr &operator=(const UniquePtr &p) = delete;
当使用如下代码时,编译器就会报错。
UniquePtr<Test> t1(new Test(666));
UniquePtr<Test> t2(t1); // 调用拷贝构造函数,报错
t2 = t1; // 调用赋值运算符,报错
3.5 移动构造函数
UniquePtr支持移动构造。使用移动构造函数构造新对象后,原对象所托管指针将被转移到新对象上。代码如下。
// Move constructor.
UniquePtr(UniquePtr &&p) noexcept: ptr_(p.ptr_) {
printf("UniquePtr[%p] Move[%p] constructor\n", this, &p);
p.ptr_ = nullptr;
}
从上面代码段可以看出左边的新对象构造以后,所托管的指针将与右边的原对象脱离,右对象的ptr_置nullptr。这也是为什么该类智能指针被称为unique_ptr的原因。因为所托管的指针只能由一个UniquePtr对象独享。
除了显示调用移动构造函数外,C++11中临时对象构造新对象时会优先调用移动构造函数构造新对象。
UniquePtr<Test> t1 = UniquePtr<Test>(new Test(666)); // 临时对象构造新对象隐式调用
UniquePtr<Test> t2 = std::move(t1); // 显示调用
以上代码段t1和t2被析构后输出结果如下。
Test[0000000000605c60].[666] constructor
UniquePtr[000000000022fde0] constructor
UniquePtr[000000000022fb80] Move[000000000022fde0] constructor
~UniquePtr[000000000022fde0]
UniquePtr[000000000022fb70] Move[000000000022fb80] constructor
~UniquePtr[000000000022fb70]
~Test[0000000000605c60].[666]
~UniquePtr[000000000022fb80]
3.6 类成员访问运算符(->)
为了让使用UniquePtr对象就像使用所托管指针一样,可以直接使用->访问托管指针对应类中成员,需要对operator->进行重写,如下。
// Return the stored pointer.
T *operator->() const noexcept { return ptr_; }
如下代码t1可以直接用->去访问Test的成员变量a_和str_。
UniquePtr<Test> t1(new Test(666));
assert(t1->a_ == 666 && !memcmp(t1->str_, "666", 3)); // 访问
第2行代码等效代码如下,相当于隐藏了t1.ptr_。ptr_为UniquePtr中用于存储托管指针的成员变量。
assert(t1.operator->()->a_ == 666 && !memcmp(t1.operator->()->str_, "666", 3)); // 访问
3.7 解引用运算符(*)
为了让使用UniquePtr对象就像使用所托管指针一样,可以直接使用*解引用托管指针,需要对operator *进行重写,如下。
// Dereference the stored pointer.
T &operator*() const noexcept { return *ptr_; }
如下代码t1可以直接用*解引用Test *,然后使用.去访问Test的成员变量a_和str_。
UniquePtr<Test> t1(new Test(666));
assert((*t1).a_ == 666 && !memcmp((*t1).str_, "666", 3)); // 访问
第2行代码等效代码如下,相当于隐藏了t1.ptr_。ptr_为UniquePtr中用于存储托管指针的成员变量。
assert(t1.operator*().a_ == 666 && !memcmp(t1.operator*().str_, "666", 3)); // 访问
3.8 常用成员函数
3.8.1 release
std::unique_ptr的release()函数会释放托管指针的所有权,但并不会释放托管指针的内存。UniquePtr也实现一个release()函数,实现如下。
// Release ownership of any stored pointer.
T *release() noexcept {
T *res = ptr_;
ptr_ = nullptr;
return res;
}
3.8.2 reset
std::unique_ptr的reset()函数会将托管指针重置为传入的指针,同时会将自身的所托管的指针内存释放掉。UniquePtr也实现一个reset()函数,实现如下。
// Replace the stored pointer.
void reset(T *p = nullptr) noexcept {
std::swap(ptr_, p);
delete p;
}
如下代码中的reset直接使用默认的参数nullptr,相当于手动释放t所托管指针的内存。
std::unique_ptr<Test> t(new Test(333));
t.reset(); // 手动释放t托管的指针
3.8.3 swap
std::unique_ptr的swap()函数会将两个对象所托管的指针互换。UniquePtr也实现一个reset()函数,实现如下。
// Exchange the pointer with another object.
void swap(UniquePtr &p) noexcept {
std::swap(ptr_, p.ptr_);
}
3.9 移动赋值运算符
移动赋值运算符相当于=右边对象将托管指针移交给=左边的对象。注意,需要将=左边对象原托管指针内存释放若有,否则会造成内存泄漏。之所以最后介绍移动赋值运算符是因为STL中它的实现使用reset()和release()函数。实现如下。你品,你细品,一行代码解决了多少逻辑。
// Move assignment operator
UniquePtr &operator=(UniquePtr &&p) noexcept {
printf("UniquePtr[%p] Move[%p] assignment operator\n", this, &p);
reset(p.release());
return *this;
}
你可能会觉得这样写貌似有些复杂,但是若有如下自赋值,上面代码的可以完美兼容,无需做自赋值判断。
UniquePtr<Test> t1(new Test(111));
t1 = std::move(t1); // self
STL中代码复用率特别高,有些复用甚至都有些繁琐,甚至觉得这样做会不会拖慢代码执行速度。只要不是高频次调用的代码完全不必要担心,比如上面代码的复用让代码逻辑清晰了好多。
代码中一半以上的逻辑都是为了处理异常而存在的,下面代码段演示如何使用移动赋值运算符。智能指针之unique_ptr简单实现-使用篇。
UniquePtr<Test> t1(new Test(111));
UniquePtr<Test> t2(new Test(222));
assert(t1->a_ == 111 && (*t2).a_ == 222);
// t1.reset(t2.release()); // 与下行等效
t1 = std::move(t2);
assert(t1->a_ == 222);
以上代码段t1和t2被析构后输出结果如下。
Test[0000000000305c60].[111] constructor
UniquePtr[000000000022fc70] constructor
Test[00000000006da7f0].[222] constructor
UniquePtr[000000000022fc60] constructor
UniquePtr[000000000022fc70] Move[000000000022fc60] assignment operator
~Test[0000000000305c60].[111]
~UniquePtr[000000000022fc60]
~UniquePtr[000000000022fc70]
~Test[00000000006da7f0].[222]
二、shared_ptr实现
简单的实现了shared_ptr,主要包括如下成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值函数
- operator * ()
- operator -> ()
代码如下:
template<typename T>
class smart
{
private:
T* _ptr;
int* _count; //reference couting
public:
//构造函数
smart(T* ptr = nullptr) :_ptr(ptr) {
if (_ptr) {
_count = new int(1);
} else {
_count = new int(0);
}
}
//拷贝构造
smart(const smart& ptr) {
if (this != &ptr) {
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
}
}
//重载operator=
smart& operator=(const smart & ptr)
{
if (this->_ptr == ptr._ptr) {
return *this;
}
if (this->_ptr) {
(*this->_count)--;
if (*this->_count == 0)
{
delete this->_ptr;
delete this->_count;
}
}
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
return *this;
}
//operator*重载
T& operator*() {
if (this->_ptr) {
return *(this->_ptr);
}
}
//operator->重载
T* operator->() {
if (this->_ptr) {
return this->_ptr;
}
}
//析构函数
~smart() {
(*this->_count)--;
if (*this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
//return reference couting
int use_count() {
return *this->_count;
}
};