C++ Primer 学习笔记 第十二章 动态内存

全局对象在程序启动时分配,在程序结束时销毁。局部自动对象在我们进入其所在的程序块时被创建,在离开块时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。

除了自动和static对象外,C++还支持动态分配对象,动态分配的对象的生存期与他们在哪里创建是无关的,只有当显式地被释放,这些对象才会被销毁。

动态对象的正确释放是编程中极其容易出错的地方,为安全地使用动态对象,标准库定义了两个智能指针管理动态分配的对象,当一个对象应该被释放时,指向它的智能指针可以确保自动释放它。

静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。

栈内存用来保存定义在函数内的非static对象。

分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块内运行时才存在。而static对象在使用前分配,在程序结束时销毁。

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

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

动态内存使用时非常容易出问题,有时我们会忘记释放内存,这种情况下会产生内存泄漏,有时在尚有指针引用内存的情况下我们就释放了它,这种情况下会产生引用非法内存的指针。

C++11为了更安全和容易地使用动态内存,提供了两种智能指针类型来管理对象。智能指针的行为类似于常规指针,但它可以自动释放所指的对象。新标准提供的两种智能指针的区别在于管理底层指针的方式:
1.shared_ptr允许多个指针指向同一个对象。
2.unique_ptr独占其指向的对象。
标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在头文件memory中。

类似于vector,智能指针也是模板,因此,创建一个智能指针时,要在尖括号内给出指针可以指向的类型:

shared_ptr<string> p1;    //可以指向string的shared_ptr

上例中默认初始化的智能指针中保存着一个空指针。

智能指针的使用方式与普通指针类似,解引用一个智能指针返回它所指向的对象,如果在一个条件判断中使用智能指针,效果是检测它是否为空指针:

if (p && p->empty()) {    //p不是空指针且p指向的string是空串时,给p指向的string赋值
    *p = "hi";
}

shared_ptr和unique_ptr都支持的操作:
在这里插入图片描述
只有shared_ptr支持的操作:
在这里插入图片描述
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr,它也定义在头文件memory中。

shared_ptr<int> p3 = make_shared<int>(42);    //p3是一个指向值为42的int的shared_ptr
shared_ptr<string> p4 = make_shared<string>(10, '9');
shared_ptr<int> p5 = make_shared<int>();    //p5指向一个值初始化的int

类似于顺序容器的emplace成员,make_shared用参数来构造一个给定类型的对象,如,调用make_share<string>时传递的参数必须与string的某个构造函数匹配。如果我们不传递任何参数,对象会进行值初始化。

我们可以用auto来推断make_shared的结果:

auto p6 = make_shared<vector<string>>();    //p6类型为shared_ptr<vector<string>>,指向一个空的vector<string>对象

当进行拷贝或赋值时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:

auto p = make_shared<int>(42);    //p指向的int只有一个引用者
auto q(p);    //p和q指向相同对象,此对象有两个引用者

我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增,如用一个shared_ptr初始化另一个shared_ptr、将它作为参数传递给一个函数、作为函数的返回值时,它的计数器就会递增。当我们给一个shared_ptr赋予新值或shared_ptr被销毁(如一个局部的shared_ptr离开作用域)时,计数器就会递减。

一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:

auto r = make_shared<int>(42);    //r指向的int只有一个引用者
r = q;    //给r赋值,让它指向另一个对象,递增q指向的对象的计数器,递减r原来指向的对象的计数器,r原来指向的对象已没有引用者,会自动释放

到底是用一个计数器还是其它数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr会自动销毁此对象,它是通过析构函数完成销毁工作的。类似于构造函数,每个类都有一个析构函数,就像构造函数控制类的初始化一样,析构函数控制此类型的对象销毁时做什么操作。

析构函数一般用来释放对象所分配的资源。如string的构造函数会分配内存来保存构成string的字符,string的析构函数就负责释放这些内存。类似地,vector的若干操作都会分配内存来保存其元素,它的析构函数负责销毁这些元素,并释放它们所占用的内存。

shared_ptr的析构函数会递减它们所指向对象的引用计数,如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

shared_ptr<int> func(int i) {
    shared_ptr<int> p = make_shared<int>(i);
    return p;    //在函数结束,p被销毁前,p被返回,计数器加一
}    //此时p离开了作用域,但它指向的内存不会被释放掉

我们要保证最后一个shared_ptr不用后不再保留它,如果忘记销毁它,程序仍会继续运行,但会浪费内存。

如果你将shared_ptr存放于一个容器,然后用了某些操作,如unique,可能会导致有些元素忘记删除,应该确保不需要的元素用erase删除。

程序使用动态内存原因:
1.程序不知道自己需要使用多少对象。
2.程序不知道所需对象的准确类型。
3.程序需要在多个对象间共享内存。

容器类是出于第一种原因使用动态内存的例子。

使用动态内存让多个对象共享底层数据,下面是自定义的Blob类的要求:

Blob<string> b1;    //空Blob
{    //新作用域
    Blob<string> b2 = { "a", "an", "the" };
    b1 = b2;    //b1和b2共享相同的元素
}    //b2被销毁了,但b2中的元素不能销毁

StrBlob是一个管理string的Blob类,我们借助标准库类型vector来管理元素所使用的内存空间。我们不能在一个Blob对象中直接保存vector对象,因为一个对象的成员会在对象销毁时销毁,如,假定b1和b2共享相同的vector,当b1销毁时,此vector也会被销毁,其中的元素都将不复存在,因此我们需要将vector定义在动态内存中。

为实现数据共享,为每个StrBlob设置一个shared_ptr来管理动态分配的vector,此shared_ptr的成员将记录有多少个StrBlob共享相同的vector,并在vector的最后一个使用者被销毁时释放vector。

class StrBlob {
public:
    typedef std::vector<std::string>::size_type size_type;
    StrBlob();
    StrBlob(std::initializer_list<std::string> il);
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    //添加和删除操作
    void push_back(const std::string &t) { data->push_back(t); }
    void pop_back();
    //元素访问
    std::string &front();
    std::string &back();
private:
    std::shared_ptr<std::vector<std::string>> data;
    //如果data[i]不合法,抛出一个异常
    void check(size_type i, const std::string &msg) const;
};

构造函数:

StrBlob::StrBlob() : data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(std::initializer_list<std::string> il) : data(make_shared<vector<string>>(il) { }

成员访问函数在操作前需要检查元素是否存在,需要check的私有工具函数:

void StrBlob::check(size_type i, const string &msg) const {
    if (i >= data->size()) {
        thorw out_of_range(msg);
    }
}

成员访问函数:

string& StrBlob::front() {
    check(0, "front on empty StrBlob");
    return data->front();
}
string& StrBlob::back() {
    check(0, "back on empty StrBlob");
    return data->back();
}
void StrBlob::pop_back() {
    check(0, "pop_back on empty StrBlob");
    data->pop_back();
}

以上的front和back应该对const进行重载。

StrBlob使用默认版本的拷贝、赋值和销毁成员函数。默认情况下,这些操作拷贝、赋值、销毁类的数据成员。

new分配动态内存,delete释放new分配的内存。

自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:

int *pi = new int;    //pi指向一个动态分配的、未初始化的无名对象

默认情况下,动态分配的对象是默认初始化的:

string *ps = new string;    //空string

我们可以使用直接初始化方式来初始化一个动态分配的对象:

int *pi = new int(42);    //pi指向的对象值为42
string *ps = new string(10, '9');    //ps指向的对象值为"999999999"

如上,我们可以使用传统的构造方式(使用圆括号),在C++11新标准下,也可以使用列表初始化:

vector<int>* pv = new vector<int>{0,1,2,3,4,5};

也可以对动态分配的对象进行值初始化:

string *ps1 = new string;    //默认初始化为空string
string *ps2 = new string();    //值初始化为空string
int *pi1 = new int;    //默认初始化,pi1指向的对象的值未定义
int *pi2 = new int();    //值初始化为0

如上,对于有默认构造函数的类类型来说,默认初始化和值初始化是一样的,两者都会调用默认构造函数来初始化。对于内置类型区别就很大了,值初始化的内置类型对象值为0,但默认初始化时值是未定义的。类似地,对于那些依赖于编译器合成的默认构造函数来赋值的内置类型数据成员,如果它们未在类内初始化,那么它们的值也是未定义的(默认初始化)。

在C++11新标准中,可使用auto从初始化器来推断我们想要分配的对象的类型:

auto p1 = new auto(obj);    //p指向一个与obj类型相同的对象,该对象使用obj来初始化
auto p2 = new auto{a,b,c};    //错误
auto p2 = new auto({0});    //错误
auto p = new auto{1};    //错误,大括号不能与new auto一起使用

如上,若obj是一个int,那么p1的类型就是int*,以此类推,新分配的对象的值用obj进行初始化。

用new分配const对象是合法的:

const int* pi = new const int(1024);    //正确,必须初始化
const int* pi = new const int;    //不合理,用未定义的值初始化,而且不能再更改,我用的编译器未报错,但不合理
const string* ps = new const string;    //使用默认构造函数初始化

类似于其他const对象,一个动态分配的const对象必须进行初始化,对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型对象必须显式初始化。

一旦一个程序用尽了它的可用自由内存,new表达式就会失败,默认,如果new不能分配要求的内存空间,它会抛出一个bad_alloc的异常,但我们可以阻止它抛出这个异常:

int* p1 = new int;    //如分配失败,new抛出bad_alloc
int* p2 = new (nothrow) int;    //如果分配失败,new返回空指针

我们称上例中第二种形式的new为定位new,定位new表达式允许我们向new传递额外的参数,此例我们传递了一个由标准库定义的名为nothrow的对象。bad_alloc和nothrow都定义在头文件new中。

为防止动态内存耗尽,在使用完毕后,必须将空间归还给系统,我们可以通过delete表达式将动态内存归还给系统。delete接受一个指针,指向我们想要释放的对象:

delete p;    //p必须指向一个动态分配的对象或是一个空指针,释放一块并非new分配的内存,或将相同的指针值释放多次,其行为是未定义的

与new类似,delete也执行两个动作:销毁给定指针指向的对象,释放对应的内存。

通常,编译器不能分辨一个指针指向的是静态还是动态分配的对象,也不能分辨一个指针指向的内存是否已经被释放了,因此可以编译通过,尽管它们是错误的。

动态对象的生存期直到被释放时为止:

Foo* factory(T arg) {
    return new Foo(arg);    //调用者负责释放此内存
]

void use_factory(T arg) {
    Foo *p = factory(arg);
}    //p离开了它的作用域,p作为指针被销毁,但它指向的内存还未被释放

上例中,p是指向factory分配的内存的唯一指针,一旦use_factory返回,就没办法释放这块内存了。

忘记delete内存,会导致内存泄漏,因为这种内存永远都不会归还给自由空间了,而且检测困难,当程序运行很长时间后,耗尽内存时才会检测到。

使用智能指针就可以避免以上问题了。

delete一个指针后,指针变为无效的,成为了空悬指针,解决方法是可以在指针即将离开作用域前释放它关联的内存,或delete后将nullptr赋予指针。但仍然有风险:

int* p(new int(42));
auto q = p;
delete p;
p = nullptr;    //虽然p成为了空指针,但q还是空悬指针

下面的函数是否有问题:

bool b() {
    int* p = new int;
    return p;
}

上例中意图应该是判断是否成功分配了空间,但并未使用定位new,因此当分配失败时不会将nullptr赋予p,而是抛出bad_alloc异常,应使用new (nothrow) int,并且分配了空间后,并没有用到分配的空间,应该在形参列表中添加一个引用或指针,将p赋值给引用或指针以使用分配的内存。

我们可以使用new返回的指针初始化智能指针:

shared_ptr<double> p1;    //p1是一个空指针
shared_ptr<int> p2(new int(42));    //p2指向一个值为42的int

接受指针参数的智能指针构造函数是explicit的,因此,我们不能将一个内置指针类型转换为一个智能指针,必须使用直接初始化形式:

shared_ptr<int> p1 = new int(1024);    //错误
shared_ptr<int> p2(new int(1024));    //正确

因为以上原因,一个返回值类型为智能指针的类型也不能返回一个普通指针:

shared_ptr<int> clone(int p) {
    return new int(p);    //错误
    return shared_ptr<int>(new int(p));    //正确
}

默认,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针使用delete释放它所关联的对象,但我们也可以将智能指针绑定在其他的非动态内存指针上,为了做到这点,需要提供我们自己的delete操作。

定义和改变shared_ptr的其他方法:
在这里插入图片描述
在这里插入图片描述
shared_ptr可以协调它和它的拷贝之间的析构,这也是推荐使用make_shared而不是new的原因,这样我们就能在分配对象的同时将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

不要混用普通指针和智能指针:

shared_ptr<int> p(new int(42));    //引用计数为1
int i = *p;    //正确,但引用计数值还是1
void process(shared_ptr<int> ptr) {  
    
}    //ptr离开作用域被销毁,引用值减1
int* x(new int(1024));
process(x);    //错误,不能把普通指针转换为智能指针
process(shared_ptr<int>(x));    //正确,但process执行完后内存会被释放
int j = *x;    //未定义行为,x已经成为了空悬指针

上例中,创建了一个临时对象传递给process,此临时对象的引用值为1并使用移动构造函数赋值给函数形参,进入process函数后,由于process(shared_ptr<int>(x));实参是一个右值,因此使用的是移动语义,引用值依然为1,然而出process函数后,ptr被销毁,引用值减1为0,即当这个调用所在的表达式结束时,临时变量被销毁。这说明,一旦将一个shared_ptr绑定到一个普通指针,我们就将内存管理的责任交给了这个shared_ptr,一旦这样做了,我们就不应该再用内置指针访问shared_ptr指向的内存了。

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时被销毁。

智能指针类型定义了一个get函数,它返回一个内置指针,指向智能指针管理的对象,当我们需要向不能使用智能指针的代码传递一个内置指针时使用它,但使用get返回的指针的代码不能delete此指针,否则会运行时报错。

虽然编译器不会报错,但将另一个智能指针也绑定到get返回的指针上是错误的:

shared_ptr<int> p(new int(42));    //引用计数为1
int* q = p.get()
{
    shared_ptr<int>(q);    //p和此临时对象指向相同的内存,但它们是独立创建的,因此各自的引用计数都是1
    //临时对象已被销毁,它指向的内存也被释放了
}    //程序块结束
int foo = *p;    //未定义,p现在是空悬指针
//之后p被销毁时,这块内存会被第二次delete,也是未定义行为

可以用reset来将一个新的指针赋予一个shared_ptr:

p = new int(1024);    //错误,不能将一个指针赋予shared_ptr
p.reset(new int(1024));    //正确,p指向一个新对象

与赋值类似,reset会更新引用计数,并释放p指向的对象(如果有),reset常与unique一起使用,来控制多个shared_ptr共享的对象:

if (!p.unique()) {
    p.reset(new string(*p));    //如果我们不是唯一的引用者,重新创建一个相同对象,并重置p为新相同对象
}
*p += newVal;    //现在我们是唯一的引用者了,可以更改对象的值而不影响其他人
void process(shared_ptr<int> ptr) {
    cout << ptr.use_count() << endl;
   
}    //ptr离开作用域被销毁,引用值减1

int main() {
    shared_ptr<int> p(new int(42));
    process(shared_ptr<int>(p));    //正确,创建了一个临时对象并赋值给形参,使得p的引用计数加1,因此在函数内引用计数为2
}

程序需要确保在异常发生后资源能被正确地释放,简单的确保方法是使用智能指针:

void f() {
    shared_ptr<int> sp(new int(42));
    //之后程序抛出异常,且在f中未捕获
}    

程序退出有两种可能,正常处理结束或异常,这两种情况都会销毁局部对象,从而保证智能指针的计数值减1,以上程序中,sp是唯一的计数值为1的智能指针,因此内存会被释放。但如果我们使用的是new管理的内存,若在delete前抛出异常,且异常未在f中被捕获,则内存不会释放。

那些分配了资源,而又没有定义析构函数来释放资源的类,可能会遇到与使用动态内存相同的错误,如在资源分配和释放之间发生了异常,程序会出现资源泄露。如:

void f() {
    connection c = connect();    //打开一个连接
}    //如f退出前忘记调用disconnect关闭c,就无法关闭c了

上例问题可以用shared_ptr保证connection被正确关闭。但shared_ptr管理的是动态内存中的对象,默认使用delete删除对象,但在管理connection时,我们必须定义一个函数来代替delete,这个删除器函数必须能完成对shared_ptr中保存的指针进行释放的操作,本例中,删除器接受的是单个类型为connection*的参数:

void end_connection(connection* p) {
    disconnect(*p);
}

我们创建一个shared_ptr时,可传递一个(可选的)指向删除器函数的参数:

void f() {
    connection c = connect();    //打开一个连接
    shared_ptr<connection> p(&c, end_connection);
}    //此时如f因异常退出,connection会正确被关闭

当销毁p时,调用的是end_connection而非delete。

使用智能指针的规范:
1.不使用相同的内置指针值初始化(或reset)多个智能指针。
2.不delete get函数返回的指针。
3.不使用get函数初始化或reset另一个智能指针。
4.如使用get()返回的指针,记得当最后一个对应的智能指针销毁后,该指针变为无效指针。
5.如果使用智能指针管理的资源不是new分配的内存,记得传递给它一个删除器。

删除器也可以是一个lambda:

shared_ptr<connection> sp(p, [](connection* c) ->void { disconnect(c); });    //记得lambda接受的参数是指针类型

一个unique_ptr拥有它所指向的对象,与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定的对象,当unique_ptr被销毁时,它所指向的对象也被销毁。与shared_ptr相同,它也是C++11新标准。

没有类似make_shared的函数来返回一个unique_ptr,因此,我们定义一个unique_ptr时要将它绑定到一个new返回的指针上。类似于shared_ptr,unique_ptr必须使用直接初始化形式:

unique_ptr<double> p1;    //可以指向double的unique_ptr
unique_ptr<int> p2(new int(42));

由于unique_ptr拥有它的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

unique_ptr<string> p1(new string("aaa"));
unique_ptr<string> p2(p1);    //错误,unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2;    //错误,unique_ptr不支持赋值

unique_ptr支持的操作:
在这里插入图片描述
虽然unique_ptr不能使用拷贝或赋值,但可以通过调用release或reset将指针的所有权从一个非const的unique_ptr转移给另一个unique_ptr:

unique_ptr<string> p2(p1.release());    //release将p1置为空
unique_ptr<string> p3(new string("Trex"));    
p2.reset(p3.release());    //reset释放了p2之前的内存

调用release会切断unique_ptr和它原来管理的对象间的联系,但不会释放它管理的对象的内存,因此:

p2.release();    //错误,p2的内存再也不会被释放,丢失了指针
auto p = p2.release();    //正确,但要记得delete p

不能拷贝unique_ptr的规则有一个例外,可以拷贝或赋值一个将要被销毁的unique_ptr,就是函数的返回时:

unique_ptr<int> clone(int p) {
    return unique_ptr<int>(new int(p));
}    //可以从函数返回一个unique_ptr

unique_ptr<int> clone(int p) {
    unique_ptr<int> ret(new int(p));
    return ret;    //还可以返回一个局部对象的拷贝
}

上例中,编译器知道要返回的对象将要被销毁,编译器执行了特殊的拷贝。

较早版本的标准库有auto_ptr的类,它具有部分unique_ptr的特性,我们不能在容器中保存auto_ptr,也不能返回auto_ptr,我们编写程序时应该使用unique_ptr。

类似于shared_ptr,unique_ptr默认使用delete删除对象,我们也可以重载一个unique_ptr中的删除器,但我们必须在尖括号中提供删除器的类型,在初始化或reset这种类型的指针时,必须同时提供删除器:

unique_ptr<objT, delT> p(new objT, fcn);

更具体的例子:

void f() {
    connection c = connect();    //打开连接
    uniqie_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
}
unique_ptr<int> p(new int(24));
unique_ptr<int> p1(p.get());    //错误,当其中一个被释放时,它指向的对象也被销毁,此时另一个指针会成为无效指针

C++11中有一种名为weak_ptr的不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使此时还有weak_ptr指向它。

weak_ptr:
在这里插入图片描述
创建一个weak_ptr时,用shared_ptr初始化它:

auto p = make_shared<int>(42);    //创建一个shared_ptr p
weak_ptr<int> wp(p);    //wp共享p,但p的引用计数未改变

上例中,由于是弱共享,创建wp不会改变p的引用计数,但wp指向的对象可能被释放。

由于weak_ptr指向的对象可能不存在,因此我们不能直接访问它所指向的对象,必须先调用lock(),当weak_ptr指向的对象仍然存在,返回一个指向该对象的shared_ptr,否则返回空shared_ptr:

if (shared_ptr<int> np = wp.lock()) {    //如果np为空shared_ptr,则不会进入if语句块
    //使用np,np与wp共享对象
}

使用weak_ptr的例子,我们为StrBlob类定义一个伴随的指针类,此伴随类命名为StrBlobPtr,会保存一个weak_ptr,指向StrBlob的data成员,这是初始化时提供给它的,通过weak_ptr,不会影响一个给定的StrBlob所指向的vector的生存周期,但可以阻止用户访问一个不再存在的vector的企图。

StrBlobPtr有两个数据成员:wptr,或者为空,或者指向一个StrBlob中的vector;curr,保存当前对象所表示的元素的下标。类似它的伴随类,指针类也有一个check成员检查解引用StrBlobPtr是否安全:

class StrBlobPtr {
public:
    StrBlobPtr() : curr(0) { }    //隐式地将wptr初始化为空weak_ptr
    StrBlobPtr(StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) { }    //接受一个非const的引用,因此不能将StrBlobPtr绑定在一个const StrBlob上
                                                                          //如想接受const StrBlob,需要再定义一个constStrBlobPtr类,其中定义一个接受const StrBlob&的构造函数
    std::string& deref() const;    //解引用指针
    StrBlobPtr& incr();    //指针的前缀递增
private:
    std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;    //若检查成功,返回一个指向vector的shared_ptr
    std::weak_ptr<std::vector<std::string>> wptr;    //保存一个weak_ptr,它的底层vector可能被销毁
    std::size_t curr;    //数组中的当前位置
};

StrBlobPtr中的check还需检查指针指向的vector是否存在:

std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg) const {
    auto ret = wptr.lock();
    if (!ret) {
        throw std::runtime_error("unbound StrBlobPtr");
    }
    if (i >= set->size()) {
        throw std::out_of_range(msg);
    }
    return ret;
}

deref:

std::string& StrBlobPtr::deref() const {
    auto p = check(curr, "dereferencr past end");
    return (*p)[curr];
}

incr:

StrBlobPtr& StrBlobPtr::incr() {
    check(curr, "increment past end of StrBlobPtr");
    ++curr;
    return *this;
}

为了实现以上访问data成员,指针类必须声明为StrBlob的友元。

C++提供两种一次分配一个对象数组的方法:new,可分配并初始化一个对象数组;一个名为allocator的类,允许我们将分配堆空间和初始化堆空间中对象分离。allocator类提供更好的性能和更灵活的内存管理能力。

当应用需要可变数量的对象时,使用vector比动态数组更简单、快速、安全。

使用new分配一个数组:

int *pia = new int[get_size()];    //方括号内必须是整型,但不必是常量

也可以用一个表示数组类型的类型别名来分配一个数组,这样new表达式中就不需要方括号了:

typedef int arrT[42];
int *p = new arrayT;    //分配一个42个int的数组,p指向第一个int

当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。由于分配的动态数组并不是数组类型,因此不能对动态数组调用begin()和end(),也不能用范围for语句来处理动态数组中的元素。

默认,new分配的对象,不管是数组中的还是单个分配的,都是默认初始化的,但我们可以对数组中的元素进行值初始化:

int *pia = new int[10];    //10个未初始化的int
int *pia2 = new int[10]();    //10个值初始化为0的int
string *psa = new string[10];    //10个空string
string *psa2 = new string[10]();    //10个空string

在C++11新标准中,可以使用花括号列表初始化:

int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
string *psa3 = new string[10]{"a","an","the",string(3,'x')};    //前四个用给定的值初始化,后边的值初始化

如果初始化列表中的元素数目大于最大元素数目,new表达式会失败,不会分配任何内存,new会抛出类型为bad_array_new_length的异常,类似于bad_alloc异常,此异常也定义在头文件new中。

虽然我们能用空括号对数组中的值进行值初始化,但不能在括号中给出初始化列表,这意味着不能使用auto分配数组(new auto后边只能跟圆括号,且圆括号里不能有初始化列表)。

动态分配一个空数组是合法的:

char arr[0];    //错误,不能定义长度为0的数组
char *cp = new char[0];    //正确,但cp不能解引用

当我们分配一个大小为0的动态数组时,new返回一个合法的非空指针,此指针保证和其他的任何new返回的指针都不相同,对于0长度动态数组,此指针就像尾后指针。

释放动态数组:

delete p;    //p必须指向一个动态分配的对象或空
delete [] pa;    //p必须指向一个动态数组对象或空

第二条语句销毁pa指向的数组中的元素,并释放相应内存,数组中元素按逆序销毁,即最后一个元素首先销毁,然后是倒数第二个。当delete用于动态数组时,即使动态数组在创建时使用的是类型别名(创建时没加方括号),如果忘记加方括号,其行为是未定义的。

用智能指针管理动态数组:

unique_ptr<int[]> up(new int[10]);
up[3] = 3;    //正确,将up的下标为3的元素值改为3
*up = 0;    //错误,必须使用下标运算符访问元素
up.release();    //自动调用delete[]销毁指针(这是书上写错了,release不会调用delete)

指向数组的unique_ptr:
在这里插入图片描述
shared_ptr与unique_ptr不同,它不支持管理动态数组,如需要shared_ptr管理动态数组,需要自定义删除器:

shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; }
sp.reset();    //使用我们提供的lambda作为删除器释放数组

如未提供删除器,上述代码的行为未定义,因为shared_ptr默认使用delete删除元素。

使用shared_ptr访问动态数组元素:

for (size_t i = 0; i != 10; ++i) {
    *(sp.get() + i) = i;    //shared_ptr未定义下标运算符和指针的算术运算
}

new不灵活,一方面表现在将内存分配和对象构造组合在了一起。类似地,delete将对象析构和内存释放组合在了一起。

我们有时想分配一大块内存,在这块内存上按需构造对象,这就需要将内存分配和对象构造分离。

将内存分配和对象构造组合在一起可能会导致不必要的浪费:

string *const p = new string[n];    //构造n个空string
string s;
string *q = p;    //q指向p中第一个string
while (cin >> s && q != p + n) {
    *q++ = s;
}
const size_t size = q - p;    //记住我们读了多少个string
delete[] p;

如上例,如果我们不需要输入n个string,那么就浪费了一部分空间,而且用到的空间在初始化之后立刻赋予了新值,每个使用到的元素都赋值了两次(默认初始化时和赋值时)。更重要的是,没有默认构造函数的类就不能动态分配数组了。

allocator类定义在头文件memory中,它可以实现内存分配和对象构造分离开。它分配的内存是原始的、未构造的。

类似vector,allocator是一个模板,为定义一个allocator对象,必须指明这个allocator可以分配的对象类型,当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置。

allocator<string> alloc;    //可以分配string的allocator对象
auto const p = alloc.allocate(n);    //分配n个未初始化的string,为n个string分配了内存,p的类型为string const*

allocator类及其算法:
在这里插入图片描述
allocator分配的内存是未构造的,我们需要在此内存中构造对象,在C++11新标准中,construct成员函数接受一个指针和0个至多个额外参数,在给定位置构造一个元素,这些额外参数用来初始化构造的对象,类似于make_shared,这些参数必须是与构造的对象的类型相匹配的合法的构造函数参数:

auto q = p;
alloc.construct(q++);    //*q为空字符串
alloc.construct(q++, 10, 'c');    //*q为"cccccccccc"
alloc.construct(q++, "hi");    //*q为"hi"

在早期的版本中,construct只接受两个参数:指向创建对象位置的指针和一个元素类型的值,因此,我们只能将一个元素拷贝到未构造空间中,而不能用元素类型的构造函数来构造一个元素。

还未构造对象的情况下就使用原始内存是错误的:

cout << *q << endl;    //灾难,q指向未构造的内存,行为未定义

当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们:

while (q != p) {
    alloc.destroy(--q);    //释放我们构造的string,q开始指向最后一个构造的元素之后
}

一旦元素被销毁,就可以重新使用这部分内存保存其它string。也可以将其归还给系统:

alloc.deallocate(p, n);    //p不能为空,且必须指向由allocator分配的内存,n必须与调用allocate分配内存时提供的大小相等

两个为allocator类定义的伴随算法,它们也定义在memory中:
在这里插入图片描述

//v1是一个vector<int>
auto p = alloc.allocate(vi.size() * 2);    //分配比vi中元素所占空间大一倍的内存
auto q = unintialized_copy(vi.begin(), vi.end(), p);    //拷贝vi中的元素到前一半空间,q为最后一个插入的元素之后的位置
unintialized_fill_n(q, vi.size(), 42);    //剩余一半空间初始化为42

使用标准库完成文本查询程序,查询结果是单词在文件中出现的次数和出现的行列表,若一行中出现多次,只列出此行一次,行按升序输出。

实现:
1.vector<string>按行保存整个文件,打印一行时,可用行号作下标提取行。
2.使用istringstream将每行分解为一个单词。
3.用set保存单词出现的行号,这保证了每行输出一次,并按升序排列。
4.使用map将单词与它出现的行号set关联起来。

虽然我们可以直接编写程序,但如果定义一个类来解决会更方便,我们从定义一个保存输入文件的类开始,将这个类命名为TextQuery,它包含一个vector和一个map,vector保存输入文件的文本,map用来关联每个单词和它出现的行号的set,这个类将会有一个用来读取给定输入文件的构造函数和一个执行查询的操作。查询操作的任务很简单,查找map成员,检查给定单词是否出现,一旦找到了一个单词,我们需要知道它出现了多少次、出现的行号以及每行的文本,返回这些结果最简单的方法是定义另一个类,命名为QueryResult,来保存查询结果,这个类内会有一个print函数,完成结果打印工作。

由于QueryResult所需要的数据都保存在一个TextQuery对象中,我们就必须确定如何访问它们,我们可以拷贝保存行号的set,但这样做很耗时,我们不希望拷贝vector,这会引起整个文件的拷贝,我们的目标只是为了打印文件的一小部分。通过返回指向TextQuery对象内部的迭代器或指针,我们可以避免拷贝操作,但如果TextQuery对象在对应的QueryResult对象之前被销毁,将引用一个不再存在的对象中的数据,因此应该使用shared_ptr共享此处的数据。

设计一个类前,先编写使用这个类的程序是有用的方法,如下,使用TextQuery类和QueryResult类,这个函数接受一个指向要处理的文件的ifstream,并与用户交互,打印给定单词的查询结果:

void runQueries(ifstream &infile) {
    TextQuery tq(infile);
    while (true) {
        cout << "enter word to look for, or q to quit: ";
        string s;
        if (!(cin >> s) || s == "q") {
            break;
        }
        print(cout, tq.query(s)) << endl;    //此处query成员函数返回类型为QueryResult
    }
}

TextQuery的定义:

class QueryResult;    //为了定义函数query的返回类型,需要先声明此类
class TextQuery {
public:
    using line_no = std::vector<std::string>::size_type;
    TextQuery(std::ifstream);
    QueryResult query(const std::string&) const;
private:
    std::shared_ptr<std::vector<std::string>> file;
    std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
};
//TextQuery接受一个ifstream的构造函数
TextQuery::TextQuery(ifstream &is) : file(new vector<string>) {
    string text;
    while (getline(is, text)) {
        file->push_back(text);    //保存此行文本
        int n = file->size() - 1;    //获取当前行号
        istringstream line(text);    //将文本分解为单词
        string word;
        while (line >> word) {
            auto& lines = wm[word];
            if (!lines) {    //第一次输入时,wm的值会初始化为空的指向set<line_no>的指针
                lines.reset(new set<line_no>);    //分配给指针一个新set
            }
            lines->insert(n);
        }
    }
}

QueryResult类:

class QueryResult {
friend std::ostream& print(std::ostream&, const QueryResult&);    //打印QueryResult的函数
public:
    QueryResult(std::string s, std::shared_ptr<std::set<line_no>> p, std::shared_ptr<std::vector<std::string>> f) : sought(s), lines(p), file(f) { }
private:
    std::string sought;    //查询的单词
    std::shared_ptr<std::set<line_no>> lines;    //保存出现的行号
    std::shared_ptr<std::vector<std::string>> file;    //输入文件指针
};

query函数:

QueryResult TextQuery::query(const string &sought) const {
    static shared_ptr<set<line_no>> nodata(new set<line_no>);    //如没找到,返回此空智能指针对象,由于此对象会在函数结束后被销毁,因此需要被声明为static的
    auto loc = wm.find(sought);    //map的find函数返回一个指向pair的迭代器
    if (loc == wm.end()) {
        return QueryResult(sought, nodata, file);    //未找到
    }
    else {
        return QueryResult(sought, loc->second, file);    //loc迭代器解引用的结果是一个pair,之后再用点运算符获取它的second成员,也就是保存行号的set成员
    }
}

打印结果的print函数:

ostream& print(ostream& os, const QueryResult &qr) {
    os << qr.sought << " occurs " << qr.lines->size() << " " << make_plural(qr.lines->size(), "times", "s") << endl;
    for (auto num : *(qr.lines)) {
        os << "\t(line " << num + 1 << ")" << *(qr.file->begin() + num) << endl;    //行号加1从1开始
    }
    return os;
}
发布了211 篇原创文章 · 获赞 11 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/tus00000/article/details/104948658