注册 登录
编程论坛 C++教室

内存分配详解(对内存有疑惑的进来看看)

南国利剑 发布于 2010-06-02 21:01, 5631 次点击
注:并非原创!

    一、内存基本构成
    可编程内存在基本上分为这样的几大部分:静态存储区、堆区和栈区。他们的功能不同,对他们使用方式也就不同。
    静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。
    栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
    堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。
    二、三者之间的区别
    我们通过代码段来看看对这样的三部分内存需要怎样的操作和不同,以及应该注意怎样的地方。
    例一:静态存储区与栈区

char* p = “Hello World1”;
char a[] = “Hello World2”;
p[2] = ‘A’;
a[2] = ‘A’;
char* p1 = “Hello World1;”

 
    这个程序是有错误的,错误发生在p[2] = ‘A’这行代码处,为什么呢,是变量p和变量数组a都存在于栈区的(任何临时变量都是处于栈区的,包括在main()函数中定义的变量)。但是,数据“Hello World1”和数据“Hello World2”是存储于不同的区域的。
    因为数据“Hello World2”存在于数组中,所以,此数据存储于栈区,对它修改是没有任何问题的。因为指针变量p仅仅能够存储某个存储空间的地址,数据“Hello World1”为字符串常量,所以存储在静态存储区。虽然通过p[2]可以访问到静态存储区中的第三个数据单元,即字符‘l’所在的存储的单元。但是因为数据“Hello World1”为字符串常量,不可以改变,所以在程序运行时,会报告内存错误。并且,如果此时对p和p1输出的时候会发现p和p1里面保存的地址是完全相同的。换句话说,在数据区只保留一份相同的数据(见图1-1)。
    例二:栈区与堆区

char*  f1()
{
   char* p = NULL;
   char a;
   p = &a;
   return p;
}
char* f2()
{
   char* p = NULL:
   p =(char*)  new  char[4];
   return p;
}

    这两个函数都是将某个存储空间的地址返回,二者有何区别呢?f1()函数虽然返回的是一个存储空间,但是此空间为临时空间。也就是说,此空间只有短暂的生命周期,它的生命周期在函数f1()调用结束时,也就失去了它的生命价值,即:此空间被释放掉。所以,当调用f1()函数时,如果程序中有下面的语句:

char* p ;
p = f1();
*p = ‘a’;

    此时,编译并不会报告错误,但是在程序运行时,会发生异常错误。因为,你对不应该操作的内存(即,已经释放掉的存储空间)进行了操作。但是,相比之下,f2()函数不会有任何问题。因为,new这个命令是在堆中申请存储空间,一旦申请成功,除非你将其delete或者程序终结,这块内存将一直存在。也可以这样理解,堆内存是共享单元,能够被多个函数共同访问。如果你需要有多个数据返回却苦无办法,堆内存将是一个很好的选择。但是一定要避免下面的事情发生:

void f()
{
   …
   char * p;
   p = (char*)new char[100];
   …
}

    这个程序做了一件很无意义并且会带来很大危害的事情。因为,虽然申请了堆内存,p保存了堆内存的首地址。但是,此变量是临时变量,当函数调用结束时p变量消失。也就是说,再也没有变量存储这块堆内存的首地址,我们将永远无法再使用那块堆内存了。但是,这块堆内存却一直标识被你所使用(因为没有到程序结束,你也没有将其delete,所以这块堆内存一直被标识拥有者是当前您的程序),进而其他进程或程序无法使用。我们将这种不道德的“流氓行为”(我们不用,却也不让别人使用)称为内存泄漏。这是我们C++程序员的大忌!!请大家一定要避免这件事情的发生。
    总之,对于堆区、栈区和静态存储区它们之间最大的不同在于,栈的生命周期很短暂。但是堆区和静态存储区的生命周期相当于与程序的生命同时存在(如果您不在程序运行中间将堆内存delete的话),我们将这种变量或数据成为全局变量或数据。但是,对于堆区的内存空间使用更加灵活,因为它允许你在不需要它的时候,随时将它释放掉,而静态存储区将一直存在于程序的整个生命周期中。


42 回复
#2
南国利剑2010-06-02 21:08
私自利用职权置顶。不好意思。。。
如果对各位有帮助,请多多回帖评分。
谢谢!
#3
lijm19892010-06-02 21:55
顶一个,不过想看看版主原创资料。。
#4
南国利剑2010-06-02 22:31
回复 3楼 lijm1989
呵呵,谢谢。
#5
ciweitou1632010-06-03 15:03
整理的不错赞一个
#6
周伟12282010-06-03 22:13
恩,不错,有帮助!
#7
ltyjyufo2010-06-04 20:09
  学习了、、、、、、、、、、
#8
南国利剑2010-06-05 20:41
各位如果有什么不同看法,可以提出来讨论讨论。
大家共同进步。
#9
最近不在2010-06-06 01:15
程序代码:
// Note:Your choice is C++ IDE
#include <iostream>
using namespace std;
int main()
{
    char *p = new char[4];
    //错误拷贝情况
   
//char *pTemp = p;
   
//delete [] pTemp; 二者等效 指向同一堆内存
   
//delete [] p; 一删皆没
   
   
//正确的做法
   
//char *pTemp = new char[4];
   
//for(...)
   
//{
   
//    *(pTemp+i) =*(p+i);
   
//}
   
//delete [] p;
    return 0;
}


[ 本帖最后由 最近不在 于 2010-6-6 23:32 编辑 ]
#10
2010-06-06 12:58
太经典了!!!
#11
2010-06-06 13:04
回复 9楼 最近不在
    //错误拷贝情况
    //char *pTemp = p;
    //delete [] pTemp; 二者等效 指向同一堆内存
    //delete [] p; 一删皆没
你的意思是不是释放内存的时候对同一块内存释放了2次?   咱们不写2个,只写其中的一个那样不就对了吗?
#12
丁宝青2010-06-06 15:45
好东西,我得好好学习学习。
#13
sunmingchun2010-06-06 19:38
写的好!顶
#14
最近不在2010-06-07 00:46
程序代码:
void fun1(char a[100])    //这里数组退化为指针。
{
    cout<<sizeof(a)<<endl;    //a变为指针,求出4
}

void fun(char (&a)[100])    //将形参声明为引用,不退化  
{
   
    cout<<sizeof(a)<<endl;  //求出100   
}
#15
最近不在2010-06-07 00:47
int main(int argc, char* argv[])
{
        short n = 5;
        
        char nc[] = "asdf";
        
        cout<<sizeof(n)<<endl;    //short的字节长
        cout<<sizeof(&n)<<endl;   //指针的字节长
        
        cout<<nc<<endl;  //1    //可能由于重载缘故,从nc所指向的地址开始输出对应的char字符,直至'\0',注意这里输出的不是16进制地址值
        cout<<&nc<<endl;  //2   //这里为地址值。所指向对象为'a'
        cout<<&nc[0]<<endl; //3  //同样&nc[0]为地址值,与1一样。因此输出的为字符串。只是在意义上有点区别,前者为字符串首地址,后者为第一个元素的地址
        
        cout<<(int *)nc<<endl; //4  //由于字符数组的特殊性,需要显示转换,才能输出地址值
        cout<<(int *)&nc<<endl; //5   //由于&nc本身就已经是一地址值,用此这里的类型转换没多大意义,结果与2一模一样
        cout<<(int *)&nc[0]<<endl; //6 //参照1与3,6与4类似
        
        cout<<sizeof(nc)<<endl;     //5  数组名并不是指针
        cout<<sizeof(&nc)<<endl;    //5                                
        cout<<sizeof(&nc[0])<<endl; //4. 因为nc[0]为一个字符对象。&nc[0]就是一地址值(指针).所有指针值都为4。
        nc[2] = 'A';

        char* p = "Hello World1";
        p[2] = 'A';


    return 0;
}

分析:nc,&nc都有2种理解,一个就是"16进制地址值"ox0012ff3c,一个是字符串"asdf".这2种是一个意思,前者是在系统中表现形式,后者为人们所熟知的表示形式
就像10,在计算机中是以2进制存储的。但nc,和&nc的区别在于,nc指向的对象为'a' 's' 'd' 'f' '\0',一个连续的整体。而&nc所指向德为'a'
我们无法通过&nc这种方式来修改nc("asdf")内的内容,因为"ox00123ff3c",因为它是常量,只能读,不能修改。但我们可以像nc[2] = 'A';
这样修改nc的内容。这并没有改变"asdf"改变。而是将"asAf"赋给了数组nc。
说通俗点。如int n = 12; 我们不能将12改成1,再赋给n,这是不现实的。但我们可以直接将1赋给n。

char* p = “Hello World1”;
p[2] = ‘A’;
p也同样2种理解。一个就是"16进制地址值,一个是字符串“Hello World1”。类似于上面的&nc,p指向的为H,因此我们通过p[2]修改字面值是不正确的。
这个例子再一次说明了这一点....指针这样用是错的。。。
typedef就是一种简化数据类型的方法,便于理解。
typedef char* p pstr;
pstr = “Hello World1”;
#16
最近不在2010-06-07 00:47
自己裹了半天,也不知道说清楚了没...
#17
最近不在2010-06-07 00:58
回复 11楼 刚开始吧
程序代码:
// Note:Your choice is C++ IDE
#include <iostream>
using namespace std;
int main()
{
    int *p = new int(10);
    int *pTemp = p;
    delete [] p;
   
    p = new int[2];
    *p = *pTemp;  //这一步错误。pTemp,p指向同一块内存,被清空。这种错误常见于字符串重新分配空间。这里简化了下例子
    *(p+1) = 3;   //如我们想将cha a[2] = "abcdef"; 变为a[3] = "abcdefg";这种情况 。就需要另外给一片堆内存是最好的
   
    return 0;
}
我没表达清楚,不好意思。

[ 本帖最后由 最近不在 于 2010-6-7 00:59 编辑 ]
#18
Devil_W2010-06-07 20:00
以后不是原创的时候,直接贴个连接就可以了。
#19
2010-06-09 12:27
回复 18楼 Devil_W
你别光说,自己也放几个经典的东西,让我们这些初学者看看学学。
#20
Devil_W2010-06-09 12:43
以下是引用刚开始吧在2010-6-9 12:27:52的发言:

你别光说,自己也放几个经典的东西,让我们这些初学者看看学学。


我经典的东西多的是,你自己不会去搜?

ps:我以前的ID是wxjeacen
#21
2010-06-09 12:55
回复 15楼 最近不在
其实没有看明白你的中心意思是什么。
#22
yi198606992010-06-10 13:33
厉害呀,不客气了
#23
2010-06-10 14:31
为什么不加点自己的见解呢?
#24
南国利剑2010-06-12 22:38
回复 23楼 回忆不是我的
没有自己的见解就是认同已有的见解。
呵呵。。。。
#25
gaoce2272010-06-13 12:04
收藏了。
#26
w6355302192010-06-14 22:38
我顶一个了,好好好
#27
xiaovs0072010-06-19 23:01
很受益!谢谢斑竹
#28
飞天猪10002010-06-22 07:38
我也想看一下原版资料啊
#29
南国利剑2010-06-22 12:27
回复 28楼 飞天猪1000
呵呵,好啊。有时间贴出来。
#30
小破鱼2010-06-23 18:25
没大看明白
不知指针为何物
书上说后面会讲解
#31
南国利剑2010-06-23 22:51
回复 30楼 小破鱼
等你学了指针和动态申请内存,然后再来看,你就会有所感悟了。
#32
怪才2010-06-24 08:46
灰常感谢 现在我收藏了~ 恩恩
#33
2010-06-29 11:35
学习
#34
风雨氵2010-07-03 16:24
顶!!!
#35
zzyu2202010-07-05 22:14
明白了,谢谢了!
#36
东海一鱼2010-07-09 22:51
未尽然也!
void f()
{
   …
   char * p;
   p = (char*)new char[100];
   …
}
仅凭这样的代码就断定内存泄露,太武断了点。因为还要看上下文的代码才知道。

譬如:
void f()
{
   …
   char * p;
   p = (char*)new char[100];
   gDataList.add(p);                    //gDataList为全局数据组织链表对象。
   ...                          
}

就没有问题呀,完全可以等释放连表对象时在逐一释放节点数据指针。



#37
南国利剑2010-07-10 13:27
回复 36楼 东海一鱼
但是这里很显然,这点你知道吧,p是一个局部变量,一旦函数调用结束,
p的内存就会被收回,但它所指的空间却还存在,
这不是它自己不用,确切的说它自己没有办法用了,
却不让别人用。
这就是内存泄露,我要说的就这么一回事。

[ 本帖最后由 南国利剑 于 2010-7-10 13:29 编辑 ]
#38
东海一鱼2010-07-10 16:51
回复 37楼 南国利剑
好,我再简化一点:

我有一个全局void* gpoint变量。

在局部变量指针p完成分配堆内存后,令 *gpoint = *p 然后局部函数返回。这时候p虽然已经不可访问了。
但已经分配的对内存地址仍然保存在gpoint中,当然可以随意使用访问。
#39
ccmike982010-07-17 15:07
学习了    LZ加油
#40
boy_good2010-07-18 01:45
好久,好久了,一年多没触C了,有些心酸,谢谢了,哥们
#41
iamthinking12010-08-09 14:46
不懂啊!!!
#42
stranger552011-04-24 10:52
谢谢楼主
但是请问下楼主一个问题
#include<iostream>
using namespace std;
char*  f1()
{
   char* p = NULL;
   char a;
   p = &a;
   return p;
}
int main()
{
  char* p ;
  p = f1();
  *p = 'a';
  return 0;
}
我编译 运行了一下  没有看见到底出现了神马异常错误阿 ?
#43
myseemylife2011-04-24 21:26
以下是引用最近不在在2010-6-6 01:15:05的发言:

// Note:Your choice is C++ IDE
#include  
using namespace std;
int main()
{
    char *p = new char[4];
    //错误拷贝情况
    //char *pTemp = p;
    //delete [] pTemp; 二者等效 指向同一堆内存
    //delete [] p; 一删皆没
   
    //正确的做法
    //char *pTemp = new char[4];
    //for(...)
    //{
    //    *(pTemp+i) =*(p+i);
    //}
    //delete [] p;
    return 0;
}
    for (int i = 0; i < 4; i++)
    {
        p[i] = (char)(i + 1);
    }
   
    p[i] = '0';
我加上这个给字符数组初始化一下,然后delete出错误。。。。。。。不是很理解。为什么不初始化就不会报错呢?
1