C++智能指针(一)——shared_ptr初探


1. 普通指针存在的问题

智能指针的引入,是为了解决普通指针在使用过程中存在的一些问题:其中内存泄漏以及空悬指针是最主要的问题。

正常使用普通指针,我们需要 new 分配内存,使用 delete 释放资源,一旦项目很庞大,尤其是在多个地方共享同一个指针时,产生内存泄漏的风险很大,且需要更多的代码来管理指针。

下面举一个具体实例,比如两个对象共享同一个指针,此时对于该指针在什么时候释放需要更多地代码来判断,以防止内存泄漏与访问空悬指针。

#include <iostream>
#include <string>
class Person
{
public:
	string name;
	Person* child;
	
	Person(const string& n, Person* c = nullptr) : name(n), child(c) {
	}
    
    ~Person() {
		std::cout << "delete" << name << std::endl;
	}
}

int main()
{
	Person* son = new Person("hhhcbw");
	Person father("c", son);
	Person mother("z", son);
	delete son;
	std::cout << father.child->name << std::endl; // ERROR: ask hanging pointer
}

为了解决普通指针的痛点,引入智能指针。


2. Class shared_ptr

shared_ptr 从字面就可以看出,该智能指针类主要用于共享资源,其能保证当最后一个对对象的引用被删除后,对象本身被删除(包括一些内存与资源的释放)。

2.1 使用 shared_ptr

使用 shared_ptr 与使用普通指针差不多。可以赋值,拷贝以及比较 shared_ptr,也可以使用操作符 *-> 来访问指针指向的对象。举一个例子:

#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
int main()
{
	// two shared pointers representing two persons by their name
	shared_ptr<string> pNico(new string("nico"));
	shared_ptr<string> pJutta(new string("jutta"));
	// capitalize person names
	(*pNico)[0] = ’N’;
	pJutta->replace(0,1,"J");
	// put them multiple times in a container
	vector<shared_ptr<string>> whoMadeCoffee;
	whoMadeCoffee.push_back(pJutta);
	whoMadeCoffee.push_back(pJutta);
	whoMadeCoffee.push_back(pNico);
	whoMadeCoffee.push_back(pJutta);
	whoMadeCoffee.push_back(pNico);
	// print all elements
	for (auto ptr : whoMadeCoffee) {
	cout << *ptr << " ";
	}
	cout << endl;
	// overwrite a name again
	*pNico = "Nicolai";
	// print all elements again
	for (auto ptr : whoMadeCoffee) {
	cout << *ptr << " ";
	}
	cout << endl;
	// print some internal data
	cout << "use_count: " << whoMadeCoffee[0].use_count() << endl;
}

上面的代码,具体表现如下图所示
输出如下

Jutta Jutta Nico Jutta Nico
Jutta Jutta Nicolai Jutta Nicolai
use_count: 4

2.1.1 初始化 shared_ptr

shared_ptr 类定义在 <memory> 里,需要注意的是,shared_ptr 的使用一个指针作为单独参数的构造函数是显式的(explicit),因此不能使用赋值符号,来将普通指针赋值给 shared_ptr

shared_ptr<string> pNico = new string("nico"); // ERROR
shared_ptr<string> pNico{new string("nico")}; // OK
shared_ptr<string> pNico = make_shared<string>("nico"); // Better

也可以使用函数 make_shared() 来创建 shared_ptr,且这样更快且更安全:更快是因为相比于前面的初始化的两次分配内存(一次给对象,一次给共享指针的共享数据),使用函数 make_shared() 只需要一次分配内存,完成两个步骤;更安全也是因为不会出现对象分配成功,控制块分配失败的情况。

注意,尽量不要对一个已有普通指针,创建共享指针,如:

string* pNico = new string("nico");
shared_ptr<string> spNico(pNico);

如果pNico被设为nullptr,spNico.use_count()=1 且字符串未被释放,正常输出 nico
但如果spNico被设为nullptr,此时字符串被释放,但pNico还保存该地址!!

2.1.2 reset

可以先声明一个共享指针,然后给该共享指针分配一个新的指针。当然,不能使用赋值操作,要使用 reset() 方法:

shared_ptr<string> pNico4;
pNico4 = new string("nico"); // ERROR: no assignment for ordinary pointers
pNico4.reset(new string("nico")); // OK

2.1.3 访问数据

与普通指针类似,使用 *->

(*pNico)[0] = ’N’;
pJutta->replace(0,1,"J");

2.1.4 use_count()

use_count() 表示当前拥有该对象的所有共享指针的数量,当一个共享指针被删除后,use_count()-1,反之,use_count()+1

上面例子中,pJutta 本身算一个,容器 vector 里还有三个,所有 use_count() = 4

2.1.5 类型转换

可以声明一个 void 类型的共享指针,其与 void* 功能一样,表示未定义类型的指针。
shared_ptr 有专门的语法转换指针的类型,但我们不能使用普通的指针类型转换操作以初始化共享指针,结果是未定义的:

shared_ptr<void> sp(new int); // shared pointer holds a void* internally
...
shared_ptr<int>(static_cast<int*>(sp.get())) // ERROR: undefined behavior
static_pointer_cast<int*>(sp) // OK

3. Deleter

当最后一个拥有者被删除后,共享指针为对象调用 delete 进行内存和资源的释放。这不一定在作用域结束处发生,比如上面的例子中,当给 pNico 赋值 nullptr 且在将 vector resize 为 2, 也会导致最后一个拥有者被删除,以至调用 delete

3.1 定义一个 Deleter

我们甚至可以自定义 Deleter,例如在删除引用对象前输出一条信息:

shared_ptr<string> pNico(new string("nico"),
[](string* p) {
cout << "delete " << *p << endl;
delete p;
});
...
pNico = nullptr; // pNico does not refer to the string any longer
whoMadeCoffee.resize(2); // all copies of the string in pNico are destroyed

这里传入一个lambda表达式,作为 shared_ptr 构造函数的第二个参数,当然对于任何可调用的对象都是可以的,比如函数与重载了()运算符的类与std::function,比如下面的代码就是重载了 () 运算符的类:

#include <string>
#include <fstream> // for ofstream
#include <memory> // for shared_ptr
#include <cstdio> // for remove()
class FileDeleter
{
private:
std::string filename;
public:
FileDeleter (const std::string& fn)
: filename(fn) {
}
void operator () (std::ofstream* fp) {
fp->close(); // close.file
std::remove(filename.c_str()); // delete file
}
};

int main()
{
// create and open temporary file:
std::shared_ptr<std::ofstream> fp(new std::ofstream("tmpfile.txt"),
FileDeleter("tmpfile.txt"));
...
}

3.2 处理数组

shared_ptr 提供的默认deleter调用 delete 而不是 delete[]。这意味着默认 deleter只有在共享指针拥有的是一个单独由new创建的对象才有效。需要注意的是,给一个数组创建共享指针是可能的,但是是错误的:

std::shared_ptr<int> p(new int[10]); // ERROR, but compiles

所以,如果要使用 new[] 来创建一个对象数组,需要定义自己的 deleter。可以传入一个函数、function object或lambda,在内部调用 delete[],例如:

std::shared_ptr<int> p(new int[10],
[](int* p) {
delete[] p;
});

也可以使用提供给 unique_ptr 的官方helper,其内部调用 delele[]

std::shared_ptr<int> p(new int[10],
std::default_delete<int[]>());

当然,unique_ptrshared_ptr 在数组的处理上有一定的区别,更详细地会在 unique_ptr 讲解

std::unique_ptr<int[]> p(new int[10]); // OK
std::shared_ptr<int[]> p(new int[10]); // ERROR: does not compile
std::unique_ptr<int,void(*)(int*)> p(new int[10],
[](int* p) {
delete[] p;
});

shared_pr 不提供操作符 []。因此如果我们想要访问数组元素,可以先获取到原指针,然后再使用 [] 操作符访问数组元素,以下两种方法是等价的,第一种使用 get() 方法获取到 shared_ptr 封装的内部指针。

  • p.get()[i] = i * 42;
  • (&*p)[i] = i * 42;

3.3 get_deleter()

get_deleter() 得到一个指向删除器函数的指针,有可能是nullptr。为了获取deleter,必须传入它的类型,作为模板参数,比如:

auto del = [] (int* p) {
delete p;
};
std::shared_ptr<int> p(new int, del);
decltype(del)* pd = std::get_deleter<decltype(del)>(p);

4. 共享指针误用的情况

首先,不能同时有多组共享指针拥有一个对象,比如下面的代码就是错误的:

int* p = new int;
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p); // ERROR: two shared pointers manage allocated int

因为,这两个共享指针在失去p的所有权之后,都会释放资源,那么就会被释放两次。所以,应该第二个共享指针应该使用第一个共享指针进行初始化:

shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1); // OK

这个问题也可能会间接产生。比如下面的例子中,想要使用this指针初始化共享指针是不被允许的:

class Person {
	public:
	...
	void setParentsAndTheirKids (shared_ptr<Person> m = nullptr,
	shared_ptr<Person> f = nullptr) {
		mother = m;
		father = f;
		if (m != nullptr) {
		m->kids.push_back(shared_ptr<Person>(this)); // ERROR
		}
		if (f != nullptr) {
		f->kids.push_back(shared_ptr<Person>(this)); // ERROR
		}
	}
	...
};

上面的例子,和两个共享指针都使用同一个普通指针初始化是一样的问题,所以C++不允许直接使用this指针初始化 shared_ptr

但是,C++提供了 std::enable_shared_from_this<> 类,我们可以让Person继承自该类,然后在类内部使用 shared_from_this() 以提供一个由this创建的共享指针,以初始化我们的共享指针:

class Person : public std::enable_shared_from_this<Person> {
public:
...
void setParentsAndTheirKids (shared_ptr<Person> m = nullptr,
shared_ptr<Person> f = nullptr) {
	mother = m;
	father = f;
	if (m != nullptr) {
	m->kids.push_back(shared_from_this()); // OK
	}
	if (f != nullptr) {
	f->kids.push_back(shared_from_this()); // OK
	}
	}
	...
};

注意,我们不能在构造函数内部调用 shared_from_this()(其实可以,不过会报运行时错误>_<)。

class Person : public std::enable_shared_from_this<Person> {
public:
...
Person (const string& n,
shared_ptr<Person> m = nullptr,
shared_ptr<Person> f = nullptr)
: name(n), mother(m), father(f) {
	if (m != nullptr) {
	m->kids.push_back(shared_from_this()); // ERROR
	}
	if (f != nullptr) {
	f->kids.push_back(shared_from_this()); // ERROR
	}
	}
	...
};

这是因为,将this指针存储为共享指针(Person父类enable_shared_from_this<>私有成员),是在Person构造函数的某尾进行的。


5. 附录

A. shared_ptr 的操作列表一

B. shared_ptr 的操作列表二


6. 参考文献

《The C++ Standard Library》A Tutorial and Reference, Second Edition, Nicolai M. Josuttis.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
shared_ptrC++中的智能指针,它采用引用计数的方法来实现释放指针所指向的资源。当使用shared_ptr时,它会记录有多少个shared_ptr指向同一个对象,只有当最后一个shared_ptr被销毁时,该对象的内存才会被释放。因此,shared_ptr可以自动管理内存,不需要手动释放。 在代码中,使用shared_ptr可以像普通指针一样操作对象。当需要创建一个shared_ptr对象时,可以使用std::make_shared函数来构造,如下所示: ``` std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(10); ``` 可以通过将shared_ptr赋值给其他shared_ptr来共享资源,如下所示: ``` std::shared_ptr<int> sharedPtr2 = sharedPtr1; ``` 当所有的shared_ptr都被销毁时,内存会自动释放。可以使用use_count()函数获取shared_ptr的引用计数,如下所示: ``` std::cout << "sharedPtr2 use count: " << sharedPtr2.use_count() << std::endl; ``` 当引用计数为0时,内存会被自动释放。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C++智能指针shared_ptr分析](https://download.csdn.net/download/weixin_38705004/13788082)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [C++11中的智能指针unique_ptrshared_ptr和weak_ptr详解](https://blog.csdn.net/chenlycly/article/details/130918547)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值