前言
初步认识
原文链接:https://blog.****.net/flowing_wind/java/article/details/81301001
参考资料:《C++ Primer中文版 第五版》
我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为*空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。
在C++中,动态内存的管理是用一对运算符完成的:new和delete,new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。
动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。
为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。
标准库提供的两种智能指针,区别在于管理底层指针的方法不同,
- shared_ptr允许多个指针指向同一个对象,
- unique_ptr则“独占”所指向的对象。
- 标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。
Ref: C++中的智能指针
自C语言以来,指针就是一个重要却又充满了麻烦的特性。使用指针的一个理由是在作用域以外使用引用语义。但是,指针的生命期和所指对象的生命期的确认是十分恼人的一件事情,尤其是多个指针指向同一个对象。例如,为了在多个集合中拥有同一个对象,必须要给每个集合传入指针。在理想的情况下,其中一个指针被销毁了,没有任何问题会发生(即没有空悬指针,也不会重复删除被引用的对象),当指向对象的最后一个指针被销毁时,该对象才会被销毁(即没有资源泄漏)。
为了避免各种问题,一种通用的解决方案是使用智能指针。智能指针之所以智能是因为它们可以支持程序员来避免上述的问题。例如,智能指针可以 智能地知道 它是不是最后一个指向对象的指针,并且据此可以实现由对象的最后一个指针来决定对象的销毁。
但是仅仅只有一种智能指针是不足够的。智能指针针对各种情况是十分智能的,但会导致其他方面的延时,因为要为智能付出代价。即便使用智能指针,也会存在误用和产生错误的情况。
生命周期
一、变量de底层原理
构建、析构
#include <iostream>
#include <stack>
#include <memory> using namespace std; struct X {
X() { cout << "X() ";}
~X() { cout << "~X() ";}
}; struct Y {
Y() { cout << "Y() ";}
~Y() { cout << "~Y() ";}
}; class A {
X x;
public:
A() { cout << "A() ";}
~A() { cout << "~A() ";}
}; class B: public A {
Y y;
public:
B() { cout << "B() ";}
~B() { cout << "~B() ";}
}; class S {
public:
S() { cout << "S() ";}
~S() { cout << "~S() ";}
}; ///////////////////////////////////////////////////////////// int main()
{
cout << "Hello World!" << endl;
#if 1
B b;
#else
A a;
{
cout << "" << endl;
A &s = a;
cout << "" << endl;
}
cout << "" << endl;
#endif return ;
}
Output:
二、变量分类
变量类型
生命周期 Lifetime: the period of time in which memory is allocated for an object
Different kinds of objects:
- 静态变量 Static objects: allocated in a global (static) area
- 栈对象 Local (or automatic or stack) objects
- 堆对象 Heap objects
是否命名
Named objects: (named by the programmer): their lifetimes determined by their scope
Heap objects (unnamed objects): their lifetimes determined by the programmer
大括号作用域
Local Objects - 大括号的重要性
常对象作用域
const - 对 lifetime的影响
命名空间
三大永生:
- static Objects
- Global Objects
- namespace <----
三、新建 Object
堆内存分配
int *pi = new int {}
pi, pj 是 named local objects。
int *f() {
int *pi = new int{} //unnamed heap object
return pi;
} int main() {
int *pj = f();
delete pj;
}
另外一些特殊的例子
特殊的 new (Dynamically Allocate and Initialise Objects)。
注意:vector<int>内存分配失败的话,可以“自定义处理”。
内存耗尽 - 异常
虽然现代计算机通常都配备大容量内存,但是*空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的空间,new表达式就会失败。
默认情况下,如果new不能分配所需的内存空间,他会抛出一个 bad_alloc 的异常,我们可以改变使用new的方式来阻止它抛出异常。
//如果分配失败,new返回一个空指针
int *p1 = new int; // 如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow)int; // 如果分配失败,new返回一个空指针
构建、拷贝、移动
Ref: Error: double free or corruption
#include <queue>
using namespace std; class Test{
int *myArray; public:
Test(){
myArray = new int[];
} ~Test(){
delete[] myArray;
} }; int main(){
queue<Test> q
Test t;
q.push(t);
}
delete 只会调用一次析构函数,而 delete[] 会调用每一个成员的析构函数。
在 More Effective C++ 中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存”。
delete与new配套,delete []与new []配套。
If your object has a RAW pointer then you need to remember the rule of 3 (now the rule of 5 in C++11).
- Constructor
- Destructor
- Copy Constructor
- Assignment Operator
- Move Constructor (C++11)
- Move Assignment (C++11)
智能指针
Smart Memory Management
一、丑陋的方案
类似于python中的 with ... as ...,出现异常时要做什么才能完美解决所有问题。
void f() {
X* x = new X{};
try {
...
}
catch (...) {
delete x;
throw; // rethrow the exception
}
delete x;
}
直接throw; 而后面不跟任何参数,是将所 catch 到的 exception 直接抛出,这样可以避免复制exception对象。
weak_ptr 是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作。
二、叁种智能指针
unique_ptr
不能赋值、拷贝。可以转移权限,但就是不能同时服侍二主!
- reset 方法
调用 unique_ptr 的 reset() 方法将删除与之绑定的 raw 指针,并且将 unique_ptr 对象置空
- release 方法
直接将对绑定 raw 指针的所有权释放,该函数会将绑定的 raw 指针返回
函数返回指针是可以的,因为函数本身消亡,最后还是只有一个指针指向资源。
move 转移法,替代了 release + reset 这个组合方法。
- std::move 方法
跟左值右值有关,具体详见:[c++] Copy Control
shared_ptr
简而言之,share_ptr的目标是自动释放对象关联的资源,当对象不再被需要的时候。
会记录有多少个shared_ptrs共同指向一个对象。这便是所谓的引用计数(reference counting)。一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。
这在非环形数据结构中防止资源泄露很有帮助。使得指针可以共享对象,并且不用考虑内存泄漏问题
1) 为了避免隐式转换,智能指针不能使用赋值的方式初始化,当然使用括号初始化或者列表初始化是没有问题的。
2) 另一种初始化的方法是使用make_shared<>,它是一种更好且更安全的方法:因为使用new时会创建一个对象,计算它的引用计数时又会创建一个对象,而make_shared只会创建一个对象,并且不会出现控制模块失效的情况。
3) 另一种可选方案是先定义一个智能指针再进行赋值。但是不能使用赋值运算符(=),必须使用reset函数。
我能想到的原因是由这些指针管理的情形同时有如下特征:
1. 对象本身比较小,可能与shared_ptr引用控制块的大小在一个数量级。
2. 指针基本上是独占对象的,没有共享。(你可以用std::unique_ptr啊!)
3. 小内存环境,对内存占用非常敏感。
4. 对象数量异常多。
5. 不可避免的循环引用。
但是话又说回来,如果真出现了上面前4点这些情况。说明内存上需要自己额外下点功夫。使用自定义的分配器管理和使用内存,合理优化分配策略以减少碎片的产生,这些事情往往又不是简单的原生new / delete能做好的。
总之,如果shared_ptr都出问题了,那么使用原生指针出问题的日子也差不了几天了。
【可以参考原链接的例子:https://zhuanlan.zhihu.com/p/71649913】
auto p = make_shared<int>();
shared_ptr<int> q = p; cout << *p << endl;
p.reset();
cout << *q << endl;
p.reset();
不用delete,而是reset()。
// shared_ptr::get example
#include <iostream>
#include <memory> int main () {
int* p = new int ();
std::shared_ptr<int> a(p); if (a.get()==p)
std::cout << "a and p point to the same location\n"; // Three ways of accessing the same address:
std::cout << *a.get() << "\n";
std::cout << *a << "\n";
std::cout << *p << "\n"; return ;
}
- get 方法
获得指针的“地址”,
int main(void)
{
int *p = new int ();
std::shared_ptr<int> a(p); cout << a << endl;
cout << *a << endl;
cout << a.get() << endl;
cout << *a.get() << endl;
cout << p << endl;
cout << *p << endl; return ;
}
0x55aa093dce70 0x55aa093dce70 0x55aa093dce70
Output
#include<iostream>
#include<memory> int main()
{
int* test = new int(); std::shared_ptr<int> t1_ptr(nullptr);
std::shared_ptr<int> t2_ptr(test); auto q1 = t1_ptr;
auto q2 = t2_ptr;
auto p1 = t1_ptr;
auto p2 = t2_ptr; std::cout << "the t1_ptr's ref count is " << t1_ptr.use_count() << std::endl;
std::cout << "the t2_ptr's ref count is " << t2_ptr.use_count() << std::endl; t1_ptr = t2_ptr;
std::cout<<"after t1_ptr = t2_ptr,the t1_ptr's ref count is " << t1_ptr.use_count() << std::endl; return ;
}
- use_count 方法
内存的指针引用计数。
weak_ptr
weak_ptr 是 为配合shared_ptr而引入 的一种智能指针来协助shared_ptr工作,
它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构 不会引起 引用记数的增加或减少。
没有重载*和->但可以使用lock获得一个可用的shared_ptr对象。
- "闭环" 相互引用
Ref: http://www.cnblogs.com/TianFang/archive/2008/09/20/1294590.html
#include <string>
#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp> class parent;
class children; typedef boost::shared_ptr<parent> parent_ptr;
typedef boost::shared_ptr<children> children_ptr; class parent
{
public:
~parent() { std::cout <<"destroying parent\n"; } public:
children_ptr children; // 不要用shared_ptr,改为weak_ptr即可解决问题
}; class children
{
public:
~children() { std::cout <<"destroying children\n"; } public:
parent_ptr parent;
}; void test()
{
parent_ptr father(new parent());
children_ptr son(new children);
// 互相引用对方
father->children = son;
son->parent = father;
} void main()
{
std::cout<<"begin test...\n";
test();
std::cout<<"end test.\n";
}
运行该程序可以看到,即使退出了test函数后,由于parent和children对象互相引用,它们的引用计数都是1,不能自动释放,并且此时这两个对象再无法访问到。这就引起了c++中那臭名昭著的内存泄漏。
一般来讲,解除这种循环引用有下面有三种可行的方法:
- 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
- 当parent的生存期超过children的生存期的时候,children改为使用一个普通指针指向parent。
- 使用弱引用的智能指针打破这种循环引用。 <-- 推荐
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。这里主要介绍一下第三种方法和boost中的弱引用的智能指针boost::weak_ptr。
- 强引用 & 弱引用
一个强引用,当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。boost::share_ptr就是强引用。
相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味着弱引用它并不对对象的内存进行管理,
在功能上类似于普通指针,然而一个比较大的区别是:弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
boost::weak_ptr 必须从一个boost::share_ptr或另一个boost::weak_ptr转换而来,这也说明,进行该对象的内存管理的是那个强引用的boost::share_ptr。boost::weak_ptr只是提供了对管理对象的一个访问手段。
boost::weak_ptr 除了对所管理对象的基本访问功能(通过get()函数)外,还有两个常用的功能函数:
1) expired() 用于检测所管理的对象是否已经释放;
2) lock() 用于获取所管理的对象的强引用指针。
- weak_ptr 打破循环引用
由于弱引用不更改引用计数,类似普通指针,只要把循环引用的一方使用弱引用,即可解除循环引用。对于上面的那个例子来说,只要把children的定义改为如下方式,即可解除循环引用:
class children
{
public:
~children() { std::cout <<"destroying children\n"; } public:
boost::weak_ptr<parent> parent;
};
最后值得一提的是,虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在程序员能预见会出现循环引用的情况下才能使用,也可以是说这个仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。因此,不要认为只要使用了智能指针便能杜绝内存泄漏。毕竟,对于C++来说,由于没有垃圾回收机制,内存泄漏对每一个程序员来说都是一个非常头痛的问题。
- 不适合数组
Smart Pointers for Arrays, shared_ptr does not provide support for arrays.
End.