C++内存管理和智能指针

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 使用了动态生存期的资源的类

程序使用动态内存出于以下三种原因之一:

  1. 程序不知道自己需要多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据

由内置指针(而不是智能指针)管理的动态内存在被显示释放前一直都会存在。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容

  • 前言 现在开发的项目中用到了大部分 C++ 代码,由于 Swift 和 C++ 混编不是很方便, 依然选择用 OC...
    不要人夸颜色好阅读 1,976评论 1 3
  • 1. 引用计数 引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增...
    突然的自我_39c1阅读 472评论 0 3
  • 参考资料:《C++ Primer中文版 第五版》我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存...
    陈星空阅读 1,059评论 0 0
  • 最近为了学习 C++ 的智能指针,带着以下问题阅读了 C++ primer 里的智能指针章节,并记录问题的解答。 ...
    tang_jia阅读 180评论 0 1
  • C++动态内存 了解动态内存在 C++ 中是如何工作的是成为一名合格的 C++ 程序员必不可少的。C++ 程序中的...
    Cor9阅读 308评论 0 1