智能指针简单实现unique_ptr和shared_ptr

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,主要包括如下成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值函数
  5. operator * ()
  6. 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;
	}
};
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值