C++ 学习笔记(12)动态内存、智能指针、new和delete、动态数组、allocator

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/l773575310/article/details/79266440

C++ 学习笔记(12)动态内存、智能指针、new和delete、动态数组、allocator

参考书籍:《C++ Primer 5th》
API:动态内存管理


  • 静态内存:
    • 保存局部static对象、类static数据成员、定义在函数外的变量。使用前分配,程序结束时销毁。
  • 栈内存:
    • 保持函数内的非static对象。存在于程序块中。
  • 堆内存(动态分配):
    • 程序控制动态对象。

12.1 动态内存与智能指针

  • 智能指针(smart pointer):负责自动释放所指向的对象。
    • shared_ptr:允许多个指针指向同一个对象。
    • unique_ptr:独占一个对象。
    • weak_ptr:弱引用,指向shared_ptr所管理的对象(不对shared_ptr产生影响)。
    • auto_ptr(C++17后抛弃):具有unique_ptr部分特性,不能在容器中保存,也不能作为函数返回值。

12.1.1 shared_ptr

  • 使用原因:
    • 常用于允许多个对象共享相同的状态。
    • 不知道需要多少对象。
    • 不知道所需对象准确类型(继承等)。

shared_ptr<string> p;
if(p && p->emty())      // 检查p是否为空,以及指向的字符串是否为空
    *p = "hi";      // 解引用p,赋予新值
  • make_shared:动态分配一个类型,参数就该类构造函数的参数列表。

  • 引用计数(reference count):shared_ptr的一个关联的计数器。

    • 拷贝shared_ptr时,计数器递增:
      • 一个shared_ptr初始化另一个shared_ptr。
      • 作为参数传递。
      • 作为函数返回值。
    • shared_ptr赋予新值或者被销毁时,计数器递减:
      • 局部shared_ptr离开作用域时。
auto p = make_shared<int>(12);      // p 指向一个int,计时器为1。
auto q(p);                          // q 和 p 指向同一个对象,计数器为2。

auto r = make_shared<int>(12);      // r 指向一个int,计数器为1。
r = q;  //  r被赋值,指向 q。
        // q 的计数器递增(为3)。
        // r 计数器递减(为0), r 原来指向对象没有引用者,被自动释放。
  • shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,其析构函数就会销毁对象,释放内存。
void function()
{
    shared_ptr<int> p =make_shared<int>(666);       // 计数器为 1。
    return p;       // 返回 p 时,实际上是返回p的拷贝值,计数器递增为 2。
}   // p 离开作用域,计数器递减,变回 1。
  • 如果将shared_ptr存放于容器中,对于不需要用的元素,记得要使用erase删除,这样才能释放内存。

12.1.2 直接管理内存

  • new:返回指向该对象的指针。
int *p1 = new int;      // 默认初始化,*p1的值未定义
int *p2 = new int();        // 值初始化为0,*p2为0

auto p3 = new auto(1);      // 正确,推出p1为int *类型
auto p4 = new int{ 1 };     // 正确
auto p5 = new auto{ 1 };    // 错误,不能使用new auto加上花括号
  • delete:释放动态分配的内存,也可以是空指针。释放非new分配的内存或者同一指针释放多次的行为是未定义的。
  • 在指针被delete之后,会变成空悬指针(dangling pointer),即指向一块已经失效的内存的指针。可以将指针赋予nullptr。

12.1.3 shared_ptr 和 new 结合使用

  • 接收指针参数的智能指针构造函数是explicit的,即不能隐式将内置指针转换成智能指针,必须使用直接初始化形式初始化一个智能指针。
shared_ptr p1 = new int(123);   // 错误,因为new产生的是内置指针,且智能指针无法隐式转换。
shared_ptr p2(new int(123));        // 正确,使用直接初始化。

if (!p2.unique())       // 如果p2不是唯一用户(计数器为1),重新分配。
    p2.reset(new int(123));         // 正确,指向新对象。

  • 不要混合使用普通指针和智能指针。
  • 不要用get初始化另一个智能指针或赋值。
  • get返回普通指针,指向智能指针管理的对象。不能delete这个指针。
void process(shared_ptr<int> p) {}      // 在函数结束后,p的计数器会递减(也可能销毁)

int *x(new int(123));       // x是普通指针
process(x);             // 错误,不能将普通指针隐式转换成智能指针。
process(shared_ptr<int>x);      // 正确,但是在函数执行完后,x的内存被释放。
int i = *x;     // 错误,x指向内容已经被释放

12.1.4 智能指针和异常

void f() 
{
    shared_ptr<int> sp(new int(123));   // 智能指针分配一个新对象
    int *ip = new int(456);             // new创建指针

    // 中间抛出异常,而且没有捕获,函数直接结束

    delete ip;      // 被跳过,没有执行,ip没有被释放
}   // sp在函数结束时被释放内容,正确
  • 默认情况下,shared_ptr 被销毁时,对指针进行delete。
  • 删除器(deleter)函数:用于代替原来delete,即自定义销毁操作。详见std:: shared_ptr::shared_ptr

  • 基本规范:

    • 不使用内置指针值初始化(或 reset)多个智能指针。
    • 不delete get()返回的指针。
    • 不使用 get() 初始化(或 reset)另一个智能指针。
    • 记住get()返回的指针,在对应最后的智能指针被销毁时,会失效。
    • 如果智能指针管理的资源不是new分配的,要自定义一个删除器。

12.1.5 unique_ptr

  • 同shared_ptr,初始化时,必须用直接初始化形式。
  • unique_ptr 只占有一个对象,所以不支持普通拷贝或者赋值操作。但可以拷贝或赋值一个将要被销毁的unique_ptr。
unique_ptr<int> p1(new int(123));
unique_ptr<int> p2(p1);             // 错误,不支持拷贝
unique_ptr<int> p3;
p3 = p1;                            // 错误,不支持赋值

// 但可以拷贝或赋值一个将要被销毁的 unique_ptr

unique_ptr<int> p2(p1.release());       // 正确,将p1的指向转给p2。p1为空,p2指向原来p1指向的对象

unique_ptr<int> clone(int value)
{
    unique_ptr<int> ret(new int(value));
    return ret;     // 正确,返回一个局部对象(unique_ptr)的拷贝
}

  • release:放弃了指向原来的对象,指针为空值。
  • reset:释放了指向对象的内存。

12.1.6 weak_ptr

  • 一种不控制所指向对象生存期的智能指针,指向shared_ptr管理的对象。

auto p = make_shared<int>(123);
weak_ptr<int> wp(p);        // wp弱共享p。p的计数器没改变。

if( shared_ptr<int> np = wp.lock() )    // 判断wp指向对象是否为空,不为空成立且获取(通过weak_ptr 获取 shared_ptr)
{
    // 这个块区间内,np和p共享对象。
}

12.2 动态数组

12.2.1 new和数组

  • 虽然用 new T[ ] 分配的内存称为“动态数组”,但实际上得到的是元素类型的指针,而不是数组类型的对象
  • 因为上面这原因,所以也不能用begin或end,或者是范围for语句。
int *p1 = new int[10];      // 10个为初始化的int
int *p2 = new int[10]();        // 10个默认初始化为0的int
int *p3 = new int[10]{ 1, 2, 3, 4 };        // 同数组列表初始化
  • 不能在括号中给出初始化器,所以不能用auto分配数组。
  • 可以用任意表达式来缺点要分配的对象的数目。
auto ap = new auto[10](1);      // 错误,不能在括号内给出初始化器,所以auto也无法判断

int v = 3;              // 一个变量
int arr1[v];            // 错误,不能用非常量表达式设置维度
int arr2[0];            // 错误,不能使用0作为维度
int *ip = new int[v];   // 正确,可以用任意表达式分配数组
int *ip = new int[0];   // 正确,但是ip不能解引用,是一个合法的非空指针

delete [] ip;       // 释放ip动态数组
  • 释放动态数组时,数组中元素按逆序销毁,即最后一个元素先销毁,再到倒数第二个,依此类推。

  • unique_ptr 可以管理动态数组。
  • C++17前,shared_ptr 不直接支持,需要提供删除器。C++17后支持管理动态数组。见std::shared_ptr::operator[]
unique_ptr<int[]> up(new int[10]);
for (size_t i = 0; i < 10; i++)
    up[i] = i;
up.reset();                 // 自动调用delete[ ] 销毁其指针

// C++17前,使用shared_ptr,必须提供删除器
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
for (size_t i = 0; i != 10; ++i)
    *(sp.get() + i) = i;        // 使用get获取一个内置指针
sp.reset();                 // 使用自定义的删除器

// C++17以后,支持[ ] 下标运算符,可直接使用
shared_ptr<int[]> sp(new int[10]);
for (size_t i = 0; i < 10; i++)
    sp[i] = i;

12.2.2 allocator 类

  • 提供可以将内存分配和对象构造分开的操作,分配的内存是原始的、未构造的。
  • destroy接收一个指针,指向对象执行的析构函数。

allocator<string> alloc;
auto const p = alloc.allocate(n);       // 分配n个未初始化的string

auto q = p;     // q指向最后构造的元素之后的位置
alloc.construct(q++);           // *q为空字符串
alloc.construct(q++, 5, 'c');   // *q为ccccc(使用了初始化器)
alloc.construct(q++, "hi"); // *q为hi

while( q != p )
    alloc.destroy(--q);     // 销毁掉前面构造的string们

alloc.deallocate(p, n);     // 释放掉内存,n与构造时的大小必须相同

  • 可以拷贝和填充为初始化内存,见上表。
vector<int> vi{ 1, 2, 3 };
allocator<int> alloc;

auto p = alloc.allocate(vi.size() * 2);     // 分配比vi占用空间大一倍的动态内存:3 * 2 = 6 个int
auto q = uninitialized_copy(vi.begin(), vi.end(), p);       // 拷贝vi,从p开始
uninitialized_fill_n(q, vi.size(), 66);     // 剩余的填充66

for (size_t i = 0; i < vi.size() * 2; i++)  // 结果为 1 2 3 66 66 66
    cout << *(p++) << endl;

猜你喜欢

转载自blog.csdn.net/l773575310/article/details/79266440