0 前言
据说用过java的人都说java好,很大一部分是java里没有指针的概念,并提供自动垃圾机制,而C/C++语言经常会被内存的释放问题搞得头疼。
全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部stratic对象在第一次使用前分配,在程序结束时销毁。对于自动和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪儿创建是无关的,只有当显示地被释放时,这些对象才会销毁。
动态对象的正确分配/释放被证明是编程中极其容易出现错误的地方,从语言层次上归纳为以下问题:
(1)野指针(空悬指针):一些内存单元已经被释放,之前指向它的指针却还在被使用。这些内存有可能被运行的系统重新分配给程序使用,从而导致了无法预测的错误。解决措施:delete后,将nullptr赋予指针,表明指针不指向任何对象。但可能有多个指针指向相同的内存,在delete内存后重置指针只对这个指针有效,对其他任何仍指向(已释放的)的内存的指针式没有作用。
int *p(new int(42) // p指向动态内存
auto q = p; // p和q指向相同的内存
delete p; // p和q均变为无效
p = nullptr; // 指出p不再绑定任何对象
(2)重复释放:程序试图去释放已经释放过的内存单元,或者释放已经被重新分配过的内存单元,就会导致重复释放错误。通常重复释放内存会导致C/C++运行时系统打印出大量错误及诊断信息。解决措施:new与delete配合使用,new几次,delete几次,但这实际要求很高。
(3)内存泄漏:不再需要使用的内存单元如果没有被释放就会导致内存泄漏。如果程序不断重复进行这类操作,将会导致内存占用剧增。解决措施:查找内存泄漏时非常困难的。(后面再提如何检测内存泄漏)
// eg01
void create()
{
int *p = new int[100];
// 这里忘记释放内存后,则退出函数后无法释放内存
}
// eg02
int& create()
{
int *p = new int[100];
return *p;
}
int main()
{
int &p = create(); // 这里必须要用引用来接收返回值,不然无法释放内存
delete &p;
}
// eg02的编程风格非常不好,及其容易产生内存泄漏
综上,为了避免以上及其容易出现的问题(并且这些问题还非常难以查找和解决),C++提供了智能指针,如auto_ptr(在C++11中已被弃用、C++17已被移除)、shared_ptr、unique_ptr,weak_ptr。
但是,任何时候都要用智能指针来代替原始指针吗,也就是不要显示/直接去管理内存吗?(我在知乎上看到了这么一个问题,底下回答意见不一,我个人水平不够就不发表意见了,贴个地址有兴趣的可以去看下:
c++是否应避免使用普通指针,而使用智能指针(包括shared,unique,weak)?
再谈下占栈内存/静态内存/堆内存,因为后面智能指针的原理就是基于栈。
静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和释放。对于栈对象(也称自动对象),仅在其定义的程序块运行时才存在,当到达块末尾时销毁;static对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称为自由空间或堆。程序用堆来存储动态分配对象,动态对象的生命周期由程序控制,所以要显式销毁它们。
而智能指针则结合了栈对象的安全性(栈对象离开作用域后会自动销毁)和堆内存的灵活性。
1 智能指针简介
智能指针是一个模板,因此我们创建一个智能指针时,必须提供额外的信息,指针可以指向的类型。
shared_ptr<string> p1;
默认初始化的智能指针中保存了一个空指针。智能指针的使用方式和普通指针类似(一般怎么使用原始指针就怎么使用只能指针)。
c++11提供了两种智能指针来管理动态对象。shared_ptr允许多个指针指向同一个对象;unique_ptr则”独占“所指向的对象。C++11还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
智能指针的主要思想是RAII思想,即”资源获取即初始化“:
(1)定义一个类来封装资源的分配和释放
(2)构造函数中完成资源的分配及初始化
(3)析构函数中完成资源的请理,可以保证资源的正确初始化和释放
(4)如果对象是在栈上创建的,那么RAII机制就会正常工作,当离开作用域对象会自动销毁,从而自动调用洗过后函数释放资源
2 为何弃用auto_ptr
C++98时就有了auto_ptr,其也个模板类型。auto以对象的方式管理堆分配的内存,并在适当的时间释放所获得的堆内存。这种堆内存管理的方式只要程序员将new操作返回的指针作为auto_ptr的初始值即可,程序员不用显示地调用delete。
但是其被弃用是由原因的,主要有:
(1)auto_pty在拷贝构造/赋值运算符函数和普通的拷贝/赋值函数不一样,当auto_ptr发生拷贝/赋值时原来的指针会被置为nullptr,所以不能将auto_ptr放入标准容器中作为容器成员。而且将auto_ptr作为函数参数传递,原指针会失去控制权。
(2)auto_ptr不能调用delete[],不能指向数组。
下面这个例子中,auto_ptr其构造函数是被声明为explicit,所以要直接初始化而不能拷贝初始化(后面的shared_ptr和unique_ptr也是这样)。当p1以p拷贝初始化时,可以看到p已经为空了。
3 unique_ptr的使用
unique_ptr的出现很好的替代了auto_ptr,同一时刻只能有一个unique_ptr指向一个给定内存。但是unique_ptr不支持普通的拷贝和赋值操作:
std::unique_ptr<int> p1(new int(5));
std::unique_ptr<int> p2(new int(6));
// p1 = p2; // unique_ptr将赋值运算符函数delete了(C++11 delete关键字)
// auto p1 = q; // unique_ptr将拷贝构造函数delete了(C++11 delete关键字)
下面是shared_ptr和unique_ptr都支持的操作
shared_ptr<T> sp 空智能指针,可以指向类型为T的对象
shared_ptr<T> up
p 将p用作一个条件判断,若p指向一个对象,则为true
*q 解引用p,获得它指向的对象
p->men 等价于(*p).men
p.get() 返回p中保存的指针
swap(p,q) 交换p和q中的指针
p.swap(q)
3.1 unique_ptr的常见初始化形式
(1)默认初始化,保存了一个空指针
std::unique_ptr<int> p;
(2)将其绑定到new返回的指针上
std::unique_ptr<int> p(new int(5));
(3)通过make_unique初始化(这个是C++14补上的)
std::unique_ptr<int> p = std::make_unique<int>(5);
(4)指向数组的unique_ptr。用unique_ptr管理动态数组,必须在对象类型后面跟一对空方括号。
std::unique_ptr<int[]> p(new int[5]);
(5)uniptr_ptr默认通过delete或delete[],也可以向unique_ptr传递自定义删除器
auto deleter = [](int* p) {
delete(p);
std::cout << "new deleter" << std::endl;
};
// p指向一个类型为int的对象,并使用一个类型为(decltype(deleter)的对象来释放对象)
// 它会调用一个名为deleter的可调用对象
// 用decltype来指明是一个函数指针,是一个可调用对象
std::unique_ptr<int, decltype(deleter)> p(new int(5), deleter);
3.2 常用用法
std::unique_ptr<int> p(new int(5));
// (1) p = nullptr; // 释放p指向的对象,将p置空
// (2) p = release(); // p放弃对指针的控制权,并返回指针,将p置为空
// (3) P = reset(); // 释放p指向的对象,并将p置为空
// (4) p = reset(q); // 如果提供了内置指针,令p指向这个对象;否则将u置为空
前面讲到,虽然我们不能普通拷贝或赋值unique_ptr,但是可以通过调用release或reset将指针的所有权从一个
(非const)unique_ptr转移给另一个unique_ptr:
std::unique_ptr<int> p1(new int(5));
std::unique_ptr<int> p2(p1.release()); // 将p1指向5的所有权转移给p2,并将p1置为空
std::unique_ptr<int> p3(new int(10));
p2.reset(p3.release()); // 将p3指向10的所有权转移给p2,将p3置为空,reset释放了p2原来指向的内存
请注意上面的转移和释放。别混了。
调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
3.3 unique_ptr的移动语义
前面说了,unique_ptr的普通拷贝构造/赋值运算符函数都被声明为delete,所以其不能拷贝和赋值。但是unique_ptr有移动语义:
注意:使用了std::move将左值变为右值后,unique_ptr会通过移动构造/赋值运算符函数初始化。但是,使用了std::move后就表明原始值是一个即将销毁的值(除了对其析构或者重新赋值,做别的事可能产生错误,这是我们使用std::move的前提)。
因此,有了移动语义,可以传递unique_ptr参数和返回unique_ptr,也可以放进容器。
3.4 unique_ptr使用注意点
// 不要用同一个资源来初始化多个std::unique_ptr对象
Pointer *p = new Pointer();
std::unique_ptr<Pointer> p1(p);
std::unique_ptr<Pointer> p2(p);
// 不用混用普通指针和智能指针
Pointer *p = new Pointer();
std::unique_ptr<Pointer> p1(p);
delete p;
4 shared_ptr的使用
shared_ptr
与unique_ptr
类似。要创建std::shared_ptr
对象,可以使用make_shared()
函数。shared_ptr
与unique_ptr
的主要区别在于前者是使用引用计数的智能指针。引用计数的智能指针可以跟踪引用同一个真实指针对象的智能指针实例的数目。这意味着,可以有多个std::shared_ptr
实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。
先介绍下shared_ptr的几种常见用法:
shared_ptr<T>p(q) // q能转换成T,拷贝初始化p,则操作会递增q中计算器
p = q // p和q能相互转化,递减p的引用计数,递增q的引用计数,若p为0,则将原管理的内存释放
p.unique() // p.use_count()为1,返回true,否则返回false
p.use_count() // 返回p共享对象的智能指针数量
shared_ptr可以进行普通的拷贝和赋值。当进行拷贝和赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
从上例子中,可以看到拷贝/赋值都会增加计数,退出作用域后计数减1。
另外注意:因为shared_ptr是通过计数来控制内存的释放,所有要记得销毁不再需要的shared_ptr。如果将share_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得erase删除不再需要的那些元素。
4.1 shared_ptr初始化方式
前三者和unique_ptr差不多就不多介绍了。
std::shared_ptr<int> p; // 默认初始化,保存了一个空指针
std::shared_ptr<int> p1(new int(5)); // 绑定到new返回的指针上
std::shared_ptr<int> p2 = std::make_shared<int>(5); // make_shared返回一个shared_ptr对象
shared_ptr可以拷贝,所以可以拷贝初始化。
shared_ptr自定义删除器。(和unique不一样,unique需要在模板类型参数那里声明是什么类型)
std::shared_ptr<int> p(new int(5), [](int *p) { std::cout << "自定义删除器" << std::endl;
delete p; });
和unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。而且不需要再模板参数那里加空方括号。
std::shared_ptr<int> p(new int[10], [](int *p) {delete[] p; });
如果shared_ptr未提供删除器,代码是未定义的。因为默认情况下,shared_ptr使用delete销毁它指向的对象。如果此对象是一个动态数组,对其使用delete所产生的问题与释放一个动态数组指针时忘记[]产生的问题一样。(这里如果是内置类型,delete 和 delete[]是没有区别的,但是自定义类型就只会执行一次析构函数,造成内存泄漏)
而且,shared_ptr不支持动态数组管理这一特性会影响我们如何访问数组中的元素。
std::shared_ptr<int> p(new int[10], [](int *p) {delete[] p; });
for (auto i = 0; i != 10; ++i) {
*(p.get() + i) = i; // 通过get获取一个内置指针
}
shared_ptr未定义下标运算符,而且智能指针不支持指针算术运算。因此为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。
4.3 shared_ptr使用注意点
// eg01: 内置指针和智能指针互用
int *p = new int(10);
std::shared_ptr<int> p1(p);
delete p; // 这里会造成两次释放
// eg02
void fun(std::shared_ptr<int>) {}
int *p(new int(10));
fun(std::shared_ptr<int>(p)); // 因为shared_ptr构造函数是explicit
int j = *p; // p内存已经被释放了,变成悬挂指针
// eg03
int *p = new int(10);
std::shared_ptr<int> p1(p);
std::shared_ptr<int> p2(p); // 这里p1和p2都是独立创建的,其计数都为1,会释放两次
// eg04 不要使用get初始化另一个智能指针或为智能指针赋值
std::shared_ptr<int> p(new int(10));
int *q = p.get(); // 正确,但是使用q的时候要注意,不能让其管理的指针被释放
{
std::shared_ptr<int>(q);
} // 程序结束,q被销毁,其指向的内存被释放
int foo = *p; // 未定义,p指向的内存已经被释放了
4.4 shared实现多态
5 weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变 shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点。
weak_ptr的出现是为了解决shared_ptr循环引用的。
class B;
class A {
public:
A() { std::cout << "A's constructor " << std::endl; }
~A() { std::cout << "A's disconstructor " << std::endl; }
std::shared_ptr<B> b;
};
class B {
public:
B() { std::cout << "B's constructor " << std::endl; }
~B() { std::cout << "B's disconstructor " << std::endl; }
std::shared_ptr<A> a;
};
int main()
{
std::shared_ptr<A> spa = std::make_shared<A>();
std::shared_ptr<B> spb = std::make_shared<B>();
spa->b = spb;
spb->a = spa;
std::cout << "sqa的强引用个数 " << spa.use_count() << std::endl;
std::cout << "sqb的强引用个数 " << spb.use_count() << std::endl;
return 0;
}
// 输出结果
A's constructor
B's constructor
sqa的强引用个数 2
sqb的强引用个数 2
从上可以看出,由于spa和spb的强引用计数永远大于等于1,所以A和B的析构函数不会被调用。
当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它
auto p = make_shared<int>(10);
weak_ptr<int> wp(p);
std::cout << "p的强引用为 " << p.use_count() << std::endl; // 为1
wp和p指向相同的对象,由于是弱引用,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。
利用weak_ptr修改上面的循环引用。
class B;
class A {
public:
A() { std::cout << "A's constructor " << std::endl; }
~A() { std::cout << "A's disconstructor " << std::endl; }
std::shared_ptr<B> b;
};
class B {
public:
B() { std::cout << "B's constructor " << std::endl; }
~B() { std::cout << "B's disconstructor " << std::endl; }
std::weak_ptr<A> a;
};
int main()
{
std::shared_ptr<A> spa = std::make_shared<A>();
std::shared_ptr<B> spb = std::make_shared<B>();
spa->b = spb;
spb->a = spa;
std::cout << "sqa的强引用个数 " << spa.use_count() << std::endl;
std::cout << "sqb的强引用个数 " << spb.use_count() << std::endl;
return 0;
}
// 结果输出
A's constructor
B's constructor
sqa的强引用个数 1
sqb的强引用个数 2
A's disconstructor
B's disconstructor
可以看出spa的强引用变为1,然后其释放后也就节方了spb。所以weak_ptr配合shared_ptr解决循环引用问题。
5.1 weak_ptr使用注意点
因为weak_ptr是一种弱引用,其指向shared_ptr,可能shared_ptr已经释放了。所以我们不能使用shared_ptr直接访问对象。先看下weak_ptr的用法:
weak_ptr<T> w //空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) //与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的S型
w = p //p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
w.reset() //将W置为空
w.use_count() //与w共享对象的shared ptr的数量
w.expired() //若 w.use_count()为0,返回true,否贝y返回 false
w.lock() //如果expired为true,返回一个空shared ptr:否则返回一个 指向w的对象的shared_ptr
所以我们用weak访问对象时,可以通过lock来判断
auto p = make_shared<int>(10);
weak_ptr<int> wp(p);
if (std::shared_ptr<int> np = wp.lock()) { // 如果np不为空则条件成立
// np和p共享对象
}
6 shared_ptr多线程
shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。这部分请看陈硕大佬的博客:为什么多线程读写 shared_ptr 要加锁?
7 结束语
智能指针深究起来还有很多可以谈,本人能力有限,先就理解到这了。 后面看源码的时候再来补上。
8 参考资料
(1)《C++ Primer》第五版
(2)《深入理解C++11 C++11新特性解析于应用》
(3)https://zhuanlan.zhihu.com/p/78123220
(4)https://segmentfault.com/a/1190000016055581
来源:CSDN
作者:每天学一点!
链接:https://blog.csdn.net/songsong2017/article/details/99933289