/****************************************************************/

/*            学习是合作和分享式的!

/* Author:Atlas                    Email:wdzxl198@163.com

/*  转载请注明本文出处:

*   http://blog.csdn.net/wdzxl198/article/details/9078965

/****************************************************************/

以前学习笔记内容回顾:传送门,传送门2.

1.C++内存管理

1.1c语言和C++内存分配

1.2区分堆、栈、静态存储区

1.3控制C++的内存分配

1.4内存管理的基本要求

1.5常见的内存错误及对策


1.6指针与数组

C/C++中对数组和指针的掌握是程序员基本的技术功底,对于数组和指针的定义、操作、它们间的区别等内容,详见我另一篇文章《C/C++数组和指针详解》。

这里以学习《c++内存管理技术内幕》为主,对其中的疑点,不明白或者没有说明清楚的地方会在学习笔记中讲解说明,需要特殊讲解的会发单独的文章。

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

数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。<-------这句话说的不恰当!当数组通过new或者malloc创建是,数组内容是在堆中,如果有指针指向这个数组,则这个指针是在栈中。其他情况可以说是要么在静态区创建,或者在栈上申请。

数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

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

(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;

(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: // 指针…
   9: int len = strlen(a);
  10: char *p = (char *)malloc(sizeof(char)*(len+1));
  11: strcpy(p,a); // 不要用 p = a;
  12: if(strcmp(p, a) == 0) // 不要用 if (p == a)
  13: …

(3)计算内存容量

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

   1: char a[] = "hello world";
   2: char *p = a;
   3: cout<< sizeof(a) << endl; //12Bytes
   4: cout<< sizeof(a) << endl; //4 Bytes

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

   1: void Func(char a[100])
   2: {
   3:     cout<< sizeof(a) << endl; // 4Bytes,not 100Bytes
   4: }

1.7指针参数是如何传递内存?

如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如下示例中,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: }

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

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

   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: }

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

   1: char *GetMemory3(int num)
   2: {
   3:    char *p = (char *)malloc(sizeof(char) * num);
   4:    return p;
   5: }
   6:  
   7: void Test3(void)
   8: {
   9:    char *str = NULL;
  10:    str = GetMemory3(100);
  11:    strcpy(str, "hello");
  12:    cout<< str << endl;
  13:    free(str);
  14: }

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

   1: char *GetString(void)
   2: {
   3:    char p[] = "hello world";
   4:    return p; // 编译器将提出警告
   5: }
   6:  
   7: void Test4(void)
   8: {
   9:    char *str = NULL;
  10:    str = GetString(); // str 的内容是垃圾
  11:    cout<< str << endl;
  12: }

用调试器逐步跟踪Test4,发现执行str = GetString语句后str不再是NULL指针,但是str的内容不是“hello world”而是垃圾。

如果把上述示例改写成如下示例,会怎么样?

   1: char *GetString2(void)
   2: {
   3:    char *p = "hello world";
   4:    return p;
   5: }
   6:  
   7: void Test5(void)
   8: {
   9:    char *str = NULL;
  10:    str = GetString2();
  11:    cout<< str << endl;
  12: }

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

1.8杜绝“野指针”

“野指针”不是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:  
   7: void Test(void)
   8: {
   9:    A *p;
  10:    {
  11:     A a;
  12:     p = &a; // 注意 a 的生命期
  13:    }
  14:  
  15:    p->Func(); // p是“野指针”
  16:  
  17: }

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

ps:编写程序查看这个问题

上面的这个示例程序,这样写没有错误的,请看程序(dev-c++)

  1. #include <iostream>
  2. using namespace std;
  3. class A
  4. {
  5. public:
  6. A():b(5){}
  7. void func(void){cout<<"func of class A"<<"输出b="<<b<<endl;}
  8. private:
  9. int b;
  10. };
  11. //void test(A* p)
  12. //{
  13. //  A a;
  14. //  p = &a;
  15. //  cout<<"作用域内p地址"<<&p<<endl;
  16. //  cout<<"作用域内p指向的内容:"<<p<<endl;
  17. //}
  18. int main()
  19. {
  20. A *p;
  21. cout<<"指针p址初始地址"<<&p<<endl;
  22. cout<<"指针p址初始地址内容"<<p<<endl;;
  23. {
  24. A a;
  25. p = &a;
  26. cout<<"作用域内p地址"<<&p<<endl;
  27. cout<<"作用域内p指向的内容:"<<p<<endl;
  28. }
  29. //  test(p);
  30. cout<<"指针p地址:"<<&p<<endl;
  31. cout<<"指针p指向内容"<<p<<endl;
  32. p->func();
  33. system("PAUSE");
  34. return 0;
  35. }

输出为:

可见,编译器这种情况下是没有把{ }作为一个区域的,如果将程序中使用注释掉的代码,则结果是:

可见,虽然test函数传入的指针地址变了,但是函数结束后指针p释放,地址和指向的内容没有变,此时类A中数据不存了,所以输出的b是一个随机值。

程序编译没有报错!。。。

1.9malloc/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如何实现对象的动态内存管理,见示例:

   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:  
  10: void UseMallocFree(void)
  11: {
  12:    Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存
  13:    a->Initialize(); // 初始化
  14:    //…
  15:  
  16:    a->Destroy(); // 清除工作
  17:    free(a); // 释放内存
  18: }
  19:  
  20: void UseNewDelete(void)
  21: {
  22:    Obj *a = new Obj; // 申请动态内存并且初始化
  23:    //…
  24:    delete a; // 清除并且释放内存
  25: }

类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也一样。

1.10 当内存耗尽是怎么办?

如果在申请动态内存时找不到足够大的内存块,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:    …
  10: }

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

   1: void Func(void)
   2: {
   3:  
   4:    A *a = new A;
   5:    if(a == NULL)
   6:    {
   7:     cout << “Memory Exhausted” << endl;
   8:     exit(1);
   9:    }
  10:  
  11:    …
  12: }

(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下用Visual C++编写了测试程序,见示例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 << “eat memory” << endl;
   8:         if(p==NULL)
   9:         exit(1);
  10:     }
  11: }

1.11malloc和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连续操作两次就会导致程序运行错误。

    1.12new和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: }
       9:  
      10: void Test(void)
      11: {
      12:      Obj *a = new Obj;
      13:      Obj *b = new Obj(1); // 初值为1
      14:     
      15:      …
      16:     
      17:       delete a;
      18:      delete b;
      19: }

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

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

    不能写成:

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

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

       1: delete []objects; // Correct
       2: delete objects; // Wrong

    后者有可能引起程序崩溃和内存泄漏。

    PS:C++内存管理部分详解已经学习完,做好了学习笔记。接下来需要学习C++中健壮的一种指针:一种智能指针,或者说是smart pointers,以及它的特性等,这部分内容需要对C++理解比较深些,会花一些时间学习。


    参考文献:c++内存管理学习纲要;

    Edit 2013/6/9  11:12  by Atlas

C++内存管理学习笔记(3)的更多相关文章

  1. C++内存管理学习笔记(5)

    /****************************************************************/ /*            学习是合作和分享式的! /* Auth ...

  2. C++内存管理学习笔记(6)

    /****************************************************************/ /*            学习是合作和分享式的! /* Auth ...

  3. C++内存管理学习笔记(7)

    /****************************************************************/ /*            学习是合作和分享式的! /* Auth ...

  4. C++内存管理学习笔记(4)

    /****************************************************************/ /*            学习是合作和分享式的! /* Auth ...

  5. C++内存管理学习笔记(2)

    /****************************************************************/ /*            学习是合作和分享式的! /* Auth ...

  6. C++内存管理学习笔记(1)

    /****************************************************************/ /*            学习是合作和分享式的! /* Auth ...

  7. Linux内存管理学习笔记 转

    https://yq.aliyun.com/articles/11192?spm=0.0.0.0.hq1MsD 随着要维护的服务器增多,遇到的各种稀奇古怪的问题也会增多,要想彻底解决这些“小”问题往往 ...

  8. Unity移动游戏加载性能和内存管理-学习笔记

    前言 正在学习Doctor 张.鑫大佬的移动游戏加载性能和内存管理,内容非常非常的干,所以我烧了很多开水,边喝边看,一边拿小本几做好笔记 本文只是关于前2章的内容笔记,关于各种资源的加载耗时 纹理资源 ...

  9. IOS内存管理学习笔记

    内存管理作为iOS中非常重要的部分,每一个iOS开发者都应该深入了解iOS内存管理,最近在学习iOS中整理出了一些知识点,先从MRC开始说起. 1.当一个对象在创建之后它的引用计数器为1,当调用这个对 ...

随机推荐

  1. 使用HashMap或Hashset优化使用循环判断字符串中是否含有重复元素

    原本遇到判断字符串中是否含有重复元素的问题总是使用for循环遍历进行判断,这一方法则需要O(n3)的时间复杂度,如果本身方法处于几个循环中,就会指数倍增加时间复杂度.类似于如下代码: String[] ...

  2. 原子类的ABA问题

    原子类AtomicInteger的ABA问题 连环套路 从AtomicInteger引出下面的问题 CAS -> Unsafe -> CAS底层思想 -> ABA -> 原子引 ...

  3. tensorflow1.0 placeholder占位符

    import tensorflow as tf #(tf.float32,[2,2]) input1 = tf.placeholder(tf.float32) input2 = tf.placehol ...

  4. php-fpm运行原理

    来源:https://blog.csdn.net/sinat_38804294/article/details/94393621 一.php-fpm由来1.cgi (common gateway in ...

  5. Cent OS 7 搭建MySQL

    搭建数据库服务器 版本众多,但为了追求稳定选择的是5.7 在使用YUM REPOSITORY官方给出的版本如下: The MySQL Yum repository includes the lates ...

  6. java中ThreadLocal的使用

    文章目录 在Map中存储用户数据 在ThreadLocal中存储用户数据 java中ThreadLocal的使用 ThreadLocal主要用来为当前线程存储数据,这个数据只有当前线程可以访问. 在定 ...

  7. java中wait和sleep的区别

    文章目录 Wait和sleep的区别 唤醒wait和sleep java中wait和sleep的区别 在本篇文章中,我们将会讨论一下java中wait()和sleep()方法的区别.并讨论一下怎么使用 ...

  8. Angular 7开发环境配置

    目录 前言 一.搭建项目  1.安装Angular CLI  2.创建项目  3.集成Element Angular 二.设置路由  1.创建路由模块  2.导入.导出RouterModule类  3 ...

  9. 阿里云ECS安装JAVA+MYSQL+NGINX

    2019独角兽企业重金招聘Python工程师标准>>> 1.准备工作 查看linux版本: linux版本为CentOS 7.4 查看系统信息: 系统为64位 确保服务器系统处于最新 ...

  10. Linux 查看进程资源--ps、top命令

    1,ps命令 ps能够给出当前系统中进程的快照.它能捕获系统在某一事件的进程状态 命令参数:          a                                      显示所有进 ...