Linux C/C++ 内存分配与释放 [摘抄整理]

 no malloc no free
no new no delete


写了一个简单类,运行的时候报了个错 ,下决心好好看下内存相关知识

class ConstChar{
	public:
		ConstChar(const char *data, int size){
			m_data = new char[size];
			m_data = data;// 在这里 m_data 已经指向新的内存块,所以在析构函数被调用的时候,就会报错了,也不符合模块化的编码规范
		}
		~ConstChar(){
			printf("%s desctruted !\n",__func__);
			delete m_data;
		}
	private:
		const char* m_data;
};

正确的应该是:

class ConstChar{
	public:
		ConstChar(const char *data, int size){
			char *tchar = new char[size];
                        strncpy(tchar,data,size);
                        m_data = tchar;
		}
~ConstChar(){printf("%s desctruted !\n",__func__);delete m_data;}private:const char* m_data;};


因为这个构造函数主要功能是用一个 const 字符串去构造一个新的 const 字符串,所以可以不用 new /delete :

class ConstChar{
	public:
		ConstChar(const char *data, int size){
			m_data = data;
		}
		~ConstChar(){
			printf("%s desctruted !\n",__func__);
		}
	private:
		const char* m_data;
};

关于 char 和 string :

准确的来说,char是数据类型,而string是类,不严格算基础数据类型。当string的生命周期结束时,会自动调用string类的析构函数,释放内存,不用手动释放。

用char时需要申请内存,使用完了要释放内存,使用string是否需要申请和释放?

使用string时不需要用到 new/delete, 而 string 自己的底层实现需要,否则就失去方便性了。


string a  = "ok";
string * b = new string("ok two");
delete b;
string * c = new string[5];
delete [] c;

转自: 小鸡啄米  点击打开链接


一、对内的分配

  32位操作系统支持4GB内存的连续访问,但通常把内存分为两个2GB的空间,每个进程在运行时最大可以使用2GB的私有内存(0x00000000—0x7FFFFFFF)。即理论上支持如下的大数组:


C++代码

    1. char szBuffer[2*1024*1024*1024]; 

       当然,由于在实际运行时,程序还有代码段、临时变量段、动态内存申请等,实际上是不可能用到上述那么大的数组的。

  至于高端的2GB内存地址(0x80000000—0xFFFFFFFF),操作系统一般内部保留使用,即供操作系统内核代码使用。在Windows和Linux平台上,一些动态链接库(Windows的dll,Linux的so)以及ocx控件等,由于是跨进程服务的,因此一般也在高2GB内存空间运行。

  可以看到,每个进程都能看到自己的2GB内存以及系统的2GB内存,但是不同进程之间是无法彼此看到对方的。当然,操作系统在底层做了很多工作,比如磁盘上的虚拟内存交换(请看下以标题),不同的内存块动态映射等等。

  二、虚拟内存

  虚拟内存的基本思想是:用廉价但缓慢的磁盘来扩充快速却昂贵的内存。在一定时刻,程序实际需要使用的虚拟内存区段的内容就被载入物理内存中。当物理内存中的数据有一段时间未被使用,它们就可能被转移到硬盘中,节省下来的物理内存空间用于载入需要使用的其他数据。

  在进程执行过程中,操作系统负责具体细节,使每个进程都以为自己拥有整个地址空间的独家访问权。这个幻觉是通过“虚拟内存”实现的。所有进程共享机器的物理内存,当内存使用完时就用磁盘保存数据。在进程运行时,数据在磁盘和内存之间来回移动。内存管理硬件负责把虚拟地址翻译为物理地址,并让一个进程始终运行于系统的真正内存中,应用程序员只看到虚拟地址,并不知道自己的进程在磁盘与内存之间来回切换。

  从潜在的可能性上说,与进程有关的所有内存都将被系统所使用,如果该进程可能不会马上运行(可能它的优先级低,也可能是它处于睡眠状态),操作系统可以暂时取回所有分配给它的物理内存资源,将该进程的所有相关信息都备份到磁盘上。

  进程只能操作位于物理内存中的页面。当进程引用一个不在物理内存中的页面时,MMU就会产生一个页错误。内存对此事做出响应,并判断该引用是否有效。如果无效,内核向进程发出一个“segmentation violation(段违规)”的信号,内核从磁盘取回该页,换入内存中,一旦页面进入内存,进程便被解锁,可以重新运行--进程本身并不知道它曾经因为页面换入事件等待了一会。

  三、内存的使用

  对于程序员,我们最重要的是能理解不同进程间私有内存空间的含义。C和C++的编译器把私有内存分为3块:基栈、浮动栈和堆。如下图:

      (1)基栈:也叫静态存储区,这是编译器在编译期间就已经固定下来必须要使用的内存,如程序的代码段、静态变量、全局变量、const常量等。

      (2)浮动栈:很多书上称为“栈”,就是程序开始运行,随着函数、对象的一段执行,函数内部变量、对象的内部成员变量开始动态占用内存,浮动栈一般都有生命周期,函数结束或者对象析构,其对应的浮动栈空间的就拆除了,这部分内容总是变来变去,内存占用也不是固定,因此叫浮动栈。

    (3)堆:C和C++语言都支持动态内存申请,即程序运行期可以自由申请内存,这部分内存就是在堆空间申请的。堆位于2GB的最顶端,自上向下分配,这是避免和浮动栈混到一起,不好管理。我们用到malloc和new都是从堆空间申请的内存,new比malloc多了对象的支持,可以自动调用构造函数。另外,new创建对象,其成员变量位于堆里面。

  我们来看一个例子:

C++代码
    const int n = 100;   //基栈
    void Func(void)   
    {   
        char ch = 0;   //浮动栈
        char* pBuff = (char*)malloc(10);   //堆栈
        //…   
    }  


       这个函数如果运行,其中n由于是全局静态变量,位于基栈,ch和pBuff这两个函数内部变量,ch位于浮动栈,而pBuff指向的由malloc分配的内存区,则位于堆栈。

  在内存理解上,最著名的例子就是线程启动时的参数传递。

  函数启动一个线程,很多时候需要向线程传参数,但是线程是异步启动的,即很可能启动函数已经退出了,而线程函数都还没有正式开始运行,因此,绝不能用启动函数的内部变量给线程传参。道理很简单,函数的内部变量在浮动栈,但函数退出时,浮动栈自动拆除,内存空间已经被释放了。当线程启动时,按照给的参数指针去查询变量,实际上是在读一块无效的内存区域,程序会因此而崩溃。

  那怎么办呢?我们应该直接用malloc函数给需要传递的参数分配一块内存区域,将指针传入线程,线程收到后使用,最后线程退出时,free释放。


四、内存bug

  无规则的滥用内存和指针会导致大量的bug,程序员应该对内存的使用保持高度的敏感性和警惕性,谨慎地使用内存资源。

  使用内存时最容易出现的bug是:

  (1)坏指针值错误:在指针赋值之前就用它来引用内存,或者向库函数传送一个坏指针,第三种可能导致坏指针的原因是对指针进行释放之后再访问它的内容。可以修改free语句,在指针释放之后再将它置为空值。

    free(p);    
    p = NULL;  

这样,如果在指针释放之后继续使用该指针,至少程序能在终止之前进行信息转储。

  (2)改写(overwrite)错误:越过数组边界写入数据,在动态分配的内存两端之外写入数据,或改写一些堆管理数据结构(在动态分配内存之前的区域写入数据就很容易发生这种情况)

    p = malloc(256);    
    p[-1] = 0;    
    p[256] = 0;  

        (3)指针释放引起的错误:释放同一个内存块两次,或释放一块未曾使用malloc分配的内存,或释放仍在使用中的内存,或释放一个无效的指针。一个极为常见的与释放内存有关的错误就像下面这样:

    struct node *p, *tart, *temp;      
    for(p = start; p ; p = p->next)      
    {      
        free(p);      
    }   

       上面的代码会在第二次迭代时对已经释放的指针再次进行释放,这样就会导致不可预料的错误。正确的迭代方法:(没看明白)

       
     struct node *p, *tart, *temp;   
     for(p = start; p ; p = temp)   
     {   
         temp = p->next;   
         free(p);   
     } 


转自:CSDN 关于 new/delete  点击打开链接 关于


C++中指针在new和delete操作的时候对内存堆都做了些什么呢,以下解:

1、指针的new操作:

指针在new之后,会在内存堆中分配一个空间,而指针中存放的是这个空间的地址。如:

void main(){
 int *p = new int(4);
 cout << p << endl;
 cout << *p << endl;
}
输出为:
0x00431BF0
4

分别为分配的空间地址和地址内存放的值。

如果写为:

void main(){
 int *p = new int(4);
 cout << *(int *)0x00431BF0 << endl;
}
输出为:4

程序的意思就是将0x00431BF0内存地址开始的4个byte的块取出转换为int类型输出,即直接读取内存。

2、指针的delete操作:

指针在delete的时候,会将指针指向的内存区域释放掉,而指针同内存区域的联系并没有被切断,仍然会只想原来指向的内存区域。如:

void main(){
 int *p = new int(4);
 cout << p << endl;
 cout << *p << endl;
 delete p;
 cout << p << endl; // delete / free 之后地址还是原来的地址
 cout << *p << endl; // 但是内容已经改变
}

程序输出:
0x00431BF0
4
0x00431BF0
-572662307

可以看到p前后指向的地址是相同的,而指向地址的内存区域被释放。

3、空指针:

空指针指向的内存区域为内存的首地址,是不可读写的区域,即空指针不提供操作。删除空指针是安全的(因为它什么也没做)。所以,在写构造函数,
赋值操作符,或其他成员函数时,类的每个指针成员要么指向有效的内存,要么就指向空,那在你的析构函数里你就可以只用简单地delete 掉他们,而不用
担心他们是不是被new 过。如:

void main(){
 int *p = NULL;
 cout << p << endl;
} //输出0x00000000

4、取出内存区域的值

在取某内存地址开始的一个区域的值的时候,取出的值取决于用来取值的类型,譬如int为4个byte,char为1个byte,程序如:

void main(){
 int a[2] = {261,0};
 int *pi = a;
 char *p = (char*)pi;
 cout << *(int *)p++ << endl;  //取出p地址,转化为取4个byte,并取出内容,之后p向后移动一位(这里注意不是 4 位,对于 char 类型的操作)
 cout << *(int *)p << endl;    //取出p地址,转化为取4个byte,并取出内容
 cout << (int)*p << endl;      //取出1个char类型,并转换为int型
 cout << (int)*(char *)pi << endl;  //取出pi地址,转换为char类型,取出内容,并转换为int型
}
程序输出:
261
1
1
5

a的存储区域安排为:byte1=5,byte2=1,byte3~byte8 = 0;所以*(int *)p++取的为byte1到byte4;

之后的*(int *)p取的是byte2到byte5;(int)*p取的是byte2;(int)*(char *)pi取的是byte1,之后转换为int型

做一些修改:

 int a[2] = {261,1}; // {261,1}; 和 {261,0}; 结果差别很大
 bitset<32>bs(261);
 cout << "bitset of 261 =  " << bs <<endl; //(261,1) 在内存的状态 [ ....0000 0001]  [0000 0000 0000 0000 0000 0001 0000 0101]
 int *pi = a;
 char *pt = (char*)pi;

 cout << "(int*)pt=" << (int*)pt << " pt=" << pt << " *(int*)pt++="<< *(int*)pt++ <<endl; 

     //取出pt地址,转化为取4个byte,并取出内容,之后pt向后移动一位  261

 cout << "(int*)pt=" << (int*)pt << " pt=" << pt << " *(int*)pt="<< *(int*)pt <<endl;     

     //取出pt地址,转化为取4个byte,并取出内容  16777217

 cout << (int)*pt << endl;          

     //取出1个char类型(8bit),并转换为int型(32bit) -- 1

 cout << (int)*(char *)pi << endl; 

     //取出pi地址(32bit),转换为char*类型(8bit),取出内容(5),并转换为int型 --- 5

bitset of 261 =  00000000000000000000000100000101
(int*)pt=0x28fecd pt= *(int*)pt++=261
(int*)pt=0x28fecd pt= *(int*)pt=16777217
1
5



转自: 博客园  点击打开链接

关于指针和数组:

int arr[100];

int *p = new int[100];

数组也可以不用[],而通过+号来得到指定元素: //当然,对于数组,更常用的还是 [] 操作符。
cout << *(arr + 0) << endl;  //*(arr+0) 等于 *arr
cout << *(arr + 1) << endl;
cout << *(arr + 1) << endl;


//其实,对于指针,这样的+及-操作用得还要多点。
cout << *(p + 0) << endl; //*(p + 0) 等于 *p
cout << *(p + 1) << endl;
cout << *(p + 1) << endl;
 

当指针变量 P 通过 new [] 指向一连续的内存空间:

1、p[N] 得到第N个元素 (0 <= N < 元素个数);

2、*(p + N) 同样得到第N个元素 (0 <= N < 元素个数) 如 p[0] 或 *(p + 0) 得到内存空间第0个元素;  

把上面右边代码中的大部分 p 替换为 arr,则和左边代码变得一模一样。


下面再来比较二者的不同:

//定义并且初始化:                                                                       int arr[20] = {0,1,2,3,4,5,6,7,8,9,0,……,19};

//定义、并且生成空间,但不能直接初始空间的内容:        int* p = new int[20] {0,1,2,3,4 ……} // 错!  只得通过循环一个个设置


//不能通过对数组本身 + 或 - 来改变数组的位置:

arr = arr + 1;  // 错!
cout << *arr << endl;
 
arr++;  // 错!
cout << *arr << endl;
 
arr--;  // 错!
cout << *arr << endl;
 
输出结果:
无,因为程序有语法错误,通不过编译。


//可以通过 + 或 - 操作直接改变指针: p = p + 1;
cout << *p << endl;
 
p++;
cout << *p << endl;
 
p--;
cout << *p << endl;


释放空间:

//数组 : 所带的空间由系统自动分配及回收,无须也无法由程序来直接释放。
//指向 : 连续空间的指针,必须使用delete [] 来释放   delete [] p;


接下来的问题也很重要:


1.指针在某些方面的表现似乎有些像“花心大萝卜”。请看下面代码,演示令人心酸的一幕。
 
/* 初始化 p  ----- p 的新婚 通过 new ,将一段新建的内存“嫁给”指针p这一段分配的内存,就是p的原配夫妻*/
int* p = new int[100];  
 
 
/*  使用 p  ----- 恩爱相处
   N 多年恩爱相处,此处略去不表*/
……
 
/* p 改变指向 ---- 分手*/
 
int girl [100];   //第三者出现
p = girl;         //p 就这样指向 girl
 
 
/* delete [] p ----  落幕前的灾难  终于有一天,p老了,上帝选择在这一时刻惩罚他*/
 
delete [] p;
 
扣除注释,上面只有4行代码。这4行代码完全符合程序世界的宪法:语法。也就是说对它们进行编译,编译器会认为它们毫无错误,轻松放行。
 
但在灾难在 delete [] p 时发生。我们原意是要释放 p 最初通过 new int[100]而得到的内存空间,但事实上,p那时已经指向girl[100]了。结果,

第一、最初的空间并没有被释放。

第二、girl[100] 本由系统自行释放,现在我们却要强行释放它。


2.一个指针被删除时,应指向最初的地址 ,当一个指针通过 +,- 等操作而改变了指向;那么在释放之前,应确保其回到原来的指向。
 
比如:
 
int* p = new int[3];
 
*p = 1;
cout << *p << endl;
 
p++;    //p的指向改变了,指向了下一元素
*p = 2;
cout << *p << endl;
 
//错误的释放:
delete [] p;
 
在 delete [] p 时,p指向的是第二个元素,结果该释放将产生错位:第一个元素没有被释放,而在最后多删除了一个元素。相当你盖房时盖的是前3间,可以在拆房时,漏了头一间,从第二间开始拆起,结果把不是你盖的第4房间倒给一并拆了。
 
如何消除这一严重错误呢?
第一种方法是把指针正确地"倒"回原始位置:
 
p--;
delete [] p;
 
但当我们的指针指向变化很多次时,在释放前要保证一步不错地一一退回,会比较困难。所以另一方法是在最初时“备份”一份。在释放时,直接释放该指针即可。
 
int* p = new int[3];
int* pbak = *p;    //备份
 
//移动 p
……
 
//释放:
delete [] pbak;  //( 这个在代码实际编写中经常会碰到)
 
由于pbak正是指向p最初分配后的地址,我们删除pbak,就是删除p最初的指向。此时我们不能再删除一次p。这也就引出new / delete 及 new[] / delete[] 在本章的最后一个问题。


3.已释放的空间,不可重复释放
第一种情况,错了最直接:
 
int* p = new int(71);
cout << *p << endl;
 
delete p; //OK!
delete p; //ERROR! 重复删除p
 
当然,如果同一指针在delete之后,又通过new 或 new[] 分配了一次内存,则需要再删除一次:
 
int* p = new int(71);
cout << *p << endl;
 
delete p; //OK!
...
p = new int(81);
delete p; //OK!
...
 
p = new int[10];
for (int i=0; i<10; i++)
  *p = i;
 
...
delete [] p; //OK!
 
上面代码中,共计三次对p进行delete 或 delete[],但不属于重复删除。因为每次delete都对应一次新的new。
我们下面所说的例子,均指一次delete之后,没有再次new,而重复进行delete。
 
第二种情况,重复删除同一指向的多个指针
 
int* p1 = new int(71);
int* p2 = p1;   //p2和p1 现在指向同一内存地址
 
cout << *p1 << endl;
cout << *p2 << endl;
 
delete p1;  //OK
delete p2;  //ERROR! p2所指的内存,已通过delete p1而被释放,不可再delete一次。
 
同样的问题,如果你先删除了p2,则同样不可再删除p1。
 
...
delete p2; //OK
delete p1; //ERROR


第三种情况,删除指向某一普通变量的指针
 
int a = 100;
int* p = &a;
delete p;  //ERROR
 
p 不是通过new 得到新的内存空间,而是直接指向固定变量:a。所以删除p等同要强行剥夺a的固有空间,会导致出错。







  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值