1. 内存管理
在C++中,内存分成5个区,他们分别是:
- 堆
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。 - 栈
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。 - 自由存储区
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。 - 全局/静态存储区
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C++堆栈中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。 - 常量存储区
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)
1.1 动态内存的使用
c++定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。
new:在动态内存中为对象分配空间,并返回一个指向该对象的指针,我们可以选择对对象进行初始化;
delete: 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
经常会有人问,malloc/free和new/delete的区别和联系:
- malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存;
- 相同点:都是在堆上分配空间,都需要手动释放;
- 不同点:malloc/free是函数,new/delete是运算符,malloc返回的是void *指针,并且不会调用构造函数初始化对象,free也不会调用析构函数销毁对象释放空间,new/delete都会;
1.2 new和delete带来的问题
带来的问题:
- 通过new和delete运算符,有时我们会忘记释放内存;
- 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这中错误;
- 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。
为了更安全的使用和管理动态内存,新的标准库提供了三种智能指针。智能指针行为类似常规指针,主要区别是它负责自动释放所指向的对象。三种指针都在头文件memory中。
坚持使用智能指针,对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。
shared_ptr:允许多个指针指向同一个对象;
unique_ptr:独占所指向的对象
weak_ptr:一种弱引用,指向shared_ptr所管理的对象。
2.智能指针
智能指针一个很重要的概念是“所有权”,所有权意味着当这个智能指针被销毁的时候,它指向的内存(或其它资源)也要一并销毁。这技术可以利用智能指针的生命周期,来自动地处理程序员自己分配的内存,避免显示地调用delete,是自动资源管理的一种重要实现方式。
2.1 shared_ptr类
shared_ptr<int> p1;
shared_ptr<list<int>> p2;
2.1.2 make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数载动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
2.1.2 使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
由内置指针(而不是智能指针)管理的动态内存在被显示释放前一直都会存在。
2.1.3 shared_ptr和new结合使用
如前所述,如果我们不初始化一个智能指针,它会被初始化为一个空指针。
shared_ptr<double> p1;
shared_ptr<int> p2(new int(12));
接受指针参数的智能指针构造函数式explicit的。因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
shared_ptr<int> p1 = new int(100); //错误
shared_ptr<int> p2(new int(100)); //正确
2.1.4 不要混合使用普通指针和智能指针
shared_ptr可以负责对象的析构,但这仅限于其自身的拷贝之间。这也是为什么我们推荐使用make_shared而不是new的原因。这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。
切记:
当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
使用一个内置指针来访问一个智能指针所负责的对象时很危险的,因为我们无法知道对象何时会被销毁。
2.2 unique_ptr
一个unique_ptr“拥有”它所指向的对象。
unique_ptr没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式。
unique_ptr<double> p1;
unique_ptr<int> p2(new int(12));
由于unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。
2.2.1 传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr,还可以返回一个局部对象的拷贝。
//从函数返回一个unique_ptr
unique_ptr func1(int a)
{
return unique_ptr<int> (new int(a));
}
//返回一个局部对象的拷贝
unique_ptr func2(int a)
{
unique_ptr<int> up(new int(a));
return up;
}
2.3 weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向有一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。
weak_ptr用来表达临时所有权的概念,当某个对象只有存在时才需要被访问,像是资源的观察者。
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::weak_ptr<int> wp(sh_ptr);
2.3.1 为什么要引入weak_ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针。
shared_ptr是一种强引用,引用和原对象是一个强联系。你的引用不解开,原对象就不能销毁。滥用强联系,这在一个运行时间长、规模比较大,或者是资源较为紧缺的系统中,极易造成隐性的内存泄漏,这会成为一个灾难性的问题。
引入weak_ptr有两个目的,一个是为了解决shared_ptr的循环引用的问题,一个是为了解决共享对象线程共享的问题。
2.3.1.1 循环引用的问题
循环引用的demo:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
shared_ptr<B> pa_;
};
class B {
public:
shared_ptr<A> pb_;
};
int main() {
shared_ptr<A> a(new A());
shared_ptr<B> b(new B());
a->pa_ = b;
b->pb_ = a;
cout << "A: " << a.use_count() << endl;
cout << "B: " << b.use_count() << endl;
}
打印结果发现是2,并没有调用析构函数,循环引用会造成内存泄漏。
解决方法:把二者任选其一修改成weak_ptr即可。
解决的demo具体如下:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
shared_ptr<B> pa_;
};
class B {
public:
weak_ptr<A> pb_;
};
int main() {
shared_ptr<A> a(new A());
shared_ptr<B> b(new B());
a->pa_ = b;
b->pb_ = a;
cout << "A: " << a.use_count() << endl;
cout << "B: " << b.use_count() << endl;
}
因为只要有一个是weak_ptr,不会递增shared_ptr指针a的引用计数,当main函数运行结束,递减引用计数发现是0,销毁shared_ptr指针a对象,就会调用析构函数释放对应的对象。a对象销毁后,智能指针b的引用计数减1,main函数结束,b的引用计数再减1变成0,调用析构函数释放对应的对象。
2.3.1.2 共享对象的线程安全问题——弱回调
有时候我们需要“如果对象还活着,就调用它的成员函数,否则忽略之”的语意,称之为弱回调。
例如:线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。
#include <iostream>
#include <thread>
using namespace std;
class Test {
public:
void Print() { cout << "Test" << endl; }
};
void Show(weak_ptr<Test> t) {
this_thread::sleep_for(std::chrono::seconds(2));
auto sp = t.lock();
if (sp) {
sp->Print();
}
}
int main() {
shared_ptr<Test> t(new Test());
thread t1(Show, t);
t1.join();
return 0;
}
3. Reference
[1] https://blog.csdn.net/qq_38410730/article/details/105903979