• 对象的生存期
    • 全局对象:程序启动时创建,程序结束时销毁
    • 局部static对象:第一次使用前创建,程序结束时销毁
    • 局部自动对象:定义时创建,离开定义所在程序块时销毁
    • 动态对象:生存期由程序控制,在显式创建时创建,显式销毁时销毁
  • 动态对象的正确释放极易出错。为安全使用动态对象,标准库定义了智能指针来管理动态对象
  • 内存空间:
    • 静态内存:局部static对象、类static数据成员、定义在任何函数之外的变量
    • 栈内存:定义在函数内的非static对象
    • 堆内存:动态对象,即运行时分配的对象
  • 静态内存和栈内存中的对象由编译器创建和销毁,堆内存中的动态对象的生存期由程序控制

动态内存与智能指针

  • C++通过一对运算符管理动态内存:
    • new算符在动态内存中为对象分配空间并返回指向该对象的指针,可选择对对象初始化
    • delete算符接受一个动态对象的指针,销毁该对象并释放内存
  • 确保在正确时间释放内存很难:
    • 若不释放内存,导致内存泄露
    • 若还有指针指向该内存就将其释放,导致指针空悬
  • C++11标准库中提供了两种智能指针来管理动态对象,定义于memory头文件:
    • shared_ptr允许多个指针指向同一对象
    • unique_ptr指针独占所指向对象
    • weak_ptr是伴随类,是一种弱引用,指向shared_ptr管理的对象

shared_ptr类

  • 智能指针也是模板类,创建时必须在模板参数中给定其指向的类型
  • 默认初始化的智能指针中保存空指针,条件判断中使用智能指针是判断其是否为空
  • 解引用智能指针返回其指向的对象
  • shared_ptr和unique_ptr都支持的操作见表12.1,shared_ptr独有的操作见表12.2 tab_12_1 tab_12_2
  • 最安全的分配和使用动态内存的方法是调用make_shared函数,该函数定义于memory头文件中,它在动态内存中分配一个对象并初始化,返回指向它的shared_ptr
  • make_shared函数用法:
    • 是模板函数,使用时必须在模板参数中给出构造对象的类型
    • 其参数必须与构造对象的构造函数参数匹配,使用这些参数构造对象
    • 若不给实参,则对象值初始化
  • 对shared_ptr进行拷贝/赋值时,每个shared_ptr会记录有多少个其他shared_ptr指向相同对象
  • 每个shared_ptr都有一个关联的计数器,称为引用计数
    • 一个shared_ptr的一组拷贝之间共享“引用计数管理区域”,并用原子操作保证该区域中的引用计数被互斥地访问
    • 互相独立的shared_ptr维护的引用计数也互相独立,即使指向同一对象。因此需避免互相独立的shared_ptr指向同一对象
  • 改变引用计数
    • 递增:拷贝shared_ptr时,包括:用一个shared_ptr初始化另一个shared_ptr、作为参数传入函数、作为返回值从函数传出
    • 递减:给shared_ptr赋新值、shared_ptr被销毁(例如离开作用域)
    • 一旦shared_ptr的计数器变为0,会自动释放管理的对象
  • C++标准并未要求使用计数器实现引用计数,其实现取决于标准库的实现
  • 指向对象的最后一个shared_ptr被销毁时,shared_ptr会通过它的析构函数完成对象的销毁
  • 析构函数控制此类型对象销毁时的操作,一般用于释放对象的资源。shared_ptr类型的析构函数被调用时递减引用计数,一旦计数为0即销毁对象。
  • 例子:使用make_shared创建factory
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//创建对象并返回智能指针
shared_ptr<Foo> factory(T arg){
    return make_shared<Foo>(arg);   //创建时计数为1,传出拷贝+1,离开作用域-1
}
//使用factory创建对象,使用完后销毁
void use_factory(T arg){
    shared_ptr<Foo> p=factory(arg); //创建对象,初始引用计数为1
    /* 使用p */                     //未传出,离开作用域时计数-1变为0,对象被销毁
}
//使用factory创建对象,使用完后不销毁
shared_ptr<Foo> use_factory(T arg){
    shared_ptr<Foo> p=factory(arg); //创建对象,初始引用计数为1
    /* 使用p */
    return p;                       //传出时拷贝+1,离开作用域-1,传出后计数为1
}
  • 必须确保shared_ptr在不使用时及时删除。例如容器中的shared_ptr在不使用时要erase
  • 使用动态内存的3种情况
    • 不知道需要使用多少对象(容器)
    • 不知道所需对象的准确类型(多态)
    • 需在多个对象间共享数据
  • 若两个对象共享底层数据,则某个对象被销毁时不可单方面销毁底层数据。此时应将共享的数据做成对象,在需共享它的两个类内分别用shared_ptr访问
  • 例子:用shared_ptr实现共享资源
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//定义StrBlob类
class StrBlob{
public:
    //定义类型
    using size_type=vector<string>::size_type;
    //两个构造函数,默认初始化和列表初始化
    StrBlob();
    StrBlob(initializer_list<string> il);
    //以下是对底层vector操作的封装
    size_type size() const {return data->size();}
    bool empty() const {return data->empty();}
    void push_back(const string &t) {data->push_back(t);}
    void pop_back();
    string &front();
    string &back();
private:
    //用shared_ptr管理底层的vector<string>数据
    shared_ptr<vector<string>> data;
    //检查索引i是否越界,越界时用msg抛出异常
    void check(size_type i, const string &msg) const;
};
//默认构造函数,底层vector<string>默认初始化,返回shared_ptr用于初始化data
StrBlob::StrBlob():
         data(make_shared<vector<string>>())
         {}
//构造函数,底层vector<string>列表初始化,返回shared_ptr用于初始化data
StrBlob::StrBlob(initializer_list<string> il):
         data(make_shared<vector<string>>(il))
         {}
//检查下标是否越界
void StrBlob::check(size_type i, const string &msg) const {
    if(i>=data->size())
        throw out_of_range(msg);
}
//以下3个函数分别实现front、back、pop_back操作,用0来check索引判断是否为空
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();
}
/* 使用StrBlob */
StrBlob b1;                         //创建新StrBlob
{                                   //进入新作用域
    StrBlob b2={"a","an","the"};    //初始化b2
    b1=b2;                          //用b2初始化b1,它们共享底层数据
}                                   //离开作用域,b2被释放,b1仍存在,共享的底层数据未丢失
while(b1.size()>0){
    cout<<b1.back()<<endl;
    b1.pop_back();
}
  • 对类对象使用默认版本的拷贝/赋值/销毁操作时,这些操作拷贝/赋值/销毁类的数据成员(包括智能指针)。

直接管理内存

  • 两个运算符分配/释放动态内存:
    • new分配内存,并构造对象
    • delete销毁对象,并释放内存
  • 使用new/delete管理动态内存的类不能依赖动态对象成员的拷贝/赋值/销毁的任何默认操作
  • 堆内存中分配的空间是匿名的,故new无法为其分配的对象命名,只能返回一个指向该对象的指针
  • 动态对象初始化
    • 默认情况下用默认初始化:内置类型的值未定义,类类型依赖默认构造函数
    • 直接初始化:用圆括号调用构造函数,或花括号列表初始化
    • 值初始化:类型名后跟一对空的圆括号。对于有默认构造函数的类类型而言,值初始化没有意义(都是调用默认构造函数),但对于内置类型值初始化可有良好定义的值
    • 拷贝初始化:使用圆括号里放单一对象,被分配的对象用它初始化。此时可用auto推导需分配的类型
  • 例子:动态对象初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//默认初始化
int *pi=new int;                            //未定义
string *ps=new string;                      //默认初始化为空字符串
//直接初始化
int *pi=new int(1024);                      //初始化为1024
string *ps=new stirng(10,'9');              //初始化为"9999999999"
vector<int> *pv=new vector<int>{0,1,2,3};   //初始化为{0,1,2,3,4,5}
//值初始化
int *pi=new int();                          //初始化为0
string *ps=new string();                    //初始化为空字符串
//拷贝初始化
auto p1=new auto(obj);                      //用obj拷贝初始化p1
auto p2=new auto{a,b,c};                    //错,只能拷贝初始化为auto
  • 用new分配const对象是合法的,const对象必须初始化。
  • 若new不能分配要求的空间,则抛出名为bad_alloc的异常。
  • 可向new算符传参来阻止抛出异常,传递了参数的new叫定位new
  • 向new传入std::nothrow,则它不会抛出异常。若不能分配内存,则返回空指针。
  • bad_alloc和nothrow都定义在头文件new
  • 例子:阻止new抛出异常
1
2
int *p1=new int;            //若分配失败则抛出bad_alloc异常
int *p2=new(nothrow) int;   //若分配失败则返回空指针
  • delete表达式将内存归还给系统,它接受一个指针,指向需要释放的对象
  • delete表达式执行两个工作:销毁指针指向的对象,释放对应的内存
  • 传递给delete表达式的指针必须指向动态内存,或是空指针
  • 用delete释放非new分配的内存,或者将同一指针释放多次,都是未定义
  • 编译器无法知道一个指针是否指向动态内存,也无法知道一个指针指向的内存是否已被释放,故这些错误不会被编译器发现
  • const对象的值不可改变,但可被销毁
  • 内置指针管理的动态对象,在显式释放之前一直存在
  • 内置类型的对象被销毁时什么都不会发生(与类类型不一样)。特别是,内置指针被销毁时不影响其指向的对象。若这个内置指针指向动态对象,则空间不会被释放
  • 例子:内置指针销毁时不会销毁指向对象
1
2
3
4
5
6
7
Foo *factory(T arg){
    return new Foo(arg);    //返回动态对象的指针,调用者负责释放它
}
void use_factory(T arg){
    Foo *p=factory(arg);    //分配对象,得到指向它的指针
    /* 使用p但不delete */
}                           //离开时指针被销毁,对象仍存在。再没有指针指向该对象,该动态对象无法回收,内存泄露
  • newdelete管理动态内存的常见问题:
    • 忘记delete内存,没有指针指向该动态内存时,内存泄露
    • 使用已释放的对象
    • 同一块内存释放两次
    • 用智能指针管理动态内存即可避免这些问题
  • delete之后,指针变为空悬指针,类似于未初始化的指针。为避免空悬指针,尽量在指针即将离开作用域时释放其管理的动态内存,也可在delete后立即将指针置为nullptr
  • delete内存后将指针置nullptr的做法只对单个指针有效,若还有其他指针指向该对象则它们变为空悬指针。由于很难知道有哪些指针指向这个对象,故很难用new和delete管理动态内存
  • 例子:产生空悬指针
1
2
3
4
int *p(new int(42));    //分配动态内存
auto q=p;               //两个指针同时指向动态对象
delete p;               //释放对象
p=nullptr;              //将构造动态对象时的指针置为nullptr,但其他指针变为空悬

shared_ptr和new结合使用

  • shared_ptr的操作如表12.3 tab_12_3
  • 可用new返回的内置指针初始化智能指针,如果不对智能指针初始化,就被初始化为空指针
  • 接受内置指针的智能指针构造函数是explicit的,即不能将内置指针隐式转换为智能指针,必须直接初始化
  • 例子:不能将内置指针隐式转换为智能指针
1
2
3
4
5
6
7
8
shared_ptr<int> p1=new int(1024);       //错,不可隐式转换
shared_ptr<int> p2(new int(1024));      //对,可以直接构造
shared_ptr<int> clone(int p){
    return new int(p);                  //错,不可隐式转换
}
shared_ptr<int> clone(int p){
    return shared_ptr<int>(new int(p)); //对,可以直接构造
}
  • 用于初始化智能指针的内置指针必须指向动态内存,因为智能指针默认使用delete释放其指向的对象。静态内存和栈内存不需要也不能使用智能指针
  • shared_ptr用于自动管理对象释放的功能,只限于其自身的一组拷贝之间,互相独立的shared_ptr其引用计数也互相独立内置指针不参与引用计数
  • 推荐使用make_shared而不用new的内置指针初始化shared_ptr,因为make_shared可保证分配对象的同时和shared_ptr绑定,避免将一块内存绑定到多个互相独立的shared_ptr
  • 例子:混合使用内置指针与智能指针
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void process(shared_ptr<int> ptr){  //传入时copy,计数+1
    /* 使用ptr */
}                                   //离开作用域,计数-1
//以下为正确用法:
shared_ptr<int> p(new int(42));     //新建一个智能指针
process(p);                         //处理后引用计数为1
int i=*p;
//以下为错误用法:
int *x(new int(1024));
process(x);                         //错,不可将内置指针隐式转换为智能指针
process(shared_ptr<int>(x));        //该智能指针的生存期只在这个函数中,离开时智能指针被释放,对象也被释放
int j=*x;                           //未定义,x指向的对象已被释放
  • 使用内置指针构造智能指针时必须立即构造,禁止混合使用两种指针,禁止传参时构造
  • 将一个shared_ptr绑定到一个内置指针时,内存管理的责任被交给shared_ptr,不应该再用该内置指针访问内存
  • shared_ptr定义了get成员函数,它返回内置指针,指向shared_ptr管理的对象。用于不兼容shared_ptr的情形。
  • get使用风险
    • 不可将get返回的内置指针dedete,因为原来的shared_ptr变为空悬
    • 不可用get返回的内置指针来初始化另一个shared_ptr,因为产生两套引用计数
  • 例子:不可用get返回的内置指针来初始化另一个shared_ptr
1
2
3
4
5
6
shared_ptr<int> p(new int(42));
int *q=p.get();         //取底层的内置指针
{
    shared_ptr<int>(q); //产生两个互相独立的shared_ptr,进行互相独立的引用计数
}                       //离开程序块时,块内定义的shared_ptr计数清零,对象被释放
int foo=*p;             //未定义,p指向的对象已被释放
  • reset成员函数为shared_ptr赋予一个新的内置指针,同时更新原来的引用计数,必要时将原对象销毁。
  • reset函数经常与unique函数一起使用,控制多个shared_ptr共享的对象。改变对象时检查自己是否是唯一的用户。若不是,可拷贝一份自己修改
  • 例子:非独有时拷贝再修改
1
2
3
if(!p.unique())                 //若p不是唯一指向该对象的shared_ptr
    p.reset(new string(*p));    //将对象拷贝一份,并用p管理这个拷贝
*p+=newVal;

智能指针和异常

  • 使用了异常处理的程序可在异常发生时让程序继续,跳进异常中断时需确保异常发生后资源被正确释放。例如new分配的对象,处理异常时要考虑delete
  • 若使用智能指针,即使程序员因异常而过早结束,智能指针也可确保在指针离开作用域时释放资源
  • 函数的退出有两种可能:正常处理结束或发生异常。两种情况下局部非static对象都会被销毁(包括智能指针,因此可使动态对象也被自动销毁)
  • 若使用new/delete管理内存,且在new和delete之间发生异常,则内存不会被释放
  • 例子:new/delete分配的内存遇到异常时可能不会释放
1
2
3
4
5
void f(){
    int *ip=new int(42);    //分配资源
    /* 这里抛出异常 */
    delete ip;              //函数未正常结束,delete不会被执行
}
  • C++的很多类都定义了析构函数用于销毁对象释放资源。但不是所有的类都有,尤其是与C交互的类,通常要求用户显式释放使用的任何资源。若在资源的分配和释放之间发生了异常,也会有资源泄露。可用智能指针管理这些未定义析构函数的类,只需自定义delete操作。
  • 默认情况下,shared_ptr假定它们指向的是动态内存,即销毁时对其管理的指针进行delete。这个delete也可自定义。
  • 创建shared_ptr时可在参数列表中给出自定义的delete函数,该delete函数的必须接受一个指向所管理对象的内置指针
  • 例子:自定义shared_ptr的delete
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//以下是C/C++兼容的网络库
struct destination;
struct connection;
connection connect(destination *);
void disconnect(connection);
//以下是使用这个库,手动管理连接
void f(destination &d){
    connection c=connect(&d);   //创建连接,类似new
    /* 使用连接 */
    disconnect(c);              //关闭连接。类似delete
}
//以下是使用这个库,shared_ptr管理连接
void end_connection(connection *p){disconnect(*p);} //自定义delete操作用于关闭连接
void f(destination &d){
    connection c=connect(&d);
    shared_ptr<connection> p(&c,end_connection);    //shared_ptr管理连接
    /* 使用连接 */
}                                                   //shared_ptr离开作用域,自动关闭连接
  • 智能指针使用基本规范
    • 不使用相同的内置指针来初始化(或reset)多个智能指针
    • 不delete get()返回的指针
    • 不使用get()初始化或reset另一个智能指针
    • 谨慎使用get()返回的指针,最后一个智能指针销毁后对象就被销毁了
    • 若使用智能指针管理的资源不是new分配的内存,要自定义delete

unique_ptr

  • 同一个时刻只能有一个unique_ptr指向给定对象。若unique_ptr被销毁,其指向的对象也被销毁
  • unique_ptr的操作列于表12.4 tab_12_4
  • 定义unique_ptr时需将其绑定到一个new返回的指针上。类似shared_ptr,用内置指针初始化时必须显式构造,不可隐式转换
  • unique_ptr不支持拷贝/赋值,因为独占其管理的对象
  • 例子:unique_ptr不支持拷贝/赋值
1
2
3
4
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1);  //错,不可拷贝
unique_ptr<string> p3;
p3=p1;                      //错,不可赋值
  • 可通过release/reset成员函数将指针所有权从一个(非const)unique_ptr转移给另一个unique_ptr
    • release函数返回unique_ptr当前保存的指针,并将unique_ptr置为空
    • reset函数将unique_ptr原来指向的对象被释放,并接受一个可选的内置指针参数,令unique_ptr重新指向给定的指针。
  • 例子:unique_ptr用release/reset转移权限
1
2
3
4
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1.release());    //将p1置空并将底层的内置指针交给p2管理
unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release());                 //释放p2管理的对象,将p3置空并将底层的内置指针交给p2管理
  • 不能拷贝unique_ptr的规则有一个例外:可以拷贝或赋值一个将要被编译器销毁的unique_ptr,这时编译器执行一种特殊的拷贝(移动)。例如可从函数中返回unique_ptr,也可返回局部unique_ptr对象的拷贝
  • 例子:可拷贝将要被编译器销毁的unique_ptr
1
2
3
4
5
6
7
8
9
//从函数中返回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。auto_ptr在标准库中仍存在,但应避免使用,应使用unique_ptr
  • unique_ptr默认用delete释放它指向的对象,也可自定义delete,但unique_ptr管理删除器的方式和shared_ptr不同
  • 重载unique_ptr的删除器会影响到unique_ptr类型以及如何构造(或reset)unique_ptr对象。因此必须在unique_ptr的模板参数中提供删除器函数的指针类型,构造或reset时需提供删除器。
  • 例子:自定义unique_ptr的delete
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//以下是C/C++兼容的网络库
struct destination;
struct connection;
connection connect(destination *);
void disconnect(connection);
//以下是使用这个库,unique_ptr管理连接
void end_connection(connection *p){disconnect(*p);} //自定义delete操作用于关闭连接
void f(destination &d){
    connection c=connect(&d);
    unique_ptr<connection,decltype(end_connection) *> 
              p(&c,end_connection);                 //unique_ptr管理连接
    /* 使用连接 */
}                                                   //unique_ptr离开作用域,自动关闭连接

weak_ptr

  • weak_ptr是一种不控制指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。
  • 将weak_ptr绑定到shared_ptr指向的对象时,不会改变shared_ptr的引用计数,一旦该对象的shared_ptr引用计数清零,对象就会被释放,即使有weak_ptr指向它。
  • weak的用法如表12.5 tab_12_5
  • 创建weak_ptr时要在模板参数中给出指向对象类型,并用shared_ptr来初始化。模板参数中的类型只需能转换为shared_ptr指向的类型即可,不需严格匹配
  • 由于weak_ptr的对象可能不存在,故不能用weak_ptr直接访问对象,而必须用lock成员函数。lock函数先检查指向对象是否存在,若存在则返回指向该对象的shared_ptr(与初始化weak_ptr的shared_ptr共享引用计数),不存在则返回空的shared_ptr
  • 例子:利用weak_ptr定义伴随指针类(类似迭代器),使用时不干涉底层对象的生存期,但在底层对象不存在时可阻止访问
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/* 上下文:前面例子中的StrBlob */
class StrBlobPtr{
public:
    StrBlobPtr():curr(0){}
    //用底层对象的shared_ptr初始化weak_ptr,在StrBlob中声明StrBlobPtr为友元才可访问
    StrBlobPtr(StrBlob &a, size_t sz=0):wptr(a.data),curr(sz){}
    //解引用,访问当前位置的元素
    string &deref() const;
    //前置递增,位置向前推进
    StrBlobPtr &incr();
private:
    //若检查成功,返回指向底层对象的shared_ptr
    shared_ptr<vector<string>> check(size_t,const string &) const;
    //weak_ptr指向底层对象
    weak_ptr<vector<string>> wptr;
    //当前位置
    size_t curr;
};
//检查两项:1、底层对象是否还存在;2、索引是否越界
shared_ptr<vector<string>> StrBlobPtr::check(size_t i,const string &msg) const{
    //使用weak_ptr检查对象是否存在
    auto ret=wptr.lock();
    if(!ret)
        throw runtime_error("unbound StrBlobPtr");
    if(i>=ret->size())              //size_t是无符号,下溢时自动变成最大值。故只需检查i>=size
        throw out_of_range(msg);
    return ret;
}
//解引用,访问当前位置的元素
string &StrBlobPtr::deref() const{
    //检查当前位置是否合法,并返回shared_ptr
    auto p=check(curr,"dereference past end");
    //访问元素
    return (*p)[curr];
}
//对位置进行前置递增
StrBlobPtr &StrBlobPtr::incr(){
    check(curr,"increment past end of StrBlobPtr");
    ++curr;
    return *this;
}
//在StrBlob定义前加上这一行,前向声明StrBlobPtr
class StrBlobPtr;
//在StrBlob定义中加上如下几行,声明StrBlobPtr为友元,并提供返回StrBlobPtr的接口
friend class StrBlobPtr;
StrBlobPtr begin();
StrBlobPtr end();
//在StrBlobPtr完整定义后,实现从StrBlob中得到StrBlobPtr的函数,*this是用于初始化StrBlobPtr的StrBlob
StrBlobPtr StrBlob::begin(){return StrBlobPtr(*this);}
StrBlobPtr StrBlob::end(){
    auto ret=StrBlobPtr(*this,data->size());
    return ret;
}
/* 使用StrBlob和StrBlobPtr */
//定义一个StrBlob对象
StrBlob b1;                         //创建新StrBlob
{                                   //进入新作用域
    StrBlob b2={"a","an","the"};    //初始化b2
    b1=b2;                          //用b2初始化b1,它们共享底层数据
}                                   //离开作用域,b2被释放,b1仍存在,共享的底层数未丢失
//从StrBlob中得到StrBlobPtr
auto p1=b1.begin(), p2=b1.end();
//通过StrBlobPtr依次访问StrBlob中的元素
for(int i=0; i<b1.size(); ++i){
    cout<<p1.deref()<<endl;
    p1=p1.incr();
}

动态数组

  • new/delete一次只分配/释放一个对象,但有时需要一次为很多元素分配内存,如容器扩张时
  • 两种一次性分配一个动态数组的功能:
    • C++语言提供:另一种new表达式,可分配并初始化一个动态数组
    • 标准库提供:allocator类,可分配多个元素的内存,并将分配和初始化分离,性能更好更灵活
  • 最佳实践:应优先使用容器而不是动态数组来管理可变数量的对象
  • 使用容器的类可用默认版本的拷贝/赋值/析构来处理容器,而分配动态数组的类必须自定义拷贝/赋值/析构操作来处理动态数组

new和数组

  • 使用new分配动态数组,要在类型名后跟一对方括号[],并在方括号中指明要分配对象的数目
  • 可用表示数组的类型别名来分配动态数组
  • 例子:分配动态数组
1
2
3
int *pia=new int[42];   //分配动态数组
typedef int arrT[42];
int *p=new arrT;        //用数组的类型别名来分配
  • new分配动态数组时,并未得到数组类型的对象,而是返回指向该数组的指针。
  • 由于new返回的不是数组类型,故不能对动态数组使用begin和end,也不能用范围for
  • new动态数组的初始化
    • 默认情况下new分配的对象(单个或数组)都是默认初始化
    • 可用花括号对动态数组做列表初始化。若列表过短则剩下的值初始化,列表过长则分配失败并抛出异常bad_array_new_length(定义于头文件new
    • 可用空的圆括号对动态数组做值初始化,括号内不能有值。不能用auto分配动态数组
  • 例子:new动态数组的初始化
1
2
3
4
5
6
7
8
9
//默认初始化
int *pia=new int[10];
string *psa=new string[10];
//列表初始化
int *pia2=new int[10]{0,1,2,3,4,5,6,7,8,9};
string *psa2=new string[10]{"a","an","the",string(3,'x')};
//值初始化(不能在括号中给值)
int *pia3=new int[10]();
string *psa3=new string[10]();
  • 用new分配大小为0的数组时,new返回一个合法的非空的指针,并保证该指针与new返回的任何其他指针都不同。对于长为0的数组,该指针类似尾后迭代器。
  • 释放动态数组时可用特殊形式的delete,在指针前加空的方括号[]
  • 动态数组中的元素按逆序销毁,即从最后一个元素开始
  • 若在delete动态数组时忽略了[],或在delete单个对象时使用了[],其行为都是未定义
  • 即使在new时使用类型别名导致new中没有[],也要在delete中写[]
  • 例子:销毁动态数组
1
2
3
typedef int arrT[42];
int *p=new arrT;
delete []p;
  • unique_ptr可管理new分配的动态数组,只需在模板参数中指定类型为数组即可。unique_ptr销毁动态数组的方式是使用delete []
  • 例子:unique_ptr管理动态数组
1
2
3
4
unique_ptr<int []> up(new int[10]); //在模板参数中指定为动态数组。由于是指针而不是数组,故不写大小
for(size_t i=0; i!=10; ++i)
    up[i]=i;                        //下标访问unique_ptr管理的动态数组
up.release();                       //自动调用delete []
  • 指向数组的unique_ptr操作如表12.6: tab_12_6
  • unique_ptr指向动态数组时,不可使用点.和箭头->算符,因为指向的是数组而不是单个对象
  • unique_ptr指向动态数组时,可用下标[]访问元素
  • 若要使用shared_ptr管理动态数组,需提供自定义delete
  • shared_ptr未定义下标算符,且智能指针都不支持指针算数运算。故shared_ptr访问数组中元素时必须用get函数取出内置指针
  • 例子:shared_ptr管理动态数组
1
2
3
4
5
shared_ptr<int> sp(new int[10],
                   [](int *p){delete []p;});    //自定义lambda,使用delete []来释放
for(size_t i=0; i!=10; ++i)
    *(sp.get()+i)=i;                            //使用get得到内置指针来访问元素
sp.reset();                                     //使用自定义的lambda释放动态数组

allocator类

  • new/delete在灵活性上的局限:将内存分配和对象构造组合在一起,将对象析构和内存释放组合在一起
  • 分配一大块内存时,通常要按需构造对象。此时希望将内存分配和对象构造分离,只在真正需要时才构造对象
  • new的局限性:
    • 分配空间时即构造对象,初始化之后再赋予新值,则每个元素被赋值两次
    • 分配空间被对象填满,可能创建了一些永远不会使用的对象
    • 没有默认构造函数的类不能用new分配动态数组
  • allocator类定义在memory头文件中,提供一种类型感知的内存分配,分配的内存是原始的、未构造的。它可将内存分配和对象构造分离,将对象销毁和内存释放分离。
  • allocator支持的操作见表12.7 tab_12_7
  • allocator也是模板类,需在模板参数中给出分配的对象类型。分配内存时根据给定的类型来确定恰当的内存大小和对齐位置
  • allocate成员函数接受一个参数,指定分配能容纳多少个该对象的内存
  • construct成员函数接受一个指针和额外参数,在指针所指位置构造一个元素,额外参数匹配到元素的构造函数
  • 为使用allocate分配的内存,必须用construct构造对象。使用未构造的内存是未定义
  • 早期的标准库construct只接受两个参数,一个指针和一个元素类型的值,只能把给定值拷贝进内存
  • 使用完对象后必须对每个对象调用destroy来销毁,该成员函数接受一个指针,执行所指元素的析构函数
  • 只能对真正构造了的元素进行destroy操作,对未构造的空间进行destroy是未定义
  • 使用destroy销毁元素后可以再构造元素,也可将内存还给系统
  • 使用deallocate成员函数释放内存,它接受两个参数,一个指向这块内存的指针和一个销毁元素的数量,该数量必须与allocate分配的数量相同(即只能全部释放)。
  • 例子:使用allocator
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
allocator<string> alloc;
auto const p=alloc.allocate(n); //分配n个string的内存,返回首指针
auto q=p;                       //指向分配区域的起始
alloc.construct(q++);           //构造空字符串,指针推进
alloc.construct(q++,10,'c');    //构造字符串,指针推进
alloc.construct(q++,"hi");      //构造字符串,指针推进
cout<<*p<<endl;                 //有效,p指向区域起始,此处有对象
cout<<*q<<endl;                 //未定义,q指向已构造空间的尾后,此处无对象
while(q!=p)
    alloc.destroy(--q);         //从尾部开始销毁元素
alloc.deallocate(p,n);          //释放所有内存
  • 标准库为allocator类定义了两个伴随算法,用于在未初始化的内存中创建对象,它们定义于memory头文件中
  • allocator的伴随算法见表12.8 tab_12_8
  • uninitialized_copy类似copy,接受3个迭代器参数,前两个表示输入序列,第三个表示目的位置。目的位置必须是未构造的内存。该函数在目的位置构造元素,并返回已构造序列的尾后迭代器
  • uninitialized_fill_n类似fill_n,接受一个指向目的位置的指针、一个计数、一个值。该函数在目的位置创建给定个数目的对象,用给定值初始化
  • 例子:使用allocator的伴随算法
1
2
3
4
allocator<int> alloc;
auto p=alloc.allocate(vi.size()*2);                 //分配vi长度两倍的内存
auto q=uninitialized_copy(vi.begin(),vi.end(),p);   //将vi拷贝进分配的内存,返回已拷贝区域的尾后迭代器
uninitialized_fill_n(q,vi.size(),42);               //剩下的内存初始化未42;

使用标准库:文本查询程序

  • 例子:查询单词在文件中出现的次数及行号,可打印所有行的内容 //输入示例: 本章英文版 //输出示例:(假如要查询单词element) element occurs 112 times: (line 36) A set element contains only a key; (line 158) operator creates a new element (line 160) Regardless of whether the element (line 168) When we fetch an element from a map, we (line 214) If the element is not found, find returns

文本查询程序设计

  • 开始程序设计的一种好方法是列出程序的操作,这会帮助分析需要什么数据结构
    • vector<string>来保存整个输入文件的一份拷贝,用行号作为下标索引一行
    • istringstream将每行分解为单词
    • set保存单词出现的行号,保证行号不重复且升序保存
    • map将每个单词与其行号set关联
    • 用一个类保存和查询,另一个类保存查询结果。用shared_ptr在它们之间共享文本数据和行号集合
  • 自顶向下:设计一个类时,在真正实现成员之前先编写程序使用这个类
  • 使用TextQuery类
1
2
3
4
5
6
7
8
9
void runQueries(std::ifstream &infile){
    TextQuery tq(infile);                           //构造查询的类
    while(true){
        std::cout<<"enter word to look for, or q to quit: ";
        std::string s;
        if(!(std::cin>>s) || s=="q") break; 		//若输入无效或使用退出指令
        print(std::cout,tq.query(s))<<std::endl;  	//打印查询结果
    }
}

文本查询程序类的定义

  • TextQuery类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class QueryResult; //前向声明
class TextQuery{
public:
    //定义行号类型
    using line_no=std::vector<std::string>::size_type;
    //根据文件流构造
    TextQuery(std::ifstream &);
    //根据给定的要查找字符串返回QueryResult对象
    QueryResult query(const std::string &) const;
private:
    //用shared_ptr管理底层文本数据
    std::shared_ptr<std::vector<std::string>> file;
    //用shared_ptr管理每个单词的行号集合,并将单词map到行号集合
    std::map<std::string,std::shared_ptr<std::set<line_no>>> wm;
};
//构造函数,初值列表中分配动态内存来构造智能指针
TextQuery::TextQuery(std::ifstream &is):file(new std::vector<std::string>){
    std::string text;
    //取出一行
    while(std::getline(is,text)){
        //逐行放入底层vector中
        file->push_back(text);
        //n是这一行在vector中的索引
        int n=file->size()-1;
        std::istringstream line(text);
        std::string word;
        //用istringstream从一行中读取单词
        while(line>>word){
        	//若word不在map中则添加到map。使用引用是因为对应的集合会改变
            auto &lines=wm[word];
            if(!lines) //line是智能指针,若指针为空,则说明刚创建,需要new一个set
                lines.reset(new std::set<line_no>);
            //将当前行号插入该单词对应的set
            lines->insert(n);
        }
    }
}
  • QueryResult类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class QueryResult{
//通过友元函数实现接口
friend std::ostream &print(std::ostream &,const QueryResult &);
public:
    using line_no=std::vector<std::string>::size_type;
    //构造函数只用来初始化成员
    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) {}
    //以下3个函数是15.9的面向对象例程需要用的
    std::set<line_no>::iterator begin(){return lines->begin();}
    std::set<line_no>::iterator end(){return lines->end();}
    std::shared_ptr<std::vector<std::string>> get_file(){return file;}
private:
    std::string sought;                             //要查找的单词
    std::shared_ptr<std::set<line_no>> lines;       //shared_ptr指向行号set
    std::shared_ptr<std::vector<std::string>> file; //shared_ptr指向文本vector
};
  • query成员函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
QueryResult TextQuery::query(const std::string &sought) const{
    //未找到给定单词时返回的空集合。由于只需要一个这样的集合,声明为static避免每次未找到都分配空间
    static std::shared_ptr<std::set<line_no>> nodata(new std::set<line_no>);
    auto loc=wm.find(sought);
    if(loc==wm.end())
        //未找到时得到指向空集合的指针
        return QueryResult(sought,nodata,file);
    else
        //找到时得到指向对应集合的指针
        return QueryResult(sought,loc->second,file);
}
  • 打印结果
1
2
3
4
5
6
7
8
std::ostream &print(std::ostream &os,const QueryResult &qr){
    os<<qr.sought<<" occurs "<<qr.lines->size()
      <<((qr.lines->size()>1)?" times":" time")<<std::endl;
    for(auto num:*qr.lines)
        os<<"\t(line "<<num+1<<") "
          <<*(qr.file->begin()+num)<<std::endl;
    return os;
}
  • 封装:将上述定义按顺序放入文件中,并将runQueries放在最后,并添加头文件和头文件保护,封装为hpp:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//文件名:TextQuery.hpp
#ifndef __TEXTQUERY_HPP__
#define __TEXTQUERY_HPP__
#include<string>
#include<vector>
#include<set>
#include<map>
#include<memory>
#include<iostream>
#include<fstream>
#include<sstream>
/* 上面的一些定义和runQueries */
#endif
  • 测试:建立cpp文件,包含上述头文件
1
2
3
4
5
6
7
//文件名:TextQuery_test.cc
#include"./TextQuery.hpp"
int main(){
    std::ifstream in("文件名");
    runQueries(in);
    return 0;
}