AndreaDO

导航

C++内存管理

关于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

实际应用场景

void check(std::weak_ptr<int> &wp)
{
    std::shared_ptr<int> sp = wp.lock(); // 转换为shared_ptr<int>
    if (sp != nullptr)
    {
        std::cout << "still: " << *sp << std::endl;
    } 
    else
    {
        std::cout << "still: " << "pointer is invalid" << std::endl;
    }
}


void mytest()
{
    std::shared_ptr<int> sp1(new int(22));
    std::shared_ptr<int> sp2 = sp1;
    std::weak_ptr<int> wp = sp1; // 指向shared_ptr<int>所指对象

    std::cout << "count: " << wp.use_count() << std::endl; // count: 2
    std::cout << *sp1 << std::endl; // 22
    std::cout << *sp2 << std::endl; // 22
    check(wp); // still: 22
    
    sp1.reset();
    std::cout << "count: " << wp.use_count() << std::endl; // count: 1
    std::cout << *sp2 << std::endl; // 22
    check(wp); // still: 22

    sp2.reset();
    std::cout << "count: " << wp.use_count() << std::endl; // count: 0
    check(wp); // still: pointer is invalid

    return;
}

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

posted on 2024-02-26 21:14  AndreaDO  阅读(142)  评论(0)    收藏  举报