现代C++(2):智能指针
目录
一级目录
二级目录
三级目录
2.共享指针:std::shared_ptr
默认使用using namespace std;
shared_ptr
本质上也是C++11标准库中的一个模板类。
主要特点:
- 引用计数:每个
shared_ptr
实例都有一个关联的引用计数,用来跟踪有多少个shared_ptr
正在指向同一个对象。共享指针会记录有多少个共享指针指向同一个对象,当记录为0的时候,程序会自动释放相应的内存。 - 所有权共享:可以通过拷贝构造函数或赋值操作符将
shared_ptr
的所有权传递给其他shared_ptr
,这会导致引用计数的增加。
另外:
shared_ptr
提供了一个名为use_count()
的成员函数,用于返回当前shared_ptr
实例所指向的对象的引用计数。这个计数值表示有多少个shared_ptr
正在共享同一个对象的所有权。
由于共享指针使用引用计数,所以时间开销也会相对更大。
1.1创建方式
- 创建一个空的
shared_ptr<int>
对象。
shared_ptr<int> p;
- 使用
std::make_shared
创建对象。(推荐使用,因为效率更高也更安全)
p = make_shared(100);
make_shared
会自动申请一片内存空间,然后让一个shared_ptr对象指向这片空间。它本质上是一个模板。
- 使用
new
创建shared_ptr
对象。
shared_pyt<int> p {new int(100)};
注意,后面跟的是大括号。
1.2使用场景
#include <iostream>
#include <memory>
using namespace std;
struct Ball
{
Ball()
{
cout << "Constructor." << endl;
}
~Ball()
{
cout << "Deconstructor." << endl;
}
void bounce()
{
cout << "A ball jumps." << endl;
}
}
int main()
{
shared_ptr<Ball> p = make_shared<Ball>();
cout << p.use_count() << endl;
shared_ptr<Ball> p2 = p;
cout << p.use_count() << p2.use_count()<< endl;
shared_ptr<Ball> p3 = p;
cout << p.use_count()
<< p2.use_count()
<< p3.use_count() << endl;
p.reset(); //1.3有讲
p2.reset();
p3.reset();
}
运行结果如下:
Constructor.
1
2 2
3 3 3
Deconstructor.
首先,我们创建了三个共享指针指向同一个对象Ball
,然后我们使用use_count
观察有多少个共享指针指向同一个对象。最后,使用reset()
来重置共享指针,使其不再指向原来的对象。当没有共享指针指向一个对象,在程序运行结束后会自动销毁,调用Ball
的析构函数。
而对于原始指针而言,如果其指向的内存是动态分配的,且未被主动释放,那么即便原始指针被销毁,这块内存仍然未被释放,就会造成内存泄漏的风险。
但是共享指针的记录为0或者程序运行结束的时候,程序都会自动释放相应的内存。
这就叫做引用计数。比如有一个对象,我们创建一个共享指针指向它,那么引用计数就加1;当某一个共享指针reset()的时候,引用计数就减1。
同样,对于unique_ptr
,原始指针的运算符对其同样适用,比如*
、->
等运算符。
1.3手动销毁
- 可以使用
reset()
来手动释放shared_ptr
下的内存,或者释放的同时指向另一片内存。
p->reset();
p->reset(new Ball{});
第一行代码会将p
下的资源全部释放,并将p
设置为nullptr
;
第二行会将p
下的资源全部释放,并将p
指向新的Ball实例。
reset()
的有参用法
shared_ptr<Ball> sp;
sp = make_shared<Ball>();
sp.reset(new Ball);
此时sp
就不再指向旧的Ball
,旧的Ball
引用计数-1,随后sp
指向新的Ball
。
1.4自定义释放
默认情况下,共享指针使用delete
释放内存,我们也可以自定义删除函数。
void close_file(FILE* fp)
{
if(fp == nullptr)return;
fclose(fp); //对文件的操作
cout << "File closed."
<< endl;
}
int main(){
FILE* fp = fopen("data.txt","w”);
shared_ptr<FILE> sfp {fp, close_file}; //对文件的操作 //用大括号初始化
//第一个参数fp是一个指向 FILE 类型的原始指针,即通过 fopen 函数打开文件后返回的文件指针。第二个参数是自定义删除函数
if(sfp == nullptr)
cerr << "Error opening file." << endl;
<< "File opened." << endl;
else cout<< "File opened."<<endl;
}
//当共享指针sfp的引用计数为0的时候,自动关闭文件
比如我们需要关闭网络连接或删除文件,则我们就可以自定义释放函数,而不是释放内存。
1.5获取原始指针
可以使用get()
获取共享指针的原始指针。
Ball* rp = p.get();
注意,当存在一份资源,有共享指针和原始指针同时指向它的时候,当所有共享指针被销毁,原始指针仍然存在的时候,底下的资源依旧会被释放。所以尽量要避免共享指针和原始指针混用。
1.6别名(Aliasing)
别名是共享指针的一个特殊用法,
struct Bar {int i = 123;};
struct Foo { Bar bar; };
int main()
{
shared_ptr<Foo> f = make _shared<Foo>(); //创建指向Foo的共享指针f
cout << f.use_count()<< endl; // prints
shared_ptr<Bar> b(f, &(f->bar)); //创建类型为Bar的共享指针b
//这个初始化的语法有所不同,括号内第一个参数是共享指针f,第二个是一个指向了f的一个成员的指针:小写bar
//这个就是共享指针的别名
cout << f.use_count()<< endl; // prints 2
cout << b->i << endl; // prints 123
}
我们定义了两个结构体,一个为Bar
,一个为Foo
。
当我们给这个例子中的共享指针起了别名bar
后,结果是:b
拥有了对f
指向的对象的管理权。定义了b
后,f
指向的对象的引用计数就+1。只要b
还存在,f
指向的内存就不会被释放。
但是,b
的指向是f
的成员,指向的是bar
。
从最后一句中也可以看出,当我们访问b
的成员的时候,访问的是Bar
的实例,而不是Foo
实例。
也就是说,共享指针的别名的类型是初始化时的类型,而它的指针的类型却和它指向的类型有关。
引用计数增加和减少的是Foo
,但指针指向的是Bar
。
共享指针的别名通常用于访问类的成员变量:我们访问某个实例的成员,但不希望调用这个成员时,实例本身被删除。所以我们增加了对这个实例本身的控制权,而访问的是成员。
1.7危险行为
int main()
{
shared ptr<int> sp {new int};
shared ptr<int> sp2 = sp;
delete sp.get();
cout << "Continued." << *sp2 << endl;
}
如果我们使用new
来创建一个共享指针,仍然可以使用delete sp.get()
这样的写法,这么做会让sp的
原始指针被销毁,就会造成后面语句的未定义行为。
在使用共享指针的时候,就尽量不要再手动delete
了。
本人才疏学浅,通篇文章有错之处在所难免,如有错误请指正,感激不尽!
以上都是群内巨佬——三色牌告诉我的,有问题可以在群里找他,我是菜鸡
群号:762514085