关于C++内存和分配的学习笔记
C++内存和分配很容易出问题,为了编写高质量的CPP代码,我们必须了解幕后的工作原理。
1.内存泄漏
例如:
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.数组
普通的数组和指针数组的不同
例子代码
// 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();
}
打印在控制台的结果:
这里即使抛出了异常,析构函数依然调用,并且释放了内存。
这里又引出了一个新的问题,多次析构函数的调用。就是一个类被多个类重复引用,例如:
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; // 计数
};
我们重新运行上面的测试代码,现在已经不会发生异常,结果为:
但上面类的引用计数在多线程中依然不是安全的,所以我们为了更方便的写法需要使用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[]>()};
上面代码的输出结果:
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的用法
- 创建weak_ptr
可以通过shared_ptr
来创建weak_ptr
:
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
- 从weak_ptr获取shared_ptr
可以使用weak_ptr
的lock
成员函数来获取一个shared_ptr
。如果weak_ptr
所指向的对象还存在,lock
会返回一个有效的shared_ptr
,否则返回一个空的shared_ptr
。
sp = wp.lock();
if (sp) {
// 对象还存在,可以使用sp
} else {
// 对象已被删除
}
- 使用weak_ptr
可以直接使用weak_ptr
来访问它所指向的对象,但这通常是不安全的,因为如果对象已经被删除,这样做会导致未定义行为。因此,一般建议在使用weak_ptr
之前先使用lock
来获取一个shared_ptr
。
weak_ptr的优点
- 解决循环引用问题:通过引入
weak_ptr
,可以有效地解决由shared_ptr
引起的循环引用问题,避免内存泄漏。 - 提高代码灵活性:
weak_ptr
允许你观察一个对象,而不必拥有它。这可以使得代码更加灵活,例如在某些情况下你可能只想观察一个对象,而不希望拥有它。
weak_ptr的限制
- 不能用于初始化多个shared_ptr:
weak_ptr
不能用于初始化多个shared_ptr
,因为这会导致引用计数增加,与weak_ptr
的设计初衷相违背。 - 不能直接访问对象:直接通过
weak_ptr
访问它所指向的对象是不安全的,因为如果对象已经被删除,这样做会导致未定义行为。因此,在使用weak_ptr
之前,通常需要先使用lock
来获取一个shared_ptr
。
实际应用场景
假设我们有一个类Parent
和一个类Child
,Parent
有一个指向Child
的shared_ptr
成员,而Child
也有一个指向Parent
的shared_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>();
// ...
};
这样,即使Parent
和Child
相互引用,也不会导致循环引用问题,因为weak_ptr
不会增加对象的引用计数。当Parent
或Child
对象被销毁时,它们的引用计数会正确地变为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