---
title: "C语言内存模型"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/c-memory-model/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/02-编程语言与运行时/C++/C语言内存模型.md"
content_hash: 022a2a5abb01862183e0f30dcdc9571d440e8cb5b53f33a93d517895adb3fc39
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# C语言内存模型

> 创建时间：2020/10/6 18:54

  * C语言内存模型
    * C语言内存布局概述
      * 内存区域总结
      * C语言编辑运行机制与内存
      * 延申知识
        * PE文件格式
        * 可执行文件、目标文件、DLL
        * Windows NT
    * 三种内存分配方式对比
      *         * 从静态存储区分配
        * 在栈上创建
        * 从堆上分配（动态内存分配）
    * 关于堆区和栈区
      * 堆和栈的区别
      * 关于结构体或类的内存分配
        * 结构体的成员
    * 内存对齐（memory alignment）
    * 函数调用的过程——函数栈
    * 经典的堆栈的讲解
    * 相关语法
      * new和malloc的区别
      * 关于free
      * 例子
    * Ref

## C语言内存布局概述

### 内存区域总结

![Alt text](/media/5f75f9332608bd8d937d.png)

栈区（stack）：由编译器自动分配与释放，存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈。
堆区（heap）：一般由程序员自动分配，如果程序员没有释放，程序结束时可能有OS回收。其分配类似于链表。
全局/静态存储区 （static .bss段和.data段）：存放全局变量、静态数据、常量。程序结束后由系统释放。全局区分为已初始化全局区（data）和未初始化全局区（bss）。
常量区（.rodata段）：存放常量字符串，程序结束后有系统释放。
代码区（.text段）：存放函数体（类成员函数和全局区）的二进制代码。

### C语言编辑运行机制与内存

在Windows平台下，高级语言写出的程序经过编译链接，最终会变成PE文件。当PE文件被装载运行后，就成了所谓的进程。
PE文件代码段中包含的二进制级别的机器代码会被装入内存的 **代码区(.text)** ，处理器将到内存的这个区域一条一条地取出指令和操作数，并送入运算逻辑单元进行运算；如果代码中请求开辟 **动态内存** ，则会在内存的 **堆区** 分配一块大小合适的区域返回给代码区的代码使用；当函数调用发生时， **函数的调用关系等信息** 会动态地保存在内存的 **栈区** ，以供处理器在执行完被调用函数的代码时，返回母函数。

### 延申知识

#### PE文件格式

**PE文件** （Portable Executable，可移植性可执行文件）是一种用于 **可执行文件** 、 **目标文件** 和 **动态链接库** 的文件格式，主要使用在32位和64位的Windows操作系统上。

> “可移植的”是指该文件格式的通用性，可用于许多种不同的操作系统和体系结构中。

PE文件格式封装了Windows操作系统加载可执行程序代码时所必需的一些信息。这些信息包括动态链接库、API导入和导出表、资源管理数据和线程局部存储数据。在Windows NT操作系统中，PE文件格式主要用于EXE文件、DLL文件、.sys（驱动程序）和其他文件类型。

#### 可执行文件、目标文件、DLL

**可执行文件** 指一种内容可 **被电脑解释为程序** 的电脑文件。通常可执行文件内，含有 **以二进制编码的微处理器指令** ，也因此可执行文件有时称为 **二进制档** 。

* * *

**目标文件** （object file）即存放目标代码的计算机文件，它常被称作 **二进制文件（binaries）** 。
**目标代码** （英语：object code）指计算机科学中编译器或汇编器处理源代码后所生成的代码，它一般由机器代码或接近于机器语言的代码组成。

目标文件是从 **源代码文件** 产生 **程序文件** 这一过程的 **中间产物** ， **链接器** 正是通过把目标文件链接在一起来生成可执行文件或库文件。

> 目标文件包含着机器代码（可直接被计算机中央处理器执行）以及代码在运行时使用的数据，如重定位信息，如用于链接或调试的程序符号（变量和函数的名字），此外还包括其他调试信息。

* * *

**DLL** (动态链接库, Dynamic-link library) 是微软公司在Windows操作系统中实现 **共享函数库** 概念的一种实现方式。这些库函数的扩展名是.DLL、.OCX（包含ActiveX控制的库）或者.DRV（旧式的系统驱动程序）。

#### Windows NT

**Windows NT（Windows New Technology）** 是美国微软公司1993年推出的纯32位操作系统 **核心** 。
Windows NT 3.1 ~ Windows 2000 ~ Windows XP ~ Windows Vista ~ Windows 10

![Windows 2000 architecture](/media/11b2e47b8f69068879cb.png)

## 三种内存分配方式对比

#### 从静态存储区分配

内存在程序编译的时候已经分配好，这块内存在程序的整个运行期间都存在。例如全局变量，static变量。

#### 在栈上创建

在执行函数时，函数内局部变量的存储单元可以在栈上创建，函数执行结束时，这些内存单元会自动被释放。
栈内存分配运算内置于处理器的指令集，效率高，但是分配的内存容量有限。

#### 从堆上分配（动态内存分配）

程序在运行的时候使用malloc或者new申请任意多少的内存，程序员自己负责在何时用free或delete释放内存。
动态内存的生命周期有程序员决定，使用非常灵活，但如果在堆上分配了空间，既有责任回收它，否则运行的程序会出现内存泄漏，频繁的分配和释放不同大小的堆空间将会产生内存碎片。

## 关于堆区和栈区

栈区（stack）用于存放局部变量、返回地址、函数的实参等，由编译器自动分配释放。栈的功能主要是函数调用、局部变量申请、函数参数传递所使用的空间，是为函数调用的实现提供一些保存、恢复操作。一般情况下栈空间都特别小。
在windows下，栈地址是向低地址扩展的数据结构，是一块连续的内存区域。栈顶的地址和栈的最大容量是系统预先规定好的，在windows下，栈的大小是2MB，而申请堆空间的大小一般小于2GB
栈存放的数据占空间比较小，而且栈内存不由程序员来管理，存放在栈里面的局部变量随着函数调用结束就自动释放了。

堆区（heap）是一个程序运行动态分配的内存区域。一般由程序员分配释放，若程序员不释放，程序结束时可能由OS回收。 程序员通过new等方式创建的对象，一般会在堆区中分配内存。

我们平常说的GC，指的是对堆内存的回收。对内存的优化一般也指的是堆内存。

### 堆和栈的区别

**管理方式** 不同： **栈** 是由 **编译器自动** 申请和释放空间， **堆** 是需要 **程序员手动** 申请和释放；
**空间** 大小不同： **栈** 的空间是有限的，在32位平台下，VC6下默认为 **1M** ， **堆** 最大可以到 **4G** ；
能否产生 **碎片** ： **栈** 和数据结构中的栈原理相同，在弹出一个元素之前，上一个已经弹出了， **不会** 产生碎片，如果不停地调用 **malloc、free堆** 造成内存 **碎片很多** ；
**生长方向** 不同： **堆** 生长方向是 **向上** 的，也就是向着内存地址增加的方向， **栈** 刚好相反，向着内存 **减小的方向** 生长。
**分配方式** 不同： **堆** 都是 **动态分配** 的，没有静态分配的堆。 **栈** 有 **静态** 分配和 **动态** 分配。 **静态** 分配是 **编译器完成** 的，比如局部变量的分配。 **动态分配** 由 **malloc 函数进行分配** ，但是栈的动态分配和堆是不同的，它的 **动态分配是由编译器进行释放** ，无需我们手工实现。
**分配效率** 不同： **栈** 的效率比堆 **高** 很多。栈是机器系统提供的数据结构，计算机在底层提供栈的支持，分配专门的寄存器来存放栈的地址，压栈出栈都有相应的指令，因此比较快。 **堆** 是由库函数提供的，机制很复杂，库函数会按照一定的算法进行搜索内存，因此比较慢。

栈区（stack）用于存放局部变量、返回地址、函数的实参等，由编译器自动分配释放。栈的功能主要是函数调用、局部变量申请、函数参数传递所使用的空间，是为函数调用的实现提供一些保存、恢复操作。一般情况下栈空间都特别小。
在windows下，栈地址是向低地址扩展的数据结构，是一块连续的内存区域。栈顶的地址和栈的最大容量是系统预先规定好的，在windows下，栈的大小是2MB，而申请堆空间的大小一般小于2GB
栈存放的数据占空间比较小，而且栈内存不由程序员来管理，存放在栈里面的局部变量随着函数调用结束就自动释放了。

栈区大小的限制，不同的资料上有不同的说法，有个文章说申请大小的限制是1或者2M，可以自己改，但是最大不超过8M

堆区（heap）是一个程序运行动态分配的内存区域。一般由程序员分配释放，若程序员不释放，程序结束时可能由OS回收。 程序员通过new等方式创建的对象，一般会在堆区中分配内存。

我们平常说的GC，指的是对堆内存的回收。对内存的优化一般也指的是堆内存。

嗯，这段描述基本适用于多数语言了。不过c# 函数里new的struct对象（值类型）一般情况都是在栈上的，和c++不一样。然后堆的空间在64位下就没有2G或者4G的限制了。

### 关于结构体或类的内存分配

```cpp
string str1 = &quot;test&quot;;
string str2 = &quot;test&quot;;

```

dog1在栈中
dog2指向堆中的结构体

  * 函数内部声明的局部变量的内存由编译器在栈中自动分配与释放，函数结束时栈会回收，局部变量也随之销毁。如果局部变量为类对象，其析构函数会被自动调用。

  * 若使用new/malloc分配内存，则该内存在堆上，需要由程序员释放。
如以下语句：

```cpp
string str1 = &quot;test&quot;;
string str2 = &quot;test&quot;;

```

这行语句的含义是，使对象obj具有“自动存储（automatic storage）”的性质。所谓“自动存储”，意思是这个对象的存储位置取决于其声明所在的上下文。如果这个语句出现在函数内部，那么它就在栈上创建对象。如果这个语句不是在函数内部，而是作为一个类的成员变量，则取决于这个类的对象是如何分配的。

#### 结构体的成员

```cpp
string str1 = &quot;test&quot;;
string str2 = &quot;test&quot;;

```

使用方法如下：

  * unit_t1申请了 **栈** 空间，不需要自己去考虑使用后释放空间的事情。

  * unit_t2中申请了 **堆** 空间，需要自己释放。

```cpp
string str1 = &quot;test&quot;;
string str2 = &quot;test&quot;;

```

## 内存对齐（memory alignment）

操作的内存地址（更准确的说，操作内存的起始地址）能够被操作的内存大小整除，满足这个要求的内存访问叫做访问对齐的内存（aligned memory access），否则就是访问未对齐的内存（unaligned memory access）。
<https://www.cnblogs.com/zhao-zongsheng/p/9099603.html>

## 函数调用的过程——函数栈

<https://blog.csdn.net/m0_37717595/article/details/80368411>

## 经典的堆栈的讲解

<https://blog.csdn.net/yingms/article/details/53188974>

## 相关语法

### new和malloc的区别

  1. new 是 **运算符** malloc是 **库函数**

  2. **new和delete底层还是由malloc和free实现** 的，区别在于new和delete在生成复杂数据类型时会执行两步: **分配内存** 和 **调用构造函数** / **调用析构函数** 和 **销毁内存** 。

  3. **malloc** 申请的内存 **没有类型** ,为(void*) 需要根据使用需求进行强制转换, **new** 不需要,返回 **对应类型的指针** 。

  4. new并不是每次都会调用构造函数，对于简单类型eg.int是不需要的。

### 关于free

>   * 当不需要再使用申请的内存时，记得 **释放** ，并 **且指针=NULL** ；释放后应该把指向这块内存的指针指向NULL， **防止程序后面不小心使用了它** 。
>
>   * free时候,指针不能发生偏移，原因是指针指向的是申请空间的首地址，而且申请的空间的大小在free源码结构体中有记录
>
>

### 例子

```cpp
string str1 = &quot;test&quot;;
string str2 = &quot;test&quot;;

```

## Ref

<https://www.cnblogs.com/houjun/p/4909413.html>
<https://www.cnblogs.com/qiuly/p/8576149.html>
<http://www.360doc.com/content/12/0102/11/1299815_176623363.shtml>
<https://www.zhihu.com/question/19729973>
<http://www.360doc.com/content/12/0102/11/1299815_176623363.shtml>
<https://blog.csdn.net/yingms/article/details/53188974>
<https://www.cnblogs.com/cchHers/p/10010275.html>
<https://zhuanlan.zhihu.com/p/78478567>
<https://www.zhihu.com/question/59238963>
<https://blog.csdn.net/cherrydreamsover/article/details/81627855>
<https://blog.csdn.net/buptzy/article/details/80149622>
<https://www.cnblogs.com/will-boot/p/3302936.html>
<https://blog.csdn.net/feliciafay/article/details/9830005>
