第一篇:C与C 经典面试题(内存泄露)汇总
C、C++语言面试题2007-07-15 18:57 1.已知strcpy 函数的原型是:
char *strcpy(char *strDest, const char *strSrc);其中strDest 是目的字符串,strSrc 是源字符串。不调用C++/C 的字符串库函数,请编写函数 strcpy 答案:
char *strcpy(char *strDest, const char *strSrc){ if(strDest == NULL || strSrc == NULL)return NULL;if(strDest == strSrc)return strDest;char *tempptr = strDest;while((*strDest++ = *strSrc++)!= ‘ ’);return tempptr;}
2.已知类String 的原型为: class String { public: String(const char *str = NULL);// 普通构造函数 String(const String &other);// 拷贝构造函数 ~ String(void);// 析构函数
String & operate =(const String &other);// 赋值函数 private: char *m_data;// 用于保存字符串 };请编写String 的上述4 个函数。答案:
String::String(const char *str){ if(str == NULL)//strlen在参数为NULL时会抛异常才会有这步判断 { m_data = new char[1];m_data[0] = ' ';} else { m_data = new char[strlen(str)+ 1];strcpy(m_data,str);} }
String::String(const String &other){ m_data = new char[strlen(other.m_data)+ 1];strcpy(m_data,other.m_data);} String & String::operator =(const String &other){ if(this == &other)return *this;delete []m_data;m_data = new char[strlen(other.m_data)+ 1];strcpy(m_data,other.m_data);return *this;} String::~ String(void){ delete []m_data;}
3.简答
3.1 头文件中的ifndef/define/endif 干什么用? 答:防止该头文件被重复引用。
3.2#include
答:对于#include
答:C++语言支持函数重载,C 语言不支持函数重载。函数被C++编译后在库中的名字与C 语言的不同。假设某个函数的原型为: void foo(int x, int y);该函数被C 编译器编译后在库中的名字为_foo,而C++ 编译器则会产生像_foo_int_int 之类的名字。
C++提供了C 连接交换指定符号extern“C”来解决名字匹配问题。
3.4 一个类有基类、内部有一个其他类的成员对象,构造函数的执行顺序是怎样的。(Autodesk)
答:先执行基类的(如果基类当中有虚基类,要先执行虚基类的,其他基类则按照声明派生类时的顺序依次执行),再执行成员对象的,最后执行自己的。3.5 请描述一个你熟悉的设计模式(Autodesk)3.6 在UML 中,聚合(aggregation)和组合(composition)有什么区别 Autodesk)答案:聚合关系更强,类似于pages 和book 的关系;组合关系要弱,类似于books和bookshelf 的关系。
3.7C#和C++除了语法上的差别以外,有什么不同的地方?(Autodesk,Microsoft)答案:(C#我只是了解,不是很精通)
(1)c#有垃圾自动回收机制,程序员不用担心对象的回收。(2)c#严禁使用指针,只能处理对象。如果希望使用指针,则仅可在unsafe 程序块中能使用指针。(3)c#只能单继承。(4)必须通过类名访问静态成员。不能像C++中那样,通过对象访问静态成员。(5)在子类中覆盖父
类的虚函数时必须用关键字override,覆盖父类的方法要用关键字new 3.8ADO.net 和ADO 的区别?
答案:实际上除了“能够让应用程序处理存储于DBMS 中的数据“这一基本相似点外,两者没有太多共同之处。但是ADO 使用OLE DB 接口并基于微软的COM 技术,而ADO.NET 拥有自己的ADO.NET 接口并且基于微软的.NET 体系架构。众所周知.NET 体系不同于COM 体系,ADO.NET 接口也就完全不同于ADO和OLE DB 接口,这也就是说ADO.NET 和ADO是两种数据访问方式。ADO.net 提供对XML 的支持。3.9 New delete 与malloc free 的区别(Autodesk)区别: 1.new 自动计算需要分配的空间,而malloc要手动计算分配的空间。2.new 是类型安全的,而malloc不是。
如: int * p = new double[3];//编译时能够检查出错误
int* p = malloc(n*sizeof(double));//编译时不能够检查出错误 3.malloc/free需要库文件支持,而new/delete不用。4.operator new 对应于malloc, 但operator new 可以重载,可以自定义内存分配策略,甚至不做内存分配。但malloc做不到。5.new 能为非内部数据分配动态内存,而malloc不能。
3.9.2那为什么有了new/delete,还要malloc/free呢?
3.10 #define DOUBLE(x)x+x(Autodesk)i = 5*DOUBLE(10); i 是多少?正确的声明是什么? 答案:i 为60。正确的声明是#define DOUBLE(x)(x+x)3.11 有哪几种情况只能用intialization list 而不能用assignment?(Autodesk)答案:当类中含有const、reference 成员变量;基类的构造函数都需要参数;类中含有其他类的成员对象,而该类的构造函数都需要参数。3.11 C++是不是类型安全的?(Autodesk)答案:不是。两个不同类型的指针之间可以强制转换。C#是类型安全的。3.12 main 函数执行以前,还会执行什么代码?(Autodesk)答案:全局对象的构造函数会在main 函数之前执行。
3.13 描述内存分配方式以及它们的区别。(Autodesk , Microsoft)答案:1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
(3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
3.14 什么是虚拟存储器?virtual memory 怎样映射到physical memory?页面替换算法有哪些?(Microsoft)见操作系统 p238 页。掌握的页面替换算法NRU(最近不用),FIFO,第二次机会页面替换算法,LRU(最近最少使用算法)
3.15 有四个同样的容器,里面装满了粒数相同的药丸,正常药丸的质量为m,变质药丸的质量为m+1,现在已知这四个容器中,有一个装的全是变质药丸,用电子秤只称一次,找出哪个容器装的是变质药丸(Microsoft)
答案:把四个容器依次编号为1、2、3、4,然后从中分别取出1、2、3、4 粒药丸,称这10 粒药丸的质量,如果质量为10m+1,则说明第一个容器装的是变质药丸,如果为10m+2 则说明第二个装的变质药丸,依次类推。
3.16 比较一下C++中static_cast 和 dynamic_cast 的区别。(Autodesk)
dynamic_casts在帮助你浏览继承层次上是有限制的。它不能被用于缺乏虚函数的类型上,它被用于安全地沿着类的继承关系向下进行类型转换。如你想在没有继承关系的类型中进行转换,你可能想到static_cast 3.17 Struct 和class 的区别(Autodesk)答案:struct 中成员变量和成员函数默认访问权限是public,class 是private 3.18 当一个类A 中没有生命任何成员变量与成员函数,这时sizeof(A)的值是多少,如果不是零,请解释一下编译器为什么没有让它为零。(Autodesk)
答案:肯定不是零。我举个反例,如果是零的话,声明一个class A[10]对象数组,而每一个对象占用的空间是零,这时就没办法区分A[0],A[1]…了 3.18 这道题我又找到答案了,为了确保每个对象都拥有唯一的地址!可查阅http://blog.csdn.net/smonster/articles/432767.aspx 3.19 在8086 汇编下,逻辑地址和物理地址是怎样转换的?(Intel)
答案:通用寄存器给出的地址,是段内偏移地址,相应段寄存器地址*10H+通用寄存器内地址,就得到了真正要访问的地址。
3.20 描述一下C++的多态(microsoft)
答案:C++的多态表现在两个部分,一个是静态连编下的函数重载,运算符重载;动态连编下的虚函数、纯虚函数(抽象类)
4.写出BOOL,int,float,指针类型的变量a 与零的比较语句。答案:
BOOL : if(!a)int : if(a == 0)float : const EXPRESSION EXP = 0.000001 if(a < EXP && a >-EXP)pointer : if(a!= NULL)
5.请说出const 与#define 相比优点 答案:
(1)const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
(2)有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
6.简述数组与指针的区别
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
(1)修改内容上的差别 char a[] = “hello”;a[0] = ‘X’;
char *p = “world”;// 注意p 指向常量字符串
p[0] = ‘X’;// 编译器不能发现该错误,运行时错误(2)用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为
同类型的指针。
char a[] = “hello world”;char *p = a;cout<< sizeof(a)<< endl;// 12 字节 cout<< sizeof(p)<< endl;// 4 字节 计算数组和指针的内存容量 void Func(char a[100]){ cout<< sizeof(a)<< endl;// 4 字节而不是100 字节 }
7.类成员函数的重载、覆盖和隐藏区别 答案:
成员函数被重载的特征:
(1)相同的范围(在同一个类中);(2)函数名字相同;(3)参数不同;
(4)virtual 关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:(1)不同的范围(分别位于派生类与基类);(2)函数名字相同;(3)参数相同;
(4)基类函数必须有virtual 关键字。
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
8.There are two int variables: a and b, don’t use “if”, “? :”, “switch” or other judgement statements, find out the biggest one of the two numbers.答案:((a + b)+ abs(a – b))/ 2
9.如何打印出当前源文件的文件名以及源文件的当前行号? 答案:
cout << __FILE__;cout<<__LINE__;__FILE__和__LINE__是系统预定义宏,这种宏并不是在某个文件中定义的,而是由编译器定义的。
10.main 主函数执行完毕后,是否可能会再执行一段代码,给出说明?
答案:可以,可以用_onexit 注册一个函数,它会在main 之后执行int fn1(void), fn2(void), fn3(void), fn4(void);
void main(void){ String str(“zhanglin”);_onexit(fn1);_onexit(fn2);_onexit(fn3);_onexit(fn4);printf(“This is executed first.n”);} int fn1(){ printf(“next.n”);return 0;} int fn2(){ printf(“executed ”);return 0;} int fn3(){ printf(“is ”);return 0;} int fn4(){ printf(“This ”);return 0;} The _onexit function is passed the address of a function(func)to be called when the program terminates normally.Successive calls to _onexit create a register of functions that are executed in LIFO(last-in-first-out)order.The functions passed to _onexit cannot take parameters.11.如何判断一段程序是由C 编译程序还是由C++编译程序编译的? 答案:
#ifdef __cplusplus cout<<“c++”;#else cout<<“c”;#endif
12.文件中有一组整数,要求排序后输出到另一个文件中 答案:
void Order(vector
{ int count = data.size();int tag = false;for(int i = 0;i < count;i++){ for(int j = 0;j < count1;j++){ if(data[j] > data[j+1]){ tag = true;int temp = data[j];data[j] = data[j+1];data[j+1] = temp;} } if(!tag)break;} } void main(void){ vector out.close();} 13.排序方法比较(intel) 排序方法平均时间 最坏时间 辅助存储 1:直接插入排序:插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。 2:起泡排序:依次比较相邻的两个数,将小数放在前面,大数放在后面(时间复杂度为O(n^2) n2/2-n/2,) 3:选择排序:每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。选择排序是不稳定的排序方法(4)快速排序;通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序 n log n(5)堆排序; 二叉树 堆排序的最坏时间复杂度为O(nlog2n)。(6)归并排序; 14.一个链表的结点结构 struct Node { int data;Node *next;};typedef struct Node Node;(1)已知链表的头结点head,写一个函数把这个链表逆序(Intel)Node * ReverseList(Node *head)//链表逆序 { if(head == NULL || head->next == NULL)return head;Node *p1 = head;Node *p2 = p1->next;Node *p3 = p2->next;p1->next = NULL;while(p3!= NULL){ p2->next = p1;p1 = p2;p2 = p3;p3 = p3->next;} p2->next = p1;head = p2; return head;}(2)已知两个链表head1 和head2 各自有序,请把它们合并成一个链表依然有序。Node * Merge(Node *head1 , Node *head2){ if(head1 == NULL)return head2;if(head2 == NULL)return head1;Node *head = NULL;Node *p1 = NULL;Node *p2 = NULL;if(head1->data < head2->data){ head = head1;p1 = head1->next;p2 = head2;} else { head = head2;p2 = head2->next;p1 = head1;} Node *pcurrent = head;while(p1!= NULL && p2!= NULL){ if(p1->data <= p2->data){ pcurrent->next = p1;pcurrent = p1;p1 = p1->next;} else { pcurrent->next = p2;pcurrent = p2;p2 = p2->next;} } if(p1!= NULL)pcurrent->next = p1;if(p2!= NULL)pcurrent->next = p2; return head;}(2)已知两个链表head1 和head2 各自有序,请把它们合并成一个链表依然有序,这次要求用递归方法进行。(Autodesk)答案: Node * MergeRecursive(Node *head1 , Node *head2){ if(head1 == NULL)return head2;if(head2 == NULL)return head1;Node *head = NULL;if(head1->data < head2->data){ head = head1;head->next = MergeRecursive(head1->next,head2);} else { head = head2;head->next = MergeRecursive(head1,head2->next);} return head;} 15.分析一下这段程序的输出(Autodesk)class B { public: B(){ cout<<“default constructor”< B Play(B b){ return b;} int main(int argc, char* argv[]){ B temp = Play(5);return 0;} 请自己执行一下看看。 16.写一个函数找出一个整数数组中,第二大的数(microsoft)答案: const int MINNUMBER =-32767;int find_sec_max(int data[] , int count)//类似于1 4 4 4这样的序列将认为1是第二大数 { int maxnumber = data[0];int sec_max = MINNUMBER;for(int i = 1;i < count;i++){ if(data[i] > maxnumber){ sec_max = maxnumber;maxnumber = data[i];} else { if(data[i] > sec_max)sec_max = data[i];} } return sec_max;} 写一个在一个字符串中寻找一个子串第一个位置的函数 这个题目的一般算法比较简单我就不给出了,如果要求高效率的话请参见数据结构中的KMP 算法,不过在笔试时间有限情况下,写出那个算法还是挺难的。 一、#include “filename.h”和#include 的区别 #include “filename.h”是指编译器将从当前工作目录上开始查找此文件 #include 是指编译器将从标准库目录中开始查找此文件 二、头文件的作用 加强安全检测 通过头文件可能方便地调用库功能,而不必关心其实现方式 三、* , &修饰符的位置 对于*和&修饰符,为了避免误解,最好将修饰符紧靠变量名 四、if语句 不要将布尔变量与任何值进行比较,那会很容易出错的。 整形变量必须要有类型相同的值进行比较 浮点变量最好少比点,就算要比也要有值进行限制 指针变量要和NULL进行比较,不要和布尔型和整形比较 五、const和#define的比较 const有数据类型,#define没有数据类型 个别编译器中const可以进行调试,#define不可以进行调试 在类中定义常量有两种方式 1、在类在声明常量,但不赋值,在构造函数初始化表中进行赋值; 2、用枚举代替const常量。 六、C++函数中值的传递方式 有三种方式:值传递(Pass by value)、指针传递(Pass by pointer)、引用传递(Pass by reference) void fun(char c)//pass by value void fun(char *str)//pass by pointer void fun(char &str)//pass by reference 如果输入参数是以值传递的话,最好使用引用传递代替,因为引用传递省去了临时对象的构造和析构 函数的类型不能省略,就算没有也要加个void 七、函数体中的指针或引用常量不能被返回 Char *func(void) { char str[]=”Hello Word”; //这个是不能被返回的,因为str是个指定变量,不是一般的值,函数结束后会被注销掉 return str; } 函数体内的指针变量并不会随着函数的消亡而自动释放八、一个内存拷贝函数的实现体 void *memcpy(void *pvTo,const void *pvFrom,size_t size) { assert((pvTo!=NULL)&&(pvFrom!=NULL)); byte *pbTo=(byte*)pvTo;//防止地址被改变 byte *pbFrom=(byte*)pvFrom; while(size-->0) *pbTo++ = *pbForm++; return pvTo; } 九、内存的分配方式 分配方式有三种,请记住,说不定那天去面试的时候就会有人问你这问题 1、静态存储区,是在程序编译时就已经分配好的,在整个运行期间都存在,如全局变量、常量。 2、栈上分配,函数内的局部变量就是从这分配的,但分配的内存容易有限。 3、堆上分配,也称动态分配,如我们用new,malloc分配内存,用delete,free来释放的内存。 十、内存分配的注意事项 用new或malloc分配内存时,必须要对此指针赋初值。 用delete 或free释放内存后,必须要将指针指向NULL 不能修改指向常量的指针数据 十一、内容复制与比较 //数组…… char a[]=”Hello Word!”; char b[10]; strcpy(b,a); if(strcmp(a,b)==0) {} //指针…… char a[]=”Hello Word!”; char *p; p=new char[strlen(a)+1]; strcpy(p,a); if(strcmp(p,a)==0) {} 十二、sizeof的问题 记住一点,C++无法知道指针所指对象的大小,指针的大小永远为4字节 char a[]=”Hello World!” char *p=a; count< count< 而且,在函数中,数组参数退化为指针,所以下面的内容永远输出为4 void fun(char a[1000]) { count< } 十三、关于指针 1、指针创建时必须被初始化 2、指针在free 或delete后必须置为NULL 3、指针的长度都为4字节 4、释放内存时,如果是数组指针,必须要释放掉所有的内存,如 char *p=new char[100]; strcpy(p,”Hello World”); delete []p;//注意前面的[]号 p=NULL; 5、数组指针的内容不能超过数组指针的最大容易。 如: char *p=new char[5]; strcpy(p,”Hello World”);//报错 目标容易不够大 delete []p;//注意前面的[]号 p=NULL; 十四、关于malloc/free 和new /delete l malloc/free 是C/C+的内存分配符,new /delete是C++的内存分配符。 l 注意:malloc/free是库函数,new/delete是运算符 l malloc/free不能执行构造函数与析构函数,而new/delete可以 l new/delete不能在C上运行,所以malloc/free不能被淘汰 l 两者都必须要成对使用 l C++中可以使用_set_new_hander函数来定义内存分配异常的处理 如何查出内存泄漏和非法操作的BUG(在Release版本下)? 检查window(release)下的内存泄漏 1、放置关键字 assert() 2、生成map 文件。它并不往可执行文件exe 中添加任何东西,只是在编译的时候将各个函数入口地址记录在后缀为.map的文件中,程序崩溃的时候可以得到一个EIP地址,通过地址知道崩溃所在函数 3、可以设置断点,在希望设置断点的地方加入 _ASM int 3 4、可以通过编译时的汇编程序看出 5、采用第三方工具 十五、C++的特性 C++新增加有重载(overload),内联(inline),Const,Virtual四种机制 重载和内联:即可用于全局函数,也可用于类的成员函数; Const和Virtual:只可用于类的成员函数; 重载:在同一类中,函数名相同的函数。由不同的参数决定调用那个函数。函数可要不可要Virtual关键字。和全局函数同名的函数不叫重载。如果在类中调用同名的全局函数,必须用全局引用符号::引用。 覆盖是指派生类函数覆盖基类函数 函数名相同; 参数相同; 基类函数必须有Virtual关键字; 不同的范围(派生类和基类)。 隐藏是指派生类屏蔽了基类的同名函数相同 1、函数名相同,但参数不同,此时不论基类有无Virtual关键字,基类函数将被隐藏。 2、函数名相同,参数也相同,但基类无Virtual关键字(有就是覆盖),基类函数将被隐藏。 内联:inline关键字必须与定义体放在一起,而不是单单放在声明中。 Const:const是constant的缩写,“恒定不变”的意思。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。 1、参数做输入用的指针型参数,加上const可防止被意外改动。 2、按值引用的用户类型做输入参数时,最好将按值传递的改为引用传递,并加上const关键字,目的是为了提高效率。数据类型为内部类型的就没必要做这件事情;如: 将void Func(A a)改为void Func(const A &a)。 而void func(int a)就没必要改成void func(const int &a); 3、给返回值为指针类型的函数加上const,会使函数返回值不能被修改,赋给的变量也只能是const型变量。如:函数const char*GetString(void);char *str=GetString()将会出错。而const char *str=GetString()将是正确的。 4、Const成员函数是指此函数体内只能调用Const成员变量,提高程序的键壮性。如声明函数 int GetCount(void)const;此函数体内就只能调用Const成员变量。 Virtual:虚函数:派生类可以覆盖掉的函数,纯虚函数:只是个空函数,没有函数实现体; 十六、extern“C”有什么作用? Extern “C”是由C++提供的一个连接交换指定符号,用于告诉C++这段代码是C函数。这是因为C++编译后库中函数名会变得很长,与C生成的不一致,造成C++不能直接调用C函数,加上extren “c”后,C++就能直接调用C函数了。 Extern “C”主要使用正规DLL函数的引用和导出 和 在C++包含C函数或C头文件时使用。使用时在前面加上extern “c” 关键字即可。 十七、构造函数与析构函数 派生类的构造函数应在初始化表里调用基类的构造函数; 派生类和基类的析构函数应加Virtual关键字。 不要小看构造函数和析构函数,其实编起来还是不容易。 #include class Base { public: virtual ~Base(){ cout<< “~Base” << endl;} }; class Derived : public Base { public: virtual ~Derived(){ cout<< “~Derived” << endl;} }; void main(void) { Base * pB = new Derived;// upcast delete pB; } 输出结果为: ~Derived ~Base 如果析构函数不为虚,那么输出结果为 ~Base 十八、#IFNDEF/#DEFINE/#ENDIF有什么作用 仿止该头文件被重复引用 转http://bbs.csai.cn/bbs/view.asp?Id={8DB2582C-97E1-428A-AD9C-358BCD02C506 Debug Assertion Failed! 这个错误的原因可能是数组越界或出现了野指针.内存释放(或资源释放)时出现了错误 这是个很一般性的错误, 就像Windows报告说执行了非法操作一样.凭此信息无法判 断具体错误位置, 只能靠自己跟踪了 当出现这个错误的时候,我重新检查了自己new的指针,由于对于这块很发怵,所以把所有new的指针都避掉。但还是出现同样的问题。 后来又上网搜了一下,在《VC++6.0中内存泄漏检测》这篇文章中提到,“可用于被多态继承的基类其析构函数应当有virtual修饰“的法则(一不小心就忘了写virtual ^_^),”,哈哈,我也违反了,后来加上virtual后就没有问题了。 下面把那篇文章贴上来以供自己日后查看。 VC++6.0中内存泄漏检测(转) VC++6.0中内存泄漏检测 这篇文章是对2004-09-02日发表的《VC++6.0中简单的内存泄漏检测事例代码》(已经删除)的更新.对C++代码而言,内存泄漏问题虽然有诸多方法避免,但实际代码编写的时候,或出于自信或出于复杂性的考虑,常常还会用到原始的operator new,这不可避免的会带来内存泄漏的可能,不久前本人因为违反了”可用于被多态继承的基类其析构函数应当有virtual修饰“的法则(一不小心就忘了写virtual ^_^),导致了内存泄漏,因此我觉得出于安全考虑,在代码中加入内存泄漏检查机制还是很必要的,也因为这次的内存泄漏事件促使我写出这一篇文章.VC++中本身就有内存泄漏检查的机制,你可以在向导生成的支持MFC的工程中看到如下代码: #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif 通过它们,你能非常容易的发现代码中的内存泄漏,但是如果手工将这个功能移植到非MFC工程中去是很繁琐的一件事,另外它还有一个bug,在多线程并发调用这个DEBUG_NEW时会导致系统级错误,因此本人在此重写了这个功能,将以下的debug_new.h和debug_new.cpp添加到工程中,并在需要检测的cpp中#include ”debug_new.h“和main中一开始处加入REG_DEBUG_NEW宏即可.1.debug_new.h 源代码 /************************************************************************/ /* comment: 此文件与debug_new.cpp配合使用,用于在调试期发现内存泄漏 */ /* 仅在VC++编译器中适用(包括Intel C++,因为它使用了相同的库)*/ /* 作者: 周星星*/ /* 版权申明: 无,可任意 使用,修改 和 发布 */ /************************************************************************/ /* sample #include #include ”debug_new.h“ // + using namespace std; int main(void) { REG_DEBUG_NEW;// + char* p = new char[2]; cout << ”--End--“ << endl; return 0; } 在VC++ IDE中按F5调试运行将会在Output窗口的Debug页看到类似如下的提示: Dumping objects-> d:test.cpp(10): {45} normal block at 0x003410C8, 2 bytes long.Data: < > CD CD Object dump complete.如果不出现如上提示请Rebuild All一次.*/ #ifndef _DEBUG_NEW_H_ #define _DEBUG_NEW_H_ #ifdef _DEBUG #undef new extern void _RegDebugNew(void); extern void* __cdecl operator new(size_t, const char*, int); extern void __cdecl operator delete(void*, const char*, int); #define new new(__FILE__, __LINE__) #define REG_DEBUG_NEW _RegDebugNew(); #else #define REG_DEBUG_NEW #endif // _DEBUG #endif // _DEBUG_NEW_H_ 2.debug_new.cpp 源代码 /************************************************************************/ /* comment: 此文件与debug_new.h配合使用,用于在调试期发现内存泄漏 */ /* 仅在VC++编译器中适用(包括Intel C++,因为它使用了相同的库)*/ /* 作者: 周星星*/ /* 版权申明: 无,可任意 使用,修改 和 发布 */ /************************************************************************/ //#include ”debug_new.h“ #ifdef _DEBUG #include #include class _CriSec { CRITICAL_SECTION criSection; public: _CriSec(){ InitializeCriticalSection(&criSection);} ~_CriSec(){ DeleteCriticalSection(&criSection);} void Enter(){ EnterCriticalSection(&criSection);} void Leave(){ LeaveCriticalSection(&criSection);} } _cs; void _RegDebugNew(void) { _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG | _CRTDBG_LEAK_CHECK_DF); } void* __cdecl operator new(size_t nSize, const char* lpszFileName, int nLine) { // comment 1: MFC中提供的debug new虽然加了锁,但我在实际测试的时候发现多线程并发 // 调用的时候还是抛出了系统错误,所以我在这里加了一个线程互斥量.// comment 2: debug new和debug delete之间需不需要互斥我并不知道,保险起见,我同样 // 加了线程互斥量.// comment 3: 按照C++标准规定,在operator new失败后应当调用set_new_handler设置的 // 函数,但是MSDN中却说”头文件new中的set_new_handler是stub的,而应该使 // 用头文件new.h中的_set_new_handler“,这简直是滑天下之大稽.// 以下是VC++6.0中的set_new_handler定义: // new_handler __cdecl set_new_handler(new_handler new_p) // { // assert(new_p == 0);// cannot use stub to register a new handler // _set_new_handler(0); // return 0; // } // 所以我也无计可施,只能舍弃set_new_handler的作用._cs.Enter(); void* p = _malloc_dbg(nSize, _NORMAL_BLOCK, lpszFileName, nLine); _cs.Leave(); return p; } void __cdecl operator delete(void* p, const char* /*lpszFileName*/, int /*nLine*/) { _cs.Enter(); _free_dbg(p, _CLIENT_BLOCK); _cs.Leave(); } #endif 3.事例代码 #include #include ”debug_new.h“ using namespace std; int main(void) { REG_DEBUG_NEW; char* p = new char[2]; p[0] = 'A'; p[1] = 'B'; cout << ”--End--" << endl; return 0; } 4.结果输出 在VC++ IDE中按F5调试运行将会在Output窗口的Debug页看到类似如下的提示: …… Dumping objects-> d:test.cpp(10): {45} normal block at 0x003410C8, 2 bytes long.Data: Object dump complete.…… C语言变量声明及内存分配 一个由c/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。程序结束时由编译器自动释放。 2、堆区(heap)— 在内存开辟另一块存储区域。一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。 3、全局区(静态区)(static)—编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(BSS)。-程序结束后由系统释放 4、文字常量区 —常量字符串就是放在这里的,程序结束后由系统释放。 5、程序代码区 —存放函数体的二进制代码。例子程序 这是一个前辈写的,非常详细 #include #include int a = 0; //全局初始化区 char *p1; //全局未初始化区 void main(){ int b=1;// 栈 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”优化成一个地方 system(“pause”); } =============== 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 = new char[10]; 如p2 =(char *)malloc(10);p2 = new char[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,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读取字符,显然慢了。3.7小结 堆和栈的主要区别由以下几点: 1、管理方式不同; 2、空间大小不同; 3、能否产生碎片不同; 4、生长方向不同; 5、分配方式不同; 6、分配效率不同; 管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。 空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。当然,这个值可以修改。 碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构。 生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。 分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由mallo函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。 分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。 从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。 虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。 无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果。 4.new/delete与malloc/free比较 从C++角度上说,使用new分配堆空间可以调用类的构造函数,而malloc()函数仅仅是一个函数调用,它不会调用构造函数,它所接受的参数是一个unsigned long类型。同样,delete在释放堆空间之前会调用析构函数,而free函数则不会。 class Time{ public: Time(int,int,int,string); ~Time(){ cout<<“call Time’s destructor by:”< } private: int hour; int min; int sec; string name; }; Time::Time(int h,int m,int s,string n){ hour=h; min=m; sec=s; name=n; cout<<“call Time’s constructor by:”< } int main(){ Time *t1; t1=(Time*)malloc(sizeof(Time)); free(t1); Time *t2; t2=new Time(0,0,0,“t2”); delete t2; system(“PAUSE”); return EXIT_SUCCESS; } 结果: call Time’s constructor by:t2 call Time’s destructor by:t2 从结果可以看出,使用new/delete可以调用对象的构造函数与析构函数,并且示例中调用的是一个非默认构造函数。但在堆上分配对象数组时,只能调用默认构造函数,不能调用其他任何构造函数。 delphi和c指针与内存操作指南 1.动态变量 对于动态分内存的变量,使用System单元的下面两个函数: procedure New(var P: Pointer);//为变更分配内存 procedure Dispose(var P: Pointer);//释放内存 其中P为变量地址 示例: type Str18 = string[18];var P: ^Str18;begin New(P);P^ := 'Now you see it...';Dispose(P);{ Now you don't...} end;///////////////////////////// Delphi里自己管理内存的两对函数 new(),dispose()和getmem(),freemem() delphi的指针 大家都认为,C语言之所以强大,以及其自由性,很大部分体现在其灵活的指针运用上。因此,说指针是C语言的灵魂,一点都不为过。同时,这种说法也让很多人产生误解,似乎只有C语言的指针才能算指针。Basic不支持指针,在此不论。其实,Pascal语言本身也是支持指针的。从最初的Pascal发展至今的Object Pascal,可以说在指针运用上,丝毫不会逊色于C语言的指针。 以下内容分为八部分,分别是 一、类型指针的定义 二、无类型指针的定义 三、指针的解除引用 四、取地址(指针赋值) 五、指针运算 六、动态内存分配 七、字符数组的运算 八、函数指针 一、类型指针的定义。对于指向特定类型的指针,在C中是这样定义的: int *ptr;char *ptr;与之等价的Object Pascal是如何定义的呢? var ptr : ^Integer;ptr : ^char;其实也就是符号的差别而已。 二、无类型指针的定义。C中有void *类型,也就是可以指向任何类型数据的指针。Object Pascal为其定义了一个专门的类型:Pointer。于是,ptr : Pointer;就与C中的 void *ptr;等价了。 三、指针的解除引用。要解除指针引用(即取出指针所指区域的值),C的语法是(*ptr),ObjectPascal则是ptr^。 四、取地址(指针赋值)。取某对象的地址并将其赋值给指针变量,C的语法是 ptr = &Object;Object Pascal则是 ptr:= @Object;也只是符号的差别而已。 五、指针运算。在C中,可以对指针进行移动的运算,如: char a[20];char *ptr=a;ptr++;ptr+=2;当执行ptr++;时,编译器会产生让ptr前进sizeof(char)步长的代码,之后,ptr将指向a[1]。ptr+=2;这句使得ptr前进两个sizeof(char)大小的步长。同样,我们来看一下Object Pascal中如何实现: var a: array [1..20] of Char;ptr: PChar;//PChar可以看作^Char begin ptr:= @a;Inc(ptr);// 这句等价于C的ptr++;Inc(ptr, 2);//这句等价于C的ptr+=2;end;只是,Pascal中,只允许对有类型的指针进行这样的运算,对于无类型指针是不行的。 六、动态内存分配。C中,使用malloc()库函数分配内存,free()函数释放内存。如这样的代码: int *ptr, *ptr2;int i;ptr=(int*)malloc(sizeof(int)* 20);ptr2 = ptr;for(i=0;i<20;i++){ *ptr =i;ptr++;} free(ptr2);Object Pascal中,动态分配内存的函数是GetMem(),与之对应的释放函数为FreeMem()(传统Pascal中获取内存的函数是New()和 Dispose(),但New()只能获得对象的单个实体的内存大小,无法取得连续的存放多个对象的内存块)。因此,与上面那段C的代码等价的Object Pascal的代码为: var ptr, ptr2 : ^integer;i: integer;begin GetMem(ptr, sizeof(integer)*20);//这句等价于C的ptr=(int*)malloc(sizeof(int)*20);ptr2:= ptr;//保留原始指针位置 for i:= 0 to 19 do begin ptr^ := i;Inc(ptr);end;FreeMem(ptr2);end;对于以上这个例子(无论是C版本的,还是Object Pascal版本的),都要注意一个问题,就是分配内存的单位是字节(BYTE),因此在使用GetMem时,其第二个参数如果想当然的写成20,那么就会出问题了(内存访问越界)。因为GetMem(ptr, 20);实际只分配了20个字节的内存空间,而一个整形的大小是四个字节,那么访问第五个之后的所有元素都是非法的了(对于malloc()的参数同样)。 七、字符数组的运算。C语言中,是没有字符串类型的,因此,字符串都是用字符数组来实现,于是也有一套str打头的库函数以进行字符数组的运算,如以下代码: char str[15];char *pstr;strcpy(str “teststr”);strcat(str, “_testok”);pstr =(char*)malloc(sizeof(char)* 15);strcpy(pstr, str);printf(pstr);free(pstr);而在Object Pascal中,有了String类型,因此可以很方便的对字符串进行各种运算。但是,有时我们的Pascal代码需要与C的代码交互(比如:用Object Pascal的代码调用C写的DLL或者用ObjectPascal写的DLL准备允许用C写客户端的代码)的话,就不能使用String类型了,而必须使用两种语言通用的字符数组。其实,Object Pascal提供了完全相似C的一整套字符数组的运算函数,以上那段代码的Object Pascal版本是这样的: var str : array [1..15] ofchar;pstr: PChar;//Pchar 也就是 ^Char begin StrCopy(@str, 'teststr');//在C中,数组的名称可以直接作为数组首地址指针来用 //但Pascal不是这样的,因此str前要加上取地址的运算符 StrCat(@str, '_testok');GetMem(pstr, sizeof(char)* 15);StrCopy(pstr, @str);Write(pstr);FreeMem(pstr);end; 八、函数指针。在动态调用DLL中的函数时,就会用到函数指针。假设用C写的一段代码如下: typedef int(*PVFN)(int);//定义函数指针类型 int main(){ HMODULE hModule =LoadLibrary(“test.dll”);PVFN pvfn = NULL;pvfn =(PVFN)GetProcAddress(hModule, “Function1”);pvfn(2);FreeLibrary(hModule);} 就我个人感觉来说,C语言中定义函数指针类型的typedef代码的语法有些晦涩,而同样的代码在ObjectPascal中却非常易懂: type PVFN =Function(para: Integer): Integer;var fn : PVFN;//也可以直接在此处定义,如:fn: function(para:Integer):Integer;hm: HMODULE;begin hm:= LoadLibrary('test.dll');fn:= GetProcAddress(hm, 'Function1');fn(2);FreeLibrary(hm);end;Delphi中指针功能非常强大,所有c中能实现的指针Delphi中都能实现.上面认为Delphi指针不是强项的只是一种误解(或者对指针的机制一知半解).由于Pascal语言的限制, 用Delphi的指针时很多情况下需要强制类型转换.Delphi中提供了很多指针类型, 而且非常方便的是你可以自定义自己的指针类型.一个经验: 要掌握一种数据类型并且能够灵活应用,一个比较好的办法是别考虑什么类型是什么名字, 而只需要考虑这种类型的变量将占用多少字节.凡是字节数相同的类型都可以认为是同一类型 :-), 提供不同类型只是为了编译器能够更方便的查找错误而已.比如: Integer, Pointer, PChar, TSmallPoint甚至 array [0..4] of Char 你都可以把他们当成是同一类型加以使用(有了这种思路, 可以实现很大的程序灵活性和代码高效性).所以我很不理解的是JAVA中不支持指针(因此我也认为用JAVA绝对不可能写出很高效的程序, 而且会有很多C/C++/DELPHI中用一句话可以完成的工作在JAVA中需要用一个复杂过程, 消耗很多额外内存才能达到相同目的).就事论事, 根据你的问题在Delphi中和C中的解决方案没什么两样.delphi指针简单入门: 看一个指针用法的例子: 1 var 2 X, Y: Integer;// X and Y 整数类型 3 P: ^Integer;// P 指向整数类型的指针 4 begin 5 X :=17;// 给 X 赋值 6 P := @X;// 把 x的地址赋给p 7 Y := P^;// 取出p所指向的数值赋给y 8 end; 第二行定义了两个变量X,y.第三行声明了p是指向整数类型的指针;意味着p能够指向x或者y的地址.第五行赋给x值,第六行把x的地址赋给p.最 后通过p指向的变量赋值给y.此时,x和y有相同的值.操作符@用来取出变量的地址,也可以取出过程和函数的地址.而符号^有两个目标, 当它出现在类型定义的前面时如 ^typename 表示指向这种类型的指针;当它出现在指针变量后边时 如 point^ 返回指针指向的变量的值; 理解指针比较容易理解面向对象的pascal语言,因为指针经常在幕后操作.任何要求动态分配大的内存空间的类型可以用指针类型.例如 ,long-string变量,实际在使用指针进行操作.另外一些高级的编程技术需要使用指针类型.有时指针是适应object pascal严格的类型限制的唯一方法.同过一个通用的指针类型,通过类型转换成不同的指针类型,如下面的例子: type PInteger = ^Integer;var R: Single;I: Integer;P: Pointer;//通用的指针 PI: PInteger;begin P := @R;//取出R的内存地址 PI := PInteger(P);//把通用类型转换成指向整数类型的指针 I := PI^;end; 当然了,实数和整数的存储格式不同.这种赋值是把原始的二进制数据从R拷贝到I,而不进行转换.保留字nil是一个特殊的常量可以赋给任何指针类型,当nil赋給一个指针时,指针什么也不指向,是一个空指针.@操作符返回变量的内存中的存储地址,或者是过程函数方法;1.如果变量,@X返回的是x的地址。如果编译选项{$T-}没有打开,着返回的事一个通用的指针,如果编译选项打开了,着返回的是x的类型对应的指 针.2.如果是例程(过程函数),@F返回的是F的入口点,@F的类型是一个指针。 3.当@用在类的方法中时,则方法的名称必须有类名,例如@TMyclass.Dosomething 指针指向TMyclass的dosomething方法。 当一个过程变量在赋值语句的左边时,编译器期望一个过程值在赋值语句的右边。这种赋值使得左边的变量可以指向右边定义的过程或者函数 入口点。换句话说,可以通过该变量来引用声明的过程或者函数,可以直接使用参数的引用。 var F: function(X: Integer): Integer;I: Integer;function SomeFunction(X: Integer): Integer;...F := SomeFunction;// 给f赋值 I := F(4);// 调用所指向的函数 在赋值语句中,左边变量的类型决定了右边的过程或者方法指针解释。 var F, G: function: Integer;I: Integer;function SomeFunction: Integer;...F := SomeFunction;// 给f赋值 G := F;// 把F的值拷贝给G I := G;// 调用函数 第一句获得函数的入口,第二句将指针复制,第三句获得函数的返回值。 有时候还可以这样使用 if F = MyFunction then...;在这里,F的出现导致一个函数调用;编译器调用F指向的函数,然后调用Myfunction,比较结果。这个规则是无论何时一个过程变量(procedural variable)出现在一个表达式中,它表示调用所指向的函数或者过程。有时F指向一个过程(没有返回值),或者f指向一个需要参 数的函数,则前面的语句会产生一个编译错误。要比较F和Myfunction需要用 if @F = @MyFunction then...;@F把F转换成一个包含地址的无类型的指针变量,@myfunction返回myfunction的地址。 获得一个过程变量的内存地址使用@@。例如,@@F返回F的地址。 @操作符通常把一个无类型的指针值赋给一个过程变量,例如: var StrComp: function(Str1, Str2: PChar): Integer;...@StrComp := GetProcAddress(KernelHandle, 'lstrcmpi');调用GetProcAddres函数,用strcomp指向这个值 任何过程变量可以赋成nil,表示指证什么也不指向。但是试图调用一个nil值的过程变量导致一个错误,为了测试一个过程变量是否可以赋值,用标准的赋值函数Assigned if Assigned(OnClick)then OnClick(X); ////////////////////////////// procedure Move(const Source;var Dest;Count: Integer); procedure CopyMemory(Destination:Pointer;Source:Pointer;Length:Cardinal); 性能测试总结之内存泄露和内存溢出 主要从以下几部分来说明,关于内存和内存泄露、溢出的概念,区分内存泄露和内存溢出;内存的区域划分,了解GC回收机制;重点关注如何去监控和发现内存问题;此外分析出问题还要如何解决内存问题。 下面就开始本篇的内容: 第一部分 概念 众所周知,java中的内存java虚拟机自己去管理的,他不想C++需要自己去释放。笼统地去 讲,java的内存分配分为两个部分,一个是数据堆,一个是栈。程序在运行的时候一般分配数据堆,把局部的临时的变量都放进去,生命周期和进程有关系。但 是如果程序员声明了static的变量,就直接在栈中运行的,进程销毁了,不一定会销毁static变量。 另外为了保证java内存不会溢出,java中有垃圾回收机制。System.gc()即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存。java语言并不要求jvm有gc,也没有规定gc如何工作。垃圾收集的目的在于清除不再使用的对象。gc通过确定对象是否被活动对象引用来确定是否收集该对象。 而其中,内存溢出就是你要求分配的java虚拟机内存超出了系统能给你的,系统不能满足需求,于是产生溢出。 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问,该块已分配出来的内存也无法再使用,随着服务器内存的不断消耗,而无法使用的内存越来越 多,系统也不能再次将它分配给需要的程序,产生泄露。一直下去,程序也逐渐无内存使用,就会溢出。 第二部分 原理 JAVA垃圾回收及对内存区划分 在Java虚拟机规范中,提及了如下几种类型的内存空间: ◇ 栈内存(Stack):每个线程私有的。 ◇ 堆内存(Heap):所有线程公用的。 ◇ 方法区(Method Area):有点像以前常说的“进程代码段”,这里面存放了每个加载类的反射信息、类函数的代码、编译时常量等信息。 ◇ 原生方法栈(Native Method Stack):主要用于JNI中的原生代码,平时很少涉及。 而Java的使用的是堆内存,java堆是一个运行时数据区,类的实例(对象)从中分配空间。Java虚拟机(JVM)的堆中储存着正在运行的应用程序所建立的所有对象,“垃圾回收”也是主要是和堆内存(Heap)有关。 垃圾回收的概念就是JAVA虚拟机(JVM)回收那些不再被引用的对象内存的过程。一般我们认为正在被引用的对象状态为“alive”,而没有 被应用或者取不到引用属性的对象状态为“dead”。垃圾回收是一个释放处于”dead”状态的对象的内存的过程。而垃圾回收的规则和算法被动态的作用于 应用运行当中,自动回收。 JVM的垃圾回收器采用的是一种分代(generational)回收策略,用较高的频率对年轻的对象(young generation)进行扫描和回收,这种叫做minor collection,而对老对象(old generation)的检查回收频率要低很多,称为major collection。这样就不需要每次GC都将内存中所有对象都检查一遍,这种策略有利于实时观察和回收。 (Sun JVM 1.3 有两种最基本的内存收集方式:一种称为copying或scavenge,将所有仍然生存的对象搬到另外一块内存后,整块内存就可回收。这种方法有效率,但需要有一定的空闲内存,拷贝也有开销。这种方法用于minor collection。另外一种称为mark-compact,将活着的对象标记出来,然后搬迁到一起连成大块的内存,其他内存就可以回收了。这种方法不 需要占用额外的空间,但速度相对慢一些。这种方法用于major collection.) 一些对象被创建出来只是拥有短暂的生命周期,比如 iterators 和本地变量。 另外一些对象被创建是拥有很长的生命周期,比如 高持久化对象等。 垃圾回收器的分代策略是把内存区划分为几个代,然后为每个代分配一到多个内存区块。当其中一个代用完了分配给他的内存后,JVM会在分配的内存 区内执行一个局部的GC(也可以叫minor collection)操作,为了回收处于“dead”状态的对象所占用的内存。局部GC通常要不Full GC要快很多。 JVM定义了两个代,年轻代(yong generation)(有时称为“nursery”托儿所)和老年代(old generation)。年轻代包括 “Eden space(伊甸园)”和两个“survivor spaces”。虚拟内存初始化的时候会把所有对象都分配到 Eden space,并且大部分对象也会在该区域被释放。当进行 minor GC的时候,VM会把剩下的没有释放的对象从Eden space移动到其中一个survivor spaces当中。此外,VM也会把那些长期存活在survivor spaces 里的对象移动到 老生代的“tenured” space中。当 tenured generation 被填满后,就会产生Full GC,Full GC会相对比较慢因为回收的内容包括了所有的 live状态的对象。pemanet generation这个代包括了所有java虚拟机自身使用的相对比较稳定的数据对象,比如类和对象方法等。 关于代的划分,可以从下图中获得一个概况: 如果垃圾回收器影响了系统的性能,或者成为系统的瓶颈,你可以通过自定义各个代的大小来优化它的性能。使用JConsole,可以方便的查看到当前应用所配置的垃圾回收器的各个参数。想要获得更详细的参数,可以参考以下调优介绍: Tuning Garbage collection with the 5.0 HotSpot VM http://java.sun.com/docs/hotspot/gc/index.html 最后,总结一下各区内存: Eden Space(heap): 内存最初从这个线程池分配给大部分对象。 Survivor Space(heap):用于保存在eden space内存池中经过垃圾回收后没有被回收的对象。 Tenured Generation(heap):用于保持已经在 survivor space内存池中存在了一段时间的对象。 Permanent Generation(non-heap): 保存虚拟机自己的静态(refective)数据,例如类(class)和方法(method)对象。Java虚拟机共享这些类数据。这个区域被分割为只读的和只写的,Code Cache(non-heap):HotSpot Java虚拟机包括一个用于编译和保存本地代码(native code)的内存,叫做“代码缓存区”(code cache) 第三部分 监控(工具发现问题) 谈到内存监控工具,JConsole是必须要介绍的,它是一个用JAVA写的GUI程序,用来监控 VM,并可监控远程的VM,易用且功能强大。具体可监控JAVA内存、JAVA CPU使用率、线程执行情况、加载类概况等,Jconsole需要在JVM参数中配置端口才能使用。 由于是GUI程序,界面可视化,这里就不做详细介绍,具体帮助支持文档请参阅性能测试JConsole使用方法总结: http:// http://Java.sun.com/javase/6/docs/technotes/tools/share/jconsole.html 在实际测试某一个项目时,内存出现泄露现象。起初在性能测试的1个小时中,并不明显,而在稳定性测试的时候才发现,应用的HSF调用在经过几个 小时运行后,就出现性能明显下降的情况。在服务日志中报大量HSF超时,但所调用系统没有任何超时日志,并且压力应用的load都很低。经过查看日志后,认为应用可能存在内存泄漏。通过jconsole 以及 jmap 工具进行分析发现,确实存在内存泄漏问题,其中PS Old Gen最终达到占用 100%的占用。如图所示: 从上图可以看到,虽然每次Full GC,JVM内存会有部分回收,但回收并不彻底,不可回收的内存对象会越来越多,这样便会出现以上的一个趋势。在Full GC无法回收的对象越来越多时,最终已使用内存达到系统分配的内存最大值,系统最后无内存可分配,最终down机。 第四部分 分析 经过开发和架构师对应用的分析,查看此时内存队列,看哪个对象占用数据最多,再利用jmap命令,对线程数据分析,如下所示: num #instances #bytes class name 1: 9248056 665860032 com.taobao.matrix.mc.domain.** 2: 9248031 295936992 com.taobao.matrix.** 3: 9248068 147969088 java.util.** 4: 1542111 37010664 java.util.Date 前三个instances不断增加,指代的是同一个代码逻辑,异步分发的问题,堵塞消息,回收多次都无法回收成功。导致内存溢出。 此外,对应用的性能单独做了压测,他的性能只能支撑到一半左右,故发送消息的TPS,应用肯定无法处理过来,导致消息堆积,而JAVA垃圾回收期认为这些都是有用的对象,导致内存堆积,直至系统崩溃。 调优方法 由于具体调优方法涉及到应用的配置信息,故在此暂不列出,可以参考性能测试小组发布的《性能测试调优宝典》 第四部分 总结 内存溢出主要是由于代码编写时对某些方法、类应用不合理,或者没有预估到临时对象会占用很大内存量,或者把过多的数据放入JVM缓存,或者性能 压力大导致消息堆积而占用内存,以至于在性能测试时,生成庞大数量的临时对象,GC时没有做出有效回收甚至根本就不能回收,造成内存空间不足,内存溢出。 如果编码之前,对内存使用量进行预估,对放在内存中的数据进行评估,保证有用的信息尽快释放,无用的信息能够被GC回收,这样在一定程度上是可以避免内存溢出问题的。第二篇:检查内存泄露
第三篇:C语言内存分配
第四篇:delphi和c指针与内存操作指南
第五篇:性能测试总结之内存泄露和内存溢出