C++笔记(五)---动态内存管理与智能指针

一个可执行程序文件在计算机硬件上运行起来,其实质就是静态的文件被加载到内存中的过程,可执行程序文件只是一个程序的载体。一个程序被加载到内存中,这块内存首先就存在两种属性:静态分配内存和动态分配内存。 
静态分配内存:是在程序编译和链接时就确定好的内存。 包括堆区栈区。
动态分配内存:是在程序加载、调入、执行的时候分配/回收的内存。包括bss段(就是存放未初始化的全局变量与未初始化的静态变量),数据段gvar( 数据段存放已初始化的全局变量和静态变量),代码段(存放程序执行代码和字符串常量)。

int a = 0;   // 数据段
char *p1; // BSS段
main(){
int b; // 栈
char s[] = "abc";//  栈
char *p2;//  栈
char *p3 = "123456"; // 123456\0在常量区,p3在栈上。
static int c =0; // BSS段
Class c1 = new Class();//new出的对象就在堆区
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

一、堆和栈的不同

    1、管理方式不同;
栈是由编译器自动管理,无需手工控制;堆的释放工作由程序员控制,容易产生memory leak。
    2、空间大小不同;
一般在32位系统下,堆内存可以达到4G的,从这个角度来看堆内存几乎没有什么限制。但是对于栈来讲,一般都是有一定的空间大小的

    3、碎片问题不同;
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他后进的栈内容已经被弹出
    4、生长方向不同;
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
    5、分配方式不同;
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆不同,动态分配由编译器进行释放,无需手工实现。

    6、分配效率不同;
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,机制很复杂。

二、malloc/free

从操作系统角度看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap (不考虑共享内存)

1)brk 是将数据段(.data)的最高地址指针 _edata 往高地址推

2)mmap 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

malloc 小于 128K 的内存,使用 brk 分配,malloc函数会调用brk系统调用,将_edata指针往高地址推,等到进程第一次读写A这块内存的时候发生缺页中断,这时内核才分配A这块内存对应的物理页。就是说如果用malloc分配了A这块内容,从来不访问它,那么A对应的物理页是不会被分配的。
malloc 大于 128K 的内存,使用 mmap 分配(munmap 释放)。默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。这样做主要是因为:brk分配的内存需要等到高地址内存释放以后才能释放(因为只有一个_edata 指针,这就是内存碎片产生的原因),而mmap分配的内存可以单独释放。
malloc是从堆里面申请内存,函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

默认情况下,当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。

三、newdelete

new所做的工作:调用operator new分配足够的空间,并调用相关对象的构造函数
默认情况下编译器会将new这个关键字翻译成这个operator new和相应的构造函数。
placement new 是对operator new 的一个标准、全局的版本的重载,它自身不能被重载
(不像普通版本的operator new和operator delete能够被替换)。
placement new允许你在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。原型中void* p实际上就是指向一个已经分配好的内存缓冲区的的首地址。

placement new的好处:
1)在已分配好的内存上进行对象的构建,构建速度快。
2)已分配好的内存可以反复利用,有效的避免内存碎片问题。

delete所做的工作:对指针指向的对象运行析构函数 调用operator delete函数释放该对象所用的内存
注意点:delete以后指示释放内存不会主动把指针置为NULL 指针会变成野指针 故需要自己主动把指针置为NULL。

#include <iostream>  
using namespace std;  
int main()  
{    
   try
  {
     int *p=new int;  
   }
   catch (const bad_alloc &e)
   {
       return -1;
    }   
    //判断了操作成功之后才能进行一系列的操作  
    //...  
    //用完指针p之后要将其删掉。这样可以杜绝野指针的存在  
    delete p;  
    //删除指针p之后要加上下面这句话,免得成为野指针  
    p=NULL;  
}  

new失败以后该怎么处理
标准处理方式1:

C++ 里,如果 new 分配内存失败,默认抛出bad_alloc异常。所以如果分配成功,p == 0 就不会成立;想检查 new 是否成功,应该捕捉异常:

        try {
            int* p = new int[SIZE];
            // 其它代码
        } catch ( const bad_alloc& e ) {
            return -1;
        }
标准处理方式2:

标准 C++ 还提供了一个方法来抑制 new 抛出异常,而返回空指针:

        int* p = new (std::nothrow) int; // 这样如果 new 失败了,就不会抛出异常,而是返回空指针
        if ( p == 0 ) 
            return -1;
        // 其它代码
有时候如果用上面这种判断NULL的形式就会出现就算分配失败了,也不会执行 if ( p == 0 ),因为分配失败时,new 就会抛出异常跳过后面的代码。

两种分配方式比较比较

1.都必须配对使用 分配了内存 必须释放内存
2.都是管理内存的工具
3.new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;

4.使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

5.new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

6.new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

7.new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法要求其做自定义类型对象构造和析构工作。
C++有new/delete为什么还要支持malloc/delete?
因为C++程序会用到很多C程序库,而C语言用的就是malloc/free

malloc、realloc、calloc的区别
1)malloc函数
void* malloc(unsigned int num_size);
int *p = malloc(20*sizeof(int));申请20个int类型的空间;
2)calloc函数
void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));
省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;
3)realloc函数
void realloc(void *p, size_t new_size);
给动态分配的空间分配额外的空间,用于扩充容量。

 四、内存泄漏

申请了一块内存空间使用完毕后没有释放。程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存没有任何一个指针指向它,那么这块内存就泄露了。内存泄漏是指程序未能释放不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制;

使用工具软件BoundsChecker,BoundsChecker是一个运行时错误检测工具,可排除内存泄漏问题。或者使用智能指针。

五、智能指针

为了更加安全地使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。智能指针本质上是一个模板类,采用RAII的思想管理动态分配的内存,防止内存泄露。

shared_ptr

这也是目前工程内使用最多最广泛的智能指针,他使用引用计数实现对同一块内存可以有多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和unique_ptr最大的区别。

最安全的分配和使用动态内存的方法是调用一个名为make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr. 与智能指针一样,make_shared 也定义在头文件memory中。
当要用make_shared 时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号, 在其中给出类型:
shared_ptr<int> p3=make_shared<int> (42) ;//指向一个值为42的int的shared_ptr
shared_ptr<string> p4 = make_shared<string>(10, '9' );//p4指向一个值为999999999"的string
shared_ptr<int> p5 = make_shared<int>() ;// p5 指向一个值初始化的int,值为0

class B;
class A{
public:
    shared_ptr<B> a=make_shared<B>(); 
};
class B{
public:
    shared_ptr<A> b=make_shared<A>(); 
};
int main({
    shared_ptrsint>sp1(new int(10));//shard ptr 构造方式1
    shared_ptr<int>sp2=make_shared<int>(10);//构造方式2
    shared_ptr<int>sp3(sp2);// 构造方式3
    shared_ptr<int> sp4();/构造方式4 这样取值的时候会默认设定为1
    shared_ptr<int> sp5=make_shared<int>();//构造方式5 这样取值的时候会默认设定为0
}

make_shared初始化的优点

1、提高性能

std::make_shared申请一个单独的内存块来同时存放对象和控制块。这个优化减少了程序的静态大小,并且这会加快代码的执行速度,因为内存只分配了一次。另外使用std::make_shared消除了一些控制块需要记录的信息,这样潜在地减少了程序的总内存占用。

2、 异常安全

函数调用时在运行期产生了一个异常,则在第一步动态分配的内存就会泄露了,因为它永远不会被存放到在第三步才开始管理它的std::shared_ptr中。使用std::make_shared可以避免这样的问题,在运行期,不管程序哪一步先被调用。如果std::make_shared先被调用,则指向动态分配出来的内存的原始指针能安全地被存放到被返回的std::shared_ptr中。另外使用std::make_unique代替new就和“使用std::make_shared来写出异常安全的代码”一样重要。

缺点是构造函数是保护或私有时,无法使用 make_shared,对象的内存可能无法及时回收。只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用,、弱引用都减为 0 时才能释放, 延迟了内存释放的时间。

#include<iostream>
using namespace std;
template<typename T>
class MySharedPtr{
public:
    MySharedPtr()=default;
    MySharedPtr(T* p); 
    MySharedPtr(MySharedPtr<T>&sp);
    MySharedPtr<T>&operator=(MySharedPtr<T>&sp);
    ~MySharedPtr();
    T operator*();
    int use_count();
    T* val=new T(O);
    int *count=new T(0);
};

template<typename T>
int MySharedPtr<T>::use_count(){
    return *count;
}
template<typename T>
MySharedPtr<T>::~MySharedPtr(){
    --(*count); 
    if(*count==0){ 
        delete count;
        delete val;
        val=NULL;
        count=NULL;
    }
}

template<typename T>
MySharedPtrT>::MySharedPtr(T*p)
{
    val=p; 
    ++*count;
}
templatestypename T>
MySharedPtrT>::MySharedPtr(MySharedPtr<T> &sp)
{
    val=sp.val;
    count=sp.count;
    ++(*(sp.count));
}
templatestypename T>
MySharedPtr<T>& MySharedPtr<T>::operator=(MySharedPtr<T>&sp)
{
    -(*count);
    if(*count==0){
        delete count;
        delete val;
        val=NULL;
        count=NULL;
    }
    ++(*sp.count);
    count=sp.count;
    val=sp.val;
    return *this;
}
template<typename T>
T MySharedPtr<T>::operator *()
{
    return *val;
}

unique_ ptr

一个unique_ ptr拥有"它所指向的对象。与shared_ ptr不同,某个时刻只能有一个unique_ ptr 指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。由于一个unique_ptr 拥有它指向的对象,因此unique_ptr 不支持普通的拷贝或赋值操作:

unique_ptr<string> p1 (new string ("Stegosaurus"));
unique_ptr<string> p2(p1); // 错误: unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2;        //错误: unique_ptr不支持赋值

虽然不能拷贝或赋值unique_ptr, 但可以通过调用release或reset将指针的所有权从一个(非const) unique_ptr转移给另一个unique:

//将所有权从p1 (指向string stegosaurus )转移给p2
unique_ptr<string> p2(pl.release()); // release 将p1置为空.
unique_ptr<string> p3(new string ("Trex"));
//将所有权从p3转移给p2
p2.reset (p3.release()); // reset 释放了p2原来指向的内存

release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2 被初始化为p1原来保存的指针,而p1被置为空。
reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。因此,对p2调用reset 释放了用"Stegosaurus"初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。
调用release会切断unique_ptr 和它原来管理的对象间的联系。release 返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。本例中管理内存的责任简单地从一个智能指针转移给另一个。但是如果不用另一个智能指针来保存release返回的指针,程序就要负责资源的释放:
p2.release() ;        //错误: p2不会释放内存,而且丢失了指针
auto P = p2.release() ;        //正确,但必须记得delete (p)

weak_ptr

weak_ ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。

weak_ptr叫弱指针,它主要是为了配合shared_ptr使用,用来解决循环引用的问题;即A内部有指向B,B内部有指向A。B必定是在A析构后B才析构,A必定是在B析构后才析构A,这就是循环引用问题,导致内存泄露。

将会出现循环引用问题的指针用weak_ptr保存着,weak_ptr并不拥有这块空间,所以weak_ptr里面不增加shared_ptr的引用计数 也就不会掌控这这块空间的生命周期。(注意weak_ptr里面也有自己的引用计数)

当我们创建一个 weak_ptr时,要用一个shared_ptr来初始化它:
auto P = make_shared<int> (42) ;
weak_ptr<int> wp(p); // wp弱共享p; p的引用计数未改变
本例中wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数,wp 指向的对象可能被释放掉。由于对象可能不存在,不能使用weak_ptr直接访问对象,而必须调用lock。
此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock 返回一个指向共享对象的shared_ptr。 与任何其他shared_ptr 类似,只要此shared_ptr 存在,它所指向的底层对象也就会一直存在。例如:
if (shared_ptr<int> np=wp.lock()) {         // 如果np不为空则条件成立.
        //在if中,np与p共享对象
}

class A {
public:
    shared_ptr<B> b;
};
class B {
public:
    shared_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    shared_ptr<A> spa = make_shared<A>();
    shared_ptr<B> spb = make_shared<B>();
    spa->b = spb;
    spb->a = spa;
    return 0;
};

可改为如下代码。

class A {
public:
    shared_ptr<B> b;
};
class B {
public:
    weak_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    shared_ptr<A> spa = make_shared<A>();
    shared_ptr<B> spb = make_shared<B>();
    spa->b = spb; //spb强引用计数为2,弱引用计数为1
    spb->a = spa; //spa强引用计数为1,弱引用计数为2
    return 0;
} //main函数退出后,spa先释放,spb再释放,循环解开了

既然weak_ptr不负责裸指针的生命周期,那么weak_ptr也无法直接操作裸指针,需要先转化为shared_ptr:

shared_ptr<int> spa = make_shared<int>(10);
weak_ptr<int> spb = spa; //weak_ptr无法直接使用裸指针创建
if (!spb.expired()) { //weak_ptr最好判断是否过期,使用expired或use_count方法,前者更快
    *spb.lock() += 10; //调用lock()转化为shared_ptr后再操作裸指针
}
cout << *spa << endl; //20

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值