C++——动态内存

76 阅读13分钟

动态内存

C/C++程序的数据内存分区包括:

  • 全局内存区:包括全局变量和静态变量;
  • 堆内存区:通过maloc和new动态申请的内存,都是堆内存;
  • 栈内存区:函数调用时传递的参数和函数中的局部变量。

1. malloc和free

在C语言中通过malloc申请内存,在内存使用完毕之后调用free将堆内存释放。

 //调用maloc申请内存
 char* buffer = malloc(100);
 ​
 //........//使用内存
 ​
 //调用free将动态内存释放
 if(buffer == NULL)
     free(buffer);

使用动态内存之前需要判断malloc函数的返回值,如果内存申请成功,则函数返回申请到的堆内存首地址;如果内存申请失败,则函数返回NULL。

2. realloc和calloc函数

calloc可以给分配的内存空间赋初值。

 int* p = calloc(10, sizeof(int));

而realloc是重新在堆区分配内存,如果分配的内存比原来的大,会有两种情况:

  • 原有空间后面有足够大的空闲空间,那么直接在原有空间后继续开辟内存,返回原有空间的首地址;
  • 原有空间后续没有足够大的空闲内存,则重新分配一个足够大的空间,并且将原有空间的内容拷贝到新空间下,释放原来的空间,将新的内存单元的首地址返回。
 p = realloc(p, sizeof(int)*20);

3. new和malloc的区别

  1. new操作符是从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。new能否在堆上动态分配内存,取决于operater new的实现细节。
  2. new操作符内存分配成时,返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换,故new是符和类型安全性的操作符;而malloc内存分配成功则是返回void*,需要通过强制类型转换将void*指针转换成需要的类型
  3. new内存分配失败时,会抛出bac_alloc异常,不会返回NULL;malloc分配内存失败时返回NULL。
  4. 使用new操作符申请内存分配无需指定内存块的大小,编译器会根据类型信息自行计算;而malloc需要显示地指出所需内存的大小。
  5. malloc函数会分配一块原始的内存空间;而new操作符会分为三步: 1)调用operater new函数分配原始的、未命名的内存空间;2)调用构造函数以构造对象,并为其传入初值;3)对象构造完成后,返回一个指向该对象的指针。
 string* ps1 = new string;       //默认初始化为空string
 string* ps2 = new string();     //值初始化为空string
 int* pi1 = new int;             //默认初始化;*pi1的值未定义
 int* pi2 = new int();           //值初始化;*pi2为0

对于内置类型这种没有默认构造函数的类型来说,使用值初始化有着良好定义的值;而默认初始化的对象的值则是未定义的。

4. 智能指针

智能指针包括shared_ptr、unique_ptr、weak_ptr这三种,它们定义在头文件中。

4.1 shared_ptr

智能指针shared_ptr会保存一个引用计数,当引用计数变为0时,会自动销毁对象,并自动释放相关联的内存。

类似vector,智能智能也是模板。创建的时候需要提供额外的信息来指明类型。例如:

 shared_ptr<string> p1;
 shared_ptr<list<int>> p2;

make_shared函数:它会在动态内存中分配一个对象并初始化它,返回指向该对象的shared_ptr。它的定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型。

 shared_ptr<string> p1 = make_shared<string>(10, 'c');
 shared_ptr<int> p2 = make_shared<int>(999);

或者我们也可以使用new来直接初始化一个智能指针。

 shared_ptr<int> p1(new int(1024));

两种创建方式的区别:默认的构造函数会申请两次内存,而make_shared只会申请一次内存。因为shared_ptr内部有一个引用计数以及存放数据的内存,等于说有两部分内存,默认构造函数为数据内存和引用计数每个分别申请一次内存,而make_shared将数据和引用计数的内存申请放到一起。

关于make_shared的缺点 make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题

shared_ptr常用的方法:

操作说明是否为shared_ptr所独有
p.get()返回p中保存的指针,谨慎使用。
p.unique()若p.use_count()为1,返回true;否则返回false
p.use_count()返回与p共享对象的智能指针数量,可能很慢

注意以下几点

  • 不要混合使用普通指针和智能指针;
  • 不要使用get初始化另一个智能指针或为智能指针赋值;
  • 在改变底层对象之前,记得检查自己是否是当前对象的仅有用户。
 if(!p.unique())
     p.reset(new string(*p));  //不是唯一用户;分配新的拷贝

shared_ptr的大致实现原理:

//
// Created by ruan'ruan on 2023/6/27.
//

#ifndef OOP_BASE_MY_SHARED_PTR_H
#define OOP_BASE_MY_SHARED_PTR_H

#include <atomic>



class RefCounter{
public:
    RefCounter(){
        ref_cnt_ = 0;
    }
    void addRef(){
        ++ref_cnt_;
    }

    void decRef(){
        --ref_cnt_;
    }

    int getRef(){
        return ref_cnt_;
    }

private:
    std::atomic<int>ref_cnt_;
};



template <typename T>
class my_shared_ptr{
public:
    //构造函数,初始化一个裸指针和引用计数指针;
    my_shared_ptr(T* ptr = nullptr): ptr_(ptr), ctrl_blk_(new RefCounter()){
        ctrl_blk_->addRef();
    }

    ~my_shared_ptr(){
        ctrl_blk_->decRef();
        if(ctrl_blk_->getRef() == 0){
            delete ptr_;
            delete ctrl_blk_;
        }
    }
    //拷贝构造函数;
    my_shared_ptr(const my_shared_ptr& rhs): ctrl_blk_(rhs.ctrl_blk_), ptr_(rhs.ptr_){
        ctrl_blk_->addRef();
    }

    //先增加右边的,再减去左边的,防止自我赋值;
    my_shared_ptr& operator=(const my_shared_ptr& rhs){
        rhs.ctrl_blk_->addRef();
        ctrl_blk_->decRef();
        if(ctrl_blk_->getRef() == 0){
            delete ctrl_blk_;
            delete ptr_;
        }
        ctrl_blk_ = rhs.ctrl_blk_;
        ptr_ = rhs.ptr_;

        return *this;
    }

    //方便测试;
    RefCounter* ctrl_blk_;
private:
    T* ptr_;
};



#endif //OOP_BASE_MY_SHARED_PTR_H

4.2 unique_ptr

一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。

unique_ptr不支持拷贝和赋值。

unique_ptr置为nullptr,会释放其所指向的对象。

 std::unique_ptr<string> p1(new string("abcdef"));
 p1 = nullptr;  
操作含义
u.release()释放所有权,但不是释放资源,返回普通指针类型
u.reset(q)释放所有权和资源;如提供内置指针q,令u指向这个对象,否则置u为空

虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另外一个uniqeu_ptr。

 unique_ptr<string> p1(new string("abcdef"));
 unique_ptr<string> p2(p1.release());  //所有权从p1->p2,且release将p1置为空
 ​
 unique_ptr<string> p3(new string("trex"));
 p2.reset(p3.release());       //reset释放了p2原来指向的内存

4.3 weak_ptr

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

**当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它。

 auto p = make_shared<int>(42);
 weak_ptr<int> wp(p);
操作含义
w = pp可以是一个shared_ptr或一个weak_ptr
w.use_count()与w共享对象的shared_ptr的数量
w.expired()若w.use_count()为0,则返回true
w.lock()如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

当我们使用weak_ptr访问对象时,必须调用lock,不能直接访问。

 if(shared_ptr<int> np = wp.lock()) { //
     //在if中,np与p共享对象
 }

4.4 weak_ptr是为了解决shared_ptr带来的循环引用的问题

4.4.1 weak_ptr解决shared_ptr循环引用的问题

定义两个类,每个类中又包含一个指向对方类型的智能指针作为成员变量,然后创建对象,设置完成后查看引用计数后退出,看一下测试结果:

 class CB;
 class CA
 {
 public:
     CA() { cout << "CA() called! " << endl; }
     ~CA() { cout << "~CA() called! " << endl; }
     void set_ptr(shared_ptr<CB>& ptr) { m_ptr_b = ptr; }
     void b_use_count() { cout << "b use count : " << m_ptr_b.use_count() << endl; }
     void show() { cout << "this is class CA!" << endl; }
 private:
     shared_ptr<CB> m_ptr_b;
 };
 ​
 class CB
 {
 public:
     CB() { cout << "CB() called! " << endl; }
     ~CB() { cout << "~CB() called! " << endl; }
     void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; }
     void a_use_count() { cout << "a use count : " << m_ptr_a.use_count() << endl; }
     void show() { cout << "this is class CB!" << endl; }
 private:
     shared_ptr<CA> m_ptr_a;
 };
 ​
 void test_refer_to_each_other()
 {
     shared_ptr<CA> ptr_a(new CA());
     shared_ptr<CB> ptr_b(new CB());
 ​
     cout << "a use count : " << ptr_a.use_count() << endl;
     cout << "b use count : " << ptr_b.use_count() << endl;
 ​
     ptr_a->set_ptr(ptr_b);
     ptr_b->set_ptr(ptr_a);
 ​
     cout << "a use count : " << ptr_a.use_count() << endl;
     cout << "b use count : " << ptr_b.use_count() << endl;
 }
 ​

测试结果如下:

 CA() called!
 CB() called!
 a use count : 1
 b use count : 1
 a use count : 2
 b use count : 2

通过结果可以看到,最后CA和CB的对象并没有被析构,其中的引用效果如下图所示,起初定义完ptr_a和ptr_b时,只有①③两条引用,然后调用函数set_ptr后又增加了②④两条引用,当test_refer_to_each_other这个函数返回时,对象ptr_a和ptr_b被销毁,也就是①③两条引用会被断开,但是②④两条引用依然存在,每一个的引用计数都不为0,结果就导致其指向的内部对象无法析构,造成内存泄漏。 image

解决这种状况的办法就是将两个类中的一个成员变量改为weak_ptr对象,因为weak_ptr不会增加引用计数,使得引用形不成环,最后就可以正常的释放内部的对象,不会造成内存泄漏,比如将CB中的成员变量改为weak_ptr对象,代码如下:

 class CB
 {
 public:
     CB() { cout << "CB() called! " << endl; }
     ~CB() { cout << "~CB() called! " << endl; }
     void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; }
     void a_use_count() { cout << "a use count : " << m_ptr_a.use_count() << endl; }
     void show() { cout << "this is class CB!" << endl; }
 private:
     weak_ptr<CA> m_ptr_a;
 };

测试结果如下:

 CA() called!
 CB() called!
 a use count : 1
 b use count : 1
 a use count : 1
 b use count : 2
 ~CA() called!
 ~CB() called!

通过这次结果可以看到,CA和CB的对象都被正常的析构了,引用关系如下图所示,流程与上一例子相似,但是不同的是④这条引用是通过weak_ptr建立的,并不会增加引用计数,也就是说CA的对象只有一个引用计数,而CB的对象只有2个引用计数,当test_refer_to_each_other这个函数返回时,对象ptr_a和ptr_b被销毁,也就是①③两条引用会被断开,此时CA对象的引用计数会减为0,对象被销毁,其内部的m_ptr_b成员变量也会被析构,导致CB对象的引用计数会减为0,对象被销毁,进而解决了引用成环的问题 image

5. 动态数组

5.1 new和数组

用new分配一个对象数组,我们要在类型名之后跟一对方括号。如下

 int *pia = new int[100];

方括号中必须是整型,但不必是常量。

默认情况下,用new分配的对象,都是默认初始化的。可以对数组中的元素进行值初始化,在方括号后面加上一对括号即可。

 int *pia = new int[100]();
 string* psa = new string[10]();
 //或者可以提供一个元素初始化器的花括号列表;
 int *pia = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
 //可以指定部分元素,剩余的进行默认初始化;
 string* psa = new string[10]{"a", "an", "the", string(3, 'x')};

5.2 智能指针和动态数组

标准库提供了一个可以管理new分配的数组unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号

 unique_ptr<int []> up(new int[10]());
 up.release();      //自动用delete []销毁指针;

shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。

6. allocator类

new有一些灵活上局限,它将内存分配和对象构造组合在了一起。当我们分配一大块内存的时候,使用allocator将内存分配和对象构造分类,只在真正需要的时候才真正执行对象创建的操作。

allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来,它分配的内存是原始的、未构造的。

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

 allocator<string> alloc;
 auto const p = alloc.allocate(n);       //分配n个未初始化的string
函数说明
allocator a定义了一个名为a的allocator对象,它可以为类型T的对象分配内存
a.allocate(n)分配一段原始的、未构造的内存,保存n个类型为T的对象
a.deallocate(p, n)释放从T*指针p开始的内存,p必须是一个先前用allocate返回的指针,且n必须是p创建时所要求的大小。
a.construct(p, args)p同上,arg被传递给类型为T的构造函数,用来构造对象。
a.destroy(p)对p所指向的对象执行析构函数。

标准库还为allocator类定义了两个伴随算法,用来拷贝和填充未初始化对象的算法

函数说明
uninitialized_copy(b, e, b2)从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。

7. new的重载和placement new

placement new(std::size_t size, void* men),它的作用就是在申请好的内存空间上使用构造函数构造对象。例如在STL的allocater类中,它通过operate newplacement new实现了把内存申请和对象构造分开。当然operater new实际调用了malloc。置位new使用案例如下:

image.png