C++内存打点详解
当前位置:以往代写 > C/C++ 教程 >C++内存打点详解
2019-06-13

C++内存打点详解

C++内存打点详解

副标题#e#

伟大的Bill Gates 曾经讲错:

640K ought to be enough for everybody — Bill Gates 1981

措施员们常常编写内存打点措施,往往惶惶不安。假如不想触雷,独一的办理步伐就是发明所有暗藏的地雷而且解除它们,躲是躲不了的。本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存打点。

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。导致发生“野指针”。


#p#副标题#e#

【法则1】用malloc或new申请内存之后,应应当即查抄指针值是否为NULL。防备利用指针值为NULL的内存。

【法则2】不要健忘为数组和动态内存赋初值。防备将未被初始化的内存作为右值利用。

【法则3】制止数组或指针的下标越界,出格要当心产生“多1”可能“少1”操纵。

【法则4】动态内存的申请与释放必需配对,防备内存泄漏。

【法则5】用free或delete释放了内存之后,当即将指针配置为NULL,防备发生“野指针”。

3、指针与数组的比拟

C++/C措施中,指针和数组在不少处所可以彼此替换着用,让人发生一种错觉,觉得两者是等价的。

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

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

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

3.1 修改内容

#p#分页标题#e#

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

char a[] = “hello”;
a[0] = ‘X’;
cout << a << endl;
char *p = “world”; // 留意p指向常量字符串
p[0] = ‘X’; // 编译器不能发明该错误
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来较量。

// 数组…
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)

// 指针…
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len+1));
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
       示例3.2 数组和指针的内容复制与较量

#p#副标题#e#

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 *)。

char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12字节
cout<< sizeof(p) << endl; // 4字节
     示例3.3(a) 计较数组和指针的内存容量

void Func(char a[100])
{
  cout<< sizeof(a) << endl; // 4字节而不是100字节
}
     示例3.3(b) 数组退化为指针

4、指针参数是如何通报内存的?

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

void GetMemory(char *p, int num)
{
  p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
  char *str = NULL;
  GetMemory(str, 100); // str 仍然为 NULL
  strcpy(str, "hello"); // 运行错误
}
      示例4.1 试图用指针参数申请动态内存

短处出在函数GetMemory中。编译器老是要为函数的每个参数建造姑且副本,指针参数p的副本是 _p,编译器使 _p = p。假如函数体内的措施修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地点改变了,可是p丝毫未变。所以函数GetMemory并不能输出任何对象。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。

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

void GetMemory2(char **p, int num)
{
  *p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
  char *str = NULL;
  GetMemory2(&str, 100); // 留意参数是 &str,而不是str
  strcpy(str, "hello");
  cout<< str << endl;
  free(str);
}
      示例4.2用指向指针的指针申请动态内存

由于“指向指针的指针”这个观念不容易领略,我们可以用函数返回值来通报动态内存。这种要领越发简朴,见示例4.3。

char *GetMemory3(int num)
{
  char *p = (char *)malloc(sizeof(char) * num);
  return p;
}
void Test3(void)
{
  char *str = NULL;
  str = GetMemory3(100);
  strcpy(str, "hello");
  cout<< str << endl;
  free(str);
}
       示例4.3 用函数返回值来通报动态内存

#p#分页标题#e#

用函数返回值来通报动态内存这种要领固然好用,可是经常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数竣事时自动消亡,见示例4.4。

char *GetString(void)
{
  char p[] = "hello world";
  return p; // 编译器将提出告诫
}
void Test4(void)
{
  char *str = NULL;
  str = GetString(); // str 的内容是垃圾
  cout<< str << endl;
}
      示例4.4 return语句返回指向“栈内存”的指针

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

假如把示例4.4改写成示例4.5,会怎么样?

char *GetString2(void)
{
  char *p = "hello world";
  return p;
}
void Test5(void)
{
  char *str = NULL;
  str = GetString2();
  cout<< str << endl;
}
     示例4.5 return语句返回常量字符串

#p#副标题#e#

函数Test5运行固然不会堕落,可是函数GetString2的设计观念却是错误的。因为GetString2内的“hello world”是常量字符串,位于静态存储区,它在措施生命期内恒定稳定。无论什么时候挪用GetString2,它返回的始终是同一个“只读”的内存块。

5、杜绝“野指针”

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

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

char *p = NULL;
char *str = (char *) malloc(100);

(2)指针p被free可能delete之后,没有置为NULL,让人误觉得p是个正当的指针。

(3)指针操纵逾越了变量的浸染范畴。这种环境让人防不胜防,示例措施如下:

class A
{
  public:
   void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void)
{
  A *p;
  {
   A a;
   p = &a; // 留意 a 的生命期
  }
  p->Func(); // p是“野指针”
}

函数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。

class Obj
{
  public :
   Obj(void){ cout << “Initialization” << endl; }
   ~Obj(void){ cout << “Destroy” << endl; }
   void Initialize(void){ cout << “Initialization” << endl; }
   void Destroy(void){ cout << “Destroy” << endl; }
};
void UseMallocFree(void)
{
  Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存
  a->Initialize(); // 初始化
  //…
  a->Destroy(); // 排除事情
  free(a); // 释放内存
}
void UseNewDelete(void)
{
  Obj *a = new Obj; // 申请动态内存而且初始化
  //…
  delete a; // 排除而且释放内存
}
     示例6 用malloc/free和new/delete如何实现工具的动态内存打点

类Obj的函数Initialize模仿告终构函数的成果,函数Destroy模仿了析构函数的成果。函数UseMallocFree中,由于malloc/free不能执行结构函数与析构函数,必需挪用成员函数Initialize和Destroy来完成初始化与排除事情。函数UseNewDelete则简朴得多。

#p#分页标题#e#

所以我们不要诡计用malloc/free来完成动态工具的内存打点,应该用new/delete。由于内部数据范例的“工具”没有结构与析构的进程,对它们而言malloc/free和new/delete是等价的。

#p#副标题#e#

既然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语句终止本函数。譬喻:

void Func(void)
{
  A *a = new A;
  if(a == NULL)
  {
   return;
  }
  …
}

(2)判定指针是否为NULL,假如是则顿时用exit(1)终止整个措施的运行。譬喻:

void Func(void)
{
  A *a = new A;
  if(a == NULL)
  {
   cout << “Memory Exhausted” << endl;
   exit(1);
  }
  …
}

(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措施员们乐坏了:横竖错误处理惩罚措施不起浸染,我就不写了,省了许多贫苦。

我不想误导读者,必需强调:不加错误处理惩罚将导致措施的质量很差,千万不行因小失大。

void main(void)
{
  float *p = NULL;
  while(TRUE)
  {
   p = new float[1000000];
   cout << “eat memory” << endl;
   if(p==NULL)
    exit(1);
  }
}

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

8、malloc/free 的利用要点

函数malloc的原型如下:

void * malloc(size_t size);

用malloc申请一块长度为length的整数范例的内存,措施如下:

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个字节。最好用以下措施作一次测试:

cout << sizeof(char) << endl;
cout << sizeof(int) << endl;
cout << sizeof(unsigned int) << endl;
cout << sizeof(long) << endl;
cout << sizeof(unsigned long) << endl;
cout << sizeof(float) << endl;
cout << sizeof(double) << endl;
cout << sizeof(void *) << endl;

在malloc的“()”中利用sizeof运算符是精采的气势气魄,但要当心有时我们会昏了头,写出 p = malloc(sizeof(p))这样的措施来。

* 函数free的原型如下:

void free( void * memblock );
#p#分页标题#e#

为什么free函数不象malloc函数那样巨大呢?这是因为指针p的范例以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。假如p是NULL指针,那么free对p无论操纵几多次都不会出问题。假如p不是NULL指针,那么free对p持续操纵两次就会导致措施运行错误。

9、new/delete 的利用要点

运算符new利用起来要比函数malloc简朴得多,譬喻:

int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];

这是因为new内置了sizeof、范例转换和范例安详查抄成果。对付非内部数据范例的工具而言,new在建设动态工具的同时完成了初始化事情。假如工具有多个结构函数,那么new的语句也可以有多种形式。譬喻

class Obj
{
  public :
   Obj(void); // 无参数的结构函数
   Obj(int x); // 带一个参数的结构函数
   …
}
void Test(void)
{
  Obj *a = new Obj;
  Obj *b = new Obj(1); // 初值为1
  …
  delete a;
  delete b;
}

假如用new建设工具数组,那么只能利用工具的无参数结构函数。譬喻

Obj *objects = new Obj[100]; // 建设100个动态工具

不能写成

Obj *objects = new Obj[100](1);// 建设100个动态工具的同时赋初值1

在用delete释放工具数组时,寄望不要丢了标记‘[]’。譬喻

delete []objects; // 正确的用法
delete objects; // 错误的用法

后者相当于delete objects[0],遗漏了别的99个工具。

10、一些心得体会

我认识不少技能不错的C++/C措施员,很少有人能拍拍胸脯说通晓指针与内存打点(包罗我本身)。我最初进修C语言时出格怕指针,导致我开拓第一个应用软件(约1万行C代码)时没有利用一个指针,全用数组来顶替指针,实在蠢笨得过度。躲避指针不是步伐,厥后我改写了这个软件,代码量缩小到原先的一半。

我的履历教导是:

(1)越是怕指针,就越要利用指针。不会正确利用指针,必定算不上是及格的措施员。

(2)必需养成“利用调试器慢慢跟踪措施”的习惯,只有这样才气发明问题的本质。

    关键字:

在线提交作业