前言


初步认识

原文链接:https://blog.csdn.net/flowing_wind/java/article/details/81301001

参考资料:《C++ Primer中文版 第五版》

我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。

在C++中,动态内存的管理是用一对运算符完成的:new和delete,new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。

标准库提供的两种智能指针,区别在于管理底层指针的方法不同,

    1. shared_ptr允许多个指针指向同一个对象,
    2. unique_ptr则“独占”所指向的对象。
    3. 标准库还定义了一种名为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的影响

命名空间

三大永生:

    1. static Objects
    2. Global Objects
    3. 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 []的真正区别

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函数。

链接:老板不让用shared_ptr,会是什么原因?

我能想到的原因是由这些指针管理的情形同时有如下特征:

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++中那臭名昭著的内存泄漏。

一般来讲,解除这种循环引用有下面有三种可行的方法:

    1. 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
    2. 当parent的生存期超过children的生存期的时候,children改为使用一个普通指针指向parent。
    3. 使用弱引用的智能指针打破这种循环引用。 <-- 推荐

虽然这三种方法都可行,但方法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.

[c++] Smart Pointers的更多相关文章

  1. [译]GotW #89 Smart Pointers

    There's a lot to love about standard smart pointers in general, and unique_ptr in particular. Proble ...

  2. C++11特性 - Smart Pointers 智能指针

    已经有成千上万的文章讨论这个问题了,所以我只想说:现在能使用的,带引用计数,并且能自动释放内存的智能指针包括以下几种:         unique_ptr: 如果内存资源的所有权不需要共享,就应当使 ...

  3. 条款17:以独立语句将newed对象置入智能指针(Store newed objects in smart pointers in standalone statements)

    NOTE: 1.以独立语句将newed对象存储于智能指针内.如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏.

  4. c++ smart pointer

    智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露.它的一种通用实现技术是使用引用计数(reference ...

  5. Smart Pointer Guidelines

    For Developers‎ > ‎ Smart Pointer Guidelines What are smart pointers? Smart pointers are a specif ...

  6. boost smart pointer

    1. boost::scoped_ptr is a smart pointer that is the sole owner of a dynamically allocated object and ...

  7. enote笔记法使用范例(2)——指针(1)智能指针

    要知道什么是智能指针,首先了解什么称为 “资源分配即初始化” what RAII:RAII—Resource Acquisition Is Initialization,即“资源分配即初始化” 在&l ...

  8. C++异常处理:try,catch,throw,finally的用法

    写在前面 所谓异常处理,即让一个程序运行时遇到自己无法处理的错误时抛出一个异常,希望调用者可以发现处理问题. 异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制. 也许我们已经使 ...

  9. Poco::JSON::Array 中object 设置preserveInsertionOrder 时,stringify出错-->深入解析

    在使用poco version 1.6.0时 Poco::JSON::Array 在object  设置preserveInsertionOrder =true 时 调用 array.stringif ...

随机推荐

  1. HTML5

    一.头部设置 <!--页面窗口自动调整到设备宽度,并禁止用户及缩放页面--> <meta name="viewport" content="width= ...

  2. Linux_10个需要了解的Linux网络和监控命令(转)

    源文地址:http://www.linuxde.net/2013/10/15325.html 1. hostname hostname 没有选项,显示主机名字 hostname –d 显示机器所属域名 ...

  3. Jenkins部署配置简介

    前段时间研究了一下自动化测试,因而接触到了Jenkins,今天有时间进行一下Jenkins部署配置相关知识的总结分享 前言:由于本次只是实验性研究,采用Windows环境,因此Jenkins可以通过下 ...

  4. Call to undefined function Think\mb_strlen()

    在php.ini 中开启php_mbstring.dll 模块后重启 apache

  5. Apache服务器在80端口配置多域名虚拟主机的方法

    我们在配置一台服务器的时候,如果只运行一个站点,往往过于浪费资源.Nginx和Apache都可以通过配置虚拟主机实现多站点.配置虚拟主机的方式主要有两种,一种是多个不同端口对应的多个虚拟主机站点,一种 ...

  6. Mysql的用户名密码设置方法

    方法如下: 1, 关闭mysql服务 /etc/init.d/mysqld stop 2,使用 –skip-grant-tables选项启动mysql服务,可以修 改/etc/inin.d/mysql ...

  7. 【开源】MQTT推送服务器——zer0MqttServer(Java编写)

    目录 说明 功能 如何使用 参考帮助 说明 重要的放前面:V1.0版本是一个非常基础的版本,除了完整的MQTT协议实现外,其他功能什么都没做. MQTT 协议是 IBM 开发的即时通讯协议,相对于 I ...

  8. SQL Server2000清除数据库日志

    sqlserver2000压缩日志 可以将jb51.ldf文件变得很小,方便备份数据库等,在sqlserver查询分析器中执行即可.复制代码 代码如下: DUMP TRANSACTION [jb51] ...

  9. 多位数每一位个系数:个位num%10;十位num/10%10.......

    请输出满足这样条件的五位数. 个位=万位 十位=千位 个位+十位+千位+万位=百位 思路: 1.定义一个要操作的五位数变量num 2.求出每一位个系数 个:num%10 十:num/10%10 百:n ...

  10. 《Linux内核设计与实现》读书笔记 第十八章 调试

    第十八章调试 18.1 准备开始          需要准备的东西: l  一个bug:大部分bug通常都不是行为可靠而且定义明确的 l  一个藏匿bug的内核版本:找出bug首先出现的版本 l  相 ...