热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

c/c++内存机制(一)

http:www.cnblogs.comComputerGarchive201202012334898.html一:C语言中的内存机制在C语言中,内存

http://www.cnblogs.com/ComputerG/archive/2012/02/01/2334898.html

 

 

一:C语言中的内存机制

在C语言中,内存主要分为如下5个存储区:

(1)栈(Stack):位于函数内的局部变量(包括函数实参),由编译器负责分配释放,函数结束,栈变量失效。

(2)堆(Heap):由程序员用malloc/calloc/realloc分配,free释放。如果程序员忘记free了,则会造成内存泄露,程序结束时该片内存会由OS回收。

(3)全局区/静态区(Global Static Area): 全局变量和静态变量存放区,程序一经编译好,该区域便存在。并且在C语言中初始化的全局变量和静态变量和未初始化的放在相邻的两个区域(在C++中,由于全局变量和静态变量编译器会给这些变量自动初始化赋值,所以没有区分了)。由于全局变量一直占据内存空间且不易维护,推荐少用。程序结束时释放。

(4)C风格字符串常量存储区: 专门存放字符串常量的地方,程序结束时释放。

(5)程序代码区:存放程序二进制代码的区域。


二:C++中的内存机制

在C++语言中,与C类似,不过也有所不同,内存主要分为如下5个存储区:

(1)栈(Stack):位于函数内的局部变量(包括函数实参),由编译器负责分配释放,函数结束,栈变量失效。

(2)堆(Heap):这里与C不同的是,该堆是由new申请的内存,由delete或delete[]负责释放

(3)自由存储区(Free Storage):由程序员用malloc/calloc/realloc分配,free释放。如果程序员忘记free了,则会造成内存泄露,程序结束时该片内存会由OS回收。

(4)全局区/静态区(Global Static Area): 全局变量和静态变量存放区,程序一经编译好,该区域便存在。在C++中,由于全局变量和静态变量编译器会给这些变量自动初始化赋值,所以没有区分了初始化变量和未初始化变量了。由于全局变量一直占据内存空间且不易维护,推荐少用。程序结束时释放。

(5)常量存储区: 这是一块比较特殊的存储区,专门存储不能修改的常量(如果采用非正常手段更改当然也是可以的了)。


三:堆和栈的区别

3.1 栈(Stack)

    具体的讲,现代计算机(冯诺依曼串行执行机制),都直接在代码低层支持栈的数据结构。这体现在有专门的寄存器指向栈所在的地址(SS,堆栈段寄存器,存放堆栈段地址);有专门的机器指令完成数据入栈出栈的操作(汇编中有PUSH和POP指令)。

    这种机制的特点是效率高,但支持数据的数据有限,一般是整数、指针、浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构(可以自定义栈结构支持多种数据类型)。因为栈的这种特点,对栈的使用在程序中是非常频繁的 。对子程序的调用就是直接利用栈完成的。机器的call指令里隐含了把返回地址入栈,然后跳转至子程序地址的操作,而子程序的ret指令则隐含从堆栈中弹出返回地址并跳转之的操作。

    C/C++中的函数自动变量就是直接使用栈的例子,这也就是为什么当函数返回时,该函数的自动变量自动失效的原因,因而要避免返回栈内存和栈引用,以免内存泄露。

3.2 堆(Heap)

    和栈不同的是,堆得数据结构并不是由系统(无论是机器硬件系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/calloc/realloc/free函数维护了一套内部的堆数据结构(在C++中则增加了new/delete维护)。

    当程序用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间(常见内存分配算法有:首次适应算法、循环首次适应算法、最佳适应算法和最差适应算法等。os的基本内容!!)。如果没有可用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回到内部堆结构中,可能会被适当的处理(比如空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。 这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下几个原因:

(1)系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);这样的话对于大量的小内存分配来说会造成浪费。

(2)系统调用申请内存可能是代价昂贵的。 系统调用可能涉及到用户态和核心态的转换。

(3)没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。

3.3 栈和堆的对比

从以上介绍中,它们有如下区别:

(1)栈是系统提供的功能,特点是快速高效,缺点是由限制,数据不灵活;

       堆是函数库提供的功能,特点是灵活方便,数据适应面广,但是效率有一定降低。

(2)栈是系统数据结构,对于进程/线程是唯一的;

       堆是函数库内部数据结构,不一定唯一,不同堆分配的内存无法互相操作。

(3)栈空间分静态分配和动态分配,一般由编译器完成静态分配,自动释放,栈的动态分配是不被鼓励的;

       堆得分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/释放内存匹配是良好程序的基本要素。

(4)碎片问题

    对于堆来讲,频繁的new/delete等操作势必会造成内存空间的不连续,从而造成大量的碎片,使程序的效率降低;对于栈来讲,则不会存在这个问题,因为栈是后进先出(LIFO)的队列。

(5)生长方向

    堆的生长方向是向上的,也就是向这内存地址增加的方向;对于栈来讲,生长方向却是向下的,是向着内存地址减少的方向增长。

(6)分配方式

      堆都是动态分配的,没有静态分配的堆;

      栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配则由alloca函数进行分配,但是栈的动态分配和堆不同,它的动态分配是由编译器进行释放,无需我们手工实现。

(7)分配效率

      栈是机器系统提供的数据结构,计算机在底层提供支持,分配有专门的堆栈段寄存器,入栈出栈有专门的机器指令,这些都决定了栈的高效率执行。

      堆是由C/C++函数库提供的,机制比较复杂,有不同的分配算法,易产生内存碎片,需要对内存进行各种管理,效率比栈要低很多。


四:具体实例分析

例子(一)

看下面的一小段C程序,仔细体会各种内存分配机制。

int a = 0; //全局初始化区,a的值为0 a = 0; //全局初始化区,a的值为0

char *p1; //全局未初始化区(C++中则初始化为NULL) *p1; //全局未初始化区(C++中则初始化为NULL)

int main() main()

{

int b; //b分配在栈上,整型 int b; //b分配在栈上,整型

char s[] = "abc"; //s分配在栈上,char *类型;"abc\0"分配在栈上,运行时赋值,函数结束销毁 char s[] = "abc"; //s分配在栈上,char *类型;"abc\0"分配在栈上,运行时赋值,函数结束销毁

char *p2; //p2分配在栈上,未初始化 char *p2; //p2分配在栈上,未初始化

char *p3 = "123456"; //p3指向"123456"分配在字符串常量存储区的地址,编译时确定 char *p3 = "123456"; //p3指向"123456"分配在字符串常量存储区的地址,编译时确定

static int c = 0; //c在全局(静态)初始化区,可以多次跨函数调用而保持原值 static int c = 0; //c在全局(静态)初始化区,可以多次跨函数调用而保持原值

p1 = (char *)malloc(10); //p1在全局未初始化区,指向分配得来得10字节的堆区地址 char *)malloc(10); //p1在全局未初始化区,指向分配得来得10字节的堆区地址

p2 = (char *)malloc(20); //p2指向分配得来得20字节的堆区地址 char *)malloc(20); //p2指向分配得来得20字节的堆区地址

strcpy(p1, "123456"); //"123456"放在字符串常量存储区,编译器可能会将它与p3所指向的"123456"优化成一块 "123456"); //"123456"放在字符串常量存储区,编译器可能会将它与p3所指向的"123456"优化成一块

return 0; return 0;

}


例子(二)

看下面的一小段代码,体会堆与栈的区别:

int foo()

{

//其余代码

int *p = new int[5];

//其余代码

return 0;

}


    其中的语句int *p = new int[5];就包含了堆与栈。其中new关键字分配了一块堆内存,而指针p本身所占得内存为栈内存(一般4个字节表示地址)。这句话的意思是在栈内存中存放了一个指向一块堆内存的指针p。在程序中先确定在堆中分配内存的大小,然后调用new关键字分配内存,最后返回这块内存首址,放入栈中。汇编代码为:

int foo() foo()

{

008C1520 push ebp

008C1521 mov ebp,esp

008C1523 sub esp,0D8h

008C1529 push ebx

008C152A push esi

008C152B push edi

008C152C lea edi,[ebp-0D8h]

008C1532 mov ecx,36h

008C1537 mov eax,0CCCCCCCCh

008C153C rep stos dword ptr es:[edi]

int *p = new int[5];int *p = new int[5];

008C153E push 14h

008C1540 call operator new[] (8C1258h) operator new[] (8C1258h)

008C1545 add esp,4

008C1548 mov dword ptr [ebp-0D4h],eax

008C154E mov eax,dword ptr [ebp-0D4h]

008C1554 mov dword ptr [p],eax

return 0;return 0;

008C1557 xor eax,eax

}

008C1559 pop edi

008C155A pop esi

008C155B pop ebx

008C155C add esp,0D8h

008C1562 cmp ebp,esp

008C1564 call @ILT+395(__RTC_CheckEsp) (8C1190h)

008C1569 mov esp,ebp

008C156B pop ebp

008C156C ret


    如果需要释放内存,这里我们需要使用delete[] p,告诉编译器,我要删除的是一个数组。

例子(三)

看下面的一小段代码,试着找出其中的错误:

#include

using namespace std; namespace std;

int main() main()

{

char a[] = "Hello"; // 分配在栈上char a[] = "Hello"; // 分配在栈上

a[0] = 'X'; 'X';

cout <

char *p &#61; "World"; // 分配在字符串常量存储区的地址char *p &#61; "World"; // 分配在字符串常量存储区的地址

p[0] &#61; &#39;X&#39;; &#39;X&#39;;

cout <

return 0; return 0;

}


    发现问题了吗&#xff1f;是的&#xff0c;字符数组a的容量是6个字符&#xff0c;其内容为"hello\0"。a的内容时可以改变的&#xff0c;比如a[0]&#61;&#39;X&#39;&#xff0c;因为其是在栈上分配的&#xff0c;也就是在运行时确定的内容。但是指针p指向的字符串"world"分配在字符串常量存储区&#xff0c;内容为"world\0"&#xff0c;常量字符串的内容时不可以修改的。从语法上来说&#xff0c;编译器并不觉得语句p[0]&#61;&#39;X&#39;有什么问题&#xff0c;但是在运行时则会出现"access violation"非法内存访问的问题。

以下几个函数的变化要看清楚了&#xff1a;吐舌笑脸

char *GetString1(void) *GetString1(void)

{

char p[] &#61; "hello,world"; //结果&#xff1a;h。由于数组指针指向第一元素的地址&#xff0c;所以调用之后是h char p[] &#61; "hello,world"; //结果&#xff1a;h。由于数组指针指向第一元素的地址&#xff0c;所以调用之后是h

return p; return p;

}

char *GetString2(void) *GetString2(void)

{

char *p &#61; "hello,world"; //结果&#xff1a;hello,world。由于p指向“hello,world”字符串常量区域地址 char *p &#61; "hello,world"; //结果&#xff1a;hello,world。由于p指向“hello,world”字符串常量区域地址

return p; return p;

}

char *GetString3(void) *GetString3(void)

{

char *p &#61; (char *)malloc(20); // 指向p所分配的堆上的内存空间。char *p &#61; (char *)malloc(20); // 指向p所分配的堆上的内存空间。

return p; return p;

}

char *GetString4(void) *GetString4(void)

{

char *p &#61; new char[20]; // 指向p所分配的内存空间,p本身在栈上的&#xff0c;p所指向的空间是堆上的。char *p &#61; new char[20]; // 指向p所分配的内存空间,p本身在栈上的&#xff0c;p所指向的空间是堆上的。

return p; return p;

}




附录&#xff1a;内存管理注意事项太阳

【规则1】用malloc或new申请内存之后&#xff0c;应该立即检查指针值是否为NULL&#xff0c;防止使用指针值为NULL的内存&#xff0c;可以在函数入口处断言检测。

【规则2】不要忘记为数组或动态内存赋初值&#xff08;比如calloc比malloc就要好&#xff09;&#xff0c;指针初始化为NULL(c&#43;&#43;中为0)。

【规则3】避免数组或指针下标越界&#xff0c;特别太阳当心发生“多1”或者"少1"太阳的操作。

【规则4】动态内存的申请和释放必须配对&#xff0c;防止内存泄露&#xff0c;具体为malloc/calloc/realloc和free配对,new和delete以及delete[]配对。

【规则5】用free或者delete释放内存后&#xff0c;应立即将指针设置为NULL(C&#43;&#43;中为0)&#xff0c;防止产生“野指针”、"悬垂指针"。

【规则6】遇到不懂得问题及时debug&#xff0c;一般的虫子灯泡debug一下就灰飞烟灭了&#xff0c;一切bug都是浮云而已。

 


推荐阅读
  • KMP算法是一种高效的字符串模式匹配算法,能够在不进行回溯的情况下完成匹配,其时间复杂度为O(m+n),其中m和n分别为文本串和模式串的长度。本文将详细介绍KMP算法的工作原理,并提供C语言实现。 ... [详细]
  • 本文介绍如何利用栈数据结构在C++中判断字符串中的括号是否匹配。通过顺序栈和链栈两种方式实现,并详细解释了算法的核心思想和具体实现步骤。 ... [详细]
  • 本文介绍了一种基于选择排序思想的高效排序方法——堆排序。通过使用堆数据结构,堆排序能够在每次查找最大元素时显著提高效率。文章详细描述了堆排序的工作原理,并提供了完整的C语言代码实现。 ... [详细]
  • 本题要求在一组数中反复取出两个数相加,并将结果放回数组中,最终求出最小的总加法代价。这是一个经典的哈夫曼编码问题,利用贪心算法可以有效地解决。 ... [详细]
  • 本文深入探讨了MySQL中常见的面试问题,包括事务隔离级别、存储引擎选择、索引结构及优化等关键知识点。通过详细解析,帮助读者在面对BAT等大厂面试时更加从容。 ... [详细]
  • 本文详细介绍了C语言中的基本数据类型,包括整型、浮点型、字符型及其各自的子类型,并探讨了这些类型在不同编译环境下的表现。 ... [详细]
  • 本文档汇总了Python编程的基础与高级面试题目,涵盖语言特性、数据结构、算法以及Web开发等多个方面,旨在帮助开发者全面掌握Python核心知识。 ... [详细]
  • 本题探讨了在大数据结构背景下,如何通过整体二分和CDQ分治等高级算法优化处理复杂的时间序列问题。题目设定包括节点数量、查询次数和权重限制,并详细分析了解决方案中的关键步骤。 ... [详细]
  • 本文详细介绍了Grand Central Dispatch (GCD) 的核心概念和使用方法,探讨了任务队列、同步与异步执行以及常见的死锁问题。通过具体示例和代码片段,帮助开发者更好地理解和应用GCD进行多线程开发。 ... [详细]
  • 深入解析Java虚拟机(JVM)架构与原理
    本文旨在为读者提供对Java虚拟机(JVM)的全面理解,涵盖其主要组成部分、工作原理及其在不同平台上的实现。通过详细探讨JVM的结构和内部机制,帮助开发者更好地掌握Java编程的核心技术。 ... [详细]
  • 深入剖析JVM垃圾回收机制
    本文详细探讨了Java虚拟机(JVM)中的垃圾回收机制,包括其意义、对象判定方法、引用类型、常见垃圾收集算法以及各种垃圾收集器的特点和工作原理。通过理解这些内容,开发人员可以更好地优化内存管理和程序性能。 ... [详细]
  • 本文深入探讨了UNIX/Linux系统中的进程间通信(IPC)机制,包括消息传递、同步和共享内存等。详细介绍了管道(Pipe)、有名管道(FIFO)、Posix和System V消息队列、互斥锁与条件变量、读写锁、信号量以及共享内存的使用方法和应用场景。 ... [详细]
  • 序列化与反序列化是数据处理中的重要技术,特别是在网络通信和数据存储中。它们允许将复杂的数据结构转换为可传输或存储的格式,再从这些格式恢复原始数据。本文探讨了序列化与反序列化的基本概念,以及它们在不同协议模型中的角色。 ... [详细]
  • 深入解析BookKeeper的设计与应用场景
    本文介绍了由Yahoo在2009年开发并于2011年开源的BookKeeper技术。BookKeeper是一种高效且可靠的日志流存储解决方案,广泛应用于需要高性能和强数据持久性的场景。 ... [详细]
  • 1、字符型常量字符型常量指单个字符,是用一对单引号及其所括起来的字符表示。例如:‘A’、‘a’、‘0’、’$‘等都是字符型常量。C语言的字符使用的就是 ... [详细]
author-avatar
1234
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有