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

发布时间:2023年12月22日

前言

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

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

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

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

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

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

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

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

shared_ptr

简介

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

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

初始化

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

make_shared函数

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

头文件:memory

原型和使用方法

make_shared<T>(args)

  • T:动态对象的类
  • args:动态对象的初始化值
	shared_ptr<string> q = make_shared<string>(10, '.');
	auto p = make_shared<int>(42);
	cout << *q << endl;
	cout << *p << endl;

常用操作

获取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 操作,这将导致二次释放内存,同样会引发未定义行为。

获取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;

?

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);

拷贝与赋值

	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仍然指向原来那块(已经被释放的)内存地址
// 在实际系统中,查找指向相同内存的所有指针是异常困难的!

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为一个悬空指针

shared_ptr使用案例

#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;
}

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返回的值是一个右值,支持拷贝(移动)
}

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 是否已经被销毁,从而解决了循环引用带来的资源管理问题。

文章来源:https://blog.csdn.net/qq_58158950/article/details/135143065
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。