Chinaunix首页 | 论坛 | 博客
  • 博客访问: 768335
  • 博文数量: 199
  • 博客积分: 3584
  • 博客等级: 中校
  • 技术积分: 2193
  • 用 户 组: 普通用户
  • 注册时间: 2008-05-12 21:18
文章分类

全部博文(199)

文章存档

2018年(6)

2013年(14)

2012年(30)

2011年(28)

2010年(24)

2009年(86)

2008年(11)

分类: C/C++

2009-01-09 15:23:19

[转] 彻底了解指针数组,数组指针,以及函数指针,以及堆中的分配规则
一 :关于指针和堆的内存分配
先来介绍一下指针: 指针一种类型,理论上来说它包含其他变量的地址,因此有的书上也叫它:地址变量。既然指针是一个类型,是类型就有大小,在达内的服务器上或者普通的PC机上,都是4个字节大小,里边只是存储了一个变量的地址而已。不管什么类型的指针,char * ,int * ,int (*) ,string * ,float * ,都是说明了本指针所指向的地址空间是什么类型而已,了解了这个基本上所有的问题都好象都变的合理了。
在C++中,申请和释放堆中分配的存贮空间,分别使用new和delete的两个运算符来完成:
 
指针类型 指针变量名 = new 指针类型 (初始化);
 
        delete 指针名;
 
例如:
 
1、 int *p = new int(0);
 
     它与下列代码序列大体等价:
 
2、int tmp = 0, *p = &tmp;
 
区别:p所指向的变量是由库操作符new()分配的,位于内存的堆区中,并且该对象未命名。
 
  
 
下面是关于new 操作的说明 : 部分引自《C++面向对象开发》
 
1、new运算符返回的是一个指向所分配类型变量(对象)的指针。对所创建的变量或对象,都是通过该指针来间接操作的,而动态创建的对象本身没有名字。
 
2、一般定义变量和对象时要用标识符命名,称命名对象,而动态的称无名对象(请注意与栈区中的临时对象的区别,两者完全不同:生命期不同,操作方法不同,临时变量对程序员是透明的)。
 
3、堆区是不会在分配时做自动初始化的(包括清零),所以必须用初始化式(initializer)来显式初始化。new表达式的操作序列如下:从堆区分配对象,然后用括号中的值初始化该对象。
 
下面是从堆中申请数组
 
1、申请数组空间:
 
指针变量名=new 类型名[下标表达式];
 
注意:“下标表达式”不是常量表达式,即它的值不必在编译时确定,可以在运行时确定。这就是堆的一个非常显著的特点,有的时候程序员本身都不知道要申请能够多少内存的时候,堆就变的格外有用。
 
2、释放数组空间:
 
delete [ ]指向该数组的指针变量名;
 
注意:方括号非常重要的,如果delete语句中少了方括号,因编译器认为该指针是指向数组第一个元素的,会产生回收不彻底的问题(只回收了第一个元素所占空间),我们通常叫它“内存泄露”,加了方括号后就转化为指向数组的指针,回收整个数组。delete [ ]的方括号中不需要填数组元素数,系统自知。即使写了,编译器也忽略。<>上说过以前的delete []方括号中是必须添加个数的,后来由于很容易出错,所以后来的版本就改进了这个缺陷。
 
下面是个例子,VC上编译通过
 
#include
 
using namespace std;
 
//#include  //for VC
 
#include
 
void main(){
 
     int n;
 
     char *p;
 
     cout<<"请输入动态数组的元素个数"<
 
     cin>>n; //n在运行时确定,可输入17
 
     p=new char[n]; //申请17个字符(可装8个汉字和一个结束符)的内存空间strcpy(pc,“堆内存的动态分配”);//
 
     cout<
 
     delete []p;//释放pc所指向的n个字符的内存空间return ; 
 
}
 
通过指针使堆空间,编程中的几个可能问题
 
1.动态分配失败。返回一个空指针(NULL),表示发生了异常,堆资源不足,分配失败。
 
   data = new double [m]; //申请空间
 
   if ((data ) == 0)…… //或者==NULL
 
2.指针删除与堆空间释放。删除一个指针p(delete p;)实际意思是删除了p所指的目标(变量或对象等),释放了它所占的堆空间,而不是删除p本身,释放堆空间后,p成了空悬指针,不能再通过p使用该空间,在重新给p赋值前,也不能再直接使用p。
 
3.内存泄漏(memory leak)和重复释放。new与delete 是配对使用的, delete只能释放堆空间。如果new返回的指针值丢失,则所分配的堆空间无法回收,称内存泄漏,同一空间重复释放也是危险的,因为该空间可能已另分配,而这个时候又去释放的话,会导致一个很难查出来的运行时错误。所以必须妥善保存new返回的指针,以保证不发生内存泄漏,也必须保证不会重复释放堆内存空间。
 
4.动态分配的变量或对象的生命期。无名变量的生命期并不依赖于建立它的作用域,比如在函数中建立的动态对象在函数返回后仍可使用。我们也称堆空间为自由空间(free store)就是这个原因。但必须记住释放该对象所占堆空间,并只能释放一次,在函数内建立,而在函数外释放是一件很容易失控的事,往往会出错,所以永远不要在函数体内申请空间,让调用者释放,这是一个很差的做法。你再怎么小心翼翼也可能会带来错误。
 
类在堆中申请内存 :
 
通过new建立的对象要调用构造函数,通过deletee删除对象要调用析构函数。
 
CGoods *pc;
 
pc=new CGoods;      //分配堆空间,并构造一个无名对象
 
                              //的CGoods对象;
 
…….
 
delete pc;  //先析构,然后将内存空间返回给堆;        堆对象的生命期并不依赖于建立它的作用域,所以除非程序结束,堆对象(无名对象)的生命期不会到期,并且需要显式地用delete语句析构堆对象,上面的堆对象在执行delete语句时,C++自动调用其析构函数。
 
正因为构造函数可以有参数,所以new后面类(class)类型也可以有参数。这些参数即构造函数的参数。
 
但对创建数组,则无参数,并只调用缺省的构造函数。见下例类说明:
 
class CGoods{
 
          char Name[21];
 
          int  Amount;
 
          float Price;
 
          float Total_value;
 
public:
 
 CGoods(){}; //缺省构造函数。因已有其他构造函数,系统不会再自动生成缺省构造,必须显式声明。  
 
 CGoods(char* name,int amount ,float price){
 
           strcpy(Name,name);
 
           Amount=amount;
 
           Price=price;
 
           Total_value=price*amount;  }
 
           ……};//类声明结束
 
下面是调用机制 :
 
void main(){
 
      int n;
 
      CGoods *pc,*pc1,*pc2;
 
      pc=new CGoods(“hello”,10,118000);
 
      //调用三参数构造函数   pc1=new CGoods();  //调用缺省构造函数  cout<<”输入商品类数组元素数”<
 
      cin>>n;
 
      pc2 = new CGoods[n];
 
     //动态建立数组,不能初始化,调用n次缺省构造函数  
 
      ……
 
      delete pc;
 
      delete pc1;
 
      delete []pc2; 
 
}
 
申请堆空间之后构造函数运行;
 
释放堆空间之前析构函数运行;
 
再次强调:由堆区创建对象数组,只能调用缺省的构造函数,不能调用其他任何构造函数。如果没有缺省的构造函数,则不能创建对象数组。
 
---------------------下面我们再来看一下指针数组和数组指针―――――――――――――
 
如果你想了解指针最好理解以下的公式 :
 
     (1)int*ptr;//指针所指向的类型是int
 
   (2)char*ptr;//指针所指向的的类型是char
 
   (3)int**ptr;//指针所指向的的类型是int* (也就是一个int * 型指针)
 
   (4)int(*ptr)[3];//指针所指向的的类型是int()[3] //二维指针的声明
 
(1)指针数组:一个数组里存放的都是同一个类型的指针,通常我们把他叫做指针数组。
 
比如 int * a[10];它里边放了10个int * 型变量,由于它是一个数组,已经在栈区分配了10个(int * )的空间,也就是32位机上是40个byte,每个空间都可以存放一个int型变量的地址,这个时候你可以为这个数组的每一个元素初始化,在,或者单独做个循环去初始化它。
 
例子:
 
int * a[2]={ new int(3),new int(4) };     //在栈区里声明一个int * 数组,它的每一个元素都在堆区里申请了一个无名变量,并初始化他们为3和4,注意此种声明方式具有缺陷,VC下会报错
 
例如 :
 
int * a[2]={new int[3],new int[3]};
 
delete a[0];
 
delet a[10];
 
但是我不建议达内的学生这么写,可能会造成歧义,不是好的风格,并且在VC中会报错,应该写成如下 :
 
int * a[2];
 
a[0]= new int[3];
 
a[1]=new int[3];
 
delete a[0];
 
delet a[10];
 
这样申请内存的风格感觉比较符合大家的习惯;由于是数组,所以就不可以delete a;编译会出警告.delete  a[1];
 
注意这里 是一个数组,不能delete [] ;
 
( 2 ) 数组指针 : 一个指向一维或者多维数组的指针;
 
int * b=new int[10]; 指向一维数组的指针b ;
 
注意,这个时候释放空间一定要delete [] ,否则会造成内存泄露, b 就成为了空悬指针.
 
int (*b2)[10]=new int[10][10]; 注意,这里的b2指向了一个二维int型数组的首地址.
 
注意:在这里,b2等效于二维数组名,但没有指出其边界,即最高维的元素数量,但是它的最低维数的元素数量必须要指定!就像指向字符的指针,即等效一个字符串,不要把指向字符的指针说成指向字符串的指针。这与数组的嵌套定义相一致。
 
int(*b3) [30] [20];  //三级指针――>指向三维数组的指针;
 
int (*b2) [20];     //二级指针;
 
b3=new int [1] [20] [30];
 
b2=new int [30] [20];
 
      两个数组都是由600个整数组成,前者是只有一个元素的三维数组,每个元素为30行20列的二维数组,而另一个是有30个元素的二维数组,每个元素为20个元素的一维数组。
 
      删除这两个动态数组可用下式:
 
delete [] b3;  //删除(释放)三维数组;
 
delete [] b2;  //删除(释放)二维数组;
 
再次重申:这里的b2的类型是int (*) ,这样表示一个指向二维数组的指针。
 
b3表示一个指向(指向二维数组的指针)的指针,也就是三级指针.
 
( 3 ) 二级指针的指针
 
看下例 :
 
int (**p)[2]=new (int(*)[3])[2];
 
       p[0]=new int[2][2];
 
       p[1]=new int[2][2];
 
       p[2]=new int[2][2];
 
       delete [] p[0];
 
       delete [] p[1];
 
       delete [] p[2];
 
       delete [] p;
 
注意此地方的指针类型为int (*),碰到这种问题就把外边的[2]先去掉,然后回头先把int ** p=new int(*)[n]申请出来,然后再把外边的[2]附加上去;
 
p代表了一个指向二级指针的指针,在它申请空间的时候要注意指针的类型,那就是int (*)代表二级指针,而int (**)顾名思义就是代表指向二级指针的指针了。既然是指针要在堆里申请空间,那首先要定义它的范围:(int(*)[n])[2],n 个这样的二级指针,其中的每一个二级指针的最低维是2个元素.(因为要确定一个二级指针的话,它的最低维数是必须指定的,上边已经提到)。然后我们又分别为p[0],p[1],p[2]…在堆里分配了空间,尤其要注意的是:在释放内存的时候一定要为p[0],p[1],p[2],单独delete[] ,否则又会造成内存泄露,在delete[]p 的时候一定先delete p[0]; delete p[1],然后再把给p申请的空间释放掉 delete [] p ……这样会防止内存泄露。
 
(3)指针的指针;
 
int ** cc=new (int*)[10]; 声明一个10个元素的数组,数组每个元素都是一个int *指针,每个元素还可以单独申请空间,因为cc的类型是int*型的指针,所以你要在堆里申请的话就要用int *来申请;
 
看下边的例子  (vc & GNU编译器都已经通过);
 
       int ** a= new int * [2];     //申请两个int * 型的空间
 
       a[1]=new int[3];        //为a的第二个元素又申请了3个int 型空间,a[1]指向了此空间首地址处
 
       a[0]=new int[4];        ////为a的第一个元素又申请了4个int 型空间,a[0] 指向了此空间的首地址处
 
       int * b;
 
       a[0][0]=0;
 
       a[0][1]=1;
 
       b=a[0];
 
       delete [] a[0]       //一定要先释放a[0],a[1]的空间,否则会造成内存泄露.;
 
       delete [] a[1];
 
       delete [] a;
 
       b++;
 
       cout<<*b<
 
注意 :因为a 是在堆里申请的无名变量数组,所以在delete 的时候要用delete [] 来释放内存,但是a的每一个元素又单独申请了空间,所以在delete [] a之前要先delete [] 掉 a[0],a[1],否则又会造成内存泄露.
 
(4) 指针数组 :
 
   
 
我们再来看看第二种 :二维指针数组
 
int *(*c)[3]=new int *[3][2];
 
如果你对上边的介绍的个种指针类型很熟悉的话,你一眼就能看出来c是个二级指针,只不过指向了一个二维int * 型的数组而已,也就是二维指针数组。
 
例子 :
 
 int *(*b)[10]=new int*[2][10];//
 
b[0][0]=new int[100];
 
b[0][1]=new int[100];
 
*b[0][0]=1;
 
cout <<*b[0][0]<
 
delete [] b[0][0];
 
delete [] b[0][1];
 
delete [] b;
 
cout<<*b[0][0]<
 
 这里只为大家还是要注意内存泄露的问题,在这里就不再多说了。
 
如果看了上边的文章,大家估计就会很熟悉,这个b是一个二维指针,它指向了一个指针数组
 
第二种 :
 
        int **d[2];表示一个拥有两个元素数组,每一个元素都是int ** 型,这个指向指针的指针:)
 
   d不管怎样变终究也是个数组,呵呵,
 
   如果你读懂了上边的,那下边的声明就很简单了:
 
   d[0]=new int *[10];
 
   d[1]=new int * [10];
 
     delete [] d[0];
 
     delete [] d[1];
 
具体的就不再多说了 :)
 
二 : 函数指针 
 
关于函数指针,我想在我们可能需要写个函数,这个函数体内要调用另一个函数,可是由于项目的进度有限,我们不知道要调用什么样的函数,这个时候可能就需要一个函数指针;
 
int a();这个一个函数的声明;
 
ing (*b)();这是一个函数指针的声明;
 
让我们来分析一下,左边圆括弧中的星号是函数指针声明的关键。另外两个元素是函数的返回类型(void)和由边圆括弧中的入口参数(本例中参数是空)。注意本例中还没有创建指针变量-只是声明了变量类型。目前可以用这个变量类型来创建类型定义名及用sizeof表达式获得函数指针的大小:
 
unsigned psize = sizeof (int (*) ()); 获得函数指针的大小
 
// 为函数指针声明类型定义
 
typedef int (*PFUNC) ();
 
PFUNC是一个函数指针,它指向的函数没有输入参数,返回int。使用这个类型定义名可以隐藏复杂的函数指针语法,就我本人强烈建议我们大内弟子使用这种方式来定义;
 
下面是一个例子,一个简单函数指针的回调(在GNU编译器上通过,在VC上需要改变一个头文件就OK了)
 
#include              //GNU 编译器 g++ 实现
 
using namespace std;
 
/*                              //vc 的实现
 
#include "stdafx.h"
 
#include
 
*/
 
#define DF(F) int F(){  cout<<"this is in function "<<#F<
 
      return 0;       \
 
}
 
//声明定义DF(F)替代 int F();函数;
 
DF(a); DF(b); DF(c); DF(d); DF(e); DF(f); DF(g); DF(h); DF(i);     //声明定义函数 a b c d e f g h i
 
// int (*pfunc)();              //一个简单函数指针的声明
 
typedef int(*FUNC)();   //一个函数指针类型的声明
 
FUNC ff[] = {a,b,c,d,e,f,g,h,i};   //声明一个函数指针数组,并初始化为以上声明的a,b,c,d,e,f,g,h,i函数
 
FUNC func3(FUNC vv){    //定义函数func3,传入一个函数指针,并且返回一个同样类型的函数指针
 
      vv();
 
      return vv;
 
}
 
/*FUNC func4(int (*vv)()){      //func3的另一种实现
 
      vv();
 
      return vv;
 
}*/
 
int main(){
 
      for(int i=0;i
 
              FUNC r=func3(ff[ i ]);
 
              cout<
 
      }
 
      return 0;
 
}
 
到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。
 
好了,先到此为止吧,写这篇文章耗费了基本上快半天的时间了,很多事情还没有做,等改天有时间再回来整理,所有的源程序都放在openlab3服务器上我的目录下lib/cpp下,大家可以去拿。不知道的登陆openlab3 然后cd ~chengx/lib/cpp就可以看到了。
 
还有很复杂的声明可能也是一种挑战 比如<>里的
 
int (*(*f4())[10]();的声明,f4是一个返回指针的函数,该指针指向了含有10个函数指针的数组,这些函数返回整形值;不是这个函数有特别之处,而是Bruce Eckel 说的“从右到左的辨认规则”是一种很好的方法,值得我们去学习,感谢他:)
 
最后我想应该跟大家说一下,写程序应该就象JERRY所说的:简单就是美;我们应该遵循一个原则 : KISS (Keep It Simple,Stupid ,尽量保持程序简单 出自 :《Practical C programming》),把自己的程序尽量的简单明了,这是个非常非常好的习惯。

数组指针与指针数组区别
2008-05-12 21:53
int (*p)[10];
定义了一个数组指针,这个指针与一般的指针没有什么区别,仅仅是这个指针指向一个数组。这里我们把数组作为了基本的元素处理。也就是说,将整个数组作为一种类型,而数组名就是这个类型的一个具体变量。例如:

int a[10];
一个数组类型,形状就是这样:int [10];a就是新定义的一个变量。

int b[3];
一个数组类型,形状就是这样:int [3];b就是新定义的一个变量。

因为这两个类型形状不一样,因此是两个不同的类型,因此a,b就是不同类型的变量。这就好比int a和double b :a和b不一样。不知道大家是否已经对数组类型有了基本的印象?

那么把数组名作为该数组类型的一个具体变量,我们就可以定义指向这个变量的指针,即数组指针。

对于数组类型:int [10],我们可以定义一个指针,int (*p) [10].注意这里一定要加上小括弧。否则就会变成了指针数组。定义了指针之后,我们可以对该指针赋值,如p=&a;如果定义了一个二维数组,int c[3][10]。我们可以认为定义了一个一维的数组,这个数组有三个int[10]的元素。因此和一般的数组一样,我们可以将该数组名赋给指针,其实也就是第一个元素的地址付给指针。即: p=c;或者p=&c[0]。其余情况以此类推。

---------------------------------------------------------------
数组指针是指针类型,它指向一个数组,代表数组的首地址。

指针数组首先是一个数组,只不过这个数组的元素是指针而己。

---------------------------------------------------------------
下面的程序有错误嘛?为什么?

#include
void main(){
   int *p=new int [10];
   int arr[10];
   int (*ptr)[10];
   ptr=p;
   ptr=arr;
   ptr=&arr;
   typedef int (*type)[10];
   type pa=&(new int[10]);
   int* pb;
   type pc=&(pb=new int[10]);
   type pd=(type)(new int[10]);
}
-------------------------------------------------------
ptr=&arr;
type pa=&(new int[10]);
type pc=&(pb=new int[10]);
窃以为此三句对。
---------------------------------------------------------------
你说:int a[90]; a是个变量,那么要分配内存,那么a有地址吗?
&a???
---------------------------------------------------------------
数组指针是定义的一个指针,而指针所指的对象是一个数组,指针指向该数组的首单元的地址,它对数组的内部元素的属性不了解,只是规定了首单元的地址,通过它可以找到该数组。

     指针数组指的是一个数组,它其中的所有元素都是指针类型,这里所有指针都指向不同的地址,而所指地址的数据也不一定相同,但是必须属于同一数据类型。

     二者相差很多。

---------------------------------------------------------------
ptr=p;//将ptr指针指向p,错误!

   ptr=arr;//将ptr这个数组指针指向int arr[10]数组的首址,错误!

   ptr=&arr;//将ptr指向哪里?不清楚
~~
   ptr应该是一个指向int型一维数组的首行才对!

   不知道我说道的对不对啊?

---------------------------------------------------------------
ptr=p; 数组指针与指针数概念浑淆
ptr=arr; 同上
type pa=&(new int[10]); 没见过
type pc=&(pb=new int[10]);   数组指针指针数组概念浑淆
---------------------------------------------------------------
指针指向类型不同的,说白了就是相邻的两个指针的之间的距离因为指针的类型不同而有所不一样的。
---------------------------------------------------------------
数组指针是指向数组的,

: int a[3][3],(*p)[3];
     p=a;
这里(*p)[3]用于告诉编译系统,它是一个指针,指向一个长度为3的整型数组。这样在用指针访问其所指向的内存单元的时候就可以用*(*(p+i)+j)来表示
a[i][j];
若:
int a[3][3],*p;
     p=a;
就需用:*(p+3*i+j)来表示
a[i][j].
指针数组是这样一种特殊的数组:它的每一个数组元素都是一个指针。

如:
int *p[3];
*p[0],*p[1],*p[2]都是一个指针。
---------------------------------------------------------------
to : elvahuang(elva) (   )
我想你应该明白汇编中的label使用吧。他不占用变量内存,但是却能和变量一样引用内存。呵呵,这样的话,你的那个问题应该没有问题了吧。

---------------------------------------------------------------
int a[10];
a是个变量,它的值是&a(它自己的地址)。

---------------------------------------------------------------
我看了你所说的有关指针问题的看法,我非常同意你所说的
.
其实我也有一点看法:所谓指针数组吗,其实就可以把他看成一个结构提指针,只是他里面的元素都是相同的类型.例如:int (*p)[22];p就是指向一个由10个整型单元构成的数组的指针.p必须是指向由10个整型单元构成的数组,就如一个结构体指针必须指向与它相同类型的一个结构体一样

---------------------------------------------------------------
我在vc上调了一下,只有

ptr=&arr;
type pd=(type)(new int[10]);// 但是此句会有警告
.
确实如xiaoluoli(C/C++思考) 所说,对于int(*p)[10]编译器会把p看作一个类似于指向结构的指针,所以其它赋值语句都是错的
.
---------------------------------------------------------------
数组就是const指针,但是规定了范围,分配了空间而已

---------------------------------------------------------------
前言:

其实数组也就是一个逻辑上的拥有首地址的连续的内存空间。

1。我们常常用改变下标的方式来访问数组,本质上和通过加减某种特定类型的

指针来实现在逻辑内存上的跳跃是相同的,其实数组的越界和程序员通过指针

强行访问逻辑内存是一样的原理,只是指针更加灵活,同时也带来的大量的

安全性问题。

2。而对于诸如   int*(*p)[i]   这样的问题,无非就是一个指针的指针,而后一

          
指针表象上就是那个数组,所以如果我们要想访问数组元素,就必须通过多加

                                                        
一个指针的方式来访问,而且还需要注意的是,后一指针也就是表象上的数组

                            
的寻址或者是跳跃方式是按照每一个元素以sizeof(int)的空间来跳跃的,我


们也可以让这样的int成为其他的基本类型或者是扩展类型,只不过改变的跳

                             
跃的空间的大小或者是方式(这里的方式可以认为是某种具有嵌套关系的扩

                                                               
展类型)。
 
 

 c/c++ 程序内存分配

关于程序内存的管理,经常遇到这么的问题,c/c++中关于变量的分配与保存位置. 
变量的类型和存贮类别是两个不同的概念.
程序中一般包括以下几个存贮区域:
1.栈(Stack),该区域主要存贮函数的参变量以及程序中的局部变量.具体的操作由编译器释放.
2.堆(heap),这部分由程序员自己分配和释放,如果程序中没有释放该空间,程序结束后由系统回收.通常的memory leak就是由这部分操作不善而产生的.
3.全局变量区,这部分主要是保存全局变量和静态变量.其中分为初始化的全局变量和未初始化的全局变量,不过这两个区域是相邻的.该区域程序结束后由系统释放.
4.文字常量区,保存程序中的常量字符串,由系统来释放.
5.代码区域,保存程序的二进制代码.
下面有个example:
二、例子程序 
 这是一个前辈写的,非常详细 
//main.cpp 
int a = 0; //全局初始化区 
char *p1; //全局未初始化区 
main() 

int b; //栈 
char s[] = "abc"; //栈  
char *p2; //栈 
char *p3 = "123456"; //123456\\0在常量区,p3在栈上。 
static int c =0; //全局(静态)初始化区 
p1 = (char *)malloc(10); 
p2 = (char *)malloc(20); 
分配得来得10和20字节的区域就在堆区。 
strcpy(p1, "123456"); //123456\\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
二、堆和栈的理论知识 

2.1申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。
2.2申请后系统的响应 
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。  
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
2.3申请大小的限制 
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。  

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
2.4申请效率的比较:  
栈由系统自动分配,速度较快。但程序员是无法控制的。  
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.  
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活
2.5堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
2.6存取效率的比较

char s1[] = "aaaaaaaaaaaaaaa";  
char *s2 = "bbbbbbbbbbbbbbbbb";
 aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
 #include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
 10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
 11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。
2.7小结:
 堆和栈的区别可以用如下的比喻来看出:
 使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
 使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
 堆和栈的区别主要分:
操作系统方面的堆和栈,如上面说的那些,不多说了。
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。  
 
 
 
 

预备知识—程序内存分配
一个由C/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。

一个正常的程序内存中通常分为程序段,数据端和堆栈三部分。程序段里放着程序的机器码和只读数据,这个段通常是只读,对它的写操作是非法的。数据段放的是程序中的静态数据。动态数据则通过堆栈来存放。在内存中,它们的位置如下:
+------------------+ 内存低端
| 程序段 |
|------------------|
| 数据段 |
|------------------|
| 堆栈 |
+------------------+ 内存高端
堆栈是内存中的一个连续的块。一个叫堆栈指针的寄存器(SP)指向堆栈的栈顶。堆栈的底部是一个固定地址。堆栈有一个特点就是,后进先出。也就是说,后放入的数据第一个取出。它支持两个操作,PUSH和POP。PUSH是将数据放到栈的顶端,POP是将栈顶的数据取出。
在高级语言中,程序函数调用和函数中的临时变量都用到堆栈。为什么呢?因为在调用一个函数时,我们需要对当前的操作进行保护,也为了函数执行后,程序可以正确的找到地方继续执行,所以参数的传递和返回值也用到了堆栈。通常对局部变量的引用是通过给出它们对SP的偏移量来实现的。另外还有一个基址指针(FP,在Intel芯片中是BP),许多编译器实际上是用它来引用本地变量和参数的。通常,参数的相对FP的偏移是正的,局部变量是负的。
程序中发生函数调用时,计算机做如下操作:首先把参数压入堆栈;然后保存指令寄存器(IP)中的内容,做为返回地址(RET);第三个放入堆栈的是基址寄存器(FP);然后把当前的栈指针(SP)拷贝到FP,做为新的基地址;最后为本地变量留出一定空间,把SP减去适当的数值。

在函数体中定义的变量通常是在栈上,用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。在所有函数体外定义的是全局量,加了static修饰符后不管在哪里都存放在全局区(静态区),在所有函数体外定义的static变量表示在该文件中有效,不能extern到别的文件用,在函数体内定义的static表示只在该函数体内有效。另外,函数中的"adgfdf"这样的字符串存放在常量区。

对比:
1 性能
栈:栈存在于RAM中。栈是动态的,它的存储速度是第二快的。stack
堆:堆位于RAM中,是一个通用的内存池。所有的对象都存储在堆中。heap

2 申请方式
stack: 由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap: 需要程序员自己申请,并指明大小,在c中malloc函数如p1 = (char *)malloc(10);
在C++中用new运算符 如p2 = (char *)malloc(10); 但是注意p1、p2本身是在栈中的。

3 申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

4 申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

5 申请效率的比较
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

6 堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

7 存取效率的比较
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。

小结:
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
 
(heap)和栈(stack)有什么区别??

简单的可以理解为:
heap:是由malloc之类函数分配的空间所在地。地址是由低向高增长的。
stack:是自动分配变量,以及函数调用的时候所使用的一些空间。地址是由高向低减少的。

预备知识—程序的内存分配

一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。

二、例子程序
这是一个前辈写的,非常详细
//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; 123456在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}


二、堆和栈的理论知识
2.1申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量
int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。
2.2
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
2.3申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
2.4申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便
.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度,也最灵活
2.5堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
2.6存取效率的比较

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指edx中,在根据edx读取字符,显然慢了。


2.7小结:
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
 
阅读(5164) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~