C++内存管理

来源:互联网 发布:什么是棋牌源码 编辑:程序博客网 时间:2024/06/16 20:24

堆和栈的区别
一、预备知识—程序的内存分配
一个由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\0在常量区,p3在栈上。
static int c =0;全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。


一个程序一般分为3段:text段,data段,bss段
text段:就是放程序代码的,编译时确定,只读,
data段:存放在编译阶段(而非运行时)就能确定的数据,可读可写
就是通常所说的静态存储区,赋了初值的全局变量和静态变量存放在这个区域,常量也存放在这个区域
bss段:定义而没有赋初值的全局变量和静态变量,放在这个区域

 

C语言 内存管理详解

 

 伟大的Bill Gates 曾经失言:
  640K ought to be enough for everybody — Bill Gates1981

  程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。

  1、内存分配方式

  内存分配方式有三种:

  (1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

  (2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  (3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

  2、常见的内存错误及其对策

  发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。常见的内存错误及其对策如下:

  * 内存分配未成功,却使用了它。

  编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行

  检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

  * 内存分配虽然成功,但是尚未初始化就引用它。

  犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

  * 内存分配成功并且已经初始化,但操作越过了内存的边界。

  例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

  * 忘记了释放内存,造成内存泄露。

  含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

  动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。

  * 释放了内存却继续使用它。
 
  有三种情况:

  (1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

  (2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

  (3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

  【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

  【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

  【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

  【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

  【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

  3、指针与数组的对比

  C /C程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。

  数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。

  指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

  下面以字符串为例比较指针与数组的特性。

  3.1 修改内容

  示例3-1中,字符数组a的容量是6个字符,其内容为hello。a的内容可以改变,如a[0]= ‘X’。指针p指向常量字符串“world”(位于静态存储区,内容为world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= ‘X’有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。

1.  char a[] = “hello”;

2.  a[0] = ‘X’;

3.  cout << a << endl;

4.  char *p = “world”; // 注意p指向常量字符串

5.  p[0] = ‘X’; // 编译器不能发现该错误

6.  cout << p << endl;

复制代码

示例3.1 修改数组和指针的内容

  3.2 内容复制与比较

  不能对数组名进行直接复制与比较。示例7-3-2中,若想把数组a的内容复制给数组b,不能用语句 b =a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b==a)来判断,应该用标准库函数strcmp进行比较。

  语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a) 1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。

1.  // 数组…

2.  char a[] = "hello";

3.  char b[10];

4.  strcpy(b, a); // 不能用 b = a;

5.  if(strcmp(b, a) == 0) // 不能用 if (b == a)

6. 

7.  // 指针…

8.  int len = strlen(a);

9.  char *p = (char*)malloc(sizeof(char)*(len 1));

10. strcpy(p,a); // 不要用 p = a;

11. if(strcmp(p, a) == 0) // 不要用 if (p == a)

12.

复制代码

示例3.2 数组和指针的内容复制与比较

  3.3 计算内存容量

  用运算符sizeof可以计算出数组的容量(字节数)。示例7-3-3(a)中,sizeof(a)的值是12(注意别忘了’’)。指针p指向a,但是 sizeof(p)的值却是4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。 C /C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。

  注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。示例7-3-3(b)中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。

1.  char a[] = "hello world";

2.  char *p = a;

3.  cout<< sizeof(a) << endl; //12字节

4.  cout<< sizeof(p) << endl; //4字节

复制代码

示例3.3(a) 计算数组和指针的内存容量

1.  void Func(char a[100])

2.  {

3.   cout<< sizeof(a)<< endl; // 4字节而不是100字节

4.  }

复制代码

示例3.3(b) 数组退化为指针


4、指针参数是如何传递内存的?

  如果函数的参数是一个指针,不要指望用该指针去申请动态内存。示例7-4-1中,Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?

1.  void GetMemory(char *p, int num)

2.  {

3.   p = (char*)malloc(sizeof(char) * num);

4.  }

5.  void Test(void)

6.  {

7.   char *str = NULL;

8.   GetMemory(str, 100); //str 仍然为 NULL

9.   strcpy(str,"hello"); // 运行错误

10. }

复制代码

示例4.1 试图用指针参数申请动态内存

  毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把 _p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。

  如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例4.2。

1.  void GetMemory2(char **p, int num)

2.  {

3.   *p = (char*)malloc(sizeof(char) * num);

4.  }

5.  void Test2(void)

6.  {

7.   char *str = NULL;

8.   GetMemory2(&str,100); // 注意参数是 &str,而不是str

9.   strcpy(str,"hello");

10.  cout<< str <<endl;

11.  free(str);

12. }

复制代码

示例4.2用指向指针的指针申请动态内存

  由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例4.3。

1.  char *GetMemory3(int num)

2.  {

3.   char *p = (char*)malloc(sizeof(char) * num);

4.   return p;

5.  }

6.  void Test3(void)

7.  {

8.   char *str = NULL;

9.   str = GetMemory3(100);

10.  strcpy(str,"hello");

11.  cout<< str <<endl;

12.  free(str);

13. }

复制代码

示例4.3 用函数返回值来传递动态内存

  用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示例4.4。

1.  char *GetString(void)

2.  {

3.   char p[] = "helloworld";

4.   return p; // 编译器将提出警告

5.  }

6.  void Test4(void)

7.  {

8.   char *str = NULL;

9.   str = GetString(); // str的内容是垃圾

10.  cout<< str <<endl;

11. }

复制代码

示例4.4 return语句返回指向“栈内存”的指针

  用调试器逐步跟踪Test4,发现执行str =GetString语句后str不再是NULL指针,但是str的内容不是“hello world”而是垃圾。
如果把示例4.4改写成示例4.5,会怎么样?

1.  char *GetString2(void)

2.  {

3.   char *p = "helloworld";

4.   return p;

5.  }

6.  void Test5(void)

7.  {

8.   char *str = NULL;

9.   str = GetString2();

10.  cout<< str <<endl;

11. }

复制代码

示例4.5 return语句返回常量字符串

  函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错误的。因为GetString2内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。

  5、杜绝“野指针”

  “野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。 “野指针”的成因主要有两种:

  (1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

1.  char *p = NULL;

2.  char *str = (char *) malloc(100);

复制代码

(2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。

  (3)指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下:

1.  class A

2.  {

3.   public:

4.    void Func(void){ cout<< “Func of class A” << endl; }

5.  };

6.  void Test(void)

7.  {

8.   A *p;

9.   {

10.   A a;

11.   p = &a; // 注意 a 的生命期

12.  }

13.  p->Func(); // p是“野指针”

14. }

复制代码

函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。


  6、有了malloc/free为什么还要new/delete?

  malloc与free是C /C语言的标准库函数,new/delete是C 的运算符。它们都可用于申请动态内存和释放内存。

  对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

   因此C 语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们先看一看malloc/free和new/delete如何实现对象的动态内存管理,见示例6。

1.  class Obj

2.  {

3.   public :

4.    Obj(void){ cout <<“Initialization” << endl; }

5.    ~Obj(void){ cout<< “Destroy” << endl; }

6.    void Initialize(void){cout << “Initialization” << endl; }

7.    void Destroy(void){ cout<< “Destroy” << endl; }

8.  };

9.  void UseMallocFree(void)

10. {

11.  Obj *a = (obj*)malloc(sizeof(obj)); // 申请动态内存

12.  a->Initialize(); // 初始化

13.  //…

14.  a->Destroy(); // 清除工作

15.  free(a); // 释放内存

16. }

17. void UseNewDelete(void)

18. {

19.  Obj *a = new Obj; // 申请动态内存并且初始化

20.  //…

21.  delete a; // 清除并且释放内存

22. }

复制代码

示例6 用malloc/free和new/delete如何实现对象的动态内存管理

  类Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于 malloc/free不能执行构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成初始化与清除工作。函数 UseNewDelete则简单得多。

  所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。

  既然new/delete的功能完全覆盖了malloc/free,为什么C 不把malloc/free淘汰出局呢?这是因为C 程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

  如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存 ”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

  7、内存耗尽怎么办?

  如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。

  (1)判断指针是否为NULL,如果是则马上用return语句终止本函数。例如:

1.  void Func(void)

2.  {

3.   A *a = new A;

4.   if(a == NULL)

5.   {

6.    return;

7.   }

8.   …

9.  }

复制代码

(2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。例如:

1.  void Func(void)

2.  {

3.   A *a = new A;

4.   if(a == NULL)

5.   {

6.    cout << “MemoryExhausted” << endl;

7.    exit(1);

8.   }

9.   …

10. }

复制代码

(3)为new和malloc设置异常处理函数。例如Visual C 可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C 使用手册。

  上述(1)(2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式(1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。

  很多人不忍心用exit(1),问:“不编写出错处理程序,让操作系统自己解决行不行?”

  不行。如果发生“内存耗尽”这样的事情,一般说来应用程序已经无药可救。如果不用exit(1) 把坏程序杀死,它可能会害死操作系统。道理如同:如果不把歹徒击毙,歹徒在老死之前会犯下更多的罪。

  有一个很重要的现象要告诉大家。对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致“内存耗尽”。我在Windows 98下用VisualC 编写了测试程序,见示例7。这个程序会无休止地运行下去,根本不会终止。因为32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98已经累得对键盘、鼠标毫无反应。

  我可以得出这么一个结论:对于32位以上的应用程序,“内存耗尽”错误处理程序毫无用处。这下可把Unix和Windows程序员们乐坏了:反正错误处理程序不起作用,我就不写了,省了很多麻烦。

  我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。

1.  void main(void)

2.  {

3.   float *p = NULL;

4.   while(TRUE)

5.   {

6.    p = new float[1000000];

7.    cout << “eatmemory” << endl;

8.    if(p==NULL)

9.     exit(1);

10.  }

11. }

复制代码

示例7试图耗尽操作系统的内存


  8、malloc/free 的使用要点

  函数malloc的原型如下:

1.  void * malloc(size_t size);

复制代码

用malloc申请一块长度为length的整数类型的内存,程序如下:

1.  int *p = (int *) malloc(sizeof(int) *length);

复制代码

我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。

  * malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。

  * malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int, float等数据类型的变量的确切字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。最好用以下程序作一次测试:

1.  cout << sizeof(char) <<endl;

2.  cout << sizeof(int) << endl;

3.  cout << sizeof(unsigned int)<< endl;

4.  cout << sizeof(long) <<endl;

5.  cout << sizeof(unsigned long)<< endl;

6.  cout << sizeof(float) <<endl;

7.  cout << sizeof(double) <<endl;

8.  cout << sizeof(void *) <<endl;

复制代码

在malloc的“()”中使用sizeof运算符是良好的风格,但要当心有时我们会昏了头,写出 p =malloc(sizeof(p))这样的程序来。

  * 函数free的原型如下:

1.  void free( void * memblock );

复制代码

为什么free 函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是 NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。

  9、new/delete 的使用要点

  运算符new使用起来要比函数malloc简单得多,例如:

1.  int *p1 = (int *)malloc(sizeof(int) *length);

2.  int *p2 = new int[length];

复制代码

这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如

1.  class Obj

2.  {

3.   public :

4.    Obj(void); // 无参数的构造函数

5.    Obj(int x); // 带一个参数的构造函数

6.    …

7.  }

8.  void Test(void)

9.  {

10.  Obj *a = new Obj;

11.  Obj *b = new Obj(1); // 初值为1

12.  …

13.  delete a;

14.  delete b;

15. }

复制代码

如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如

1.  Obj *objects = new Obj[100]; // 创建100个动态对象

复制代码

不能写成

1.  Obj *objects = new Obj[100](1);// 创建100个动态对象的同时赋初值1

复制代码

在用delete释放对象数组时,留意不要丢了符号‘[]’。例如

1.  delete []objects; // 正确的用法

2.  delete objects; // 错误的用法

复制代码

后者相当于delete objects[0],漏掉了另外99个对象。

  10、一些心得体会

  我认识不少技术不错的C /C程序员,很少有人能拍拍胸脯说通晓指针与内存管理(包括我自己)。我最初学习C语言时特别怕指针,导致我开发第一个应用软件(约1万行C代码)时没有使用一个指针,全用数组来顶替指针,实在蠢笨得过分。躲避指针不是办法,后来我改写了这个软件,代码量缩小到原先的一半。

  我的经验教训是:

  (1)越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的程序员。

  (2)必须养成“使用调试器逐步跟踪程序”的习惯,只有这样才能发现问题的本质。

C语言程序的内存分配方式

1.内存分配方式

  内存分配方式有三种:
  [1]从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
  [2]在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  [3]从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

2.程序的内存空间

  一个程序将操作系统分配给其运行的内存块分为4个区域,如下图所示。
  一个由C/C++编译的程序占用的内存分为以下几个部分,
  1、栈区(stack)—  由编译器自动分配释放 ,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等。其操作方式类似于数据结构中的栈。
  2、堆区(heap) —  一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  3、全局区(静态区)(static)—存放全局变量、静态数据、常量。程序结束后由系统释放。
  4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放。
  5、程序代码区—存放函数体(类成员函数和全局函数)的二进制代码。

  下面给出例子程序,

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

3.堆与栈的比较
  
3.1申请方式

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

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

  栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
  堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
  
3.4申请效率的比较
  栈由系统自动分配,速度较快。但程序员是无法控制的。
  堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
  另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是栈,而是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
  
3.5堆和栈中的存储内容

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

  char s1[] = "a";
  char *s2 = "b";
  a是在运行时刻赋值的;而b是在编译时就确定的;但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
  int main(){
  char a = 1;
  char c[] = "1234567890";
  char *p ="1234567890";
  a = c[1];
  a = p[1];
  return 0;
  }
  对应的汇编代码
  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,dwordptr [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读取字符,显然慢了。

3.7小结

  堆和栈的主要区别由以下几点:
  1、管理方式不同;
  2、空间大小不同;
  3、能否产生碎片不同;
  4、生长方向不同;
  5、分配方式不同;
  6、分配效率不同;
  管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
  空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。当然,这个值可以修改。
  碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构。
  生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
  分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由mallo函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
  分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
  从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址, EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
  虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
  无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果。

 

 

c++内存分配[整理]

2010-03-08 20:488人阅读评论(0)收藏举报

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

二、三者之间的区别
我们通过代码段来看看对这样的三部分内存需要怎样的操作和不同,以及应该注意怎样的地方。
例一:静态存储区与栈区

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仅仅能够存储某个存储空间的地址,数据“HelloWorld1字符串常量,所以存储在静态存储区。虽然通过p[2]可以访问到静态存储区中的第三个数据单元,即字符‘l’所在的存储的单元。但是因为数据“Hello World1”字符串常量,不可以改变,以在程序运行时,会报告内存错误。并且,如果此时对pp1输出的时候会发现pp1里面保存的地址是完全相同的。换句话说,在数据区只保留一份相同的数据(见图11)。

例二:栈区与堆区

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的话),我们将这种变量或数据成为全局变量或数据。但是,对于堆区的内存空间使用更加灵活,因为它允许你在不需要它的时候,随时将它释放掉,而静态存储区将一直存在于程序的整个生命周期中。
我们此专题仅仅是简要的分析了内存基本构成以及使用它们时需要注意的问题。对内存的分析和讨论将一直贯穿于我们以后所有的专题,这也就是为什么把它作为第一讲的原因。

1.以字符串形式出现的,编译器都会为该字符串自动添加一个0作为结束符,如在代码中写
  "abc",那么编译器帮你存储的是"abc/0"

2."abc"是常量吗?答案是有时是,有时不是。

  不是常量的情况:"abc"作为字符数组初始值的时候就不是,如
                 char str[] = "abc";
   
因为定义的是一个字符数组,所以就相当于定义了一些空间来存放"abc",而又因为
    字符数组就是把字符一个一个地存放的,所以编译器把这个语句解析为
    char str[3] = {'a','b','c'};
                 
又根据上面的总结1,所以char str[] = "abc";的最终结果是
    char str[4] = {'a','b','c','/0'};
   
做一下扩展,如果charstr[] = "abc";是在函数内部写的话,那么这里
    "abc/0"因为不是常量,所以应该被放在栈上。
 
 
是常量的情况:  "abc"赋给一个字符指针变量时,如
                 char* ptr = "abc";
   
因为定义的是一个普通指针,并没有定义空间来存放"abc",所以编译器得帮我们
    找地方来放"abc",显然,把这里的"abc"当成常量并把它放到程序的常量区是编译器
    最合适的选择。所以尽管ptr的类型不是const char*,并且ptr[0] ='x';也能编译
    通过,但是执行ptr[0] ='x';就会发生运行时异常,因为这个语句试图去修改程序
    常量区中的东西。
    记得哪本书中曾经说过char*ptr = "abc";这种写法原来在c++标准中是不允许的,
    但是因为这种写法在c中实在是太多了,为了兼容c,不允许也得允许。虽然允许,
    但是建议的写法应该是const char*ptr = "abc";这样如果后面写ptr[0] ='x'
    话编译器就不会让它编译通过,也就避免了上面说的运行时异常。
    又扩展一下,如果char*ptr = "abc";写在函数体内,那么虽然这里的"abc/0"
    放在常量区中,但是ptr本身只是一个普通的指针变量,所以ptr是被放在栈上的,
    只不过是它所指向的东西被放在常量区罢了。

3.数组的类型是由该数组所存放的东西的类型以及数组本身的大小决定的。
  chars1[3]char s2[4]s1的类型就是char[3]s2的类型就是char[4]
  也就是说尽管s1s2都是字符数组,但两者的类型却是不同的。

4.字符串常量的类型可以理解为相应字符常量数组的类型,
  "abcdef"的类型就可以看成是const char[7]

5.sizeof是用来求类型的字节数的。如int a;那么无论sizeof(int)或者是sizeof(a)
  是等于4,因为sizeof(a)其实就是sizeof(typeof a)

6.对于函数参数列表中的以数组类型书写的形式参数,编译器把其解释为普通
  的指针类型,如对于voidfunc(char sa[100],int ia[20],char *p)
 
sa的类型为char*ia的类型为int*p的类型为char*


7.
根据上面的总结,来实战一下:
 
对于char str[] ="abcdef";就有sizeof(str) == 7,因为str的类型是char[7]
 
也有sizeof("abcdef")== 7,因为"abcdef"的类型是const char[7]
 
对于char *ptr ="abcdef";就有sizeof(ptr) == 4,因为ptr的类型是char*
 
对于char str2[10]= "abcdef";就有sizeof(str2) == 10,因为str2的类型是char[10]
 
对于void func(charsa[100],int ia[20],char *p);
 
就有sizeof(sa) ==sizeof(ia) == sizeof(p) == 4
 
因为sa的类型是char*ia的类型是int*p的类型是char*



本文作者:黄邦勇帅
本文是学习C++的基础内容,指针是C 或C++所特有的,因此应熟练掌握指针的使用,本文集中介绍C 或C++中的各种
指针,包括指针数组,数组指针,常量(const)指针,指向指针的指针,尤其是对二维数组和指针进行了详细精辟的解释,
在读完本文的二维数组和指针的讲解之后,相信你就会对指针有一个车底的了解了。
本文内容完全属于个人见解与参考文现的作者无关,其中难免有误解之处,望指出更正。
声明:禁止抄袭本文,若需要转载本文请注明转载的网址,或者注明转载自“黄邦勇帅”。
主要参考文献:
1、C++.Primer.Plus.第五版.中文版[美]Stephen Prata 著孙建春韦强译人民邮电出版社2005 年5 月
2、C++.Primer.Plus.第四版.中文版Stanley B.Lippman、Barbara E.Moo 著李师贤等译人民邮电出版社2006 年3 月
3、C++.Primer.Plus.第三版.中文版Stanley B.Lippman 等著潘爱民张丽译中国电力出版社2002 年5 月
4、C++入门经典第三版[美]Ivor Horton 著李予敏译清华大学出版社2006 年1 月
5、C++参考大全第四版[美]Herbert Schidt 著周志荣朱德芳于秀山等译电子工业出版社2003 年9 月
6、21 天学通第四版C++ [美]Jesse Liberty 著康博创作室译人民邮电出版社2002 年3 月
第一部分:指针
11.1 基础
1.指针是一个变量,它存储着另一个变量或函数的地址,也就是说可以通过指针间接地引用变量。指针变量包含一个地
址,而且可以存储任何数据类型的内存地址,但指针变量却被声明为特定的数据类型,一个指向整型数据类型的指针
不能存储一个浮点型的变量地址。
2.指针声明的形式为,数据类型*指针变量名;其中*星号是指针运算符,例如int *x;声明x 为int 型指针.
11.2 指针运算符*和&地址运算符
1.&地址运算符是一元运算符,能反回它的操作数的内存地址.如y=&x;把变量x 的地址输入到y 中,它与x 的值无
关,比如x 的值为1000,而x 的地址为55 则,y 将接收到地址55.
2.*指针运算符是一元运算符,它是&运算符的相反形式,*运算符能反回位于其操作数所指定的地址的变量的值.例
如y = &x;z = *y;假设x 的值为1000,地址为55,则第二条语句说明z 的值为1000,*y 把由y 所指向的内存的地
址的变量x 的值赋给z。*运算符可理解为“在地址中”,则z=*y 可描术为“z 接收了在址址y 中的值。”,
3.其实可以把*y 当成一个变量来使用,即可以为*y 赋值等,例如*y=100;(*y)++;等,但要注意的是对*y 的操作相当
于是对此指针指向的地址中的变量的操作,即对*y=100 的赋值语句,相当于是x=100,而(*y)++则相当于x++。
11.3 指针的运算
0.指针只支持4 种算术运算符:++,――,+,-.指针只能与整数加减.指针运算的原则是:每当指针的值增加时,
它将指向其基本类型的下一个元素的存储单元.减少时则指向上一个元素的存储单元.
1.++,――运算符,假设int 型x 的地址为200,且int 型占4 个字节,定义int *p;p=&x;则p++的地址将是204,而
不是201,因为当指针p 的值增加时,它都将指向下一个int 型数据.减少时也是这样,如p――则,p 的地址将是196.
2.+,-,运算符,注意两个指针不能相加.例int *p;p=&x;假设x 的地址为200,则p+9 将的指针地址将是200+4*9=236,
即p 指向了从当前正指向的元素向下的第9 个元素.
3.两指针相减,同类型的一个指针减去另一个指针的值将是两个指针分开的基本类型的元素的个数.
11.4 指针和数组
1.在C++语言中使用没有下标的数组名会产生一个指向数组中第一个元素的指针.如char x[20];char *p;p=x;此语句
说明将x 数组的第一个元素的地址赋给指针p.
2.*(p+4)和x[4]两句都可以访问数组中第5 个元素,这里假设int x[33];int *p;p=x;因为p 是指向数组x 的第一个元
素地址的指针,而p+4 就是指向第五个元素的指针,而*(p+4)就是第五的个元素了.
3.p[i]语句相当于*(p+i)或x[i]即数组中第i+1 个元素的值,假设char x[20];char *p;p=x;
11.5 字符串常量
在C++中字符串常量会被存储到程序的串表中,所以语句char  *p;p=”hyong”;是合法的,该语句将字符串常量存储
在串表中的地址赋给指针变量p.
11.6 指针数组
声明形式int *p[10];该语句声明了10个指针数组,每个数组中存储一个整数值地址.p[2]=&x;语句为指针变量的第
三个元素赋予x变量的地址,现在要访问x变量的值需要编写*p[2].即访问指针数组第三个元素的地址指向的变量的值.
11.7 空指针
如果指针包含了空(0)值,就假定其未指向任何存储,这样能避免意外使用未初始化的指针.例:int *p=0;初始化为空
11.8 指向指针的指针
语句:
int **x,*y,z;
z=25;
y=&z;//z的地址赋给指针y
//注意y没有加星号*
x=&y;//指针y的地址赋给
//指向指针的指针x
cout<<**x ;//输出z的值25
cout<<*x ;//输出y的值或z的地址50
cout<<x; //输出y的地址&y或**x的值100
cout<<*y ;//输出z的值25
cout<<y; //输出z的地址&z或*y的值50
11.9 const指针
1.int x=1; const int *p=&x; 声明了一个指向int型常量的指针,也就是说不能用p来修改x的值,也就是说*p的值不能
被修改。即语句*p=2将是错误的。虽然p不能修改x的值,但可以通过变量x来修改x的值。p的声明并不代表p指
向的值实际上是一个常量,而只是对p而言这个值是常量。可以让p指向另一个地址,如int y=2;p=&y;这时p指向的
值为2,但指针p同样不能修改他所指向的变量y的值。
2.const int x=1; const int *p=&x;表明既不能用变量x来改变x的值,也不能用指针p来改变变量x的值。
const int x=1; int *p=&x;这样做将发生错误,因为如果把x的地址给了p,而指针p又修改了x的值时,这时x就违反
了是常量的规定,所以不允许这样做。
3.int x=1; int * const p=&x;这种方式使得指针p只能指向x的地址,即指针p的地址不能改变,但可以通过指针p来修
改p所指向的变量x的值。
4.int x=1; const int * const p=&x; 这种方式使得指针p既不能修改变量x的值,也不能改变p所指向的地址。
5.当const指针用作函数形参时int hyong(const int *p, int x)意味着,函数不能修改传递给指针p的变量的值。
11.10 指针与二维数组(对指针的透彻理解)
1、两条基本准则:
a、首先要明白,指针运算符的作用,我用一言以概之,你在哪里使用都不会错。指针运算符*的作用是求出*后面所指地
址里的值。因此只要*后面的变量表示的是一个地址就可以使用*运算符,来求出这个地址中的值,你不用管这个地址
的表示形式是怎样的,只要是地址就可以使用*来求出地址中的值。
b、[ ]这个运算符的的运算法则是,把左侧的地址加上[ ]内的偏移量然后再求指针运算,注意有[ ]运算符的地方就有个隐
含的指针,比如x[2]表示的就是将指针x偏移2个单位量后再求指针运算。也就说x[2]与*(x+2)是相等的。
2、对二维数组的讲解:
a、对一维数组地址的详细讲解:
比如一维数组a[5],众所周知,一维数组的数组名a表示的是第一个元素a[0]的地址,也就是说数组名与&a[0]是等价
的,因此数组名a的地址,并不是这个一维数组的数组的地址。那么&a,表示的又是什么呢?因为,a是一个一维数
组,所以&a表示的就是这个一维数组的地址,也就是说&a中的地址是一个包含有4个元素的一维数组的地址。就好
比int i中的&i,才表示的是这个变量i的地址一样。
b、对二维数组地址的讲解:
比如二维数组b[3][4],我们首先从一维开始分析,众所周知b[0],b[1]分别表示的是二维数组中第一行第一个元素和
第二行第二个元素的地址,也就是说b[0]是与&b[0][0]等价的,b[1]是与&b[1][0]等介的。因此数组名就与&b[0]等介
的,也就是说数组名表示的是二维数组中第一行所包含的一维数组的地址,说简单一点,就是说二维数组名是二维数
组中第一行的行地址。因此二维数组名b所包含的地址中包含有二维数组中第二维中元素的个数的一维数组,也就是
b的地址中包含一个含有4个元素的一维数组的地址(也就是所谓的数组的数组了)。
c、对二维数组中地址的相加运算的讲解:
**x的内存形式 *y的内存形式 z的内存形式
地址值
100            50
地址值
50            25
地址值
200          100
访问值25 的方式
有**x,*y,z
访问地址50 的方
式有*x,y,&z
访问值50 的
方式有*x,y
访问地址100的
方式有x,&y
访问值100的
方式有x
解释:指针y的值就是变量z的地址,字符y就是指指针y的值,
而单独的字符x表示指针y的地址100,x也表示指针x的
值,所以*x就表示指针y的地址的值50即指针y所指向的
变量z 的地址50,而**x 则表式,指针y 所指向的变量z
的地址的值25
指针理解方法:例如*y,如果y 表示变量z 的地址,那么*y就表
示指向变量z的地址的值.可以把*y当作变量z来处理
同样以b[3][4]为例来讲解,在上面讲到b[0]表示的是&b[0][0],因此对b[0]进行相加运算,比如b[0]+1,那么就将使
地址偏移一个单位,也就是地址被偏移到了&b[0][1]处,也就是b[0]+1表示的是b[0][1]的地址。上面也讲到数组名b,
表示的是一个一维数组的地址,因此对数组名进行偏移,比如b+1,则将使指针偏移一个一维数组的长度,也就是b+1,
将是&b[1]的地址,因此b+1 的地址,表示的是二维数组中第二行所包含的一维数组的地址,简单点就是第二行的行
地址。
d、对二维数组的指针运算:
*b
在上面讲解过,因为b表示的是二维数组中第一行所包含的一维数组的地址,因此*b=*(b+0)=*(&b[0])=b[0],所以*b
表示的是二维数组中第一行第一个元素的地址,即*b=&b[0][0],用语言来描术*(b+0)就是,把数组名的地址偏移0个
单位,然后再求这个地址所包含的值。在二维数组中,这个值就是指的b[0],因此这个值是与b[0][0]的地址相等的,
结果就是*(b+0)与b是相等的,这在多维数组中是个怪现象,至少本人无法理解这是为什么。因为对同一个地址,进
行指针运算得到了不同的结果,比如*b 和*b[0],因为b 和b[0]都是相同的地址,但对他们进行指针运算却得到了不
同的结果,*b得到了&b[0][0]的地址,而*b[0]得到了b[0][0]的值,这是对同一个地址进行指针运算却得到了不同的值,
对于这个问题,无法理解。
*(b+1)和*(b+0)
对于*(b+1)也和*(b+0)是同样的道理,*(b+1)=b[1]。我们再来看*b[1],因为*b[1]=*(b[1]+0)=*(&b[1][0])=b[1][0],可以
看出,这就是二维数组中第二行第一个元素的地址。
*(*(b+1)+1)
因为*(*(b+1)+1)=*(*(&b[1])+1)=*(b[1]+1)=*(&b[1][0]+1)=*(&b[1][1])=b[1][1],语言描术就是,b+1使地址偏移到了二
维数组中第二行所包含的一维数组的地址(或第二行的行地址),然后再对这个行地址求指针(或求值)运算,因此就得
到第二行第一个元素的地址,然后再对这个地址偏移一个单位,就得到第二行第二个元素的地址,再对这个地址进行
指针运算,就得到了这个元素的值,即b[1][1],其他的内容可以以止类推。
e、对二维数组的指针和[ ]的混合运算
在下面的指针和[ ]的混合计算中,要记住两点关键法则,记住了这两点在哪里计算都不会出错
a、对于像b[1]这样的地址,最好应表示为&b[1][0]再进行偏移计算,比如对于b[1]+1,这不是直接在对b[1]加1,也
就是b[1]+1 不等于b[2],因为b[1]表示的是第二行行1 个元素的地址,对其加1,应该表示的是第二行第二个元
素的地址,也就是&b[1][1],而b[2]则表示的是第二行第一个元素的地址,因此错误,所以在计算时应把b[1]转换
为&b[1][0]之后,才能直接进行地址的偏移,也就是说b[1]+1=&b[1][0]+1=&b[1][1],这样才能得到正确的结果,
并且不会出错。
b、对于有小括号的地方,一定不要省略小括号。比如(&b[1])[1]与&b[1][1]将表示的是不同的结果,第二个是显然的,
对于第一个(&b[1])[1]=*((&b[1])+1)=*(&b[1]+1)=*(&b[2])=b[2],可以看到,表示的是第3 行第1 个元素的地址,
因此这两个的结果是显然不一样的。因此对于(b+1)[1] 这样的运算, 不能省略小括号,即
(b+1)[1]=(&b[1])[1]=*((&b[1])+1)=*(&b[1]+1)=*(&b[2])=b[2],如果省略了小括号,则是(b+1)[1]=&b[1][1],这将是
不易发现的错误。因此这是两个完完全全不同的符案。
c、总结,指针和[ ]混合运算2点关键,
第1:应把是地址的[ ]运算,转换为地址的形式,比如b[1]应转换为&b[1][0]。因为只有这样才能进行直接的地址
相加运算,即&b[1][0]+1=&b[1][1],而b[1]+1不等于b[2]。
第2:有小括号的地方不能省略小括号,如(b+1)[1]=(&b[1])[1]=*((&b[1])+1)=*(&b[1]+1)=*(&b[2])=b[2],也&b[1][1]
是完全不同的。
(*(b+1))[1] ,(*b)[1]
最简单的理解方法为*(b+1)和语句b[1]等价,即(*(b+1))[1]和语句b[1][1]是相同的,也就是二组数组第2 行第2 个元
素的值b[1][1],理解方法2逐条解释,如上面的解释*(b+1)表示的是二维数组b的第二行第一个元素的地址,也就是
b[1],然后再与后面的[1]进行运算,就得到b[1][1]。或者(*(b+1))[1]=*((*(b+1))+1)=*(*(b+1)+1)这个语句上面解释过。
同理(*b)[1]=b[0][1]
*(b+1)[1]:
计算方法1把[ ]分解为指针来计算:因为[ ]运算符高于指针,因此应先计算[ ]再计算指针,因为[1]表示的是把左侧的
地址偏移1 个单位,再求指针,因此(b+1)[1]=*((b+1)+1) ,最后再计算一次指针运算,也就是
*(b+1)[1]=**((b+1)+1)=**(b+2)=*(*(b+2))=*b[2]=b[2][0],可以看到,最后表示的是b中第3行第一个元素的值。
计算方法2把指针化为[ ]来计算:*(b+1)[1]=*(&b[1])[1]=*(*(&b[1]+1))=**(&b[2])=*b[2]=b[2][0],注意*((&b[1])[1])表
达式中应把(&b[1])括起来,若不括起来,则[ ]运算符的优先级高于&运算符,因此(&b[1])[1]与&b[1][1]是不一样的,
后一个表示的是第二行第二个元素的地址,而头一个(&b[1])[1]则表示的是,对b 的第二行的行地址偏移1 个单位后
再求指针的结果,也就是*(&b[1]+1)=*(&b[2])=b[2],所以性质是不一样的。
(*b+1)[2]
计算方法1化简[ ]运算符:(*b+1)[2]=*((*b+1)+2)=*(*b+3)=*(&b[0][0]+3)=*(&b[0][3])=b[0][3],这里要注意*b表示的
是b[0]=&b[0][0],在计算时最好不要代入b[0]来计算,而应把b[0]转换为&b[0][0]后再计算,因为b[0]+3,很容易被
错误的计算为b[3],而实际上b[0]指向的是第一行第一个元素的地址,对其偏移3个单位应该是指向第一行第4个元
素的地址,即&b[0][3],而b[3],则指向了第3行第1个元素的地址,这是不同的。
计算方法2化简*运算符:(*b+1)[2]=(b[0]+1)[2]=(&b[0][0]+1)[2]=(&b[0][1])[2]=*(&b[0][1]+2)=*(&b[0][3])=b[0][3],注
意,在计算过程中小括号最好不要省略,省略了容易出错,因为[ ]运算符的优先给更高,如果省略了,某些地方将无
法计算。比如(&b[0][0]+1)[2]=(&b[0][1])[2],如果省略掉括号,则成为&b[0][1][2],这对于二维数组来讲,是无法计
算的。
(*(b+1))[5]
计算方法:(*(b+1))[5]=(*(&b[1]))[5]=(b[1])[5]=*(b[1]+5)=*(&b[1][0]+5)=*(&b[1][5])=b[1][5],结果等于第二行第6 个
元素的值。
f、注意,在二维或者多维数组中有个怪现象,比如对于多维数组a[n][m][i][j],那么这些地址是相同的,即数组名a, a[0],
a[0][0], a[0][0][0], &a[0][0][0][0],都是相同的地址。而且对数组名依次求指针运算将得到,比如*a=a[0],*a[0]=a[0][0],
*a[0][0]=a[0][0][0], *a[0][0][0]=a[0][0][0][0],可以看到,只有对最后这个地址求指针运算才真正得到了数组中的值,
因此对数组名求指针运算,要得到第一个元素的值,应该****a,也就是对4 维数组需要求4 次指针运算。同样可以
看到,对数组名进行的前三次指针运算的值都是相同的,即*a, **a, ***a和a的值都是&a[0][0][0][0]的值,这就是这
个怪问题,按理说对地址求指针应该得到一个值,但对多维数组求指针,却得到的是同一个地址,只是这些地址所包
含的内容不一样。
3、数组指针与二维数组讲解:
下面我们将以y[4]={1,2,3,4}这个一维数组为例来层层讲解,指针和数组的关系。
1、数组指针:
定义形式为:int (*p)[4];表示定义了一个指向多维数组的指针,即指针p指向的是一个数组,这个数组有4个元素,
对这个指针p 的赋值必须是有4 个int 元素的数组的地址,即只要包含有四个元素的数组的地址都能赋给指针p,不
管这个数组的行数是多少,但列数必须为4。即int y[4],x[22][4];都可以赋给指针p。赋值方式为p=&y或p=x,对于
&y和二维数组数组名前面已讲过,&y中的地址是一个包含有4个元素的一维数组的地址,二维数组名x也是一个含
有4 个元素的一维数组的地址,因此可以这样赋值。而这样赋值将是错误的p=y,或者p=x[0]; 因为y和x[0]的地址
只包含一个元素,他们不包含一个数组,因此出错。
2.注意()必须有,如果没有的话则,int *p[4];则是定义了一个指针数组,表示每个数组元素都是一个指针,即p[2]=&i;
指向一个int型的地址,而*p[2]则表示p[2]所指向的元素的值。
3.初始化数组指针p:
a、当把int y[4]赋给指针p时p=y将是错误的,正确的方式为p=&y因为这时编译器会检查赋给指针p的元素是否是
含有四个元素的数组,如果是就能正确的赋值,但语句p=y中的y代表的是数组y[4]第一行第一列的元素的地址
也就是&y[0]的地址,因此y 指向的地址只有一个元素,而指针p 要求的是有4 个元素的数组的地址,因此语句
p=y将出错。而&y也表示的是一个地址,因为数组名y表示的是&y[0]的地址,因此&y=&(&y[0])。可以把&y理
解为是数组y[4]的第一行的行地址,即&y包含了数组y[4]的第一行的所有元素,在这里&y包含有4个元素,因
则p=&y才是正确的赋值方法,在这里要注意,数组的某行的行地址是第本行的第一个元素的地址是相同的。
b、把x[22][4]赋给指针p有几种方法,方法一:p=x;我们这样来理解该条语句,首先x[0]表示的是二维数组x[22][4]
的第1行第一列元素的地址,这个地址包含一个元素,这是显而易见的,而数组名x也表示一个地址,但这个地
址包含的是一个数组(即数组的数组),这个数组是包含4个元素的数组,这4个元素就是数组x第一行的4个元
素,也就是说x表示的是数组x[22][4]的第1行的行地址,即数组名x就包含了数组x[22][4]第1行的4个元素,
因此这种赋值方式是正确的。这时指针p就相当于是数组名一样,比如p[2][1]访问的就是数组x的第3行的第2
个元素。
c、方法二:p=x+1或者p=&x[1];注意必须要有地址运算符&,同理语句&x[1]表示的是数组x[22][4]第2行的行地址,
因为x[1]表示的是数组x[22][4]第的第2行第1列的元素的地址,因此&x[1]表示的就是数组x的第2行的行地址,
因为&x[1]这个地址包含了一个数组,这个数组的起始地址是从x[1]这个地址开始的,这,即数组x[22][4]中x[1]
这一行的4 个元素。在这一行中包含了4 个元素。而x+1 本身就是指的x[1]的地址。这时指针p 的起始地址是
&x[1],所以p[0][1]不再是访问的x的第一行第二个元素,而是访问的x的第二行第二个元素。
d、注意,再次提示,数组的某行的行地址是与本行的第一个元素的地址是相同的。
4.访问成员:
a. 可以把数组指针p 当成数组名来理解,即可以像使用数组一样使用指针,比如p[1][2]访问数组x 中的第二行第
三个元素7。这种方法容易理解。
b. 访问数组元素的方法:记住二条法则*(p+1)与语句p[1]或者x[1]等价。
法则二:[ ]这个运算符的的运算法则是,把左侧的地址加上[ ]内的偏移量然后再求指针运算,注意有[ ]运算符
的地方就有个隐含的指针,比如x[2]表示的就是将指针x偏移2个单位量后再求指针运算。
5.int (*p)[4]与语句int x[][4]等价。
6.int (&p)[4]引用和指针有一些差别因为对引用赋值必须是变量的名字,所以int y[4];int (&p)[4]=y;是正确的而用&y将
得到一个错误的结果,同样int x[22][4]; int (&p)[4]=x[1];才是正确的,不能用x+1或者&x[1]来初始化引用。int (&p)[4]=x
也将得到一个错误的结果,只能是int (&p)[4]=x[0];对于引用来说语句p[0][1]将是错误的,只能是p[0],p[1],依次
访问p 所引用的对象的第一个元素的值和第二个元素的值,int (&p)[4]=x[1];p[1]将访问的是p 所引用的第二个元素
的值也就是数组x第二行第2个元素的值。
例:指针与二维数组int (*p)[4].
int main()
{ int x[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int y[4]={13,14,15,16};
int (*p)[4]=&y; //必须要有&运算符,因为这时的y只是一个有四个元素的名字和int i中的i是一样的只是个名字,所以必须要有&运算符
//p=y;   //错误y现在不是地址,而是一个名字。
cout<<p[0][1];//输出p所指向的第一行的第二个元素,即y的第二个元素14。
cout<<(*p)[1];//输出p所指向的第二个元素,即y的第二个元素14。
cout<<*p[0];//输出p所指向的第一行的第一个元素,即y的第一个元素13。
cout<<(*p)[5];//输出p所指向的第六个元素的值,这里超出数组y的范围,因为y只有一行元素,只有四个值,所以为随机值
cout<<*p[1];//输出p所指向的第二行的第一个元素,这里p只有一个数组元数y,所以输出随机值。
cout<<p[0]; //输出p所指向的第一行的地址,即&y的地址
cout<<y<<&y;//输出y和&y的地址,这里是相同的值
cout<<p[1]<<"\n";//输出p所指向的第二行的址址,这里超出数组y的范围,为随机值。
p=x; //把数组x的地址赋给指针p
cout<<p[1][1];//输出p所指向对象的第二行的第二个元素的值,即x的第二行第二个元素的值6。
cout<<(*p)[5];//输出p所指向的元素的第六个元素的值,即x的第二行第二个元素的值6。
cout<<*p[1];//输出p所指向元素的第二行第个一元素的值,即x的第二行第一个元素的值5。
cout<<p[1]<<"\n";//输出p所指向元素的第二行的地址,即x[1]的地址。
p=x+1;//或者p=&x[1];
cout<<p[1][1];//输出p所指对象的第二行第二个元素的值,这里p是指向x[1]的地址的,所以输出的将是x的第三行第二个元素的值10。
cout<<(*p)[5];//输出p所指对象的第六个元素的值,即p所指元素的第二行第二个元素,即x的第三行第二个元素10。cout<<*p[1];//输出p
所指向元素的第二行的第一个元素的值,即x的第三行第一个元系的值9。
}
第二部分:动态分配内存new关见字
1. 全局对象和局部对象的生命期都是严格定义的,程序员不能以任何方式改变他们的生命期。但是有时候需要创建一
些生命期能被程序员控制的对象,他的分配和释放可以根据程序运行中的操作来决定。这时就需要使用new操作符
了。
2. 动态分配内存,将从堆中分配内存,动态分配的存储区是在运行时确定的,动态分配的存储区可以随着需求而自动
扩大.
3. 局部变量一般存储在堆栈中,堆栈是先进后出的存储结构,而堆却不是.
4. 在C++中使用new动态分配内存,delete释放以前new分配的内存.
5. 使用new运算符系统将从空闲存储区中为对象分配内存,并返回一个指向该对象的指针即该对象的地址。new运算
符的特点是,用new运算符分配的对象没有名字,对该对象的操作都要通过指针间接地完成操作。例如new int,就
是从空闲存储区分配了一个int 型对象,但没法对这个对象进行操作,只是从存储区分配了这么一个空间。语句int
*p=new int 表示从空闲存储区分配一个int 对象并把这个对象的地址赋给p,现在p就是用new分配的int 对象的地
址,而*p就是那里的值。语句int i;int*p=&i;和int *p=new int都是将int变量的地址赋给了指针,但不同的是前句可
以用名称i和*p来访问该int型变量,而后句则只能用*p来访问该变量,也就是说p指向的内存没有名称。
6. 动态创建数组:int *p=new int [11];创建动态数组时必须有[]方括号,且里面要有创建的维数,但该数组的第一维可
以是一个复杂的表达式。访问地址中的内容的方法为*p 访问数组中的第一个元素,p[1]该问第二个元素,以此类推。
创建二组数组的例子:int (*p)[102]=new int [4][102]。
7. 动态创建对象的初始化:int *p=new int(102)该语句表明由p指向的新创建你对象被初始化为102。动态创建对象的
初始化方式为在类型名后面用一对括号来被始化。
8. 动态创建对象的默认初始化:方式为在类型名后面跟一对空的圆括号初始化, int *p=new int (); int *ps=new string();
cls *pc=new cls();第一条语句把对象的值初始化为0,第二条语句对于提供了默认构造函数的string类,不论程序是
要明确的不初始化,还是要求进行值初始化都会调用默认构造函数初始化该对象。而对于内置类型或没有默认构造
函数的类型,则采用不同的初始化方式就会有显著不同的差别。例如:int *p=new int; int *p=new int();第一条语句没
有被初始化,而第二条被初始化为0。
9. 用new动态创建的数组不能被初始化,不能创建初始化值集。
10.耗尽内存:如果程序用完了所有可用的内存,new表达式就有可能失败。如果new表达式无法获得要需要的内存空
间,系统将会抛出名为bad_alloc异常。
11.可以在一个函数内使用new运算符分配内存,而在另一个函数中使用delete释放内存空间.delete只能用于释放前次
使用new分配的空间,不要使用delete来释放不是new分配的内存,不要使用delete释放相同的内存两次,应使用
delete []来释放动态数组,例如:int *p=new int [10];delete [] p;删除数组必须要有[]方括号。delete不一定要用于new
的指针,例如int *p=new int ; int *x=p; delete x;将是合法的.如果指针的值为0,则在其上作delete操作是合法的,
但没有任务意义。
12.悬垂指针:执行delete p后只是把p所指向的地址的内容给释放掉了,并没有删掉指针p本身,还可以将p重新指向
到另一个新的内存块,因此p还指向原来他指向的对象的地址,然而p所指向的内容却已经被释放了,因此p不再
有效而变得没有定义了。这样的指针称为悬垂指针。悬垂指针往往导致程序错误而且很难检测出来。
13.静态联编:如果通过声明来创建数组,则在程序被编译时为他分配内存空间,不管程序是否使用数组,数组都在那
里,占用了内存。在编译时给数组分配内存被称为静态联编。意味着数组是在编译时加入到程序中的。但使用new
时,如果在运行阶段需要数组,则创建他,如果不需要,则不创建,还可以在程序运行时选择数组的长度,这被称
为动态联编。意味着数组是在程序运行时创建的,这种数组叫做动态数组,使用静态联编时必须在编写程序的时候
指定数组的长度,使用动态联编时,程序将在运行时确定数组的长度。
14.const常量对象的动态分配和回收:与其他常量一样,动态创建的const对象必须在创建时初始化,并且一经初始化,
其值不能再修改。例如:const int *p= new const int(111);删除方法为:delete p;尽管程序员不能修改const 对象的值,
但可以撤消对象本身。
15.注意:不能在空闲存储区上创建内置类型元素(除类数组string外)的const数组。因为我们不能初始化用new创建的
内置类型数组的元素。如const int *p=new const int [11];将是错误的。
16.注意:如果用new分配了资源,而没有用delete释放该资源的话,那么用new分配的资源将一指被占用。
17.常见错误:如果对某个指针动态分配了内存,又把另一个变量的地址付给这个指针,这时这个指针就指向了一个静
态地址,而不是原先的动态地址。如果再用delete删掉这个指针时就会出错,因为这时这个指针是指向静态地址的,
不能用delete删除一个指向静态地址的指针。比如int *p =new int(1),这时指针p指向一个动态内存可以对他进行
delete删除,但如果再执行语句int a=2; p=&a;这时就改变了指针指向的内容,使原先指向的动态内存地址变成了指
向现在的静态内存地址,如果这时对指针p进行delete操作就会出错,因为你在对一个静态指针静行删除,而delete
只能删除动态指针。
例:动态分配对象new
class hyong
{public:int a,b,c;   hyong (){a=b=c=0;}   hyong(int i){a=b=c=i;}   ~hyong(){cout<<"xigou"<<"\n";}  };
int main()
{ hyong *p=new hyong;   hyong *p1=new hyong(); cout<<p->a<<p1->a<<"\n"; //输出两个0,都调用默认构造函数初始化指针。
int *p2=new int;      int *p3=new int();      int *p4=new int(1); //new分配内存的初始化方式。对于类置类型来说p2没有被初
始化得到的是一个随机值,p3被初始化为0,p4被初始化为1。
cout<<*p2<<"\n"<<*p3<<"\n"<<*p4<<"\n";  //输出一个随机值,一个0,一个1
int i=10;  delete p4;  cout<<*p4<<"\n"; //p4现在是悬垂指针,delete只是释放掉了指针p4所指向地址的内容,但指针p4仍然指向原
来的地址,但没有内容,指针p4仍然可以再指向其他地址。
p4=&i; cout<<*p4<<"\n"; //可以对悬垂指针p4重新赋地址。
const int *p5=new int; const int *p6=new int(4)  ;cout<<*p5<<*p6<<"\n";//输出一个随机值和4,const常量必须在声明时初始化。
int *p7=new int[2];   p7[0]=5;p7[1]=6;    cout<<p7[0]<<p7[1]<<"\n";//定义动态数组,动态数组不能在声明时初始化。
//const int *p8=new int[2];  //错误,因为动态数组不能在声明时初始化,而const又必须要求在声明时初始化,发生冲突,出错。
delete p1;      delete p;    
//delete p,p1;   //注意,如果使用该语句将则只调用一次析构函数。
delete p2,p3,p4,p5,p6;    delete [] p7;
//int *p8=new int(9); int a=8;  p8=&a; delete p8;  //错误,现在的指针p8重新指向了一个静态的地址,用delete删掉一个静态地
址将发生错误。
}


内存管理与“三地址”的千丝万缕

  前记:我先谈一下一点很深的体会:你有时候会为某个知识点在那绞尽脑汁地想解决,但是我通过这阶段的学习知道,有时候想不通一个问题,也许是自己的知识积累不够造成的,所以这时请你不要急躁,迷茫。请你静下心来好好再把这个问题涉及的知识点看看,多找点书看,当你能够融会贯通的时候,你便能轻而易举地解决掉它了,不管你信不信,反正我是信了!祝有志于在嵌入式方面一展宏图的你能够事事顺心!

 

   开始我声明几点:

① 、这里的cpu都是以pentium(32位)或是8086(20位)为例子的,内存管理则指的是arm9或pentium

② 、由于知识所限,所提到的内容是自己的理解,所以很有可能会有错误,请大家指正。

③ 、我们先了解几个在linux或是arm中遇到的重要概念,这对于读内核或是熟悉arm的硬件架构很有好处。

A、字节对齐。16位的cpu其地址总线有20根,所以其最大寻址为1MB,也就是说此时的内部存储器(内存,不要告诉我说不知道内存是指什么啊)最大只能是1MB,大了cpu找不到其他的空间(这在当时,也就是1978年,1MB已经是很大了)。其地址范围应该是从0x00000~0xfffff(注意了,这便是传说中的物理地址了)!Intel是这样组织内存的:整个内存由两个512KB的存储体组成,一个为奇地址存储体(就是地址的最低位必须为1的存储空间),另外一个当然是偶地址存储体了(这个定义大家想必也能知道吧,这就叫举一反三),具体见图1。

    这样16位的cpu对存储器的访问就有按字节访问与按字访问。其中nBHE(知道n的意思吧?那是指低电平有效,这在arm的管脚图中随处可见)和A0来选择是否对奇/偶存储体操作。详细见表1。所以按字访问时就有对齐和非对齐两种方式,字的对齐方式要求起始地址为偶地址,比如要访问的地址为0x00010,那么用一个总线周期就可以往该地址写进或读出D0~D15的数据了。但是字的非对齐方式却是这样的:访问的地址为0x00011,那么第一个总线周期将往奇地址存储体操作(读或写)一个字节,等另一个总线周期才往0x00012操作高八位数据。所以这样就浪费了时间,因此编程时尽量用偶地址访问!!

图1:16位的cpu内存组织形式

A0

nBHE

操作方式

0

1

按字节访问偶地址存储体,数据在D0~D7传输

1

0

按字节访问奇地址存储体,数据在D8~D15传输

0

0

按字访问奇偶存储体,数据在D0~D15传输

1

1

不能访问任何存储体

表1:A0与nBHE的搭配方式

 下面讲的是32位的cpu的内存组织,其他我就不再详细讲解了,和16位的类似,只不过地址线成了32根,其内存组织形式见图2:总共有4个存储体,可以组成双字的形式,32位存储体要满足对8位、16位、32位各种规格的数据访问。同样,这也有字对齐与非对齐的方式,另外增加了双字对齐的,那就是要求起始地址是4的倍数(比如0Xxxxxx100),如果用奇地址进行字访问,或不是用4的倍数的地址进行双字访问,就会造成非对齐状态,这时需要2个总线周期完成字的传输或双字传输。比如要访问地址0x0000 0006的双字,cpu认为此双字在6、7、8、9这四个存储单元中,在传输时cpu会用第一个总线周期访问6、7存储体,再用一个总线周期访问8、9存储体。这同样浪费了时间,不可取!!以上就是字与双字对齐的解析,不知你明白了吗?

图2:32位cpu的内存组织形式(自己做的,有点粗糙,情大家谅解)

 B、逻辑地址:或称虚拟地址,这时程序员所看到的地址,也就是你编程时写的地址(这下知道了吧,原来自己经常写的地址竟然是虚的,没错!),注意!记住前提条件是在cpu的保护模式或是arm具有MMU并使用段与页机制的情况下。

C、线性地址:是在虚拟地址与物理地址之间的中间产物。说得具体点吧,拿个例子,有表达式x=2*y,而y=3*z,这里的x是指虚拟地址,y指的是线性地址,z是物理地址了,通过线性地址才有可能转换为物理地址。(上面拿个例子只是例子,并不代表什么,因为当没有页机制时,物理地址就等于线性地址)。

D、物理地址:物理存储器(通常指的是所说的内存)指由地址总线直接访问的存储空间,其地址就是物理地址。显然,地址总线的位数决定了物理存储器的最大容量。(请注意标大字体的“直接”两字,这就区别了外存与内存的区别了)

E、缺页中断机制:对于linux来讲,当进程需要执行的程序代码超出已经载入的代码范围时,linux就加载4KB的后续代码,不需要的代码就暂时不加载,这个工作由do_no_page函数调用bread_page函数完成。在第一次执行shell程序时就用到了这个机制。(因为shell是被动加载的)

好,开始正题!

 cpu的内存管理模式。对于8086,大家很容易知道利用段基址加逻辑地址(偏移地址)来找到物理地址的。而段基址又有分为4种,即DS、CS、SS、ES。每段的起始地址在开始时由操作系统定义了。这就是所谓的段机制。在8086的运行中,物理地址的形成因操作而异,取指令时cpu会用CS和IP的内容相加形成指令所在的单元的20位地址,而进行堆栈时会用SS与SP(或基址指针BP)的内容相加来形成20位物理地址。在这里我们扯到一个很好的很有用的概念,这将会在linux系统与arm中提到。内存分段为程序的浮动装配创造了条件:即要求程序不涉及物理地址,进一步讲,就是要求程序与段地址没有关系,而只与偏移地址有关系,这样的程序装在哪一段都能运行了,只需在程序中不涉及段地址就行了,凡是遇到转移指令或调用指令则都采用相对转移或相对调用。(当你看到一些说是与地址无关指令时再回头看这段文字你就很明白了)!

 而对于32位的cpu,那就相对麻烦些了!我们从cpu的运作来说(含操作系统,这里选了linux)吧!第一步,开机加电后,工作在实模式下,这时还是8086的模式,即地址是0x00000到0xfffff,cpu先从0xffff0(这是物理地址)执行,利用BIOS将存放在硬盘中的bootsect.s复制到内存中,并设置中断向量表。而后是执行bootsect.s,将setup.s和内核程序复制到内存中。执行setup.s转入保护模式(为什么成为保护模式呢?因为在这种模式下,cpu将程序分为了4个级别,linux只采用了2种级别,那就是内核特权级以及用户级,操作系统便是内核特权级,在这种模式下,能起到更好保护操作系统不被更改的目的,所以谓之“保护”,在这里提一下,一些病毒便是利用这种级别不同来侵入电脑),这时进入保护模式后执行head.s,创建页表,页目录,GDT、IDT、LDT等,一旦这些设置完,便有了虚拟地址了。

 设置完后就进入了内核的main.c函数中,第一个执行的函数是start_kernel的函数。OK!以后便是创建进程以及撤销进程的事了。你打开一个应用程序便是创建新进程,记住这些新进程都是进程0通过一个个子进程得来的,所以有了父进程与子进程的概念。程序运行时,cpu用虚拟地址逻辑地址访问主存(内存),在此过程中先通过硬件和软件找出虚拟地址到物理地址的关系,判断要访问的单元的内容是否已装入内存中,如果是,直接访问,否则说明出现虚拟地址往物理地址转换故障,此时会出现一个异常中断叫缺页中断,然后调用中断处理程序,把要访问的单元及有关数据从辅存(软盘或是硬盘)调入内存,覆盖掉主存中原有的一部分数据,然后将虚拟地址转换成物理地址。这样就实现了内存好像很大的样子,因为存储管理单元会将用到的程序以及数据一块块的调入内存中。

 现在有人也许会问:那怎么样才能实现三者之间的转换呢?

 逻辑地址转换成线性地址:48位的逻辑地址(程序常常以32位的偏移地址出现)包含了16位的段选择子以及32位的偏移地址,16位的段选择子用来索引全局描述符表(GDT)或局部描述符表(LDT),每一个表项有8字节的长度,里面包含了段基址,段界限(段限长)、段的属性。段选择子并不是全部都用来索引的,其中只有13位用来索引,所以可以索引8192项*2个符表=16384。然后呢,便用索引的符表项中的段基址加上偏移地址就得到线性地址了!更具体的操作过程是:在程序中,一个偏移量可能由立即数和另外一、二个寄存器给出的值构成,分段部件把各地址分量送到一个加法器中,形成有效地址,然后再经过另一个加法器和段基址相加,得到线性地址,同时嗨通过一个32位的减法器和段的界限值比较。检查是否越界。

线性地址转换成物理地址(保护模式下):现在有了线性地址,32位的线性地址中高十位用来索引页目录表,中间十位用来索引页表,最后十二位是页面内的偏移值,页目录每一个页目录项是四字节,共有4KB,即1024项。目录项里有页表的基地址(页表也是4KB,但有4个页表),而后加上线性地址的中间十位即页表的偏移地址得到索引的页表项,页表项里又含有页的基址,加上线性地址的最后十二位值得到物理存储器的物理地址,因为是十二位偏移,所以每页是4KB。这只是简单的过程,具体的大家还是要参考别的书籍了!

现在讲一下arm(以S3C2410为例)的内存管理机制,这和cpu很相似,要是你懂了cpu的,这就不在话下了,其中有些许的细节差别我略微的讲一下。

你发现有一点很明显的区别了吗?Pentium的所有外设是独立编址的,但是arm9却不是这样,它利用存储控制器将外设的访问地址统一起来,这其中就包括了NandFlash与SDRAM,这也正是嵌入式的魅力,利用4G的空间分配给所有外设,或者可以按pentium的观点来说便是“存储器与外设统一编址”(记住,两者不是一样的啊,这里只是形象称呼而已),这在嵌入式系统中可以节省很多元件,使体积大大减少,符合嵌入式系统的定义。

Arm9的页机制与intel的很类似,,用表格存储虚拟地址对应的物理地址。有一个细微的差别是页的大小分为三种:大页(64KB)、小页(4KB)、极小页(1KB)。(尽管pentium的页大小也是可调的,但是只有4KB与4MB供选择,这通过CR4控制寄存器的PSE位),另外arm9的页表还分为粗页表与细页表。其他的包括段寻址是一样的。顺便提一下,给定的虚拟地址转换成线性地址中,虚拟地址的各位代表的意义也是与pentium不一样的,这同样体现在线性地址转换成物理地址。这里就不再赘述了,可以参考arm9的书籍。

还有就是arm9的TLB与cache的问题,这里就不提了,这个很容易理解的,它与pentium一样。两者都是为了让cpu执行得更快而做出的设计。

现在讲一下使用arm9时一个很重要的细节。在通常的bootloader中,未开启MMU前,我们程序用的地址都是物理地址,开启之后就变成了虚拟地址,这就涉及到bootloader中有一些程序的物理地址必须与虚拟地址一样,这样才不致于开启MMU后程序执行错了。具体实现是在页表设置中实现。当数据和代码还在NandFlash中时,内存中没有数据,读取数据时便需要地址转换,这在bootloader的lowlevel_init.s程序中体现了,代码如下:

Ldr           r0,=SMRDATA

Ldr           r1,_TEXT_BASE

Sub     r0,r0,r1

Ldr           r1,BWSCON

Add    r2,r0,#13*4

这时内存中(SDRAM)没有数据,不能使用连接脚本程序里面的确定地址读取数据。

以上就是我最近学习linux的总结,这并不是最终版本,一旦有新的想法,我会及时更新这篇文章!这里估计有很多错误的,欢迎大家指正批评!




 

0 0
原创粉丝点击