关于内存管理

(1)C++中的内存分配如下图:

这里写图片描述

栈用于函数参数以及局部变量值,编译器分配,每个函数有自己的一个堆栈帧,提供独立的内存空间;

堆中放置的是自己申请的内存,如malloc和new申请的;

全局静态区分配的内存分为两部分,初始化以及未初始化的,分开存放;

注意:

类似:p1 = (char *)malloc(10);int *p2=new int(10);

这种分配的空间实在堆中,但是p1,p2这两个指向内存的指针是在栈中;因为这两个指针是进行声明的;

栈分配:只要栈的剩余空间大于所需的空间,就为程序提供内存,否则栈溢出异常;

堆分配:系统中有一个记录空闲内存地址的链表,收到申请之后,遍历链表寻找第一个大于该链表的块;内存块的首地址会记录本次分配的大小,便于delete进行正确释放;分配多余之后的那一块会放回空闲链表中。容易产生碎片。

(2)指针也是堆区,记得初始化,未初始化的指针会指向内存中的随意位置(很危险),如果不想初始化,可以令其为空指针nullptr;

使用完指针之后,要用delete释放;释放之前先设置为nullptr,防止指针再次使用时出错;

(3)动态分配的数组:new[]给数组分配的内存,要用delete[]删除(申请/删除多个内存地址);

(4)智能指针:超出作用域的时候,会自动释放内存;

在C++11之前,有一种智能指针的简单实现:auto_ptr,只不过该指针在STL中使用的时候,会出现问题,所以,被

unique_ptr,shared_ptr,weak_ptr所代替。

(5)new会返回一个指向内存的指针,如果忽略返回值,这块内存也会孤立,内存泄漏;

在delete之前某段代码,程序运行异常退出(或者循环退出。。。),也会因为未调用delete而导致内存泄漏

new和malloc比较:

malloc和free是库函数(编译器不可控),new和delete是运算符;new不仅分配内存,还构建对象;delete会调用对象的析构函数,所以在类里面应用new和delete比较多。

(6)堆栈中声明数组:int array[5];

  堆中声明数组:int* arrayptr=new int[5];  可以动态指定大小,5替换成一个可变的参数

(7)堆栈中多维数组分配:

char board[3][3];

分配结果:在堆栈中分配一堆连续的内存,分别为[0][0],[0][1],[0][2],[1][0],,,,等;

堆中分配多维数组:

(char** board=new char[i][j];这种方式会分配错误!因为多维数组内存布局不连续)

正确的分配方式:先为多维数组的第一个下标分配连续的数组,每个元素是指向另一个数组的指针:

char** myarray = new char*[i];

for(m;m<i;m++) myarray[m]=new char[j];

释放时:先delete[]每一个myarray[i];再delete[] myarray;

注:可以使用vector<vector<char>>的形式!!

(8)将一个指针减去另一个同类型的指针,会得到两个指针指向的元素之间的元素个数;

(9)函数指针:指向函数的指针,存的是函数的地址。类型取决于返回类型;

方法的指针:指向类成员的某一个方法地址,必须要使用&取地址符。

(10)指针和数组:

指针存放的是一个地址;数组是一个连续内存,里面存放相同类型的数据,很多用到数组名的地方,会将其转换成指向首元素的指针;

智能指针:

避免内存泄漏建议采用的技术,原理:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。由于是一个模板,所以在声明的时候需要指定数据类型。

主要使用unique_ptr和shared_ptr  #include<memory>

(1)unique_ptr

声明:unique_ptr<simple> simple_ptr(new simple());   其中simple 为一个类或者是一个数据类型

没有make_unique函数,只能使用new返回指针这种绑定的形式

unique不支持拷贝或者是赋值的操作,但是可以调用release方法将一个unique指针指向对象的所有权转移给另一个unique指针:

unique_ptr<string> p1(new string("hello"));

unique_ptr<string> p2(p1);//错误,不支持拷贝

unique_ptr<string> p2(p1.release());//转移之后p1为空

unique_ptr<string> p3(new string("world"));

p3.reset(p2.release());//p2转移给p3



(2)shared_ptr

特点:引入了计数器(计数器是共享的),可以有多个shared_ptr指向同一块内存,只有在最后一个实例离开作用域的时候,才释放这片内存,故而,它是多线程安全的!!

每一个shared_ptr都知道有多少个相同的shared_ptr指向同一个对象,并进行更新。

两种声明方式:

①使用裸指针的形式:

shared_ptr<string> myPtr(new string("hello"));//会有两次内存分配

其实等价于:

string* p1 = new string("hello");

shared_ptr<string> myPtr(p1);//千万别myPtr = p1,会报错

但是:像上面这样分配不好,一个原因是会引起两次内存分配,第二个原因是如果p1还给另一个shared_ptr赋值,可能会引起两次内存释放。所以一般采用下面的方式:

②使用make_shared函数(c++14才有)make_shared和new一样都是进行创建

shared_ptr<string> myPtr = make_shared<string>("hello");

shared_ptr<string> myPtr2 = make_shared<string>(10,'a');//跟上对象创建的参数即可

也可以auto myPtr = make_shared<string>("hello");


shared_ptr的拷贝和赋值

每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

  一个例子如下:

int  main()
{
     shared_ptr<Test> ptest( new  Test( "123" ));
     shared_ptr<Test> ptest2( new  Test( "456" ));
     cout<<ptest2->getStr()<<endl;
     cout<<ptest2.use_count()<<endl;
     ptest = ptest2; //"456"引用次数加1,“123”销毁
     ptest->print();
     cout<<ptest2.use_count()<<endl; //2
     cout<<ptest.use_count()<<endl; //2
     ptest.reset();
     ptest2.reset(); //此时“456”销毁
     cout<< "done !\n" ;
     return  0;
}

image

可以看出:在进行拷贝的时候,会复制拷贝对象的引用计数(先加一);

在进行赋值的时候,右边的计数加一,左边的减一(若为0,则销毁),然后再将右边对象的计数给左边


智能指针的循环引用问题?

template<typename T>
struct ListNode{
    T _value;
    std::shared_ptr<ListNode> _prev;
    std::shared_ptr<ListNode> _next;
 
    ListNode(const T & value)
        :_value(value)
        ,_prev(NULL)
        ,_next(NULL){}
 
    ~ListNode(){
        std::cout<<"~ListNode()"<<std::endl;
    }
};
 
void TestWeekPtr(){
    std::shared_ptr<ListNode<int>> sp1(new ListNode<int>(10));
    std::shared_ptr<ListNode<int>> sp2(new ListNode<int>(20));
    sp1->_next = sp2;
    sp2->_prev = sp1;
    //构成死锁,出了函数作用域,也没有调用析构函数
    std::cout<<sp1.use_count()<<std::endl;  //sp1的引用计数
    std::cout<<sp2.use_count()<<std::endl;  //sp2的引用计数
}
循环引用结果是:两个指针指向的对象都等待对方先释放,最后谁也没有释放,造成了内存泄漏


解决方式:使用weak_ptr,weak_ptr对象引用资源时不会增加引用计数,但是它能够通过lock()方法来判断它所管理的资源是否被释放,从而避免访问非法内存。

weak_ptr必须从一个已经存在的shared_ptr(或者weak_ptr)转换而来。


  (不能用shared_ptr管理旧版C语言的数组,但是unique_ptr可以)

shared_ptr<int> myPtr(new int[10]);//这种会出错,在回收的时候,只回收myPtr[0],其他的会内存泄漏

解决方法是进行封装,自己手动删除。

但是,unique可以管理。

unique_ptr<int[]> myPtr(new int[10]);


可以参考:使用智能指针时常见的错误

内存常见的陷阱:

1.字符串分配不足,尾部要有‘\0’哨兵字符;(C风格的char s[]出现)

解决方法:(1)使用C++风格字符串string

  (2)将内存分配在堆上

2.访问内存越界

写入数组尾部的内存产生缓冲区溢出错误

解决方法:使用string和vector(自动管理内存)

3.内存泄漏————就是分配了没有正常释放(可能忘记了,或者是释放函数未正常执行)

解决方法:使用智能指针

4.双重删除和无效指针

双重删除:第二次在同一个指针上执行delete操作,可能会释放重新分配给另一对象的内存

解决办法:将指针设置为nullptr

野指针--未初始化,或者释放之后未制空,引起程序崩溃


关于allocator类(#include <memory>)

将内存分配和对象创建分离开来,使用者按需创建对象,用法如下:

allocator<string> alloc;

auto const p =alloc.allocate(n);//分配n个未构造的string

alloc.construct(q,args);//执行构造,q是首地址,args为构造函数


关于内存中的边界对齐

class类默认按照最大的那个基本类型大小进行对齐。

class c1

{

char c;

int a;

char d;

}

大小为12

class c2

{

int a;

char c;

char d;

}

大小为8

内存边界对齐的好处是可以增加访问速度;

例如一个0~3的int只需要取一次,一个1~4的int可能要取两次

详见:c/c++内存对齐的原则及作用

对于一个结构体包含另外一个结构体的情况:-----取内部结构体中最长

如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.


关于类的大小还有两点需要注意:

1.空类的大小为一个字节(大小为0,那么就是啥也没有了)

2.如果有虚函数,那么还隐含包含了一个指向虚函数表的指针,不管有多少个虚函数,只需要一个指针即可;并且这个指针应当放在对象实例的最前面的位置。


RAII-resource acquisition is initialization 

提供了一种资源自动管理的方式,当产生异常、回滚等现象时,RAII可以正确地释放掉资源。

原理:利用stack上的临时对象生命期是程序自动管理的这一特点,将我们的资源释放操作封装在一个临时对象中。

class Resource{};  
class RAII{  
public:  
    RAII(Resource* aResource):r_(aResource){} //获取资源  
    ~RAII() {delete r_;} //释放资源  
    Resource* get()    {return r_ ;} //访问资源  
private:  
    Resource* r_;  
};  

其实,可以用智能指针~~~

注意:对RAII对象执行复制的时候,一定要复制管理的资源。

  一般执行复制会出问题,常见的解决方法是,采用抑制复制或者引用计数的方式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值