c++动态内存与智能指针

1 前言

  • 静态内存:用于保存局部静态变量、类内的静态数据成员以及全局变量
  • :用于保存函数内部的非static变量
  • :存储动态分配的对象(程序运行时分配的对象)

静态内存和栈内存的对象由编译器自动创建和销毁

堆区动态的内存,其生命周期由程序控制

虽然动态内存是必要的,但是管理动态内存是非常棘手的一件事 。

在c++中,动态内存的管理由new/delete这对运算符完成,其中:

  • new负责为对象分配一块内存空间,并返回指向该空间的指针
  • delete负责销毁由new指向的内存空间

虽然c++提供了动态内存的管理机制,但是程序员在使用时很容易忘记释放内存,这样就很容易造成内存泄漏的问题,并且在合适的时间释放内存也是及其困难的问题。

因此,为了更加容易和安全的使用动态内存,c++提供了两种智能指针类管理动态内存对象——shared_ptr与unique_ptr,而weak_ptr是shared_ptr的一种伴随类。

2 shared_ptr

2.1 简介

shared_ptr即共享指针,顾名思义,其允许多个指针指向同一个对象。每当有一个指针指向该共享对象时,该共享对象的引用计数就加1,只有当最后一个指向该对象的 shared_ptr 被销毁时,对象的内存才会被释放。

也就是说,我们不必再关心在何时释放掉所申请的动态内存,因为,智能指针内置的引用计数为0时会自动调用析构来释放掉这块内存。

其实现机制可以简单理解为:普通指针+引用计数,如下图所示,有两个shared_ptr类型的智能指针q和p指向了同一个共享内存对象,则此时p.use_count()与q.use_count()都为2(use_count()方法用于获取该智能指针对所引用对象的引用计数)

2.2 初始化

shared_ptr的使用与普通指针的使用基本相同,可以使用*对指针进行解引用

2.2.1 make_shared函数

功能:标准的分配和初始化动态内存

头文件:memory

原型和使用方法

make_shared<T>(args)

  • T:动态对象的类
  • args:动态对象的初始化值
	//申请一块string类型的动态内存对象,并使用指针q指向它,且初始化数据为10个.
    shared_ptr<string> q = make_shared<string>(10, '.');
    //申请一块int类型且初始值为42的动态对象,并使用指针p指向它
	auto p = make_shared<int>(42);
    //使用解引用运算符*来访问内存
	cout << *q << endl;
	cout << *p << endl;

2.3 常用操作

2.3.1 获取shared_ptr中的指针

get()函数

功能:获取shared_ptr的指针

shared_ptr<int> p = make_shared<int>(42);
int *pp = p.get();

注意,永远不要用 get 初始化另一个智能指针或者为另一个智能指针赋值。

shared_ptr<int> p(new int(42)); // 引用计数为1
// 新程序块
{
    // 两个独立的shared_ptr指向相同的内存    
    shared_ptr<int> q(p.get());   // 错误:将get用来初始化指针指针
}   // 程序块结束,q被销毁,它指向的内存被释放

int foo = *p;   // 未定义:p指向的内存已经被释放了

另外,也不要delete get()返回的指针,这可能会导致以下问题:

  1. 多个智能指针共享同一块内存时,如果其中一个智能指针调用了 delete 操作符,会导致其他智能指针持有的原始指针成为悬挂指针,从而引发未定义行为或内存访问错误。

  2. 如果智能指针已经释放了其管理的资源,而你仍然尝试通过 get 返回的指针进行 delete 操作,这将导致二次释放内存,同样会引发未定义行为。

2.3.2获取shared_ptr所指对象的引用计数

use_count()

功能:获取共享对象的引用计数

	//创建一个动态对象,其值被初始化为42,并使用shared_ptr类型的共享指针p指向该动态内存
	shared_ptr<int> p = make_shared<int>(42);
	//由于此时只有一个shared_ptr类型的指针p指向值为42的动态内存,故该动态对象的引用计数为1
	cout << "指针p所指向动态对象的引用计数:" << p.use_count() << endl;
	auto q(p);//拷贝动态指针,使得shared_ptr指针q也指向与p所指向的动态内存
	cout<<"指针q所指向动态对象的引用计数:" << q.use_count() << endl;
	cout << "指针p所指向动态对象的引用计数:" << p.use_count() << endl;

2.3.3 reset()函数

功能:修改shared_ptr所管理的资源,或将shared_ptr置空


	shared_ptr<int> p = make_shared<int>(42);
	cout << "p1:" << *p << endl;
//修改共享指针p的指向为值为24的动态内存,原来的p指向的值为42的动态内存被释放
	p.reset(new int(24));
	cout << "p2:" << *p << endl;
	p.reset();//若参数为空,则表示将共享指针置空

一般情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因此智能指针默认使用 delete 释放它所关联的对象。但是也可以将智能指针绑定到一个指向其他类型的资源的指针上,只是此时需要提供自己的释放操作来替代 delete

// 如果p是唯一指向其对象的shared_ptr,reset会释放此对象。
p.reset();

// 若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。
p.reset(q);

//若还传递了参数d,将会调用d而不是delete来释放q
p.reset(q, d);

2.3.4 拷贝与赋值

	shared_ptr<int> p = make_shared<int>(42);
	auto q(p);//拷贝指针p,让指针p与指针q指向同一块内存
int* p(new int(1024));  // p指向动态内存
auto q = p;             // p和q指向同一块内存
delete p;               // p和q均变为无效
p = nullptr;            // 此时p不再绑定到任何对象
// 此时重置p对q没有任何作用,q仍然指向原来那块(已经被释放的)内存地址
// 在实际系统中,查找指向相同内存的所有指针是异常困难的!

2.3.5 shared_ptr与new的结合使用

接受指针参数的智能指针的初始化函数是explicit的,故而不能将一个普通指针隐式转换为智能指针,必须使用直接初始化方式来初始化一个智能指针,如下:

shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));  // 正确:使用了直接初始化形式

至此,我们已经知道了两种初始化shared_ptr类型指针的方式,分别是使用make_shared函数和使用关键字new进行直接初始化 

同理,一个返回 shared_ptr 的函数也不能在其返回语句中隐式转换一个普通指针:

shared_ptr<int> clone(int p){
    return new int(p);                  // 错误
    return shared_ptr<int>(new int(p)); // 正确
}

注意:不要混合使用普通指针和智能指针使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁

// ptr是传值方式传递,因此拷贝时会递增其引用次数。
void process(shared_ptr<int> ptr){
    // 使用ptr
}   // ptr离开作用域,ptr会被销毁。由于ptr销毁了,引用次数会递减

// process正确的使用方法:
shared_ptr<int> p(new int(42)); // 此时引用计数为1
process(p);     // 拷贝p会递增其引用次数,此时在process中引用计数值为2
int i = *p;     // 离开了process作用域,引用计数值为1 

// process错误的使用方法:
int *x(new int(1024));  // 危险:x是普通指针,不是智能指针
process(x);             // 错误:不能将int* 转换为shared_ptr<int>
process(shared_ptr<int>(x)); // 合法,但是由于传参时使用的是一个临时变量,
                             // 临时变量在传递完之后会被销毁,所以传完参
                             // 之后的process里的ptr引用计数为1
int j = *x; // 未定义:离开了process作用域,引用计数值为0,此时x为一个悬空指针

2.4 shared_ptr使用案例

// 数据不共享的情况:
vector<string> v1;    // 空 vector

{   // 新作用域
    vector<string> v2 = { "a", "an", "the" };
    v1 = v2;    // 从 v2 拷贝元素到 v1 中
}   // v2 被销毁,其中的元素也被销毁
    // v1 有三个元素,是原来 v2 中元素的拷贝


// 数据共享的情况:
// 假定我们希望定义一个名为 StrBlob 的类,保存一组元素
// 与容器不同,我们希望 StrBlob 对象的不同拷贝之间共享相同的元素
StrBlob b1;    // 空 StrBlob

{   // 新作用域
    StrBlob b2 = { "a", "an", "the" };
    b1 = b2;    // b2 和 b1 共享相同的元素
}   // b2 被销毁了,但是 b2 中的元素不能销毁
    // b1 指向最初的由 b2 创建的元素

接下来我们定义 StrBlob 类(使用 vector 来保存元素,并将 vector 保存在动态内存中)

  • 当我们拷贝、赋值或销毁一个 StrBlob 对象时,它的 Shared_ptr 成员也会被拷贝、赋值或销毁

  • 拷贝一个 shared_ptr 会递增其引用计数;将一个 shared_ptr 赋予另一个 shared_ptr 会递增赋值号右侧的 shared_ptr 的引用计数,而递减左侧 shared_ptr 的引用计数。

  • 如果一个 shared_ptr 的引用计数变为 0 ,它所指向的对象会被自动销毁。因此,对于由 StrBlob 构造函数分配的 vector,当最后一个指向它的 StrBlob 对象被销毁时,它会随之被自动销毁

#include<iostream>
#include<vector>
#include<string>
#include<memory>
#include<exception>
#include<initializer_list>
using namespace std;

typedef vector<string>::size_type size_type;
class StrBlob {
private:
	shared_ptr<vector<string>> data;
	void check(const size_type& i, const string& msg);

public:
	//构造函数
	StrBlob();
	StrBlob(initializer_list<string> li);//使用初始化列表构造函数

	size_type size();
	bool empty();

	void push_back(const string& str);
	void pop_back();

	string front();
	string back();

	friend ostream& operator<<(ostream& os, const StrBlob& data);
};


StrBlob::StrBlob():data(make_shared<vector<string>>()){}
StrBlob::StrBlob(initializer_list<string> li) :data(make_shared<vector<string>>(li)){}

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

size_type StrBlob::size()
{
	return data->size();
}

bool StrBlob::empty()
{
	return data->empty();
}

void StrBlob::push_back(const string& str)
{
	data->push_back(str);
}

void StrBlob::pop_back()
{
	check(0, "pop_back on empty StrBlob");
	data->pop_back();
}

string StrBlob::front()
{
	check(0, "front on empty StrBlob");
	return data->front();
}
string StrBlob::back()
{
	check(0, "back on empty StrBlob");
	return data->back();


}

ostream& operator<<(ostream& os, const StrBlob& str)
{
	for (auto p = str.data->cbegin(); p != str.data->cend(); ++p)
	{
		os << *p << " ";
	}
	return os;
}

 

3 unique_ptr

  • 与shared_ptr相反,unique_ptr智能指针独占其所指向动态对象的所有权
  • 故unique_ptr不支持拷贝与赋值操作
  • 但是我们可以拷贝一个右值类型unique_ptr,也就是一个即将被销毁的unique_ptr
  • 定义一个 unique_ptr 时,需要将其绑定到一个 new 返回的指针上。初始化 unique_ptr 必须采用直接初始化形式。

	unique_ptr<int> up(new int(42));
	cout << *up << endl;
	
	unique_ptr<int> uq(up);//错误,unique_ptr不支持拷贝操作
	unique_ptr<int> uq=up;//错误,unique_ptr不支持赋值操作

	//unique_ptr支持移动操作,也就是将一个即将销毁的对象赋给一个unique_ptr
	unique_ptr<int> upp = std::move(up);
unique_ptr<int> clone(int s)
{
	return unique_ptr<int>(new int(s));//return返回的值是一个右值,支持拷贝(移动)
}

 

4 weak_ptr

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

  • 将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。即使有 weak_ptr 指向对象,对象也还是会被释放。因此,weak_ptr 的名字抓住了这种智能指针 “弱” 共享对象的特点。

  • 由于对象可能不存在,我们不能使用 weak_ptr 直接访问对象,而必须调用 lock,检查对象是否存在。如果存在,lock 返回一个指向共享对象的 shared_ptr,否则返回一个空 shared_ptr。这使得 weak_ptr 可以用于检测所关联的 shared_ptr 是否已被销毁,从而避免了循环引用问题。

    shared_ptr<int> ptr1 = std::make_shared<int>(5);
    weak_ptr<int> ptr2 = ptr1;//定义一个weak_ptr,使其指向shared_ptr类型的ptr1所指向的内存对象

    cout << ptr1.use_count() << endl;//输出1,weak_ptr不占用shared_ptr的引用计数
    cout << *ptr2.lock() << std::endl;  // 输出5

    ptr1.reset();//释放ptr1的内存对象
    if (!ptr2.lock()) {
        std::cout << "ptr1 has been destroyed." << std::endl;  // 输出ptr1 has been destroyed.
    }

weak_ptr的性质可以用来解决shared_ptr的循环引用问题,如下: 

#include <iostream>
#include <memory>

class B;  // 前置声明

class A {
public:
    std::shared_ptr<B> bPtr;

    ~A() {
        std::cout << "A destructor called." << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> aWeakPtr;

    ~B() {
        std::cout << "B destructor called." << std::endl;
    }
};

int main() {
    std::shared_ptr<A> aPtr = std::make_shared<A>();
    std::shared_ptr<B> bPtr = std::make_shared<B>();

    aPtr->bPtr = bPtr;
    bPtr->aWeakPtr = aPtr;

    return 0;
}

在这个示例中,我们定义了两个类 AB,它们之间存在循环引用关系。A 类包含一个 shared_ptr<B> 成员变量,而 B 类包含一个 weak_ptr<A> 成员变量。

main() 函数中,我们创建了 A 类和 B 类的实例,并将它们互相引用。这样就形成了一个循环引用结构,如果不采取措施,这些对象将永远无法被销毁,导致内存泄漏。

但是,由于 B 类使用了 weak_ptr 来引用 A 类的对象,weak_ptr 不会增加引用计数。当 shared_ptr 指向 A 类的对象被销毁时,A 类的析构函数会被调用,并且与之关联的 B 类对象的 weak_ptr 将自动失效。

因此,在这个示例中,循环引用问题得到了解决,AB 对象都能够正常销毁,并在其析构函数中输出相应的消息。

总之,通过使用 weak_ptr,我们可以打破 shared_ptr 的循环引用,避免内存泄漏问题。weak_ptr 允许我们检测所关联的 shared_ptr 是否已经被销毁,从而解决了循环引用带来的资源管理问题。

参考:

《c++ Primer》

C++ Primer 笔记-第12章 动态内存

  • 26
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值