【C/C++ 堆栈以及虚拟内存分段 】C/C++内存分布/管理:代码区、数据区、堆区、栈区和常量区的探索

简介: 【C/C++ 堆栈以及虚拟内存分段 】C/C++内存分布/管理:代码区、数据区、堆区、栈区和常量区的探索

以下是程序运行时内存区域的图示,包括代码区、数据区、堆区、栈区、常量区和静态存储区,以及它们的特性:

  • 代码区:编译期间确定,运行时只读,不可修改。
  • 数据区:编译期间大小确定,运行时可读写。
  • 堆区:运行时动态分配,可读写。
  • 栈区:函数调用时动态创建,函数返回时销毁。
  • 常量区:编译期间确定,运行时只读,不可修改。
  • 静态存储区:存储生命周期为整个程序运行期间的对象。

各个内存区介绍

代码区

代码区,也被称为文本区,是内存中的一个区域,用于存储程序的机器代码。这部分内存在程序编译期间就已经确定,并且在程序运行期间是只读的,不可修改。这是因为修改正在执行的代码是非常危险的,可能会导致程序崩溃或者产生不可预知的行为。

代码区通常包括以下内容:

  1. 机器代码:这是由编译器从源代码生成的二进制代码,CPU可以直接执行这些代码。
  2. 常量:一些编译期间就已经确定的常量值,如字符串字面量,也会被存储在代码区。
  3. 跳转表:这是一些用于实现跳转指令的数据结构,如用于实现switch语句的跳转表。

代码区是共享的,也就是说,如果多个进程运行同一个程序,那么它们可以共享同一份代码区。这是因为代码区是只读的,所以不会有一个进程修改代码区影响到其他进程的问题。

总的来说,代码区是存储程序代码的地方,它在编译期间就已经确定,运行期间是只读的,不可修改,可以被多个进程共享。

数据区

数据区,也被称为全局存储区,主要存储全局变量和静态变量。这部分在编译期间大小就已经确定,运行时可以读写。

全局变量是在函数外部定义的变量,它们在整个程序的生命周期内都是可见的,可以被程序中的所有函数访问。全局变量在程序启动时分配内存,在程序结束时释放内存。

静态变量是在函数内部或者外部使用static关键字定义的变量。静态变量在程序启动时分配内存,在程序结束时释放内存。静态变量的生命周期是整个程序运行期间,但它的作用域(即可以访问它的代码范围)取决于它是在函数内部还是外部定义的。如果静态变量在函数内部定义,那么它只能在该函数内部访问;如果在函数外部定义,那么它可以在定义它的文件中的任何函数内部访问。

数据区的大小在编译时就已经确定,因为全局变量和静态变量的大小在编译时就已经知道。在运行时,可以读取和修改数据区中的变量。

总的来说,数据区是用于存储全局变量和静态变量的内存区域,这些变量的生命周期是整个程序运行期间,数据区的大小在编译时就已经确定。

堆区

堆区是程序运行时动态分配的内存空间。它的大小并不在编译时确定,而是在运行时根据程序的需要进行动态分配。堆区的内存分配和释放是由程序员手动控制的,这就给了程序员更大的灵活性,但同时也带来了更大的责任,因为忘记释放已分配的内存会导致内存泄漏。

在C++中,你可以使用new关键字在堆上分配内存,然后使用delete关键字释放这些内存。在C语言中,你可以使用malloc函数分配内存,然后使用free函数释放这些内存。

堆区的内存是全局可见的,也就是说,一旦你在堆上分配了内存,你可以在程序的任何地方使用这块内存,只要你有一个指向它的指针。这使得堆区成为了存储大型数据结构(如数组和对象)和在函数调用之间共享数据的理想场所。

然而,堆区的内存分配和释放通常比栈区要慢,因为操作系统需要在堆上找到一块足够大的连续内存空间来满足请求。此外,频繁的分配和释放堆内存可能会导致内存碎片化,进一步降低性能。

总的来说,堆区提供了一种灵活的内存管理方式,但使用它需要谨慎,以避免内存泄漏和性能问题。

  1. 堆的方向:从理论上讲,堆可以向任何方向增长,这取决于具体的实现。但在许多系统中,堆确实是向高地址扩展的。
  2. 堆的连续性:堆内存通常是不连续的,这是因为它是由操作系统的内存管理器在需要时动态分配的。当你从堆中请求内存时,内存管理器会查找足够大的空闲块并返回其地址。这些块可能分散在内存中的各个地方。
  3. 链表和堆:操作系统使用多种数据结构(如链表、树等)来跟踪空闲的内存块,但这与应用程序如何使用堆无关。应用程序看到的是一个连续的内存块,即使这些块在物理内存或虚拟内存中是不连续的。
  4. 堆的大小:堆的大小受到有效虚拟内存的限制。在32位系统上,这通常是4GB(虽然实际可用的可能少于这个值)。在64位系统上,虚拟内存的大小可以远远超过物理内存的大小。
  5. 堆的灵活性:堆确实提供了灵活性,允许动态分配和释放内存。但这也带来了额外的复杂性,因为程序员需要确保正确地管理内存,避免内存泄漏和其他问题。

栈区

栈区(Stack)是由编译器自动分配和释放的,存放函数的参数值、局部变量等。其操作方式类似于数据结构中的栈。

当一个函数被调用时,会在栈区中分配一块连续的空间,这个空间被称为"栈帧"(Stack Frame)。每个栈帧对应一个被调用的函数。栈帧中包含了这个函数需要的局部变量、参数值以及其他的一些信息。

当函数调用结束后,这个函数对应的栈帧就会被销毁,释放出的空间可以用来存储其他函数的栈帧。这就是为什么局部变量只在函数调用期间存在,函数调用结束后,局部变量就会消失。

栈区的分配和释放是由编译器自动完成的,速度非常快,但是分配的空间大小有限。如果请求的空间超过了栈的剩余空间,就会导致"栈溢出",这通常是由于递归调用过深导致的。

总的来说,栈区是用来存储每次函数调用的上下文信息的,这些信息包括但不限于函数的返回地址、函数的参数、局部变量等。

你的描述基本上是正确的,但有一些细节需要澄清:

  1. 栈的方向:在大多数现代的计算机架构中,栈确实是向低地址扩展的。这意味着当你向栈中压入数据时,栈顶的地址实际上是减小的。
  2. 栈的大小
  • Windows:默认情况下,线程的栈大小是1MB。但这个值可以在编译时通过编译器选项进行修改。
  • Linux:对于Linux,主线程的默认栈大小通常是8MB,但对于新创建的线程,栈大小可能是更小的,例如2MB。这个值也可以通过线程属性或ulimit命令进行调整。
  1. 栈溢出:如果一个程序使用的栈空间超过了为它分配的空间,它会导致栈溢出,这通常会导致程序崩溃。这是因为超出栈的空间可能会覆盖其他重要的内存区域。
  2. 栈 vs 堆:栈空间是有限的,而且相对较小。如果需要大量的内存或动态分配内存,通常会使用堆。堆的大小只受限于计算机的虚拟内存大小,但使用堆分配的内存需要手动管理。

变量区

在程序运行时,变量存储在内存的不同区域,具体取决于变量的类型和生命周期。以下是详细的解释:

  1. 全局变量和静态变量:这些变量存储在数据区,也被称为全局存储区。全局变量是在函数外部定义的变量,它们在程序的整个生命周期内都是有效的。静态变量是在函数内部或外部使用static关键字定义的变量,它们的生命周期也是整个程序的运行期间,但它们的作用域(即可以访问它们的代码范围)是局限于定义它们的函数或文件。
  2. 局部变量:这些变量存储在栈区。局部变量是在函数内部定义的变量,它们只在函数调用的期间存在。当函数返回时,存储这些变量的内存被释放,这些变量的值就丢失了。
  3. 动态变量:这些变量存储在堆区。动态变量是使用new或malloc等函数动态创建的变量。这些变量的生命周期由程序员控制,即程序员可以在任何时候创建和销毁这些变量。动态变量在堆区中的位置是在运行时决定的,而不是在编译时。

这些变量区的主要区别在于变量的生命周期(即变量存在多长时间)和作用域(即哪些代码可以访问变量)。全局变量和静态变量在程序的整个生命周期内都存在,而局部变量只在特定的函数调用期间存在,动态变量则在程序员决定的任何时间存在。

静态存储区

静态存储区是程序运行时内存的一部分,主要用于存储全局变量和静态变量。

  1. 全局变量:全局变量是在函数外部定义的变量,它们在程序的整个生命周期内都是存在的。全局变量可以在程序的任何地方被访问和修改。
  2. 静态变量:静态变量是在函数内部或者类中定义的,但是它们的生命周期和全局变量一样,会在程序的整个生命周期内存在。静态变量在第一次被定义时初始化,之后即使函数或者类的实例被销毁,静态变量也不会被销毁。静态变量只能在定义它的函数或者类中被访问。

静态存储区的大小在编译时就已经确定,运行时可以读写。这部分内存在程序结束时由操作系统自动回收。

静态存储区与堆区和栈区的主要区别在于生命周期和存储的数据类型。堆区和栈区的数据在使用完后需要手动或自动回收,而静态存储区的数据在程序结束时才会被回收。堆区主要用于存储动态分配的数据,栈区主要用于存储函数调用的上下文,而静态存储区主要用于存储全局变量和静态变量。

编程种的运用

设置和获取各种内存区域的数据

在C++中,你可以通过以下方式设置和获取各种内存区域的数据:

  1. 代码区:代码区主要存储程序的机器代码,这部分在编译期间确定,运行时只读,不可修改。你无法在运行时修改代码区的内容。
  2. 数据区(静态存储区):数据区用于存储全局变量和静态变量。你可以通过以下方式设置和获取这些变量:
  • 全局变量:全局变量在函数外部定义,可以在程序的任何地方被访问和修改。例如:
int global_var = 10; // 定义全局变量
  • 静态变量:静态变量在函数内部或类中定义,但它们的生命周期和全局变量一样。例如:
void func() {
    static int static_var = 0; // 定义静态变量
    static_var++;
}
  1. 堆区:堆区用于存储动态分配的内存。你可以使用newdelete(或new[]delete[]对于数组)来分配和释放堆内存。例如:
int* heap_var = new int; // 在堆上分配一个整数
*heap_var = 10; // 设置这个整数的值
delete heap_var; // 释放这个整数
  1. 栈区:栈区用于存储函数调用的上下文,包括局部变量、函数参数和返回地址等。你可以通过定义局部变量来在栈上分配内存。例如:
void func() {
    int stack_var = 10; // 在栈上分配一个整数
}
  1. 注意,当函数返回时,所有在函数内部定义的局部变量都会被自动销毁。
  2. 常量区:常量区用于存储常量数据,这部分在编译期间确定,运行时只读,不可修改。你可以通过定义常量来在常量区分配内存。例如:
const int const_var = 10; // 在常量区分配一个整数

请注意,尽管我们可以在C++中操作这些内存区域,但是我们通常不需要直接管理代码区和常量区,因为它们是由编译器自动处理的。我们主要需要关注的是如何正确地在堆区分配和释放内存,以及如何在栈区和数据区使用变量。

获取内存区大小

在C++中,获取各种内存区域的大小并不直接。这是因为这些内存区域的大小通常是由操作系统和硬件决定的,而且在程序运行时可能会发生变化。然而,你可以使用一些方法来估计或限制这些内存区域的大小:

  1. 代码区:代码区的大小等于程序的机器代码的大小,这可以通过查看编译后的可执行文件的大小来估计。在Unix-like系统中,你可以使用size命令来查看各个区域的大小。
  2. 数据区(静态存储区):数据区的大小等于全局变量和静态变量的总大小。你可以通过统计所有全局变量和静态变量的大小来估计这个值。
  3. 堆区:堆区的大小取决于程序运行时动态分配的内存的总量。你可以通过跟踪所有的newdelete操作来估计这个值。在Unix-like系统中,你可以使用mallinfo()函数来获取堆的使用情况。
  4. 栈区:栈区的大小取决于函数调用的深度和每个函数调用所需的栈空间。你可以通过设置栈大小的编译器选项来限制栈的大小。在Unix-like系统中,你可以使用ulimit -s命令来查看或设置栈的大小。
  5. 常量区:常量区的大小等于程序中所有常量的总大小。你可以通过统计所有常量的大小来估计这个值。

请注意,以上方法只能提供估计值,因为实际的内存使用情况可能会受到许多因素的影响,包括操作系统的内存管理策略、硬件的限制等。如果你需要精确的内存使用情况,你可能需要使用专门的内存分析工具。

创建线程时设置

在Linux中,创建进程或线程时,各个内存区域的大小大部分是由系统自动管理的,而不是由用户显式设置的。这是因为这些内存区域的大小通常取决于程序的代码、数据和动态内存分配,而这些都是在程序运行时才能确定的。

然而,你可以通过一些方式来影响这些内存区域的大小:

  1. 代码区:代码区的大小等于程序的机器代码的大小,这是在编译时就已经确定的,用户无法在运行时修改。
  2. 数据区(静态存储区):数据区的大小等于全局变量和静态变量的总大小。这也是在编译时就已经确定的,用户无法在运行时修改。
  3. 堆区:堆区的大小取决于程序运行时动态分配的内存的总量。你可以通过malloccallocreallocfree函数(或者在C++中使用newdelete)来在堆上分配和释放内存。Linux系统还提供了brksbrk函数来改变程序的堆大小。
  4. 栈区:栈区的大小取决于函数调用的深度和每个函数调用所需的栈空间。你可以通过ulimit -s命令来查看或设置栈的大小。在创建线程时,你可以通过pthread_attr_setstacksize函数来设置新线程的栈大小。
  5. 常量区:常量区的大小等于程序中所有常量的总大小。这也是在编译时就已经确定的,用户无法在运行时修改。

请注意,以上方法只能影响各个内存区域的大小,而不能精确地设置它们的大小。如果你需要精确地控制内存使用,你可能需要使用更底层的编程技术,如汇编语言或操作系统的内核编程。

在Linux中,创建线程时可以通过pthread_attr_setstacksize函数来设置新线程的栈大小。以下是一个例子:

#include <pthread.h>
void* thread_func(void* arg) {
    // 线程函数
}
int main() {
    pthread_t thread;
    pthread_attr_t attr;
    // 初始化线程属性
    pthread_attr_init(&attr);
    // 设置线程栈大小
    size_t stacksize = 1024*1024; // 1MB
    pthread_attr_setstacksize(&attr, stacksize);
    // 创建线程
    pthread_create(&thread, &attr, thread_func, NULL);
    // 销毁线程属性对象
    pthread_attr_destroy(&attr);
    // 等待线程结束
    pthread_join(thread, NULL);
    return 0;
}

这段代码创建了一个新的线程,并设置了该线程的栈大小为1MB。pthread_attr_setstacksize函数的第一个参数是一个线程属性对象,第二个参数是新的栈大小。

请注意,虽然你可以设置线程的栈大小,但是你不能直接设置线程的堆大小、代码区大小、数据区大小或常量区大小。这些内存区域的大小是由程序的代码、数据和动态内存分配决定的。

另外,虽然你可以设置线程的栈大小,但是你应该谨慎地选择栈大小。如果栈太小,线程可能会因为栈溢出而崩溃。如果栈太大,可能会浪费内存,甚至导致内存不足。你应该根据线程的实际需要来选择合适的栈大小。

变更内存区

在C++中,你可以通过一些特定的函数来操作内存区域,但是这些操作通常限于堆区和栈区。以下是一些例子:

  1. 堆区:你可以使用newdelete(或new[]delete[]对于数组)来在堆上分配和释放内存。例如:
int* heap_var = new int; // 在堆上分配一个整数
*heap_var = 10; // 设置这个整数的值
delete heap_var; // 释放这个整数
  1. 你也可以使用mallocfree函数来分配和释放堆内存,这些函数在C语言中常用。
  2. 栈区:你可以通过定义局部变量来在栈上分配内存。例如:
void func() {
    int stack_var = 10; // 在栈上分配一个整数
}
  1. 当函数返回时,所有在函数内部定义的局部变量都会被自动销毁。

对于代码区、数据区(静态存储区)和常量区,你通常不能在运行时改变它们的大小。这些区域的大小在编译时就已经确定,运行时只能读取(对于代码区和常量区)或读写(对于数据区),但不能改变它们的大小。

请注意,尽管你可以在C++中操作这些内存区域,但是你应该谨慎地管理内存。不正确的内存管理(例如,忘记释放分配的内存,或试图访问已经被释放的内存)可能会导致程序崩溃或其他未定义的行为。

目录
相关文章
|
1月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
22 3
|
2月前
|
存储 SQL 安全
理解堆栈和内存溢出
【10月更文挑战第05天】
45 3
|
2月前
|
算法 C++
|
2月前
|
算法 C++
【算法单调栈】 矩形牛棚(C/C++)
【算法单调栈】 矩形牛棚(C/C++)
|
4月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
4月前
|
存储 程序员 编译器
堆和栈内存的区别是什么
【8月更文挑战第23天】堆和栈内存的区别是什么
394 4
|
4月前
|
存储 Java 编译器
Java内存区域与内存溢出异常 - 运行时数据区
【8月更文挑战第2天】Java运行时数据区包括:1) 程序计数器:记录线程执行字节码的行号,线程私有;2) Java虚拟机栈:描述方法执行的内存模型,线程私有,深度过大抛出`StackOverflowError`;3) 本地方法栈:服务于Native方法,线程私有;4) Java堆:所有线程共享,对象实例在此分配内存;5) 方法区:存储类信息、常量等数据;6) 运行时常量池:方法区的一部分,存放字面量和符号引用。不当使用如无限创建对象或过度递归调用会导致各种内存溢出错误。
|
6月前
|
存储 监控 算法
Java堆栈内存管理与优化技巧的实践指南
Java堆栈内存管理与优化技巧的实践指南
|
5月前
|
存储 Java 数据库连接
Java堆栈内存管理与优化技巧的实践指南
Java堆栈内存管理与优化技巧的实践指南
|
5月前
|
存储 C语言 C++
【C/C++】动态内存管理( C++:new,delete)
C++的`new`和`delete`用于动态内存管理,分配和释放内存。`new`分配内存并调用构造函数,`delete`释放内存并调用析构函数。`new[]`和`delete[]`分别用于数组分配和释放。不正确匹配可能导致内存泄漏。内置类型分配时不初始化,自定义类型则调用构造/析构。`operator new`和`operator delete`是系统底层的内存管理函数,封装了`malloc`和`free`。定位`new`允许在已分配内存上构造对象,常用于内存池。智能指针等现代C++特性能进一步帮助管理内存。