C++内存分配与管理.docx

上传人:rrsccc 文档编号:10420440 上传时间:2021-05-15 格式:DOCX 页数:16 大小:61.07KB
返回 下载 相关 举报
C++内存分配与管理.docx_第1页
第1页 / 共16页
C++内存分配与管理.docx_第2页
第2页 / 共16页
C++内存分配与管理.docx_第3页
第3页 / 共16页
C++内存分配与管理.docx_第4页
第4页 / 共16页
亲,该文档总共16页,到这儿已超出免费预览范围,如果喜欢就下载吧!
资源描述

《C++内存分配与管理.docx》由会员分享,可在线阅读,更多相关《C++内存分配与管理.docx(16页珍藏版)》请在三一文库上搜索。

1、.专业整理 .在 C+中,存分成5 个区,他们分别是堆、栈、自由存储区、全局/ 静态存储区和常量存储区。栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。堆,就是那些由 new 分配的存块, 他们的释放编译器不去管, 由我们的应用程序去控制,一般一个 new 就要对应一个 delete 。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。自由存储区, 就是那些由 malloc 等分配的存块, 他和堆是十分相似的, 不过它是用 free 来结束自己的生命的。全局 / 静态存储区,全局变量和静态变量被分配到同一块存中,在以前

2、的C 语言中,全局变量又分为初始化的和未初始化的,在C+里面没有这个区分了,他们共同占用同一块存区。常量存储区, 这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改 (当然,你要通过非正当手段也可以修改,而且方法很多,在 const 的思考一文中,我给出了 6 种方法)明确区分堆与栈在 bbs 上,堆与栈的区分问题, 似乎是一个永恒的话题, 由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。首先,我们举一个例子:void f() int* p=new int5; 这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆存, 那么指针 p 呢?他分配

3、的是一块栈存,所以这句话的意思就是:在栈存中存放了一个指向一块堆存的指针p。在程序会先确定在堆中分配存的大小,然后调用operator new分配存,然后返回这块存的首地址,放入栈中,他在VC6下的汇编代码如下:00401028 push 14h0040102A call operator new (00401060)0040102F add esp,400401032 mov dword ptr ebp-8,eax00401035 mov eax,dword ptr ebp-800401038 mov dword ptr ebp-4,eax这里,我们为了简单并没有释放存,那么该怎么去释放呢?

4、是deletep 么?澳,错了,应该是 deletep ,这是为了告诉编译器: 我删除的是一个数组, VC6就会根据相应的Cookie. 学习帮手 .专业整理 .信息去进行释放存的工作。好了,我们回到我们的主题:堆和栈究竟有什么区别?主要的区别由以下几点:1、管理方式不同;2、空间大小不同;3、能否产生碎片不同;4、生长方向不同;5、分配方式不同;6、分配效率不同;管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak 。空间大小: 一般来讲在 32 位系统下, 堆存可以达到 4G 的空间, 从这个角度来看堆存几乎是没有什么

5、限制的。 但是对于栈来讲, 一般都是有一定的空间大小的, 例如,在 VC6下面,默认的栈空间大小是 1M(好像是,记不清楚了) 。当然,我们可以修改:打开工程, 依次操作菜单如下:Project-Setting-Link,在 Category中选中 Output ,然后在 Reserve 中设定堆栈的最大值和commit 。注意: reserve 最小值为 4Byte ;commit 是保留在虚拟存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加存的开销和启动时间。碎片问题:对于堆来讲,频繁的new/delete势必会造成存空间的不连续,从而造成大量的碎片, 使程序效率降低。 对于栈来讲

6、, 则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个存块从栈中间弹出,在他弹出之前, 在他上面的后进的栈容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。生长方向: 对于堆来讲,生长方向是向上的, 也就是向着存地址增加的方向; 对于栈来讲,它的生长方向是向下的,是向着存地址减小的方向增长。分配方式: 堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的, 他的动态分配是由编译器进行释放, 无需我们手工实现。

7、. 学习帮手 .专业整理 .分配效率: 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C+函数库提供的,它的机制是很复杂的,例如为了分配一块存,库函数会按照一定的算法(具体的算法可以参考数据结构/ 操作系统)在堆存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于存碎片太多),就有可能调用系统功能去增加程序数据段的存空间, 这样就有机会分到足够大小的存,然后进行返回。 显然,堆的效率比栈要低得多。从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的存

8、碎片; 由于没有专门的系统支持,效率很低; 由于可能引发用户态和核心态的切换,存的申请,代价变得更加昂贵。 所以栈在程序中是应用最广泛的, 就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。虽然栈有如此众多的好处, 但是由于和堆相比不是那么灵活, 有时候分配大量的存空间,还是用堆好一些。无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果, 就算是在你的程序运行过程中, 没有发生上面的问题, 你还是要小心, 说不定什么

9、时候就崩掉, 那时候 debug 可是相当困难的: )1、什么是const?常类型是指使用类型修饰符 const 说明的类型,常类型的变量或对象的值是不能被更新的。(当然,我们可以偷梁换柱进行更新:)2、为什么引入const ?const推出的初始目的,正是为了取代预编译指令,消除它的缺点, 同时继承它的优点。3、 cons 有什么主要的作用?( 1)可以定义 const 常量,具有不可变性。例如:const int Max=100; int ArrayMax;( 2)便于进行类型检查,使编译器对处理容有更多了解,消除了一些隐患。例如:void f(const int i) .编译器就会知道i

10、 是一个常量,不允许修改;( 3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。同宏定义一样,可以做到不变则已,一变都变!如(1)中,如果想修改Max的容,只需要:const int Max=you want;即可!( 4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。还是上面的例子,如果在函数体修改了i ,编译器就会报错;. 学习帮手 .专业整理 .例如:void f(const int i) i=10;/error! (5)为函数重载提供了一个参考。class A.void f(int i).file:/一个函数void f(int i) const .fil

11、e:/上一个函数的重载.;(6)可以节省空间,避免不必要的存分配。例如:#define PI 3.14159file:/常量宏const doulbePi=3.14159;file:/此时并未将 Pi 放入 ROM中.double i=Pi;file:/此时为 Pi 分配存,以后不再分配!double I=PI;file:/编译期间进行宏替换,分配存double j=Pi;file:/没有存分配double J=PI;file:/再进行宏替换,又一次分配存!const定义常量从汇编的角度来看,只是给出了对应的存地址,而不是象#define一样给出的是立即数,所以,const 定义的常量在程序运

12、行过程中只有一份拷贝,而#define定义的常量在存中有若干个拷贝。( 7) 提高了效率。编译器通常不为普通const 常量分配存储空间,而是将它们保存在符号表中, 这使得它成为一个编译期间的常量,没有了存储与读存的操作,使得它的效率也很高。3、如何使用const ?( 1)修饰一般常量一般常量是指简单类型的常量。这种常量在定义时,修饰符const 可以用在类型说明符前,也可以用在类型说明符后。例如:int const x=2;或const int x=2;( 2)修饰常数组定义或说明一个常数组可采用如下格式:int const a5=1, 2, 3, 4, 5;const int a5=1,

13、 2, 3, 4, 5;. 学习帮手 .专业整理 .( 3)修饰常对象常对象是指对象常量,定义格式如下:class A;const A a;A const a;定义常对象时,同样要进行初始化,并且该对象不能再被更新,修饰符const可以放在类名后面,也可以放在类名前面。( 4)修饰常指针const int *A;file:/const修饰指向的对象,A 可变,A 指向的对象不可变int const *A;file:/const修饰指向的对象,A 可变, A指向的对象不可变int *const A;file:/const修饰指针A,A 不可变, A 指向的对象可变const int *const

14、 A;file:/指针 A 和 A 指向的对象都不可变( 5)修饰常引用使用 const 修饰符也可以说明引用,被说明的引用为常引用,该引用所引用的对象不能被更新。其定义格式如下:const double & v;( 6)修饰函数的常参数const修饰符也可以修饰函数的传递参数,格式如下:void Fun(const int Var);告诉编译器Var 在函数体中的无法改变,从而防止了使用者的一些无意的或错误的修改。( 7)修饰函数的返回值:const修饰符也可以修饰函数的返回值,是返回值不可被改变,格式如下:const int Fun1();const MyClass Fun2();( 8)

15、修饰类的成员函数:const修饰符也可以修饰类的成员函数,格式如下:class ClassNamepublic:int Fun() const;. ;这样,在调用函数Fun 时就不能修改类里面的数据( 9)在另一连接文件中引用const 常量extern const int i;file:/正确的引用extern const int j=10;file:/错误!常量不可以被再次赋值另外,还要注意,常量必须初始化!. 学习帮手 .专业整理 .例如:const int i=5;4、几点值得讨论的地方:( 1)const 究竟意味着什么?说了这么多,你认为const意味着什么?一种修饰符?接口抽象?

16、一种新类型?也许都是,在Stroustup最初引入这个关键字时,只是为对象放入ROM做出了一种可能,对于const 对象, C+既允许对其进行静态初始化,也允许对他进行动态初始化。理想的 const 对象应该在其构造函数完成之前都是可写的,在析够函数执行开始后也都是可写的,换句话说,const 对象具有从构造函数完成到析够函数执行之前的不变性,如果违反了这条规则,结果都是未定义的!虽然我们把const 放入 ROM中,但这并不能够保证const 的任何形式的堕落,我们后面会给出具体的办法。无论const 对象被放入ROM中,还是通过存储保护机制加以保护,都只能保证,对于用户而言这个对象没有改变

17、。换句话说,废料收集器 (我们以后会详细讨论, 这就一笔带过) 或数据库系统对一个 const 的修改怎没有任何问题。( 2)位元 const V.S. 抽象 const?对于关键字const 的解释有好几种方式,最常见的就是位元const和 抽象 const 。下面我们看一个例子:class Apublic:.A f(const A& a);.;如果采用抽象 const 进行解释,那就是 f 函数不会去改变所引用对象的抽象值,如果采用位元 const 进行解释,那就成了 f 函数不会去改变所引用对象的任何位元。我们可以看到位元解释正是 c+对 const 问题的定义, const 成员函数不

18、被允许修改它所在对象的任何一个数据成员。为什么这样呢?因为使用位元const 有 2 个好处:最大的好处是可以很容易地检测到违反位元const 规定的事件:编译器只用去寻找有没有对数据成员的赋值就可以了。另外,如果我们采用了位元const ,那么,对于一些比较简单的const 对象,我们就可以把它安全的放入ROM中,对于一些程序而言,这无疑是一个很重要的优化方式。(关于优化处理,我们到时候专门进行讨论)当然,位元const 也有缺点,要不然,抽象const 也就没有产生的必要了。首先,位元 const 的抽象性比抽象 const 的级别更低!实际上,大家都知道,一个库接口的抽象性级别越低,使用

19、这个库就越困难。其次,使用位元const 的库接口会暴露库的一些实现细节,而这往往会带来一些负面效应。所以,在库接口和程序实现细节上,我们都应该采用抽象const 。有时,我们可能希望对const 做出一些其它的解释,那么,就要注意了,. 学习帮手 .专业整理 .目前, 大多数对 const的解释都是类型不安全的,这里我们就不举例子了,你可以自己考虑一下,总之,我们尽量避免对const 的重新解释。( 3)放在类部的常量有什么限制?看看下面这个例子:class Aprivate:const int c3 = 7;/ ?static int c4 = 7;/ ?static const floa

20、t c5 = 7;/ ?.;你认为上面的3 句对吗?呵呵,都不对!使用这种类部的初始化语法的时候,常量必须是被一个常量表达式初始化的整型或枚举类型,而且必须是static和 const形式。这显然是一个很严重的限制!那么,我们的标准委员会为什么做这样的规定呢?一般来说,类在一个头文件中被声明,而头文件被包含到许多互相调用的单元去。但是,为了避免复杂的编译器规则, C+要求每一个对象只有一个单独的定义。如果C+允许在类部定义一个和对象一样占据存的实体的话,这种规则就被破坏了。( 4)如何初始化类部的常量?一种方法就是static和 const并用,在部初始化,如上面的例子;另一个很常见的方法就是

21、初始化列表:class Apublic:A(int i=0):test(i) private:const int i; ;还有一种方式就是在外部初始化,例如:class Apublic:A() private:static const int i;file:/注意必须是静态的! ;const int A:i=3;( 5)常量与数组的组合有什么特殊吗?我们给出下面的代码:const int size3=10,20,50; int arraysize2;. 学习帮手 .专业整理 .有什么问题吗?对了,编译通不过!为什么呢?const可以用于集合,但编译器不能把一个集合存放在它的符号表里,所以必须分

22、配存。在这种情况下,const 意味着“不能改变的一块存储”。然而,其值在编译时不能被使用,因为编译器在编译时不需要知道存储的容。自然, 作为数组的大小就不行了:)你再看看下面的例子:class Apublic:A(inti=0):test2(1,2)file:/你认为行吗?private:const int test2; ;vc6 下编译通不过,为什么呢?关于这个问题,前些时间,njboy 问我是怎么回事?我反问他:“你认为呢?”他想了想,给出了一下解释,大家可以看看: 我们知道编译器堆初始化列表的操作是在构造函数之,显式调用可用代码之前,初始化的次序依据数据声明的次序。初始化时机应该没有什

23、么问题,那么就只有是编译器对数组做了什么手脚!其实做什么手脚,我也不知道,我只好对他进行猜测:编译器搜索到test发现是一个非静态的数组,于是,为他分配存空间,这里需要注意了,它应该是一下分配完,并非先分配test0,然后利用初始化列表初始化,再分配 test1, 这就导致数组的初始化实际上是赋值!然而,常量不允许赋值,所以无法通过。呵呵,看了这一段冠冕堂皇的话,真让我笑死了! njboy 别怪我揭你短呀: )我对此的解释是这样的:C+标准有一个规定,不允许无序对象在类部初始化,数组显然是一个无序的,所以这样的初始化是错误的!对于他,只能在类的外部进行初始化,如果想让它通过,只需要声明为静态的

24、,然后初始化。这里我们看到, 常量与数组的组合没有什么特殊!一切都是数组惹的祸!( 6)this 指针是不是 const 类型的?this指针是一个很重要的概念,那该如何理解她呢?也许这个话题太大了,那我们缩小一些:this指针是个什么类型的?这要看具体情况:如果在非const 成员函数中, this指针只是一个类类型的;如果在const 成员函数中, this指针是一个const类类型的;如果在volatile成员函数中 ,this指针就是一个volatile类类型的。( 7)const 到底是不是一个重载的参考对象?先看一下下面的例子:class A.void f(int i).file:

25、/一个函数void f(int i) const .file:/上一个函数的重载.;上面是重载是没有问题的了,那么下面的呢?. 学习帮手 .专业整理 .class A.void f(int i).file:/一个函数void f(const int i) .file:/?.;这个是错误的,编译通不过。那么是不是说明部参数的const 不予重载呢?再看下面的例子:class A.void f(int& ).file:/一个函数void f(const int& ) .file:/?.;这个程序是正确的,看来上面的结论是错误的。为什么会这样呢?这要涉及到接口的透明度问题。按值传递时,对用户而言,这

26、是透明的,用户不知道函数对形参做了什么手脚, 在这种情况下进行重载是没有意义的, 所以规定不能重载! 当指针或引用被引入时,用户就会对函数的操作有了一定的了解,不再是透明的了,这时重载是有意义的,所以规定可以重载。( 8)什么情况下为const 分配存?以下是我想到的可能情况,当然, 有的编译器进行了优化,可能不分配存。A 、作为非静态的类成员时;B 、用于集合时;C 、被取地址时;D 、在 main 函数体部通过函数来获得值时;E 、const 的 class或 struct有用户定义的构造函数、析构函数或基类时; 。F 、当 const 的长度比计算机字长还长时;G 、参数中的const

27、;H 、使用了 extern时。不知道还有没有其他情况,欢迎高手指点:)( 9)临时变量到底是不是常量?很多情况下,编译器必须建立临时对象。像其他任何对象一样,它们需要存储空间而且必须被构造和删除。区别是我们从来看不到编译器负责决定它们的去留以及它们存在的细节。对于 C+标准草案而言:临时对象自动地成为常量。因为我们通常接触不到临时对象,不能使用与之相关的信息,所以告诉临时对象做一些改变有可能会出错。当然,这与编译器有关,例如: vc6 、 vc7 都对此作了扩展,所以,用临时对象做左值,编译器并没有报错。( 10)与 static 搭配会不会有问题?假设有一个类:class A. 学习帮手

28、.专业整理 .public:.static void f() const .;我们发现编译器会报错,因为在这种情况下static不能够与 const 共存!为什么呢?因为static没有 this指针,但是 const 修饰 this指针,所以.( 11)如何修改常量?有时候我们却不得不对类的数据进行修改,但是我们的接口却被声明了 const ,那该怎么处理呢?我对这个问题的看法如下:1 )标准用法:mutableclass Apublic:A(int i=0):test(i) void SetValue(int i)const test=i; private:mutable int test

29、;file:/这里处理! ;2 )强制转换:const_castclass Apublic:A(int i=0):test(i) void SetValue(int i)constconst_cast(test)=i;/ 这里处理!private:int test; ;3 )灵活的指针:int*class Apublic:A(int i=0):test(i) void SetValue(int i)const *test=i; private:int* test;file:/这里处理!. 学习帮手 .专业整理 . ;4 )未定义的处理 class Apublic:A(int i=0):test

30、(i) void SetValue(int i)constint *p=(int*)&test; *p=i; /这里处理!private:int test; ;注意,这里虽然说可以这样修改,但结果是未定义的,避免使用!5 )部处理: this指针class Apublic:A(int i=0):test(i) void SetValue(int i)const (A*)this)-test=i; /这里处理!private:int test; ;6 )最另类的处理:空间布局 class Apublic:A(int i=0):test(i),c(a) private:char c;const i

31、nt test;int main()A a(3); A* pa=&a;char* p=(char*)pa;int*pi=(int*)(p+4); / 利用边缘调整*pi=5;file:/此处改变了test的值!return 0;. 学习帮手 .专业整理 .虽然我给出了 6 中方法,但是我只是想说明如何更改,但出了第一种用法之外,另外 5 种用法,我们并不提倡,不要因为我这么写了,你就这么用,否则,我真是要误人子弟了:)( 12)最后我们来讨论一下常量对象的动态创建。既然编译器可以动态初始化常量,就自然可以动态创建,例如:const int* pi=new const int(10);这里要注意

32、2 点:1 ) const 对象必须被初始化!所以(10) 是不能够少的。2 ) new返回的指针必须是const 类型的。那么我们可不可以动态创建一个数组呢?答案是否定的,因为new置类型的数组,不能被初始化。这里我们忽视了数组是类类型的,同样对于类部数组初始化我们也做出了这样的忽视,因为这涉及到数组的问题,我们以后再讨论。浅析 C+里面的宏收藏说到宏,恐怕大家都能说出点东西来:一种预处理,没有分号(真的吗?)。然后呢?嗯.茫然中 .好吧,我们就从这开始说起。最常见的宏恐怕是#include了,其次就是 #define还有 .还是从宏的用途分类吧:1、 #include主要用于包含引用文件,

33、至今其地位无人能替代;2、注释掉代码。例如:#if 0.#endif;这种机制是目前注释掉代码的最佳选择,为摩托罗拉公司员工所普遍采用;3、代码版本管理。例如:#ifdef DEBUGfile:/调试版本#elsefile:/非调试版本#endif;4、声明宏。例如:#define DECLARE_MESSAGE(x) x();x()file:/有没有分号?哈哈/.class Apublic:. 学习帮手 .专业整理 .DECLARE_MESSAGE(A);.想起什么了,呵呵:)对,VC 里面有好多这样的东东,有空我会写我的VC历程,到时候会把VC里的各种宏详细的解释一下,那可是一个庞大的工程

34、:)5、符号常量。例如:#define PI 3.141596、联函数。例如:#define CLEAR(x) (x)=0)7、泛型函数。例如:#define ABS(x) (x)0? (x):-(x)x=3没问题!x=1.3也没问题!如果是这样呢:#include #define A(x) (x)0? (x):-(x)void main()int i=-1;coutA(1)endl;coutA(+i)endl;有问题了,不过以后再说,大概讲const or inline时会说的:)8、泛型类型。例如:#define Stack(T)Stack_ #T#define Stackdeclare(

35、T)class Stack(T) .Stackdeclare(int);Stackdeclare(char);.Stack(int) s1;Stack(char) s2;9、语法扩展。例如:Set s;/假设 Set 为一个描述集合的类int i;FORALL(i,s);.宏最大的问题便是易引起冲突,例如:libA.h:. 学习帮手 .专业整理 .#define MACRO stuff同时:libB.h:#define MACRO stuff下面我们对他们进行引用:user.cpp:#include libA.h#include libB.h.糟糕,出现了重定义!还有一种冲突的可能:libB.

36、h:(没有定义宏MACRO)class x void MACRO();.;那么程序运行期间,libA.h中的宏讲会改变libB.h中的成员函数的名字,导致不可预料的结果。宏的另一个问题,便是如 7 中出现的问题, 如果你把7 中的 x 设为 a ,程序也不会给出任何警告,所以他是不安全的。针对以上的问题,我们说:1 、尽可能的少用公用宏, 能替换掉就替换掉;2 、对那些不能替换的宏,使用命名约定;1 、符号常量预处理程序我们可以用const or enum来代替:const int TABLESIZE=1024;enum TABLESIZE=1024 ;2 、非泛型联函数的预处理程序可以使用真

37、正的联函数来代替: inline void clear(int& x) x=0;奥,对了,还有这样一种情况:#defineCONTROL(c)(c)-64).switch(c)case CONTROL(a) :.case CONTROL(b) :.case CONTROL(c) :.case CONTROL(d) :.这时候就不能单独使用联函数来取代了,因为 case 标签禁止函数调用,我们只好做如下转换:inline char control(char c) return c+64; . 学习帮手 .专业整理 .switch(control(c)case a:.case b:.case c:.case d:.当然这样做是以牺牲时间作为代价的(你想想为什么:) )3 、对于泛型预处理程序,我们可以用函数模板或类默板来代替: templateT ABS(const T& t) return t0 ? t : -t;templateClass Stack.;4 、最后对于语法扩展程序几乎都可以用一个或多个C+类代替:Set s;int i;Set_iter iter(s);while(iter.next(i).与使用宏相比,我们只是牺牲了一点程序的简洁性而已。当然并不是所有的宏都能替换(我们

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 社会民生


经营许可证编号:宁ICP备18001539号-1