C++Primer笔记——第十二章:动态内存

一、本章学习总结

  1. 使用动态内存(堆内存)的必要性。了解手动管理动态内存时一些易错的场景。
  2. 智能指针的定义和初始化:①make_shared②用new返回的指针初始化直接初始化③对于空指针p,可用p.reset(new Type iniVal)来转移指向的地址。
  3. 共享指针的核心概念:
  • 每个shared_ptr都有一个关联的引用计数(reference count)。可以用p.use_count()来查看。
  • 无论何时只要拷贝一个shared_ptr(①用该ptr初始化另一个pt②该ptr作为参数传递给一个函数③该ptr作为函数的返回值),计数器就会递增;
  • 给该ptr赋值或者有其他共享ptr被销毁(如离开其作用于),该ptr的计数器会递减;
  • 一旦一个shared_ptr计数器变为0,它就会自动释放自己管理的对象
例子:
int *q = new int(42), *r = new int(100);
r = q;
auto q2 = make_shared(42), r2 = make_shared(100);
r2 = q2;

这段代码中,第二行存在两个问题:
①r原来指向的内存(100对应的)变成了孤儿内存,即内存泄漏问题;
②对于多个指针指向同一块内存的情况,如果释放其中一个,还在使用另一个空悬指挥进行读、写等操作,会产生未定义的后果。
而使用智能指针就可以不用管这些。r2被q2赋值,故r2指向的内存对应计数值减1变成0,所以内存释放;q2的计数值加1.

二、各小节知识点回顾

12.1 动态内存与智能指针

引入1:为什么要使用动态内存?

  首先应该知道,计算机系统共分为四块内存区域:1)栈:栈中储存的是一些我们定义的局部变量以及形参;2)字符常量区:主要是储存一些字符常量,比如:char *p_str = "cgat" (其实c++11不支持这么写,会报警告:ISO C++11 does not allow conversion from string literal to 'char *'); 其中" cgat"就储存在字符常量区;3)全局区:在全局区储存一些全局变量和静态变量‘4)堆:堆主要储存动态分配的对象’’。

  举个例子来说明动态内存的必要性:假如有一个网站,有的时候10人在线,有的时候1000000人在线,需要为每个在线用户分配一个session对象,这种情况只能动态分配。

引入2: 什么是内存泄漏?

  如上述所说,动态分配的变量储存在堆里面。但是堆的空间并不是无限大的。对于小程序可能不会出现明显问题,但是对于一些大程序,假如我们没有及时释放堆的空间,而一直在向堆中添加新的对象,导致堆的空间都被我们动态分配了,这样再使用动态内存去分配空间时,堆中就没有可供使用的内存。这时候堆会将之前使用过的内存重新分配给我们,从而导致原来储存的数据被破坏。这个问题即内存泄漏。

  为了避免内存泄漏,及时释放分配的动态内存成为程序员的一个重要工作。在以前的c++版本中,程序员通过new为对象分配动态内存,然后通过delete释放该内存。但是这种管理方法非常容易出错,见下面的例子:

  • 场景一:
//程序员A写的接口函数
Foo* factory(T arg)
{
    // 处理arg等操作
    return new Foo(arg); //返回指向动态内存的指针
}
//程序员B调用该函数
void use_factory(T arg)
{
    Foo *p = factory(arg);
    // 使用指针p...
}
  • 场景二:程序遇到异常
void f()
{
    int *ip = new int(42);
    // 中间这段代码抛出一个异常,且在f中未被捕获
    delete ip;
}

  类似场景一,程序员B非常容易忘记释放该动态指针,进而造成内存泄漏。而场景二中虽然写了释放内存的指令,由于异常导致程序未执行到最后,也会造成内存泄漏。

  • 场景三:空悬指针
      虽然这个问题和内存泄漏不是同一个问题,我也把它整理到这里,作为使用new手动分配内存的一个常见麻烦:
int *p(new int(42));
auto q = p;
delete p;  //p和q均变为无效
p = nullptr;  //指出p不再绑定到任何位置

  这一段程序虽然显示地释放了动态内存,同时重置了其中一个指针,但是可能有其他和该指针指向同一块已释放内存的指针,仍处于空悬状态。一个一个重置这些指针显然是很麻烦的。

由于new和delete容易出错,C++11引入智能指针
12.1.1 智能指针的定义与初始化

i. 最简单的方式:使用make_shared进行初始化


Fig. 1. make_shared初始化shared_ptr

ii. shared_ptr与new结合:

Fig. 2. new和shared_ptr结合

  使用new初始化share_ptr的一个好处在于其初始化方式比较多。使用new初始化一个动态分配的对象,可以使用:①列表初始化②直接初始化③值初始化④默认初始化。值得注意的是,内置类型(如int)和类类型的值初始化和默认初始化是不同的。这里通过例子区分一下:
  【更新】奇怪的一点是,对于下面的pi2,如果打印出*pi2会发现值为0,并不是未定义..也没有报错。

//注意一点,使用new无法为其分配的对象命名,而是返回一个指向该无名对象的指针
int *pi = new int(1024);  //直接初始化
vector *pv = new vector{1,2,3,4,5,6}; //列表初始化
string *ps1 = new string();  //类的值初始化使用类的默认初始化
int *pi1 = new int();  //内置类型的值初始化为0
string *ps2 = new string;  //类类型默认初始化将调用默认构造函数
int *pi2 = new int; //内置类型默认初始化,值未定义

注意:接受指针参数的智能指针构造函数是explicit的,故我们不能将一个内置指针隐式转换为一个智能指针。同理,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针。

shared_ptr clone(int a) {
    //return new int(a);  /错误!return 的值必须能隐式转换为返回类型!  见P200
    return make_shared (a); //或者return shared_ptr(new int(a));
}
  • 一种较复杂的情形是我们要给一个定义在先,但是没有进行初始化的shared_ptr进行赋值。一种办法是让其指向一个新的,不为空的动态内存。如下面代码:
 shared_ptr p1;
 if (!p1)
     cout<< "p1 is empty"<empty())
     //cout<<"p1 point to empty"<empty()<

注意:由于p1未初始化,故为一个空指针。不能调用p1->empty(),因为p1实际并没有指向一个string对象。这句命令会报错:segmentation fault。另一种产生空shared_ptr的常见的情景是通过map对象下标访问一个key不存在而mapped_type为shared_ptr的对象。这样map对象会生成一个key为给定值,而值为空shared_ptr的pair对象。这时候如果我们要使用这个空的动态指针,也要通过上述方法,即:

 map> my_map;
 auto rst = my_map[2];
 rst.reset(new vs{"a","new","vector"});
 cout<size()<
12.1.2 为什么程序要使用动态内存

  前面引入部分简单介绍了使用动态内存的必要性。这里再正式总结一下。程序使用动态内存出于下面三种原因:

  • 程序不知道自己要使用多少对象
  • 程序不知道所需对象的准确类型
  • 程序需要在多个对象间共享数据

  在这三点中,第二点后面会学到。第一点书上说的很简单,“容器是出于第一种原因而使用动态内存的典型例子”。实际上我的水平不足以理解容器的动态内存分配。所以先放一放。目前能理解的只有第三点,即不同对象之间共享数据。

  首先,考虑如下场景。假如我们在开发一个类似OpenCV的程序。一个我们必须考虑的问题是图像处理中大量的pipeline:对于同一个array我们往往要调用一系列的方法去处理。假如我们使用容器存储图像矩阵,在某一个处理阶段结束时,该容器可能会被释放,其中的元素也会被销毁。一种共享容器内容的方法是不断的使用拷贝:

 vector v1;
 {
     vector v2={"a","an","the"};
     v1 = v2;
 }

  显然大量的拷贝矩阵(尤其是图像这种大型数据)并不是一个好方法。我们试图寻找一个可以在多个对象之间共享低层元素的方法,这里就可以使用动态内存。为了简单起见,可以将这个场景简化为管理vector中的string对象。如下面的例子中,我们创建一个strBlob类来管理string类型。简单的说,我们要定义一个新的集合类型,它可以像普通vector一样管理string,同时需要满足一个功能:假如有两个共享相同vector的对象b1,b2。当b1离开作用域时,vector中的元素应该继续存在。

  因此一个简单的解决办法,是利用vector的成员方法来定义strBlob类的操作(即成员函数);同时将vector保存在动态内存中。

// strBlob.h
 #ifndef STRBOLB_H
 #define STRBOLB_H
 #include
 #include
 #include
 #include
 using std::string;
 using std::vector;
 typedef vector vs;

 class strBlob
 {
 public:
     strBlob();
     strBlob(std::initializer_list il);
     unsigned size() const{return data->size();};
     void push_back(const string &s) {data->push_back(s);};
     void print() const;
     bool empty(){return data->empty();};
     unsigned count_shared(){return data.use_count();}
 private:
     std::shared_ptr data;
 };

 strBlob::strBlob():data(new vs()) {}
 strBlob::strBlob(std::initializer_list il): data(new vs(il)) {}

 void strBlob::print() const
 {
     for (const auto &x : *data)
     {
         std::cout<

测试程序:

#include 
#include 
#include "strBlob.h"
using namespace std;

int main()
{
    strBlob b1;
    {
        strBlob b2({"this","is","a","new","strBlob"});
        b1 = b2;
        cout<<"use_count b2: "< blobs;
    blobs.insert(b1);
    blobs.insert({"new","blob"});
    cout<<"[INFO] print all strBlob objects in set: "<

打印出结果:

$ ./compile.sh strBlob_Main.cpp && ./strBlob_Main
[INFO] only one arg passed.
use_count b2: 2
use_count b2: 1
[INFO] print all strBlob objects in set:
new, blob,
this, is, a, new, strBlob,

  上述程序实现一个使用智能指针管理动态内存版本的vector。在对象b2的作用于结束之后,b2指向的内存由于use_count()值大于1,该内存并不会被释放掉,故通过共享指针实现了数据在不同对象之间的共享。

  另外注意一个地方:set类型的对象blobs的插入顺序和打印顺序是不一样的。因为set会对元素进行排序。这样是为什么strBlob类中专门重载了<运算符。

12.1.3 share_ptr使用中的注意事项

i. 不能将一个内置指针隐式转换为一个智能指针

//错误情况一:
shared_ptrp2 = new int(42);
//错误情况二:
shared_ptr clone(int p)
{return new int(p);}

ii. 不要将智能指针和普通指针混用。

void process(shared_ptr ptr)
{
    ...//使用ptr
} //ptr离开作用域,被销毁
int *x(new int(42));
process(shared_ptr(x));
int  i = *x; //错误: x未定义

正确用法:

void process(shared_ptr ptr)
{
    ...//使用ptr
} //ptr离开作用域,被销毁
shared_ptr p  = new int(42);
process(p);
int i = *p;

iii. 不要使用get初始化另一个智能指针或者为智能指针赋值
只有在确定代码不会delete指针的情况下,才能使用get。

12.1.4 智能指针和异常

  情景:所有标准库类都定义了析构函数,负责清理对象使用的资源。但是不是所有类都是这样良好定义的。对于这些分配了资源又没有定义西沟函数的类,可能会遇到与使用动态内存相同的错误——程序员忘记释放资源。我们可以使用智能指针来管理未定义析构函数的类。

  例子:见书P416。基本语法是:

shared_ptr p(q, d); //其中q为指针

注意:p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象(callable)d来代替delete。

三、章节练习:

  题目:使用标准库——文本查询程序。允许用户在一个给定文件中查询单词。查询结果是单词在文件中出现的次数及其所在行的列表。如果一个单词在一行中出现多次,此行只列出一次。行会按照升序输出。代码:

//TextQuery.h
#ifndef TEXTQUERY_H
#define  TEXTQUERY_H
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "my_utils.h"
using std::string;
using std::vector;
using std::cout;
using std::endl;
typedef vector vs;

class QueryResult;
class TextQuery
{
public:
    TextQuery(std::ifstream &in);
    QueryResult query(const string &s) const; //prepare wm and file

private:
    std::map>> wm;
    std::shared_ptr file;
};

class QueryResult
{
public:
    friend std::ostream& print(std::ostream &out, const QueryResult &qr); // return type: ostream&
    QueryResult(const string &s,
                std::shared_ptr> p,
                std::shared_ptr f): sought(s), lines(p), file(f) {};

private:
    string sought;
    std::shared_ptr> lines;
    std::shared_ptr file;
};

//TextQuery::TextQuery(std::ifstream &in) //without initializing member file, this code will cause an error: "segmentation fault".
TextQuery::TextQuery(std::ifstream &in): file(new vs)  //again, file declared in class is not initialized and remains an empty pointer.
{
    string word, line;
    unsigned n=0;
    while(std::getline(in, line))
    {
        ++n;
        file->push_back(line);
        std::istringstream stream(line);
        while(stream>>word)
        {
            auto &ret = wm[word]; //when the current word not exists in wm, will ret point to any memory?  NO! ret will be empty pointer.
            //ret->insert(n); //if word not exist, this will raise an error: null passed to a callee that requires a non-null argument
            if (!ret) //check if ret is empty
                ret.reset(new std::set);
            ret->insert(n);
        }
    }
}

QueryResult TextQuery::query(const string &s) const
{
    static std::shared_ptr> nodata(new std::set);
    auto loc = wm.find(s);
    if (loc==wm.end())
        return QueryResult(s, nodata, file);
    else
        return QueryResult(s, loc->second, file);
}

std::ostream& print(std::ostream &out, const QueryResult &qr)
{
    out<size()<<" "<size(), "time")<begin()+num)<;
    }
    return out;
}
#endif

主程序:

//main.cpp
#include
#include "TextQuery.h"
#include

using namespace std;

int main(int argc, char * argv[])
{
    ifstream in(argv[1]);
    if (!in)
    {
        cout<<"Fail to open file."<>key) || key=="q")
            break;
        print(cout, tq.query(key))<

运行:

$ ./main word_list
Please enter a key word:
this
this occurs 3 times
        (line 2) store some lines
        (line 5) but it can be
        (line 7) and now end.

Please enter a key word:
q

你可能感兴趣的:(C++Primer笔记——第十二章:动态内存)