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

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

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

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

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

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

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

 vector<string> v1;
 {
     vector<string> 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<iostream>
 #include<vector>
 #include<memory>
 #include<string>
 using std::string;
 using std::vector;
 typedef vector<string> vs;

 class strBlob
 {
 public:
     strBlob();
     strBlob(std::initializer_list<string> 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<vs> data;
 };

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

 void strBlob::print() const
 {
     for (const auto &x : *data)
     {
         std::cout<<x<<", ";
     }
     std::cout<<std::endl;
 }

 inline bool operator< (const strBlob &b1, const strBlob &b2)
 {
     return b1.size() < b2.size();
 }
 #endif

测试程序:

#include <iostream>
#include <set>
#include "strBlob.h"
using namespace std;

int main()
{
    strBlob b1;
    {
        strBlob b2({"this","is","a","new","strBlob"});
        b1 = b2;
        cout<<"use_count b2: "<<b2.count_shared()<<endl;
    }
    cout<<"use_count b2: "<<b1.count_shared()<<endl;
    set<strBlob> blobs;
    blobs.insert(b1);
    blobs.insert({"new","blob"});
    cout<<"[INFO] print all strBlob objects in set: "<<endl;
    for (const auto &b : blobs)
        b.print();
    return 0;
}

打印出结果:

$ ./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_ptr<int>p2 = new int(42);
//错误情况二:
shared_ptr<int> clone(int p)
{return new int(p);}

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

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

正确用法:

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

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

12.1.4 智能指针和异常

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

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

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

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

三、章节练习:

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

//TextQuery.h
#ifndef TEXTQUERY_H
#define  TEXTQUERY_H
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <memory>
#include <sstream>
#include <fstream>
#include "my_utils.h"
using std::string;
using std::vector;
using std::cout;
using std::endl;
typedef vector<string> vs;

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

private:
    std::map<string, std::shared_ptr<std::set<unsigned>>> wm;
    std::shared_ptr<vs> file;
};

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

private:
    string sought;
    std::shared_ptr<std::set<unsigned>> lines;
    std::shared_ptr<vs> 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<unsigned>);
            ret->insert(n);
        }
    }
}

QueryResult TextQuery::query(const string &s) const
{
    static std::shared_ptr<std::set<unsigned>> nodata(new std::set<unsigned>);
    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<<qr.sought<<" occurs "<<qr.lines->size()<<" "<<make_plura(qr.lines->size(), "time")<<endl;
    for (auto num: *qr.lines)
    {
        out<<"\t(line "<<num + 1<< ") "
        <<*(qr.file->begin()+num)<<endl;  //*** cout qr.file: shared_ptr<vs>;
    }
    return out;
}
#endif

主程序:

//main.cpp
#include<iostream>
#include "TextQuery.h"
#include<fstream>

using namespace std;

int main(int argc, char * argv[])
{
    ifstream in(argv[1]);
    if (!in)
    {
        cout<<"Fail to open file."<<endl;
        exit(1);
    }
    TextQuery tq(in);
    string key;
    while(true)
    {
        cout<<"Please enter a key word: "<<endl;
        if(!(cin>>key) || key=="q")
            break;
        print(cout, tq.query(key))<<endl;
        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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,302评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,563评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,433评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,628评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,467评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,354评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,777评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,419评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,725评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,768评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,543评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,387评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,794评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,032评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,305评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,741评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,946评论 2 336

推荐阅读更多精彩内容