C++_03_动态内存与智能指针

动态内存

  • 静态内存:用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
  • 栈内存:用来保存定义在函数内的非static对象。
  • 自由空间(free store)或堆(heap):程序用堆存储动态分配的对象——程序运行时分配的对象。

分配在静态内存和栈内存中的对象由编译器自动创建和销毁。动态对象的生存期由程序来控制(代码显示的创建和销毁对象)。

使用动态内存的原因:

  • 程序不知道自己需要使用多少对象;
  • 程序不知道所需对象的准确类型;
  • 程序需要在多个对象间共享数据;

动态内存的使用很容易死出现问题:

  • 动态内存使用时,忘记释放内存,就会产生内存泄漏。
  • 有指针引用内存的情况下,我们就释放了它,导致引用非法内存的指针。

智能指针

C++11新标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要区别是它负责自动释放所指向的对象。

  • stared_ptr: 允许多个指针指向同一个对象。
  • unique_ptr: 独占所指向的对象。
  • weak_ptr: 弱引用,指向stared_ptr所管理的对象。

auto_ptr在C++11中被废弃,不再讨论。

stared_ptr指针

stared_ptr和unique_ptr都支持的操作

stared_ptr sp; unique_ptr up 空智能指针,可以指向类型为T的对象
p 将p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象
p→mem 等价于(*p).mem
p.get() 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针锁指向的对象也就消失了。
swap(p, q)
p.swap(q) 交换p和q中的指针

shared_ptr中独有的操作

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

一旦一个shared_ptr的计数器为0,它就会自动释放自己所管理的对象。

由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要。

注意:如果将shared_ptr存放于一个容器中,而后不在需要全部的元素,而只是使用其中的一部分,要记得用erase删除不在需要的那些元素。

shared_ptr与new结合使用

不能将内置指针隐式转换为一个智能指针,必须使用直接初始化形式,如下:

shared_ptr<int> p1 = new int(1024); // 错误
shared_ptr<int> p2(new int(1024)); // 正确

p2 = new int(1024); // 错误
p2.reset(new int(1024)); // 正确

注意:

  • 不要混用普通指针与智能指针!
  • 不要使用get初始化另一个智能指针或为智能指针赋值!

容易出现同一块内存被delete两次,而出现core!

如果使用智能指针,即使程序块过早结束,智能指针也能确保在内存不再需要时将其释放。而如果是普通指针,在new和delete之间发生了异常,且异常未捕获,则内存就永远不会被释放了。

自定义删除器

例子:

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

void f(distination& d) {
	connection c = connection(&d);
	shared_ptr<connection> p(&c, end_connection);
}

智能指针陷阱

智能指针使用规范:

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

unique_ptr指针

与shared_ptr不同的是,unique_ptr某个时刻只能有一个unique_ptr指向一个给定对象。

unique_ptr操作

unique_ptr u1; unique_ptr<T, D> u2 空unique_ptr,可以指向类型为T的对象,u1会使用delete来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针。
unique_ptr<T, D> u(d) 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr 释放u指向的对象,将u置为空
u.release() u放弃对指针的控制权,返回指针,并将u置为空
u.reset() 释放u指向的对象
u.reset(q)
u.reset(nullptr) 如果提供了内置指针q,令u指向这个对象;否则将u值为空

unique_ptr不支持普通的拷贝或赋值操作:

unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1);  // 错误,unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误,unique_ptr不支持赋值

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

// 将所有权从p1转移给p2
unique_ptr<string> p2(p1.release()); 
unique_ptr<string> p3(new string("hello"));
// 将所有权从p3转移给p2
p2.reset(p3.release()); // reset释放了p2原来指向的内存

weak_ptr

weak_ptr是一种不可控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。

将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

weak_ptr的引入是为了解决shared_ptr存在的一个问题——循环引用 。

weak_ptr操作

weak_ptr w 空weak_ptr可以执行类型为T的对象
weak_ptr w(sp) 与shared_ptr 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直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。

经验之谈

不要混用

指针之间的混用,有时候会造成不可预知的错误,所以建议尽量不要混用。包括裸指针和智能指针以及智能指针之间的混用****

裸指针和智能指针混用

void fun() {
  auto ptr = new Type;
  std::shared_ptr<Type> t(ptr);
  
  delete ptr;
}

在上述代码中,将ptr所有权归shared_ptr所拥有,所以在出fun()函数作用域的时候,会自动释放ptr指针,而在函数末尾又主动调用delete来释放,这就会造成double delete,会造成segment fault

智能指针混用

void fun() {
  std::unique_ptr<Type> t(new Type);
  std::shared_ptr<Type> t1(t.get());
}

在上述代码中,将t关联的对象又给了t1,也就是说同一个对象被两个智能指针所拥有,所以在出fun()函数作用域的时候,二者都会释放其关联的对象,这就会造成double delete,会造成segment fault

需要注意的是,下面代码在STL中是支持的:

void fun() {
  std::unique_ptr<Type> t(new Type);
  std::shared_ptr<Type> t1(std::move(t));
}

不要管理同一个裸指针

void fun() {
  auto ptr = new Type;
  std::unique_ptr<Type> t(ptr);
  std::shared_ptr<Type> t1(ptr);
}

在上述代码中,ptr所有权同时给了t和t1,也就是说同一个对象被两个智能指针所拥有,所以在出fun()函数作用域的时候,二者都会释放其关联的对象,这就会造成double delete,会造成segment fault

避免使用get()获取原生指针

void fun(){
  auto ptr = std::make_shared<Type>();

  auto a= ptr.get();

  std::shared_ptr<Type> t(a);
  delete a;
}

一般情况下,生成的指针都要显式调用delete来进行释放,而上述这种,很容易稍不注意就调用delete;非必要不要使用get()获取原生指针。****

不要管理this指针

class Type {
 private:
  void fun() {
    std::shared_ptr<Type> t(this);
  }
};

在上述代码中,如果Type在栈上,则会导致segment fault,堆上视实际情况(如果在对象在堆上生成,那么使用合理的话,是允许的)。****

只管理堆上的对象

void fun() {
   Type t;
   std::shared_ptr<Type> ptr(&t);
};

在上述代码中,t在栈上进行分配,在出作用域的时候,会自动释放。而ptr在出作用域的时候,也会调用delete释放t,而t本身在栈上,delete一个栈上的地址,会造成segment fault

优先使用unique_ptr

根据业务场景,如果需要资源独占,那么建议使用unique_ptr而不是shared_ptr,原因如下:

  • 性能优于shared_ptr
    • 因为shared_ptr在拷贝或者释放时候,都需要操作引用计数
  • 内存占用上小于shared_ptr
    • shared_ptr需要维护它指向的对象的线程安全引用计数和一个控制块,这使得它比unique_ptr更重量级

使用make_shared初始化

我们看下常用的初始化shared_ptr两种方式,代码如下:

std::shared_ptr<Type> p1(new Type);
std::shared_ptr<Type> p2 = std::make_shared<Type>();

那么,上述两种方法孰优孰劣呢?我们且从源码的角度进行分析。

第一种初始化方法,有两次内存分配:

  • new Type分配对象
  • 为p1分配控制块(control block),控制块用于存放引用计数等信息

第一种初始化方式(new方式)共有两次内存分配操作,而第二种初始化方式(make_shared)只有一次内存申请,所以建议使用make_shared方式进行初始化。


Reference:

posted @ 2022-06-12 15:57  吹不散的流云  阅读(90)  评论(0编辑  收藏  举报