12.1 动态内存和智能指针 | dynamic memory & smart pointer

12章之前的程序中使用的对象都有严格定义的生存期。

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

除了自动和static对象,还支持动态分配对象。这些对象的生存期与创建位置无关,显式被释放时才会被销毁。

为了安全使用动态对象,标准库中有两个智能指针类型管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

静态内存和栈内存

  • 静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
  • 栈内存用来保存定义在函数内的非static对象。
  • 分配在静态或栈内存中的对象由编译器自动创建和销毁。栈对象仅在其定义的程序块运行时存在;static对象在使用前分配,程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这些内存称作自由空间(free store)堆(heap)

程序用堆来存储动态分配(dynamically allocated)对象,即在程序运行时分配的对象。动态对象的生存期由程序控制,因此在不需要时需要显式销毁。

动态内存和智能指针

C++中的动态内存管理是通过一堆运算符完成的:

  • new在动态内存中为对象分配空间并返回一个指向该对象的指针;
  • delete接受一个动态对象的指针,销毁该对象并释放关联的内存。

标准库中提供了两种智能指针(smart pointer)类型管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:

  • shared_ptr允许多个指针指向同一个对象
  • unique_ptr“独占”指向的对象

标准库还有一个名为weak_ptr的伴随类,是一种弱引用,指向shared_ptr所管理的对象。

上述三种类型都定义在头文件中。

shared_ptr

类似vector,智能指针也是模板,因此创建一个智能指针时,必须给出指向的类型:

std::shared_ptr p1;
std::shared_ptr> p2;

默认初始化的智能指针中保存着一个空指针。智能指针的使用方式类似普通指针,可以解引用返回对象。

shared_ptr和unique_ptr都支持的操作

expression -
shared_ptr sp | unique_ptr up 空智能指针
p 将p用作一个条件判断,若指向一个对象则true
*p 解引用
p->mem *p.mem
p.get() 返回p中保存的指针。注意是否已经释放了对象
swap(p, q)|p.swap(q)

shared_ptr独有操作

expression -
make_shared(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化该对象
shared_ptrp(q) p是shared_ptr q的拷贝。该操作会递增q中的计数器,q中的指针必须可以转换为T*
p=q 二者都是shared_ptr且保存的指针必须可以相互转换。该操作会递减p的引用计数,递增q的引用计数。若p的引用计数变为0,则将其管理的原内存释放。
p.unique() p.use_count()为1(独占状态)则true,否则false
p.use_count() 返回与p共享对象的智能指针数量;主要用于调试

make_shared函数

make_shared是最安全的分配和使用动态内存的方法,避免了在定义后才初始化可能造成的错误。该函数同样定义在中:

int main(){
    std::shared_ptr p1 = std::make_shared(42);
    std::shared_ptr> p2 = std::make_shared>(10, 0);
    auto p3 = std::make_shared>();
}

使用make_shared构造智能指针时可以使用auto方式。
若不传递任何参数,则会使用值初始化。
类似顺序容器的emplace,make_shared用其参数构造给定类型对象时传递的参数必须与string的某个构造函数相匹配。
可以使用make_shared和初始化列表

auto sp = std::make_shared>(std::initializer_list({1,2,32}));
//或者
auto sp_map = std::make_shared>();
*sp_map = {{"A", 0},{"B", 1}};

shared_ptr的拷贝和赋值

进行拷贝和赋值操作,每个shared_ptr都会记录有多少个其他shared_ptr指向相同对象:

auto p = make_shared(42);
auto q(p);

每个shared_ptr都有一个关联的计数器,通常称为引用计数(reference count)。无论何时拷贝一个shared_ptr,计数器都会递增。

  • 将一个shared_ptr初始化另一个shared_ptr,或将其作为参数传给另一个函数以及作为函数的返回值,则它所关联的计数器就会递增
  • 当给shared_ptr一个新值或者被销毁(如局部shared_ptr)离开作用域,计数器会递减
  • 一旦一个shardd_ptr的计数器变为0,则会自动释放自己管理的对象。
int main(){
    int a = 0;
    int b = 1;

    auto sp3 = std::make_shared(a);
    auto sp4 = std::make_shared(b);
    std::cout<(b);    //令sp3指向其他对象,原对象a的引用计数-1
    std::cout<

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁(所指向对象的引用计数归0),shared_ptr类会自动调用该对象类的析构函数(destructor)销毁该对象

shared_ptr自动释放相关联的内存

动态对象不再使用时,shared_ptr会自动释放动态对象。

例如有一个函数返回shared_ptr,指向一个Foo类型的动态分配的对象:

struct Foo{
public:
    std::string s;
    int i;
    Foo(std::string str, int ii);
};

Foo::Foo(std::string str, int ii) {
    this->i = ii;
    this->s = str;
}

std::shared_ptr foo(std::string s, int i){
    return std::make_shared(s, i);
}

auto try_ptr(std::string s, int i, bool return_ptr = 0){
    auto p = foo(s, i);
    //离开该作用域,p指向的对象被自动释放
    //若存在返回,则指向对象的引用计数+1,因此不会被释放
    return return_ptr? p: nullptr;
}

int main(){
    std::string str = "A";
    int ii = 1;
    auto ptr = foo(str, ii);
    std::cout<i<<'\t'<s<

除此之外,shared_ptr会在无用之后依然保留的情况是,将shared_ptr放在一个容器中,之后重排了容器,从而不需要某些元素。这种情况下应该使用erase删除那些不必要的shared_ptr元素。

使用了动态生存期资源的类

程序使用动态内存的原因:

  1. 不知道自己需要多少对象
  2. 不知道对象的准确类型
  3. 需要在多个对象间共享数据

对于容器类,是由于第二种原因而使用的。

对于vector,拷贝一个vector时原有的vector和副本vector中的元素时相互分离的:

std::vector v1;
{
    std::vector v2 = {0, 1, 2};
    v1 = v2;
}
//此时离开作用域,v2及其中的元素被销毁但是v1拷贝的元素存在

假定类Blob会在不同对象的拷贝间共享相同的元素,则离开作用域时

Blob b1
{
    Blob b2 = {0, 1, 2};
    b1 = b2;
}
//离开作用域时需保证b2元素不能销毁

定义StrBlob类、构造函数、成员函数

class StrBlob{
public:
    typedef std::vector::size_type size_type;
    //默认构造函数
    StrBlob();
    //以接受初始化列表的构造函数
    StrBlob(std::initializer_list il);
    //基本方法
    size_type size() const {return data->size();};
    bool empty() const {return data->empty();};
    void push_back(const std::string &t) {
        std::cout<<"push_back"<push_back(t);};
    void pop_back();
    std::string& front();
    std::string& back();
    std::vector::iterator begin();
    std::vector::iterator end();

private:
    //使用shared_ptr管理装入的容器类型
    std::shared_ptr> data;
    //检查是否合法。data[i]不合法时抛出异常
    void check(size_type i, const std::string &msg) const;
};

//类外定义构造函数,类成员写在函数体之前、函数名的冒号之后

StrBlob::StrBlob(): data(std::make_shared>()) {};

StrBlob::StrBlob(std::initializer_list il):
    data(std::make_shared>(il)) {};

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

//类外定义的成员方法

void StrBlob::pop_back() {
    check(0, "no element to pop");
    std::cout<<"pop_back"<pop_back();
}

std::string& StrBlob::front() {
    check(0, "no element in the front");
    std::cout<<"front"<front();
}

std::string& StrBlob::back() {
    check(0, "no element in the back");
    std::cout<<"back"<back();
}

std::vector::iterator StrBlob::begin() {
    check(0, "no element");
    return data->begin();
}

std::vector::iterator StrBlob::end() {
    check(0, "no element");
    return data->end();
}

int main(){
    StrBlob s1 = {"a", "b", "c"};
    s1.pop_back();
    //使用范围for必须实现begin和end
    s1.push_back("d");
    std::ostream_iterator a(std::cout, " ");
    for (auto j: s1)
        a = j;
}

直接管理内存

使用new动态分配和初始化对象

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

int* pi = new int;

内置类型和组合类型通常被默认初始化,因此值是ub。类类型使用默认构造函数初始化

string* ps = new string;
int* pi = new int;

可以使用直接初始化、列表初始化等构造方式动态分配对象。

int* p = new int(10);
std::string s = new string(10, '!');
auto v = new std::vector{0, 1, 2, 3, 5};

也可以使用值初始化,加括号即可:

int* p1 = new int;          //默认初始化
int* p2 = new int();        //值初始化
std::cout<<*p1<

若提供了括号包围的初始化器,则可以使用auto。注意当括号中只有单一初始化器才可以使用auto。

动态分配const对象

可以使用new动态分配一个const对象。

int main(){
    int i = 1024;
    const auto* p = new int(i);
    std::cout<

内存耗尽

当某个程序用尽可用内存,new会失败。默认情况下若new不出所需内存空间则会抛出类型为bad_alloc的异常。可以改变使用new的方式阻止其抛出异常:

#include 
int* p1 = new int;
int* p2 - new(nothrow) int 
//分配失败时不throw而是返回一个空指针

这种形式的new称为定位new(placement new),允许向new传递额外的参数。如此例中的nothrow。

bad_alloc和nothrow都定义在头文件中。

释放动态内存

为了防止内存耗尽,通过delete表达式(delete expression)将动态内存归还给系统。delete接受一个指针,指向想要释放的对象。
与new类似,该表达式执行两个动作:销毁给定的指针指向的对象;释放对象对应的内存。

指针值和delete

传递给delete的指针必须指向动态分配的内存,或是一个空指针。释放一块并非new分配的内存或将相同的指针值多次释放是UB。
释放一个空指针总是不会发生错误。

int main(){
    int i = 0, *p = &i, *pn = nullptr;
    delete p;  //报错。不是动态分配的地址
    delete pn; //总是不报错
}

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

shared_ptr管理的内存在销毁时被自动释放。但对于内置指针管理的内存,在显式释放之前都是存在的。就算离开作用域,该处的内存依然未被释放。

int main() {
    int ** addr;
    int* p = new int;
    {
        int* ps = new(std::nothrow) int(100);
        p = ps;
        addr = &ps;
    }
    std::cout<<*p<<**addr<

delete之后则上述*p**addr均变为ub:

int main() {
    int ** addr;
    int* p = new int;
    {
        int* ps = new(std::nothrow) int(100);
        p = ps;
        addr = &ps;
        delete ps;
    }
    std::cout<<*p<<**addr<

delete之后重置指针值(仅提供有限的保护)

当delete一个指针后,指针值变为无效,但指针依然保存着被释放的动态内存地址。delete之后该指针成为了空悬指针(dangling pointer),即指向一块曾保存对象而已经无效的内存的指针。

空悬指针具有未初始化指针的所有缺点。

避免方式为:在指针即将离开其作用域之前释放掉所关联的内存。若需要保留指针本身,则需在delete之后将nullptr赋予指针。

但是如此操作仅对该指针有效。若存在多个指针指向一块内存地址的情况,则对其他指针无效。例如:

int* p = new int(0);
auto q = p;
delete p;
p = nullptr;
//此时q依然是空悬指针

结合使用shared_ptr和new

不初始化的智能指针是一个空指针。
此外,可以使用new返回的指针来初始化智能指针。

接受指针参数的智能指针构造函数是explicit的,因此需要直接初始化。不能直接将内置指针转化为智能指针:

shared_ptr p1 = new int(100);  //错误
shared_ptr p2(new int(100));   //正确:直接初始化

也可以使用make_shared(相当于仅仅用了new出的指针的对象本身而不是这个指针),注意类型:

int* p = new int(42);
auto a = std::make_shared(*p);   //只是利用了*p的值
std::shared_ptr b(p);

另外,若要返回一个shared_ptr,在return语句中的正确写法为:

return shared_ptr(new int(p))

而不是简单的return new int(p)

定义和改变shared_ptr的其他方法

- -
shared_ptr p(q) p管理内置指针q指向的对象;q必须指向new分配到内存并且能转换为T*类型
shared_ptr p(u) p从unique_ptr u处接管了对象所有权:将u置空
shared)ptr p(q, d) p接管了内置指针q指向的对象的所有权,q必须能转换为T*类型。p将使用可调用对象d代替delete
shared_ptr p(p2, d) p是shared_ptr p2的拷贝,使用可调用对象d代替delete
p.reset() | p.reset(q) | p.reset(q, d) 若p是唯一指向其对象的shared_ptr,释放此对象。若传递了q,则令p指向q,否则置空。若传递了d,则会调用可调用对象d而不是delete。

不要混合使用智能指针和普通指针

shared_ptr可以协调对象的析构,但仅限于自身的拷贝之间。因此推荐使用make_shared而不是new。这样就能在分配对象的同时将shared_ptr与之绑定,避免无意中将同一块内存绑定到多个独立创建的shared_ptr上。

考虑如下函数:

void process ptr>{
    //do something
};  //离开作用域即被销毁

该函数的传参属于传值方式,实参被拷贝到ptr中,会增加引用计数。若传递给该函数一个shared_ptr:

shared_ptr p;  //引用计数为1
precess(p);       //在拷贝传参时,引用计数+1,为2
auto i = *p;      //正确,p的引用计数为1

若传递一个由内置指针转化了的shared_ptr:

T* x(new T());
process(x);                  //错误,参数类型不一致
process(shared_ptr(x));   //正确,但括号内语句作为临时指针,在该表达式结束后就被销毁
auto j = *x;      //错误:x已经是空悬指针

将一个shared_ptr绑定到一个普通指针时,内存的管理责任已经属于该shared_ptr,一旦这样做了就不应该再使用内置指针访问该地址。

不要使用get初始化另一个智能指针或为智能指针赋值

智能指针类型定义了名为get的函数,返回一个内置指针,该内置指针指向智能指针指向的对象。

  • get的设计情况是:需要向不能使用智能指针的代码传递一个内置指针。也就是将指针的访问权限传递给代码

  • 使用get返回的指针的代码不能delete该指针。因此只有在确定代码不delete指针的情况下才能使用get。

  • 编译器不会报错,但不能将另一个智能指针也绑定到get返回的这个内置指针。永远不要用get初始化另一个智能指针或为另一个智能指针赋值

其他shared_ptr操作

可以使用reset将新的指针赋予一个shared_ptr。与赋值类似,会更新引用计数。

reset常和unique一起使用来控制多个shared_ptr共享的对象。在改变底层对象之前检查自己是否是当前对象仅有的用户。若不是,则制作一份新的拷贝。

if(!p.unique()) p.reset(new string(*p)); //用对象的值分配新的拷贝,而不是指向原来的对象
*p += newVal;  //拷贝后改变对象的值

另外,reset不接受智能指针作为参数,因此下列操作非法:

b.reset(std::make_shared(*b));
b.reset(std::shared_ptr(*b));

智能指针和异常

异常处理程序能在异常发生后零程序继续,而该类程序需要确保在异常发生后资源能被正确释放。一个简单的确保资源正常释放的方法就是使用智能指针。

函数退出的两种可能:正常处理结束、发生异常。两种情况都会销毁局部对象。

如果使用智能指针,即使程序块过早结束也能确保在内存不再需要时将其释放:

void foo(){
    shared_ptr sp(new int(42)); 
    //假设该处抛出一个未捕获的异常
}//函数结束后自动释放内存

与之相对,发生异常时直接管理的静态内存即使发生异常也不会在delete之前不会自动释放。

智能指针和哑类

析构函数负责清理对象使用的资源。但是并非所有类都良好定义了析构函数,因此需要用户显式释放使用的资源。若在资源分配和释放之间发生异常,则程序会发生资源泄漏。

对于没有析构函数的类,使用智能指针相当有效。

使用自己的释放操作

在shared_ptr销毁时默认对管理的指针进行delete操作,但可以定义一个删除器(deleter)代替默认的delete操作。例如:

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

该函数接受一个connection类的指针,来进行指定的释放操作。
在创建shared_ptr时即可传递一个指向删除器函数的参数:

void f(destination& d){
    connection c = connect(&d);
    shared_ptr p (&c, end_connection);
    //使用connection类
    //即使程序异常也能正常释放
}

unique_ptr

unique_ptr“拥有”其指向的对象,当其被销毁时,指向的对象也被销毁。

不同于shared_ptr,unique_ptr没有类似make_shared的函数返回一个unique_ptr。因此定义时只能通过绑定一个new出的指针上。

类似shared_ptr,使用new返回的指针进行初始化只能采用直接初始化。

由于独占指向对象,unique_ptr不支持普通的拷贝和赋值操作。

但是可以通过release或reset将指针的所有权从一个非const的unique_ptr转移给另一个unique_ptr:

int main(){
    std::unique_ptr p1(new int(1));
    //将所有权从p1转给p2的同时p1置空
    std::unique_ptr p2(p1.release());
    std::cout<<*p2< p3(new int(2));
    //将所有权从p3转给p2并释放p2原来指向的内存
    p2.reset(p3.release());
    std::cout<<*p2<

总之,对于unique_ptr,要交出所有权的一方作为参数均需要.release()。调用release会切断unique_ptr和其原来管理的对象之间的联系。release返回的指针通常被用来初始化另一个指针或者为另一个指针赋值。

- -
unique_ptr u1 | unique_ptr u2 空unique_ptr,可以指向类型T的对象。u1使用delete释放它的指针;u2使用类型D的可调用对象释放他的指针。
unique_ptr u(d) 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr 释放u指向的对象
u.release() u放弃控制权并置空,返回指向原对象的(内置)指针
u.reset() | u.reset(q) | u.reset(nullptr) 若提供了内置指针则指向该对象,否则置空

传递unique_ptr参数和返回unique_ptr

不能拷贝unique_ptr的规则有一个例外:可以拷贝一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:

unique_ptr clone(int p){
    return unique_ptr(new int(p));
}

或返回一个局部对象的拷贝:

unique_ptr clone(int p){
    unique_ptr ret(new int(p));
    return ret;
}

这是一种特殊“拷贝”。在13.6.2节详述。

向unique_ptr传递删除器

重载unique_ptr中的默认删除器和shared_ptr的机制不同。

重载unique_ptr的删除器会影响到unique_ptr的类型和构造,必须在尖括号中指名删除器类型(shared_ptr仅影响构造,尖括号内类型只有一个)。

weak_jptr

  • weak_ptr不控制所指向对象生存期,它指向一个由shared_ptr管理的对象。
  • 将一个weak_ptr绑定到一个shared_ptr不会改变其引用次数。
  • 一旦最后一个指向对象的shared_ptr被销毁,对象就被释放,无论是否有weak_ptr。

因此,名称符合其weak的特点

expression -
weak_ptr w 空weak_ptr可以指向类型为T的对象
weak_ptr w(sp) 与shared_sp指向相同对象的weak_ptr,T必须能转换为sp指向的类型
w = p p可以是一个shared_ptr或者weak_ptr,复制后w和p共享对象
w.reset() 将w置空
w.use_count() 返回与w共享的shared_ptr的数量
w.expired() 如果w.use_count()为0则返回true,否则false
w.lock() 如果expired为true则返回空shared_ptr,否则返回指向w的对象的shared_ptr

创建一个weak_ptr需要用一个shared_ptr进行初始化:

auto p = make_shared(42);
weak_ptr wp(p);

由于weak_ptr的对象可能不存在,不能直接使用其访问对象,而是利用lock检查指向的对象是否依然存在。如果返回的shared_ptr存在,则其指向的底层对象也一直存在。

核查指针类

尝试为StrBlob定义一个伴随指针类StrBLobPtr,该类会保存一个weak_ptr指向StrBlob的data成员,这是初始化时提供的。通过使用weak_ptr不会影响给定的StrBlob的生存期,但是可以阻止用户访问一个不存在的vector。

含有运算符重载、shared_ptr、weak_ptr的“完全体”StrBlob和StrBlobPtr类:

#include 
#include 
#include 
#include 
#include 
#include 


class StrBlobPtr;
class StrBlob{
public:
    friend class StrBlobPtr;
    typedef std::vector::size_type size_type;
    //默认构造函数
    StrBlob();
    //以接受初始化列表的构造函数
    StrBlob(std::initializer_list il);
    //基本方法
    [[nodiscard]] size_type size() const {return data->size();};
    [[nodiscard]] bool empty() const {return data->empty();};
    void push_back(const std::string &t) {
        std::cout<<"push_back"<push_back(t);};
    void pop_back();
    std::string& front();
    std::string& back();
    StrBlobPtr begin();
    StrBlobPtr end();

private:
    //使用shared_ptr管理装入的容器类型
    std::shared_ptr> data;
    //检查是否合法。data[i]不合法时抛出异常
    void check(size_type i, const std::string &msg) const;
};

//类外定义构造函数,类成员写在函数体之前、函数名的冒号之后

StrBlob::StrBlob(): data(std::make_shared>()) {};

StrBlob::StrBlob(std::initializer_list il):
        data(std::make_shared>(il)) {};

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

//类外定义的成员方法

void StrBlob::pop_back() {
    check(0, "no element to pop");
    std::cout<<"pop_back"<pop_back();
}

std::string& StrBlob::front() {
    check(0, "no element in the front");
    std::cout<<"front"<front();
}

std::string& StrBlob::back() {
    check(0, "no element in the back");
    std::cout<<"back"<back();
}


class StrBlobPtr{
public:
    StrBlobPtr(): curr(0) {};
    StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) {};
    std::string& operator*() const;
    StrBlobPtr& operator++();
    bool operator==(StrBlobPtr&) const;
    bool operator!=(StrBlobPtr&) const;

private:
    [[nodiscard]]std::shared_ptr> check (std::size_t, const std::string&) const;
    std::weak_ptr> wptr;
    std::size_t curr;
};

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

//重载运算符方式
std::string& StrBlobPtr::operator*() const {
    auto p = check(curr, "dereference past end");   //是lock返回的指针
    return (*p)[curr];
}

StrBlobPtr& StrBlobPtr::operator++() {
    check(curr, "increment past end of StrBlobPtr");  //已经是尾后则不可递增
    ++curr;
    return *this;
}

StrBlobPtr StrBlob::begin() {
    check(0, "no element");
    return StrBlobPtr(*this);
}

StrBlobPtr StrBlob::end() {
    check(0, "no element");
    auto ret = StrBlobPtr(*this, data->size());
    return ret;
}

bool StrBlobPtr::operator==(StrBlobPtr& p) const{
    return this->curr == p.curr? true : false;
}

bool StrBlobPtr::operator!=(StrBlobPtr& p) const{
    return this->curr == p.curr? false : true;
}


int main(){
    StrBlob s1 = {"aa", "bb", "cc"};
    s1.pop_back();
    //使用范围for必须实现begin和end成员
    s1.push_back("dd");

    auto sp = StrBlobPtr(s1, 0);
    //注意#include
    std::cout<<*sp<

你可能感兴趣的:(12.1 动态内存和智能指针 | dynamic memory & smart pointer)