首页 > 系统相关 >C++内存管理

C++内存管理

时间:2024-02-26 21:24:57浏览次数:28  
标签:管理 int weak C++ 内存 new shared ptr

关于C++内存和分配的学习笔记

C++内存和分配很容易出问题,为了编写高质量的CPP代码,我们必须了解幕后的工作原理。

1.内存泄漏

img
例如:

void leaky()
{
  new int;//这里就是内存泄漏
  cout<<"我泄漏了一个int的内存!"<<endl;
}

自由存储区中的数据库无法被栈或者间接访问,这块内存被遗弃了(泄漏了)。

正确代码:

int *ptr{new int};//这里是C20的写法
delete ptr;
ptr=nullptr;

所以每一行用new关键字分配的内存,都必须有一行delete关键字释放内存,建议同时把指针设置成nullptr.
关于malloc()函数,这是c语言中分配内存的函数,在C++中用new代替,在C++中尽可能不要把C和C++混合写,只应该使用new和delete。

malloc和new

Foo* myfooC  {(Foo*)malloc(sizeof(Foo))};
Foo* myfooCpp {new Foo()};

当你new失败的时候,大多数会抛出一个异常,C++20中有个不抛出异常的方法,相反,它会返回一个nullptr。

int *intc{new (nothrow) int};

2.数组

普通的数组和指针数组的不同
img
例子代码

// 2数组内存的分配
void fun2()
{
  // 在栈上分配数组
  int array3[5]{1, 2, 3, 4, 5};
  int array4[5]{};             // 全0
  int array5[]{1, 2, 3, 4, 5}; // 自动推断

  // 在自由内存中分配数组
  int *arrays1{new int[]{1, 2, 3, 4, 5}}; // 分配数组内存
  int *arrays2{new (nothrow) int[5]};     // new失败返回nullptr
  int size{5};
  int *arrays3{new int[size]}; // 指定内存大小
  delete[] arrays1;            // 清理内存
  delete[] arrays2;            // 清理内存
  delete[] arrays3;            // 清理内存
}

数组可以自动使用指针表示,但不是所有指针都是数组。

指针申请的数组内存地址必须使用delete[]去释放。

realloc()函数

有一个C语言的函数realloc(),这个C语言中会改变数组大小,采取的方式是分配新大小的内存块,然后将所有旧数据复制到新位置,再删除旧内存块。然而这个在C++中十分危险,因为用户定义的类对象不一定能很好适应按位复制。为了自己的代码安全不要在C++中使用这个函数,切记。

3.类的内存管理与释放

在C++中delete也不能保证完全内存不会泄漏,比如:

try
  {
    int *ptr{new int(10)};
    throw 1;
    delete ptr;
  }
  catch (int i)
  {}

在delete之前发生了异常,就会绕开delete取消释放内存,从而发生内存泄漏问题。

而C++中类的析构函数不会因为异常而取消释放内存。
例子如下:

//编写测试类
class MyClass
{
public:
  MyClass(int v)
  {
    ptr = new int(v);
    cout << "MyClass 构造" << endl;
  }
  ~MyClass()
  {
    delete ptr;
    ptr = nullptr;
    cout << "MyClass 析构" << endl;
  }

private:
  int *ptr;
};
//编写测试函数
void fun3()
{
  MyClass m(10);
  throw 1; // 故意抛出异常
}
//调用测试函数
void fun3_()
{
  try
  {
    fun3();
  }
  catch (int i)
  {
    //C++20的format函数,格式化打印
    cout << format("错误信息:{}", i) << endl;
  }
}
int main()
{
  fun5();
}

打印在控制台的结果:

img

这里即使抛出了异常,析构函数依然调用,并且释放了内存。

这里又引出了一个新的问题,多次析构函数的调用。就是一个类被多个类重复引用,例如:

void fun5()
{
  MyClass a(10);
  MyClass b{a};
  MyClass c{b};
  MyClass d{c};
  MyClass e{d};
}

这里许多人都引用一个类,直接析构函数释放内存就会发生异常,为了避免,我们需要在所有人使用完后再释放内存,修改上面的类,添加引用计数功能。

class MyClass
{
public:
  // 初始化计数为1
  MyClass(int v) : ptr(new int(v)), m_used(new int(1))
  {
  }
  MyClass(const MyClass &other)
  {
    m_used = other.m_used;
    ptr = other.ptr;
    (*m_used)++; // 增加计数
  }
  ~MyClass()
  {
    (*m_used)--;
    cout << "MyClass 析构" << endl;
    if (*m_used < 1)
    {
      delete ptr;
      delete m_used;
      cout << "MyClass 析构引用全部结束" << endl;
    }
  }
  // 重载=运算符
  MyClass &operator=(const MyClass &other)
  {
    // 避免自我赋值
    if (this == &other) // 用&转换成指针再来比较
    {
      return *this;
    }
    m_used = other.m_used;
    ptr = other.ptr;
    (*m_used)++; // 引用计数加1
    return *this;
  }

private:
  int *ptr;    // 值
  int *m_used; // 计数
};

我们重新运行上面的测试代码,现在已经不会发生异常,结果为:

img

但上面类的引用计数在多线程中依然不是安全的,所以我们为了更方便的写法需要使用C++11的工具类“智能指针”。

4.智能指针

智能指针(Smart Pointer),它利用了一种叫做 RAII(资源获取即初始化)的技术将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。这使得智能指针实质是一个对象,行为表现的却像一个指针。

智能指针主要分为shared_ptr、unique_ptr和weak_ptr三种,使用时需要引用头文件

shared_ptr

下面是一个初始化的例子

shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<int> p2(new int(1024));
//或者 C20写法
shared_ptr<string> sp1{new string("hello")};
shared_ptr<string> sp2{new string("tom")};
(*sp1)[0] = 'c'; // 修改单个字符
sp2->replace(0, 1, "M");

可以和平常的指针一样修改指向的对象

shared_ptr的引用计数功能如下:

vector<shared_ptr<string>> arr_ptr;
arr_ptr.push_back(sp1);
arr_ptr.push_back(sp2);
arr_ptr.push_back(sp2);
arr_ptr.push_back(sp1);
// 输出一下
for (auto ptr : arr_ptr)
{
  cout << *ptr << endl;
}
cout << arr_ptr[0].use_count() << endl; // 3 输出这个变量的引用计数
sp1.reset(new string("Jack"));
cout << *sp1 << endl;
cout << arr_ptr[0].use_count() << endl; // 2 上面修改了
arr_ptr[3].reset(new string("BoP"));
cout << arr_ptr[0].use_count() << endl; // 1 上面修改了

shared_ptr还可以在构造参数后面添加lambda表达式,来自己处理析构函数。

C++的lambda表达式简单介绍,类似于写一个简易函数表达式:

auto l = [](const string &str)
{
  cout << str << endl;
};
l("hello");//hello
int x = 1;
auto l2 = [](const int &i)
{
  cout << i << endl;
};
l2(x);//1

shared_ptr的析构例子:

// 设置删除时候的打印信息,lambda
shared_ptr<int> spt1{new int(20),
                     [](int *p)
                     {
                       cout << format("p已经被删除{}", *p) << endl;
                       delete p;
                     }};
spt1 = nullptr; // 输出20
// 对于数组,必须自己写删除的表达式
shared_ptr<int> spt{new int[10],
                    [](int *p)
                    {
                      cout << format("p数组已经被删除{}", *p) << endl;
                      delete[] p;
                    }};
// 或者这样用默认参数
shared_ptr<int> spt2{new int[10],
                     default_delete<int[]>()};

上面代码的输出结果:

img

shared_ptr但还有个很严重的问题,就是互相引用,shared_ptr是靠内部的引用计数来析构的,当引用计数为0就回收内存,但如果两个shared_ptr互相引用彼此,就不会回收内存,从而导致内存泄漏。
先编写练习类用于互相引用

class Parent;
class Son;
class Parent
{
public:
Parent()
{
  cout << "Parent构造" << endl;
}
~Parent()
{

  cout << "Parent析构" << endl;
}
shared_ptr<Son> child;
};
class Son
{
public:
Son()
{
  cout << "Son构造" << endl;
}
~Son()
{

  cout << "Son析构" << endl;
}
shared_ptr<Parent> parents;
};

先简单调用这两个类

void demo1()
{
shared_ptr<Parent> p{new Parent()};
shared_ptr<Son> s{new Son()};
// 输出结果
//Parent构造
//Son构造
//Son析构
//Parent析构
}

但产生互相引用就会出bug了

void demo1()
{
  shared_ptr<Parent> p{new Parent()};
  shared_ptr<Son> s{new Son()};
  p->child = s;
  s->parents = p;
}
//输出结果
//Parent构造
//Son构造

这里就没有调用析构函数,发生了内存泄漏。
为了解决这个问题,我们可以使用weak_ptr

weak_ptr

weak_ptr是C++11引入的一个智能指针类型,它是为了配合shared_ptr来使用的,主要目的是解决shared_ptr可能导致的循环引用问题。理解weak_ptr之前,首先需要了解shared_ptr

shared_ptr

shared_ptr是一个智能指针,它用于自动管理对象的生命周期。当最后一个shared_ptr指向一个对象被销毁或重置时,它会自动删除所指向的对象。这是通过引用计数实现的,每当一个shared_ptr指向一个对象时,引用计数加1,每当一个shared_ptr被销毁或重置时,引用计数减1。

循环引用问题

然而,当两个或多个shared_ptr相互引用时,即使它们在其他地方都没有被使用,它们的引用计数也不会变为0,因此它们所指向的对象不会被删除,这就造成了内存泄漏。这就是所谓的循环引用问题。

weak_ptr

weak_ptr就是为了解决这个问题而引入的。它是一个不控制对象生命周期的智能指针,它指向一个由shared_ptr管理的对象。weak_ptr的存在不会增加对象的引用计数,也不会延长对象的生命周期。它主要用于观察一个对象,而不是拥有它。

weak_ptr的用法

  1. 创建weak_ptr

可以通过shared_ptr来创建weak_ptr

std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
  1. 从weak_ptr获取shared_ptr

可以使用weak_ptrlock成员函数来获取一个shared_ptr。如果weak_ptr所指向的对象还存在,lock会返回一个有效的shared_ptr,否则返回一个空的shared_ptr

 sp = wp.lock();
if (sp) {
    // 对象还存在,可以使用sp
} else {
    // 对象已被删除
}
  1. 使用weak_ptr

可以直接使用weak_ptr来访问它所指向的对象,但这通常是不安全的,因为如果对象已经被删除,这样做会导致未定义行为。因此,一般建议在使用weak_ptr之前先使用lock来获取一个shared_ptr

weak_ptr的优点

  1. 解决循环引用问题:通过引入weak_ptr,可以有效地解决由shared_ptr引起的循环引用问题,避免内存泄漏。
  2. 提高代码灵活性weak_ptr允许你观察一个对象,而不必拥有它。这可以使得代码更加灵活,例如在某些情况下你可能只想观察一个对象,而不希望拥有它。

weak_ptr的限制

  1. 不能用于初始化多个shared_ptrweak_ptr不能用于初始化多个shared_ptr,因为这会导致引用计数增加,与weak_ptr的设计初衷相违背。
  2. 不能直接访问对象:直接通过weak_ptr访问它所指向的对象是不安全的,因为如果对象已经被删除,这样做会导致未定义行为。因此,在使用weak_ptr之前,通常需要先使用lock来获取一个shared_ptr

实际应用场景

假设我们有一个类Parent和一个类ChildParent有一个指向Childshared_ptr成员,而Child也有一个指向Parentshared_ptr成员。这种情况下,如果不使用weak_ptr,就会导致循环引用问题,因为两个对象都会互相引用对方,即使它们在其他地方都没有被使用,它们的引用计数也不会变为0,因此它们所指向的对象不会被删除,造成内存泄漏。

通过使用weak_ptr,我们可以解决这个问题。在Child类中,我们可以使用一个weak_ptr来替代shared_ptr

class Parent;

class Child {
public:
    std::weak_ptr<Parent> parent;
    // ...
};

class Parent {
public:
    std::shared_ptr<Child> child = std::make_shared<Child>();
    // ...
};

这样,即使ParentChild相互引用,也不会导致循环引用问题,因为weak_ptr不会增加对象的引用计数。当ParentChild对象被销毁时,它们的引用计数会正确地变为0,所指向的对象也会被正确地删除。

unique_ptr

unique_ptr采用的是独占式拥有,并且不用程序员自己手动去释放,会自动回收申请的内存
简单例子

 std::unique_ptr<std::string> us(new std::string("hello"));
 //std::unique_ptr<std::string> us2(us);//编译错误,独占
//us2想获取:us2 = std::move(up1);
  (*us)[0]='J';
  us->append("OK");
  std::cout<<*us<<'\n';
  // 把us制空
  // us.reset(); // us=nullptr;
  std::string *sp = us.release(); //放弃所有权,转让给别人
  std::cout<<*sp<<'\n';

//输出
//JelloOK
//JelloOK

对于array

 std::unique_ptr<int[]> as(new int[10]);  //int array
 std::unique_ptr<std::string[]> ss(new std::string[10]); //string array
//  不能用*操作符和->,采用[]操作符
  ss[0]='O';
  std::cout<<ss[0];

不用担心内存泄漏,unique会在失去所有权的时候调用delete[]
当然你如果想在delete的时候打印信息,你可以这样写

 std::unique_ptr<int[]> as(new int[10]);  //int array
 std::unique_ptr<std::string[]> ss(new std::string[10]); //string array
//  不能用*操作符和->,采用[]操作符
  ss[0]='O';
  std::cout<<ss[0];

标签:管理,int,weak,C++,内存,new,shared,ptr
From: https://www.cnblogs.com/AndreaDO/p/17858635.html

相关文章

  • C++ 关键字
    C++关键字alignas和alignof用法alignasalignas指定了内存按照多少对齐。alignas(0)这种写法无效,编译器会无视你的这个代码structalignas(8)S{};//表示是8个字节的对齐方式structalignas(1)U{Ss;};//虽然里面有个S,但是依然指定了该结构体的内存对齐要求为1字......
  • C++库大全
    基础类1、DinkumwareC++Library参考站点:http://www.dinkumware.comP.J.Plauger编写的高品质的标准库。P.J.Plauger博士是Dr.Dobb's程序设计杰出奖的获得者。其编写的库长期被Microsoft采用,并且最近Borland也取得了其OEM的license,在其C/C++的产品中采用Dinkumware的库。2......
  • 接口管理怎么做,软件接口测试工具推荐
    1.背景作为互联网工作者,不论是前端、后端还是测试,接口管理都是一个重要的任务。通常情况下,我们需要依赖以下解决方案来完成整个接口管理过程:使用Swagger管理API文档使用Postman调试API使用RAP或其他MockAPI工具使用JMeter进行API自动化测试可以看出,每个步骤......
  • vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?
    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助一、是什么权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发页面加载触发页面上的按钮点击触发总的来说,所有......
  • 简化 Python 日志管理:Loguru 入门指南
    简化Python日志管理:Loguru入门指南在开发和维护软件项目时,高效的日志管理系统对于监控应用程序的行为、调试代码和追踪异常至关重要。Python的标准日志模块虽然功能强大,但其配置和使用往往较为复杂,尤其是对于新手开发者。这就是Loguru库发挥作用的地方,它以极简的方式重新定......
  • C++ 刷题必备
    目录语言必备语言必备在C++中刷Leetcode时,有一些常用的语言技巧和最佳实践可以帮助你更有效地解决问题。以下是一些建议:熟悉STL(StandardTemplateLibrary):使用vector,list,set,map等容器来存储和操作数据。使用algorithm库中的函数,如sort,binary_search,unique等。......
  • 意志力的心理学:了解如何提高自控力和自我管理能力
    在我们日常的生活中,经常会遇到需要自控力和自我管理能力的情境。比如说,我们需要集中注意力完成一项任务,需要克制自己不去吃零食或者控制自己的情绪不爆发等等。这些情境都需要我们调动我们的意志力来完成,而意志力是一种可以被训练和提高的心理能力。本文将介绍意志力的心理学,帮助......
  • RBAC用户角色权限管理
    RBAC用户角色权限管理:https://blog.csdn.net/qq_51386003/article/details/126469172?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170892813716800186544941%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=17089281371680018654494......
  • 分库分表如何管理不同实例中几万张分片表?
    大家好,我是小富~ShardingSphere实现分库分表,如何管理分布在不同数据库实例中的成千上万张分片表?上边的问题是之前有个小伙伴看了我的分库分表的文章,私下咨询我的,看到他的提问我第一感觉就是这老铁没用过ShardingSphere,因为这个问题在ShardingSphere中已经有了很好的解决方案,接下......
  • 能碳双控| AIRIOT智慧能碳管理解决方案
     在当前全球气候变化和可持续发展的背景下,建设能碳管理平台成为组织迎接挑战、提升可持续性的重要一环,有助于组织实现可持续发展目标,提高社会责任形象,同时适应未来碳排放管理的挑战。能碳管理是一个涉及跟踪、报告和减少组织碳排放的复杂过程,往往存在如下痛点: 数据收集的准......