收藏 分享(赏)

C++内存分配及管理.doc

上传人:精品资料 文档编号:10917934 上传时间:2020-01-20 格式:DOC 页数:15 大小:73.50KB
下载 相关 举报
C++内存分配及管理.doc_第1页
第1页 / 共15页
C++内存分配及管理.doc_第2页
第2页 / 共15页
C++内存分配及管理.doc_第3页
第3页 / 共15页
C++内存分配及管理.doc_第4页
第4页 / 共15页
C++内存分配及管理.doc_第5页
第5页 / 共15页
点击查看更多>>
资源描述

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

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

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

4、放内存,那么该怎么去释放呢?是 delete p 么?澳,错了,应该是 delete p,这是为了告诉编译器:我删除的是一个数组,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 Avoid f(int i) file:/一个函数void f(int i) const file:/上一个函数的重载

11、;(6) 可以节省空间,避免不必要的内存分配。例如:#define PI 3.14159 file:/常量宏const doulbe Pi=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; con

13、st int a5=1, 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 i

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

15、2();(8)修饰类的成员函数:const 修饰符也可以修饰类的成员函数,格式如下:class ClassName public: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;如果采用抽象 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 float c5

20、 = 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(int i=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 Avoid f(int i) f

25、ile:/一个函数void f(int i) const file:/上一个函数的重载;上面是重载是没有问题的了,那么下面的呢?class Avoid f(int i) file:/一个函数void f(const int i) file:/?;这个是错误的,编译通不过。那么是不是说明内部参数的 const 不予重载呢?再看下面的例子:class Avoid f(int这个程序是正确的,看来上面的结论是错误的。为什么会这样呢?这要涉及到接口的透明度问题。按值传递时,对用户而言,这是透明的,用户不知道函数对形参做了什么手脚,在这种情况下进行重载是没有意义的,所以规定不能重载!当指针或引用被引入时

26、,用户就会对函数的操作有了一定的了解,不再是透明的了,这时重载是有意义的,所以规定可以重载。(8)什么情况下为 const 分配内存?以下是我想到的可能情况,当然,有的编译器进行了优化,可能不分配内存。A、作为非静态的类成员时;B、用于集合时;C、被取地址时;D、在 main 函数体内部通过函数来获得值时;E、const 的 class 或 struct 有用户定义的构造函数、析构函数或基类时;。F、当 const 的长度比计算机字长还长时;G、参数中的 const;H、使用了 extern 时。不知道还有没有其他情况,欢迎高手指点:) (9)临时变量到底是不是常量?很多情况下,编译器必须建立

27、临时对象。像其他任何对象一样,它们需要存储空间而且必须被构造和删除。区别是我们从来看不到编译器负责决定它们的去留以及它们存在的细节。对于 C+标准草案而言:临时对象自动地成为常量。因为我们通常接触不到临时对象,不能使用与之相关的信息,所以告诉临时对象做一些改变有可能会出错。当然,这与编译器有关,例如:vc6、vc7 都对此作了扩展,所以,用临时对象做左值,编译器并没有报错。(10)与 static 搭配会不会有问题?假设有一个类:class Apublic:static void f() const ;我们发现编译器会报错,因为在这种情况下 static 不能够与 const 共存!为什么呢?

28、因为 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; file:/这里处理!;2)强制转换:const_castclass Apublic:A(int i=0):test(i) void SetValue(int

29、 i)const const_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(i) void SetValue(int i)const int *p=(int*) *p=i; /这里处理!private:int test; ;注意,这里虽然说可以这样修改,但结果是未定

30、义的,避免使用!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 int test;int main()A a(3);A* pa=char* p=(char*)pa; int* pi=(int*)(p+4);/ 利用边缘调整*pi=5; file:/此处改变了 test

31、的值!return 0;虽然我给出了 6 中方法,但是我只是想说明如何更改,但出了第一种用法之外,另外 5 种用法,我们并不提倡,不要因为我这么写了,你就这么用,否则,我真是要误人子弟了:)(12 )最后我们来讨论一下常量对象的动态创建。既然编译器可以动态初始化常量,就自然可以动态创建,例如:const int* pi=new const int(10);这里要注意 2 点:1)const 对象必须被初始化!所以 (10)是不能够少的。2)new 返回的指针必须是 const 类型的。那么我们可不可以动态创建一个数组呢?答案是否定的,因为 new 内置类型的数组,不能被初始化。这里我们忽视了数

32、组是类类型的,同样对于类内部数组初始化我们也做出了这样的忽视,因为这涉及到数组的问题,我们以后再讨论。浅析 C+里面的宏 收藏说到宏,恐怕大家都能说出点东西来:一种预处理,没有分号(真的吗?)。然后呢?嗯.茫然中好吧,我们就从这开始说起。最常见的宏恐怕是#include 了,其次就是#define 还有.还是从宏的用途分类吧:1、#include 主要用于包含引用文件,至今其地位无人能替代;2、注释掉代码。例如:#if 0.#endif;这种机制是目前注释掉代码的最佳选择,为摩托罗拉公司员工所普遍采用;3、代码版本管理。例如:#ifdef DEBUGfile:/调试版本#elsefile:/非

33、调试版本#endif;4、声明宏。例如:#define DECLARE_MESSAGE(x) x();x() file:/有没有分号?哈哈/class Apublic:DECLARE_MESSAGE(A);想起什么了,呵呵:)对,VC 里面有好多这样的东东,有空我会写我的 VC 历程,到时候会把 VC 里的各种宏详细的解释一下,那可是一个庞大的工程:)5、符号常量。例如:#define PI 3.141596、内联函数。例如:#define CLEAR(x) (x)=0)7、泛型函数。例如:#define ABS(x) (x)0? (x):-(x)x=3 没问题! x=1.3 也没问题!如果是

34、这样呢:#include #define A(x) (x)0? (x):-(x)void main()int i=-1;cout 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.h:(没有定义宏 MACRO)class x void MACRO(); .;那么程

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

36、一种情况:#define CONTROL(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、对于泛型预处理程序,我们可以用函数模板

37、或类默板来代替:templateT ABS(const T templateClass Stack ;4、最后对于语法扩展程序几乎都可以用一个或多个 C+类代替:Set s;int i;Set_iter iter(s);while(iter.next(i).与使用宏相比,我们只是牺牲了一点程序的简洁性而已。当然并不是所有的宏都能替换(我们也并不主张替换掉所有的宏!),对于不能替换的宏,我们应该对他们实行命名约定,例如:#define COMPANY_XYZ_LIBABC_MACRO stuff同时我们也要采取一定的方法,进行预防:#ifndef COMPANY_XYZ_LIBABC_MACRO#define COMPANY_XYZ_LIBABC_MACRO stuff#endif当然,在程序库实现内部定义的宏没有这个约束:my.cpp:#define MACRO stuff我们给出几个常见的宏: #define A(x) T_#x#define Bx) #x#define Cx) #x我们假设:x=1 ,则有:A(1)=T_1B(1)=1C(1)=“1“还有一个比较常见的宏:_TTCHAR tStr = _T(“t code“);_T 宏的作用就是转换成 TCHAR。

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

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报