动态内存指的是在程序运行时阶段,由我们程序员自己负责分配和回收的内存。

C++用一对运算符管理动态内存:

  • new
    • 为对象分配空间,并返回指向该对象的指针,可选择初始化对象。
    • 一次只能分配一个对象的内存块。
  • delete
    • 接受一个动态对象指针,销毁对象,并释放与之关联的内存。
    • 一次只能释放一个对象。

一、new:分配动态内存

支持的操作

  1. // 没圆括号,默认初始化:类执行默认构造,内置类型未定义。
  2. T* p = new T; // 分配一个T类型对象的内存。
  3. // T是类时,对象执行默认初始化
  4. // T是内置类型时,值未定义。
  5. // 有圆括号(),值初始化:类执行默认构造,内置类型有初始值,为0或者空。
  6. T* P = new T(); // T是类时,对象执行默认初始化
  7. // T是内置类型时,值初始化0。
  8. T* P = new T(args); // args:逗号隔开的参数列表
  9. // 调用T(args)构造函数初始化对象
  10. T* p = new T[n]; // 分配一个长度为n,元素类型为T的数组空间,返回第一个元素指针。
  11. // 元素执行默认初始化。
  12. T* p = new T[n](); // 同上,但是元素执行值初始化。
  13. T* p = new T[n]{...}; // 同上,{...}列表初始化数组元素
  14. // 列表值数量不够,剩下的值初始化
  15. // 列表值数量超标,分配失败,抛出异常:bad_array_new_length
  16. const T* p = new const T(); // const版。
  17. auto p = new auto(obj); // 与下等价
  18. Type p = new Type(obj); // Type是obj的类型。

使用例子

  1. int *pi = new int; // 值未定义
  2. int *pi = new int(); // 值初始化为0
  3. string *ps = new string; // 默认初始化,空string
  4. string *ps = new string(); // 值初始化,空string
  5. int *pia = new int[n]; // 方括号[]:表示动态分配一个数组。返回指向第一个元素的指针
  6. int *pia = new int[10]; // 10个未初始化的int
  7. int *pia2 = new int [10](); // 10个值初始化为0的int
  8. string *psa = new string[10]; // 10个空string
  9. string *psa2 = new string[10](); // 10个空string
  10. int *pia3 = new int[10]{0,1,2,3}; // 数量不够,剩下的值初始化
  11. // 数量超标,分配失败抛出异常
  12. char *cp = new char[0]; // 正确:但cp不能解引用,new返回一个合法的非空指针
  13. typedef int arrT[42];
  14. int *p = new arrT;
  15. int *pi = new int(1024) ;
  16. string *ps = new string(10,' 9');
  17. vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  18. Type obj{1,2,3}
  19. auto p1 = new auto(obj); // 与下等价
  20. Type p1 = new Type(obj);
  21. auto p2 = new auto{a, b, c}; // 错误:括号中只能有单个初始化器
  22. // 用new分配const对象是合法的:
  23. const int *pci = new const int(1024); // 分配并初始化一个const int
  24. const string *pcs = new const string; // 分配并默认初始化一个const的空string

new失败

new分配内存可能失败,比如内存耗尽,返回空指针,同时抛出一个bad_alloc异常。
可以用定位new的形式来阻止异常抛出。

  1. int *p1 = new int; //如果分配失败,返回空指针,抛出异常std::bad_alloc
  2. int *p2 = new (nothrow) int ; // 如果分配失败,new返回一个空指针,阻止抛出异常。
  3. // 定位new,向new传递了额外的参数nothrow,告诉new不要抛异常
  4. // nothrow、bad_alloc在new头文件中。

二、delete:释放动态内存

执行了两个动作:

  • 销毁对象(析构)
  • 释放内存

重复delete,行为未定义。
delete一个非动态分配对象(局部对象),这非常危险。
new出来的对象,一定要delete才会销毁,否则生命周期和程序一样长,如同全局变量。

  1. delete p; // p必须指向一个动态分配的对象或是一个空指针,注意delete一个空指针是没有问题的代码。
  2. delete[] parr; // 删除数组必须加上[],逆序销毁
  3. // 忘记加[],行为未定义
  4. const int *pci = new const int(1024);
  5. delete pci; // 正确:释放一个 const 对象
  6. pci = nullptr; // pci成了空悬指针(野指针),指向的地址非法了。手动赋值成空。
  7. // 但这也仅仅是解决了pci的问题,其他相同指向的指针就爱莫能助了。
  • new、delete出错的三个常见原因

    • 忘记delete,俗称内存泄露。
    • 使用已经delete的对象。
    • 重复delete,这非常危险,可能破坏堆空间。

      三、智能指针

      因为动态内存管理起来比较麻烦,容易出现内存泄露,因此C++11标准库提供了智能指针的来管理动态对象:

    • shared_ptr:共享指针,几个shared_ptr共享一个动态对象。

    • unique_prt:独占指针,
    • weak_ptr:弱引用指针,指向shared_ptr管理的对象,但不影响动态对象的引用计数,因此不对内存管理产生任何影响。

都是模板类,定义在memory头文件中。

1、shared_ptr

共享指针,共享一个动态对象。shared_ptr内部维护了一个动态对象的引用计数,表示这个动态对象被多少个共享指针所引用,当引用计数为0时,动态对象将被释放。
shared_ptr都有一个关联的引用计数器counter,记录着另外还有多少个shared_ptr指向所管理的动态对象。

  • p.counter + 1
    • shared_ptr p(q);
    • shared_ptr p = q;
    • fuck( p );
    • return p;
  • p.counter - 1
    • p = q
      • q的动态对象的引用计数+1,p的动态对象的引用计数-1,此时如果counter=0,则释放p的动态对象,最后p改成管理q的动态对象。
      • p:“我不管以前的了,我要管q管的那个对象了”。
    • p被销毁(如p作为局部变量被销毁),触发了析构函数。
  • counter == 0时
    • 释放所管理动态对象,默认触发delete。

      支持的操作

      ```cpp

shared_ptr sp; // 空智能指针,可指向T类型对象。

shared_ptr p = q; // q必须是shared_ptr类型,若q是内置指针,则是错误的。 // 因为接受一个内置指针参数的构造函数是explicit的 // 不能直接赋值普通指针值。

shared_ptr sp(args); // args构造一个T对象。sp管理这个对象。和普通指针不存在隐式转换 // 换句话说,类似容器的emplace方法。

shared_ptr p(q); //1、q是shared_ptr时: // p是q的拷贝:此操作会递增q中的计数器。 // p、q必须同类型。 //2、q是unique_ptr时: // p从q那里接管了对象的所有权:将q置为空 //3、q是内置指针时: // q必须指向动态内存,且q能够转换为T*类型

shared_ptr p(q, delFunc); //1、q是shared_ptr时: // p是q的拷贝 //2、q是内置指针时: // q必须能转换成T*类型 //d是可调用对象,代替delete,自定义删除器。

make_shared(args); // 返回一个shared_ptr,指向一个动态分配的T类型对象,args初始化此对象 // 这是最安全的动态内存分配方法。类似容器的emplace(args)

p; // 和普通指针一样操作。 p; // 解引用,获得指向的对象。 p->mem; // 等价于(p).mem p.get(); // 返回p中保存的内置指针 swap(p, q); // 交换指针 p.swap(q); // 同上

p = q; // p的引用计数-1,q的引用计数+1,计数为0,释放管理的内存。 // p、q是shared_ptr,所保存的指针必须能互相转换。

p.unique(); // p.use_count() == 1,当前p是否独占对象。 p.use_count(); // 与p共享对象的智能指针数量,可能很慢,用于调试。

// 如果p独占对象, p.reset()会释放对象 // p:“我不管手上的对象了,我要管q所管的对象” p.reset(); // 将p置为空,对象的智能指针引用计数-1 p.reset(q); // 等价于p = q,q是内置指针类型 p.reset(q, d); // 等价于p = q,调用d来释放q,而不是delete

  1. <a name="eJBVA"></a>
  2. ### 使用例子
  3. ```cpp
  4. shared_ptr<string> p1; // shared_ptr, 可以指向 string
  5. shared_ptr<list<int>> p2; // shared_ptr, 可以指向 int 的 list
  6. auto p3 = make_shared<int>(42); // 指向值为42的int的shared_ptr
  7. auto p4 = make_shared<string>(2,'9'); // 指向值为“99”的string
  8. auto p5 = make_shared<int>(); // 指向值初始化的int,即值为0
  9. auto p6 = make_shared<vector<string>>(); // 指向动态分配的空vector<string>
  10. p = new int(1024}; // 错误:不能将一个指针赋予shared_ptr
  11. p.reset(new int(1024}}; // 正确:p指向一个新对象
  12. if (!p.unique())
  13. p.reset(new string(*p)); // 我们不是唯一用户,分配新的拷贝
  14. *p += newVal; // 现在我们知道自己是唯一的用户,可以改变对象的值
  15. shared_ptr<int> sp(new int[10], [](int *p){ delete[] p; }); // shared_ptr管理数组,必须提供删除器
  16. sp.reset(); // 使用我们提供的lambda释放数组,它使用delete[]
  17. // shared_ptr未定义下标运算符,并且不支持指针的算术运算
  18. for(size_t i = 0; i != 10; ++i)
  19. *(sp.get() + i) = i; //使用get获取一个内置指针

new、shared_ptr结合使用

  1. shared_ptr<double> p1; // shared_ptr 可以指向一个double
  2. shared_ptr<int> p2(new int(42)); // p2 指向一个值为 42 的 int
  3. shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式
  4. shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式,转换构造函数是explicit的。

注意,智能指针和内置指针不能混用,因为转换构造函数是explicit的。

  1. void process(shared_ptr<int> ptr){}
  2. int *x(new int(1024));
  3. process(x); // 错误:不能将int*转换为一个shared_ptr<int>
  4. process(shared_ptr<int> (x)); // 合法的,但process执行完之后,动态对象x的内存会被释放!
  5. int j = *x; // 未定义的:x是一个空悬指针!

不要把get()值赋值给智能指针,因为get返回的是内置指针,赋值给智能指针的话,就对同一块内存形成了两套计数,也就是说会被释放2次!

  1. shared_ptr<int> p(new int(42)); // 引用计数为 1
  2. int* q = p.get(); // 正确:但使用 q 时要注意,不要让它管理的指针被释放
  3. {
  4. shared_ptr<int>(q);
  5. }
  6. // 程序块结束,q被销毁,它指向的内存new int(42)被释放,此时p将指向非法内存
  7. int foo = *p ; // 未定义: p 指向的内存已经被释放了

使用规范

  • 不使用相同的内置指针值初始化(或reset ) 多个智能指针。
  • 不delete掉get()返回的指针。
  • 不使用 get()初始化或reset另一个智能指针。
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

    原理(简单代码实现)

    ```cpp

template class shared_ptr{ public: explicit shared_ptr(const T*); // 通过内置类型构造,声明为显式。 shared_ptr(const shared_ptr& sptr); // 拷贝构造,引用计数+1 ~shared_ptr(); // 析构,引用计数-1

  1. shared_ptr<T>& shared_ptr& operator=(const shared_ptr& sptr); // 拷贝赋值
  2. T& operator*(); // (*p).fuck();
  3. T* operator->(); // p->fuck();
  4. T* getTarget() const; // 当前引用的动态对象
  5. size_t useCount() const; // 当前引用计数

private: void release(); // 引用计数-1

private: size_t _count; // 动态对象_ptr的引用计数 T _ptr; // 引用的动态对象 }

template shared_ptr::shared_ptr(const T* t) : _count(nullptr) , _ptr(nullptr) { if(t) { _count = new size_t(1); _ptr = t; } }

template shared_ptr::shared_ptr(const shared_ptr& sptr) : _count(sptr._count) , _ptr(sptr._ptr) { if(_ptr) ++(*_count); }

template shared_ptr::~shared_ptr(){ release(); }

template shared_ptr& shared_ptr::operator=(const shared_ptr& sptr){ if(this == &_sptr) return;

  1. auto tmpCount = sptr._count;
  2. auto tmpPtr = sptr._ptr;
  3. release();
  4. _count = tmpCount;
  5. _ptr = tmpPtr;
  6. if(_ptr) ++(*_count);
  7. return *this;

}

template T& shared_ptr::operator(){ return _ptr; }

template T* shared_ptr::operator->(){ return _ptr; }

template T* shared_ptr::getTarget() const{ return _ptr; }

template size_t shared_ptr::useCount() const{ if(!_count) return 0; return *_count; }

template void shared_ptr::release(){ if(_ptr && !(—(*_count))){ delete _count; delete _ptr; _count = nullptr; _ptr = nullptr; } }

  1. <a name="SgbEJ"></a>
  2. ### 循环引用
  3. 智能指针的目的是为了避免内存泄露,但智能指针本身也会产生内存泄露,比如循环引用,就是两个shared_ptr智能指针之间互相引用,导致这两个智能指针的引用计数一直不会归0,从而产生内存泄露,具体看下:
  4. ```cpp
  5. class Parent; //
  6. class Child; // Child和Parent结构完全相同。
  7. class Parent {
  8. public:
  9. Parent() = default;
  10. ~Parent() = default;
  11. void setChild(const shared_ptr<Child>& child){
  12. _child = child;
  13. }
  14. public:
  15. // 循环引用问题解决方法,只需要这里其中一个shared_ptr改为weak_ptr即可。
  16. // 这样就打破了引用环。weak_ptr的介绍见下面。
  17. shared_ptr<Child> _child;
  18. };
  19. int main(){
  20. weak_ptr<Parent> parent;
  21. weak_ptr<Child> child;
  22. {
  23. parent = shared_ptr<Parent>(new Parent);
  24. child = shared_ptr<Parent>(new Child);
  25. parent->setChild(child);
  26. child->setParent(parent);
  27. std::cout << "parent use count : " << parent.useCount() << std::endl; // 2
  28. std::cout << "child use count : " << child.useCount() << std::endl; // 2
  29. }
  30. std::cout << "parent use count : " << parent.useCount() << std::endl; // 1,这里应该是0的
  31. std::cout << "child use count : " << child.useCount() << std::endl; // 1
  32. }

2、unique_ptr

独占指针,任意时刻最多只能有一个unique_ptr指针指向同一个对象,所以不支持拷贝控制,但是可以进行移动控制。

  1. unique_ptr<int> ptr1(new int);
  2. unique_ptr<int> ptr2;
  3. ptr2 = ptr1; // 错误,这样发生了拷贝,就不是独占了,因此不会支持拷贝控制
  4. ptr2 = std::move(ptr1); // 正确,ptr1将被置空。

支持的操作

  1. unique_ptr<T> up;
  2. p; // 和普通指针一样。
  3. *p; // 解引用,获得指向的对象。管理的是new[]的数组时,不支持。
  4. p->mem; // (*p).mem,管理的是new[]的数组时,不支持。
  5. p.get(); // 返回p中保存的内置指针
  6. swap(p, q); // 交换指针
  7. p.swap(q); // 同上
  8. p[i]; // 返回u拥有的数组中位置i处的对象,up必须指向一个数组
  9. // 空unique_ptr,可以指向类型为T的对象
  10. unique_ptr<T> u1; // 调用delete来释放对象
  11. unique_ptr<T, D> u2; // D类型的可调用对象来释放对象。
  12. unique_ptr<T, D> u(d); // 空unique_ptr, 指向类型为T的对象,
  13. // 用类型为D的可调用对象d来代替delete
  14. unique_ptr<T[]> u; // u可以指向一个动态分配的数组,数组元素类型为T
  15. unique_ptr<T[]> u(p); // u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
  16. // up指向一个包含10个未初始化int的数组
  17. unique_ptr<int[]> up(new int[10]);
  18. up.release(); // 自动用delete[]销毁其指针
  19. u = nullptr; // 释放u指向的对象,将u置为空
  20. u.release(); // u放弃对指针的控制权,返回内置指针,井将u置为空
  21. // 并不会触发delete,仅仅是放弃。
  22. // 管理的是new T[]的数组时,会触发delete[]销毁。
  23. u.reset(); // 释放u指向的对象
  24. u.reset(q); // 释放u指向的对象,令u指向内置指针q指向的对象
  25. u.reset(nullptr); // 释放u指向的对象,u置位空。

使用例子

  1. unique_ptr<string> p2(p1.release()); //将所有权从p1转移给p2,release将p1置为空
  2. unique_ptr<string> p3(new string("Trex"));
  3. p2.reset(p3.release()) ; // 1、p3.release()释放所有权
  4. // 2、reset释放了p2原来指向的内存
  5. // 3、p2拥有对p3管理对象的所有权。
  6. p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
  7. auto p = p2.release(); // 正确,但我们必须记得delete(p)
  8. unique_ptr<double> p1; // 可以指向一个double的unique_ptr
  9. unique_ptr<int> p2(new int(42)); // p2指向一个值为42的int
  10. unique_ptr<string> p1(new string( "Stegosaurus"));
  11. unique_ptr<string> p2 (pl); // 错误:unique_ptr不支持拷贝构造
  12. unique_ptr<string> p3;
  13. p3 = p2; // 错误:unique_ptr 不支持拷贝赋值
  14. unique_ptr<string> p4(new int(42));
  15. unique_ptr<string> p5 = std::move(p4); // 移动构造,p4将被清空。

3、weak_ptr

“弱指针”,类似Lua的“weak table”(如果你知道的话)。
不控制所指向对象生存期的智能指针,始终指向一个由shared_ptr管理的对象,weak_ptr不影响shared_ptr的引用计数。可以解决shared_ptr带来的循环引用问题。

支持的操作

  1. weak_ptr<T> w; // 空weak_ptr可以指向类型为T的对象
  2. weak_ptr<T> w(sp); // 与sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型
  3. // 记住,weak_ptr只和shared_ptr发生关系。
  4. w = p; // p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
  5. w.reset(); // 将w置为空
  6. w.use_count(); // 与w共享对象的shared_ptr的数量
  7. w.expired(); // w.use_count() == 0
  8. w.lock(); // 如果expired为true,返回一个空shared_ptr:
  9. // 否则返回一个指向w的对象的shared_ptr
  10. // 不能直接用weak指针来访问对象,应该使用lock返回shared_ptr来访问
  11. // 这样的话才能确保对象不存在了也不会出错。

规范使用

  1. auto p = make_shared<int>(42);
  2. weak_ptr<int> wp(p); // wp弱共享p,p的引用计数未改变
  3. .....
  4. // 使用前必须lock确保对象还在
  5. if (auto np = wp.lock()){ // np不为空说明所指对象还在。
  6. }

四、allocator(内存池)

解决需求:先分配一大块内存,后面按需初始化创建对象。
功能:内存分配与对象构造分离。
定义在memory头文件,是一个模板。
根据给定的对象类型来确定恰当的内存大小和对齐位置

支持的操作

  1. allocator<T> allocator; // 定义一个可以为T类型对象分配内存的分配器。
  2. auto p = a.allocate(n); // 分配(原始)内存
  3. // 分配一段原始内存(为构造初始化),可以保存n个T类型对象。
  4. // n=0,返回nullptr
  5. a.deallocate(p, n); // 释放(原始)内存
  6. // allocate的反过程,参数p、n必须是上面的p、n。
  7. // 正确的逻辑应该是在此之前调用析构函数,
  8. a.construct(p, args); // 在原始内存中构造对象
  9. // 在p所指内存上构造一个T对象,构造参数args,
  10. // p指向allocate分配的原始内存的某一个地址。
  11. a.destroy(p); // 触发p所指对象的析构函数。

使用例子

  1. auto q = p; // q指向最后构造的元素之后的位置
  2. alloc.construct(q++); // *q为空字符串
  3. alloc.construct(q++, 10, 'c'); // *q为cccccccccc
  4. alloc.construct(q++, "hi") ; // *q为hi!
  5. cout << *q << endl; // 灾难:q指向未构造的内存!
  6. while(q != p) alloc.destroy(--q); // 析构前面构造的内存
  7. allocator<string> alloc; // 可以分配string的allocator对象
  8. auto const p = alloc.allocate(n); // 分配n个未初始化的string

allocator算法

拷贝和填充原始内存的算法。

  1. // 这些函数在给定目的位置创建元素,而不是由系统分配内存给它们。
  2. uninitialized_copy(b,e,b2); // 迭代器b、e范围拷贝到b2开始的内存。必须足够大。
  3. uninitialized_copy_n(b,n,b2); // n个迭代器b的元素拷贝到b2开始的内存,必须足够大。
  4. // 返回尾迭代器,后面可以接着分配。
  5. uninitialized_fill(b,e,t); // 在迭代器b、e内存范围填充t的拷贝
  6. uninitialized_fill_n(b,n,t); // 从迭代器b指向的内存地址开始,填充n个t的拷贝。
  7. // 原始内存必须足够大。
  8. vector<int> vi; // 假设填充了若干元素。
  9. auto p = alloc.allocate(vi.size() * 2); // 分配2倍的动态内存空间。
  10. auto q = uninitialized_copy(vi.begin(), vi.end(), p); // 范围拷贝
  11. uninitialized_fill_n(q, vi.size(), 42); // 剩下的用42来初始化

注意

allocator的内容本身比较简单,但是非常容易出错,典型的问题就是deallocate之前忘记destroy。

五、C动态内存管理

分配内存

  • malloc
    • 分配(连续的)动态内存,返回内存地址指针,内存不足返回NULL。
  • calloc
    • 分配动态内存,会初始化为0,和malloc功能类似。
  • realloc
    • 重新分配动态内存。对原有的动态内存要么扩大要么缩小。扩大,新内存接到尾部,缩小释放尾部内存。
  • alloca
    • 在栈上申请内存。
    • 程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。 ```cpp

void malloc(size_t size); void calloc(size_t elem_num, size_t elem_size); void realloc(void ptr, unsigned int new_size);

realloc(NULL, new_size); // 两者等价。 malloc(new_size); // 两者等价。

  1. <a name="iCHfH"></a>
  2. ## 释放内存
  3. free:把动态内存返回给内存池。
  4. ```cpp
  5. void free(void* ptr); // ptr必须是malloc、calloc、realloc返回的指针。

一个安全的C动态内存分配器

malloc、calloc、realloc可能返回NULL导致程序崩溃,但很有可能是程序本身逻辑问题导致,我们可以隐藏这些库函数,代替以封装的函数。

  1. // alloc.h
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #define malloc // 禁止使用malloc
  5. #define MALLOC(elem_num, elem_size) // alloc((elem_num) * sizeof(elem_size))
  6. void* alloc(size_t size);
  1. // alloc.c
  2. #include "alloc.h"
  3. #include <iostream>
  4. #undef malloc
  5. using std::cout;
  6. using std::endl;
  7. void* alloc(size_t size){
  8. if(size == 0) return nullptr;
  9. void* pRet = nullptr;
  10. pRet = malloc(size);
  11. if(!pRet) {
  12. cout<< "out of memory." << endl;
  13. exit(1);
  14. }
  15. return pRet;
  16. }
  1. void function() {
  2. int *new_memory;
  3. new_memory = MALLOC(25, int);
  4. }

六、内存泄露(Memory Leak)

由于疏忽或错误导致程序未能释放已经不再使用的内存。一般就是指堆内存泄露。

  1. char *p = (char*) malloc(10);
  2. char *p1= (char*) malloc(10);
  3. p = p1; // p原本指向的动态内存泄露了。