C++智能指针

发布时间:2024年01月07日

1. 概述

??C++ 没有自动回收垃圾,这是在程序运行时释放堆内存和其他资源的一个内部进程。 C++ 程序负责将所有已获取的资源返回到操作系统。 未能释放未使用的资源称为“泄漏”。 在进程退出之前,泄漏的资源无法用于其他程序。 特别是内存泄漏是 C 样式编程中 bug 的常见原因。

??C++中的智能指针是一种对象,它允许对原始指针进行封装,以提供自动内存管理和其他功能。智能指针的目的是防止内存泄漏和悬挂指针等问题,使内存管理更加安全和便捷。

??C++11标准库引入了三种主要的智能指针类型:unique_ptr、shared_ptr和weak_ptr。每种智能指针都有其特定的用途和语义。

2. 智能指针的使用

??智能指针是在C++标准库的<memory>头文件中定义的,位于std命名空间下。它们在资源管理和对象生命周期管理中起到了关键作用,遵循了RAII(Resource Acquisition Is Initialization)原则,也被称为“获取资源即初始化”。这个原则的核心思想是确保资源获取与对象初始化同时进行,这样可以在创建对象的同时为其分配所需的资源,并在某行代码中完成所有准备工作。

实际上,RAII的核心原则是将堆分配的资源(如动态分配的内存或系统对象句柄)的所有权赋予对象的析构函数。析构函数中包含了用于释放或释放这些资源的代码,以及任何相关的清理代码。通过这种方式,当对象不再需要时,其析构函数会自动执行,从而自动清理分配的资源,避免内存泄漏和其他资源管理问题。

??大多数情况下,当初始化原始指针或资源句柄以指向实际资源时,会立即将指针传递给智能指针。 在现代 C++ 中,原始指针仅用于范围有限的小代码块、循环或者性能至关重要且不会混淆所有权的功能或辅助性函数中。

下面的示例将原始指针声明与智能指针声明进行了比较。

void testRawPointer()
{
    // 使用原始指针--不建议使用。
    Task* p = new Task(参数...); 

    // 使用p...
    //p->run();
    
    // 使用完成后要释放资源
    delete p;   
}

void testSmartPointer()
{
    // 在堆栈上声明一个智能指针,并将原始指针传递给它。
    unique_ptr<Task> task(new Task(参数...));

    // 使用task...
    // task->run();
    //...

} // task在此自动删除。

??如示例所示,智能指针是你在堆栈上声明的类模板,并可通过使用指向某个堆分配的对象的原始指针进行初始化。 在初始化智能指针后,它将拥有原始的指针。 这意味着智能指针负责删除原始指针指定的内存。 智能指针析构函数包括要删除的调用,并且由于在堆栈上声明了智能指针,当智能指针生命周期结束时将调用其析构函数。对于智能指针类将重载指针运算符(-> 和 *)这些运算符以返回封装的原始指针。

??智能指针的设计原则是在内存和性能上尽可能高效。 例如,unique_ptr 中的唯一数据成员是封装的指针。 这意味着,unique_ptr 与该指针的大小完全相同,不是四个字节就是八个字节。 使用重载了 * 和 -> 运算符的智能指针访问封装指针的速度不会明显慢于直接访问原始指针的速度。

??智能指针通过使用“点”表示法访问的成员函数。 例如,一些 C++ 标准库智能指针具有释放指针所有权的重置成员函数。 如果你想要在智能指针超出范围之前释放其内存将很有用,这会很有用,如以下示例所示:

void testSmartPointer1()
{
    // 创建对象并将其传递给智能指针
    std::unique_ptr<string> p(new string("1234567"));

    //调用被管理对象的方法
    cout << p->size() << endl;

    // 通过"."运算符访问智能指针对象的方法,在我们退出功能块之前释放内存,当然这不是必须的。
    p.reset();

    //... 
}

3. unique_ptr

??unique_ptr 不共享它的指针。 它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何 C++ 标准库算法。 只能移动 unique_ptr。 这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。 因此将对象限制为由一个所有者所有,是非常好的选择,因为多个所有权会使程序逻辑变得复杂。 当需要智能指针用于纯 C++ 对象时,可使用 unique_ptr。

在这里插入图片描述
测试代码:

#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main()
{
	auto pA = make_unique<string>("123456");
	auto pB = std::move(pA);
	cout << pA.get() << endl;
	cout << pB.get() << endl;
	return 0;
}

??unique_ptr 在 C++ 标准库的 <memory> 标头中定义。 它与原始指针一样高效,可在 C++ 标准库容器中使用。 将 unique_ptr 实例添加到 C++ 标准库容器很有效,因为通过 unique_ptr 的移动构造函数,不再需要进行复制操作。

3.1 示例 1

以下示例演示如何创建 unique_ptr 实例并在函数之间传递这些实例。

#include <iostream>
#include <memory>
#include <string>
using namespace std;

unique_ptr<string> createFactory(const std::string& str)
{
	// Implicit move operation into the variable that stores the result.
	return make_unique<string>(str);
}


int main()
{
	// 创建智能指针对象
	auto p1 = make_unique<string>("hello p1");

	// 使用 unique_ptr.
	cout << p1->size() << endl;

	// 将p1对象的原始指针移动到p2所有,移动后p1将不再管理该指针
	auto p2 = std::move(p1);
	cout << p2->size() << endl;

	// 通过函数的返回值获取unique_ptr。
	auto p3 = createFactory("hello p2");
	cout << *p3 << endl;
	return 0;
}

??这个示例说明了 unique_ptr 的基本特征:可移动,但不可复制。 “移动”将所有权转移到新 unique_ptr 并重置旧 unique_ptr。

3.2 示例 2

以下示例演示如何创建 unique_ptr 实例并在list中使用这些实例。

#include <iostream>
#include <memory>
#include <string>
#include <list>
using namespace std;

int main()
{
	list<unique_ptr<string>> strings;

	// 创建unique_ptr实例,使用隐式移动语义,将它们添加到list中
	strings.push_back(make_unique<string>("string object 1"));
	strings.push_back(make_unique<string>("string object 2"));
	strings.push_back(make_unique<string>("string object 3"));
	strings.push_back(make_unique<string>("string object 4"));

	// 尽可能通过常量引用来避免复制。
	for (const auto& i : strings)
		cout << i->size() << ":" << *i << endl;
	return 0;
}

??在范围循环中,注意 unique_ptr 通过引用来传递。 如果你尝试通过此处的值传递,由于删除了 unique_ptr 拷贝构造函数,编译器将引发错误。

3.3 示例 3

以下示例演示如何初始化类成员 unique_ptr。

class A
{
private:
	unique_ptr<string> content_;
public:
	//使用默认构造函数初始化
	A() : content_(make_unique<string>())
	{
	}
	//使用有参造函数初始化
	A(const string &str) : content_(make_unique<string>(str))
	{
	}
	//访问
	size_t size() {
		return content_->size();
	}
};

3.4 示例 4

??可使用 make_unique 将 unique_ptr 创建到数组,但无法使用 make_unique 初始化数组元素。

#include <iostream>
#include <memory>
using namespace std;


int main()
{
	int n = 10;
	auto p = make_unique<int[]>(n);

	// 初始化数组
	for (int i = 0; i < n; ++i)
	{
		p[i] = i;
	}

	for (int i = 0; i < n; ++i) {
		cout << p[i] << endl;
	}
	return 0;
}

4. shared_ptr

??shared_ptr 类型是 C++ 标准库中的一个智能指针,是为多个所有者可能必须管理对象在内存中的生命周期的方案设计的。 在您初始化一个 shared_ptr 之后,您可复制它,按值将其传入函数参数,然后将其分配给其他 shared_ptr 实例。 所有实例均指向同一个对象,并共享对一个“控制块”(每当新的 shared_ptr 添加、超出范围或重置时增加和减少引用计数)的访问权限。 当引用计数达到零时,控制块将删除内存资源和自身。
在这里插入图片描述

4.1 示例 1

??如有可能,第一次创建内存资源时,请使用 make_shared 函数创建 shared_ptr。 make_shared 异常安全。 它使用同一调用为控制块和资源分配内存,这会减少构造开销。 如果不使用 make_shared,则必须先使用显式 new 表达式来创建对象,然后才能将其传递到 shared_ptr 构造函数。 以下示例演示了同时声明和初始化 shared_ptr 和新对象的各种方式。

#include <iostream>
#include <memory>
#include <string>
using namespace std;


int main()
{
	// 尽可能使用make_shared函数
	auto p1 = make_shared<string>("string 1");

	//使用new表达式作为构造函数参数, 可以使用,但效率稍低。
	shared_ptr<string> p2(new string("string 2"));

	// 当初始化必须与声明(例如类成员)分离时,
	//使用nullptr进行初始化,以明确您的编程意图。
	shared_ptr<string> p3(nullptr);

	p3 = make_shared<string>("string 3");

	cout << p1->size() << ":" << *p1 << " 引用计数:" << p1.use_count() << endl;
	cout << p2->size() << ":" << *p2 << " 引用计数:" << p2.use_count() << endl;
	cout << p3->size() << ":" << *p3 << " 引用计数:" << p3.use_count() << endl;
	return 0;
}

4.2 示例 2

??以下示例演示如何声明和初始化对其他 shared_ptr 已分配的对象具有共享所有权的 shared_ptr 实例。

#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main()
{
	auto p1 = make_shared<string>("string A");
	auto p2 = make_shared<string>("string B");

	//引用计数+1
	auto sp3(p1);

	//引用计数+1
	auto sp4 = p1;

	//使用nullptr初始化,sp5为空。
	shared_ptr<string> sp5(nullptr);

	//使用另一个shared_ptr初始化。
	//sp1和sp2交换指针以及引用计数。
	cout << "p1=" << p1.use_count() << ", p2=" << p2.use_count() << endl;
	p1.swap(p2);
	cout << "p1=" << p1.use_count() << ", p2=" << p2.use_count() << endl;
	return 0;
}

4.3 示例 3

??在你使用复制元素的算法时,shared_ptr 在 C++ 标准库容器中很有用。 您可以将元素包装在 shared_ptr 中,然后将其复制到其他容器中(请记住,只要您需要,基础内存就会一直有效)。 以下示例演示如何在向量中对 remove_copy_if 实例使用 shared_ptr 算法。

#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>
using namespace std;

int main()
{
	vector<shared_ptr<string>> v1 {
		make_shared<string>("string 1"),
		make_shared<string>("string 2"),
		make_shared<string>("string 3")
	};

	vector<shared_ptr<string>> v2;
	remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](shared_ptr<string> s)
	{
		return *s == "string 1";
	});

	for (const auto& s : v2)
		cout << *s << endl;
	return 0;
}

4.4 示例 4

??您可以使用 dynamic_pointer_cast、static_pointer_cast 和 const_pointer_cast 来转换 shared_ptr。 这些函数类似于 dynamic_cast、static_cast 和 const_cast 运算符。 以下示例演示如何测试基类的 shared_ptr 向量中每个元素的派生类型,然后复制元素并显示有关它们的信息。

#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>
using namespace std;

int main()
{
	vector<shared_ptr<string>> v1{
		make_shared<string>("string 1"),
		make_shared<string>("string 2"),
		make_shared<string>("string 3")
	};

	vector<shared_ptr<string>> strings;

	copy_if(v1.begin(), v1.end(), back_inserter(strings), [](shared_ptr<string> p)
	{
		//使用dynamic_pointer_cast测试
		shared_ptr<string> temp = dynamic_pointer_cast<string>(p);
		return temp.get() != nullptr;
	});

	for (const auto& p : strings)
	{
		//当我们确定strings只包含shared_ptr<string>对象时,可以static_cast。
		cout << *static_pointer_cast<string>(p) << endl;
	}
	return 0;
}

5. weak_ptr

??有时,对象必须存储一种方法来访问 shared_ptr 的基础对象,而不会导致引用计数递增。 通常,在 shared_ptr 实例之间有循环引用时,会出现这种情况。

??最佳设计是尽量避免指针的共享所有权。 但是,如果必须拥有 shared_ptr 实例的共享所有权,请避免它们之间的循环引用。 如果循环引用不可避免,甚至出于某种原因甚至更可取,使用 weak_ptr 为一个或多个所有者提供对另一个 shared_ptr 所有者的弱引用。 通过使用 weak_ptr,可以创建一个联接到现有相关实例集的 shared_ptr,但前提是基础内存资源仍然有效。 weak_ptr 本身不参与引用计数,因此,它无法阻止引用计数变为零。 但是,可以使用 weak_ptr 尝试获取初始化该副本的 shared_ptr 的新副本。 若已删除内存,则 weak_ptr 的 bool 运算符返回 false。 若内存仍然有效,则新的共享指针会递增引用计数,并保证只要 shared_ptr 变量保留在作用域内,内存就会有效。

在这里插入图片描述

5.1 示例 1

#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

class Test
{
public:
	int n;
	string str;

	vector<weak_ptr<Test>> others;
	explicit Test(int i) : n(i), str("On")
	{
		cout << "构造Test" << n << endl;
	}

	~Test()
	{
		cout << "析构Test" << n << endl;
	}

	//演示如何测试
	//指向内存是否仍然存在。
	void check() const
	{
		for_each(others.begin(), others.end(), [](weak_ptr<Test> wp) {
			auto p = wp.lock();
			if (p)
			{
				cout << "str = " << p->n << " = " << p->str << endl;
			}
			else
			{
				cout << "Null object" << endl;
			}
		});
	}
};

int main()
{
	vector<shared_ptr<Test>> v1{
		make_shared<Test>(0),
		make_shared<Test>(1),
	};


	//每个控制器都依赖于未被删除的所有其他控制器。
	//给每个控制器一个指向所有其他控制器的指针。
	for (int i = 0; i < v1.size(); ++i)
	{
		for_each(v1.begin(), v1.end(), [&v1, i](shared_ptr<Test> p) {
			if (p->n != i)
			{
				v1[i]->others.push_back(weak_ptr<Test>(p));
				cout << "push_back to v[" << i << "]: " << p->n << endl;
			}
		});
	}

	for_each(v1.begin(), v1.end(), [](shared_ptr<Test> &p) {
		cout << "use_count = " << p.use_count() << endl;
		p->check();
	});
	return 0;
}
文章来源:https://blog.csdn.net/yegu001/article/details/135377400
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。