C++学习之内存模型

简介: C++学习之内存模型

C++ 的内存模型

C++的内存模型由以下几个组成部分

  1. 栈(Stack):栈是用于存储局部变量、函数参数、函数调用信息等的一块内存区域。它以后进先出(LIFO)的方式进行管理。栈的分配和释放是自动进行的,当一个函数被调用时,会将函数的局部变量和参数压入栈中,当函数调用结束时,这些变量被自动释放。
  2. 堆(Heap):堆是一块动态分配内存的区域,用于存储程序运行时动态创建的对象。在堆上分配的内存需要程序员显式地进行分配和释放。C++中,使用 new 操作符来动态分配堆内存,使用 delete 操作符来释放堆内存。如果没有释放堆内存,就会导致内存泄漏。
  3. 代码区(Code Area):代码区是存储程序的可执行指令的区域,也称为文本区。这部分内存是只读的,不允许被修改。代码区的内容在程序的执行过程中不会发生改变。
  4. 全局区(Global Area):全局区用于存储全局变量和静态变量,包括初始化过的全局变量和静态变量。这些变量在程序启动时就被分配内存,在整个程序执行过程中都存在。
  5. 常量区(Constant Area):常量区存储程序中的常量数据,如字符串常量等。这部分内存区域是只读的,不允许修改。
  6. 动态分配(Dynamic Allocation):动态分配是指通过使用 new 操作符在堆上动态创建对象,并返回指向该对象的指针。这种方式允许在程序运行时根据需要进行内存的动态分配和释放。

动态分配内存的好处是可以灵活地管理内存资源,但也需要注意避免内存泄漏和悬空指针等问题。对于每次使用 new 操作符分配的内存,应该使用对应的 delete 操作符来释放内存,以避免内存泄漏。同时,要确保在使用指向动态分配内存的指针时,指针所指向的内存块仍然有效,避免出现悬空指针导致的未定义行为。

C++内存模型:栈

栈是C++内存模型中的一部分,用于存储局部变量、函数参数、返回地址和函数调用信息等。以下是栈的几种常见使用情况和相应的示例:

  1. 存储局部变量:栈最常见的用途就是存储函数内的局部变量。当一个函数被调用时,该函数的局部变量会被存储在栈上,并在函数调用结束后自动释放。
void foo() {
    int x = 10; // x 是局部变量,存储在栈上
    // ...
}
  1. 存储函数参数:函数参数也会存储在栈上。当调用函数时,参数被传递给函数并在栈上分配内存空间来存储这些参数。
void bar(int a, int b) {
    // a 和 b 是函数参数,存储在栈上
    // ...
}
int main() {
    int x = 5, y = 7;
    bar(x, y);
    // ...
}
  1. 函数调用和返回地址:栈还用于存储函数调用和返回地址,以及其他与函数调用相关的信息。每当一个函数被调用时,当前函数的返回地址会被推入栈中,以便在函数执行完成后返回到正确的位置。
void func1() {
    // ...
}
void func2() {
    func1(); // 调用另一个函数
    // ...
}
int main() {
    func2();
    // ...
}
  1. 内存分配和释放:栈上的内存分配和释放是自动进行的。当函数被调用时,函数所需的内存空间被分配到栈上;函数调用结束后,随之分配的内存空间会自动释放。
char* createString() {
    char str[] = "Hello"; // 字符串存储在栈上
    return str;
}
int main() {
    char* ptr = createString(); // 返回栈上的字符串指针(潜在问题)
    // ...
}

函数 createString() 在栈上创建了一个字符串数组 str,并返回该字符串的指针。然而,由于该字符串数组是存储在栈上的局部变量,当函数调用结束后,该数组将被释放,导致返回的指针指向一个无效的内存区域。这种情况应该避免,可以通过动态分配内存来解决。

请注意:栈上的内存是有限的,过多的栈内存使用可能导致栈溢出的问题。因此,在使用栈时,需要确保合理分配和释放内存,避免大量的局部变量或递归调用导致栈空间耗尽。

C++内存模式:堆


堆是C++内存模型中的一部分,用于动态分配内存,它提供了在程序运行时请求和释放内存的能力。下面是堆的几种常见使用情况和相应的示例:

  1. 动态创建对象:堆可以用于动态创建对象,这些对象的生存周期不受函数调用的限制,并可以在程序的任何地方访问。
class MyClass {
    // ...
};
int main() {
    MyClass* ptr = new MyClass(); // 在堆上动态创建一个 MyClass 对象
    // ...
    delete ptr; // 释放堆上的对象内存
    // ...
}
  1. 动态创建数组:堆可以用于动态创建数组,这使得数组的大小可以在运行时确定。
int size = 10;
int* arr = new int[size]; // 在堆上动态创建一个整数数组
for (int i = 0; i < size; i++) {
    arr[i] = i;
}
// ...
delete[] arr; // 释放堆上的数组内存
// ...
  1. 使用动态分配的字符串:堆非常适合在运行时创建和操作字符串,字符串的长度可以在运行时决定。
char* str = new char[20]; // 在堆上动态创建一个字符数组
strcpy(str, "Hello, World!");
// ...
delete[] str; // 释放堆上的字符数组内存
// ...
  1. 维护动态数据结构:堆的动态分配使得可以实现动态数据结构,如链表、树和图等。
struct Node {
    int value;
    Node* next;
};
Node* createNode(int value) {
    Node* newNode = new Node();
    newNode->value = value;
    newNode->next = nullptr;
    return newNode;
}
int main() {
    Node* head = createNode(1); // 创建一个链表节点
    // ...
    delete head; // 释放链表节点的内存
    // ...
}

在使用堆分配的内存时,要注意正确释放已分配的内存,以防止内存泄漏。使用 new 关键字动态分配的内存应该使用 delete 来手动释放,而使用 new[] 动态分配的数组内存应该使用 delete[] 来释放。

避免悬挂指针和内存泄漏等问题,即确保在释放内存之前,不要保留对该内存的引用。否则,堆上的内存将无法再次访问,导致内存泄漏或未定义的行为。

C++内存模式:代码区

代码区(Code Area)是C++内存模型中用于存储程序的可执行指令的区域,也称为文本区。它是只读的,不允许被修改。以下是代码区的几种常见使用情况和相应的示例:

  1. 存储程序的函数定义:在代码区中,存储着程序中所有函数的二进制表示形式,包括函数体的指令和操作码。
int add(int a, int b) {
    return a + b;
}
int main() {
    int result = add(5, 3);
    // ...
    return 0;
}

函数 add() 的定义和代码存储在代码区中。在程序执行时,执行指令从代码区加载并执行。

  1. 存储程序的全局变量和静态变量:全局变量和静态变量也会存储在代码区中,它们的初始值在编译时就确定了。
const int MAX_SIZE = 100; // 存储在代码区的常量
int globalVar = 10; // 存储在代码区的全局变量
static int staticVar = 20; // 存储在代码区的静态变量
int main() {
    // ...
    return 0;
}

MAX_SIZEglobalVarstaticVar 都存储在代码区中,在程序运行期间它们的值保持不变。

  1. 存储字面常量和字符串常量:字面常量和字符串常量也存储在代码区中。它们是在程序编译时分配的,并在整个程序的生命周期中保持不变。
#include <iostream>
int main() {
    const int x = 5; // 存储在代码区的字面常量
    const char* str = "Hello!"; // 存储在代码区的字符串常量
    std::cout << x << std::endl; 
    std::cout << str << std::endl;
    // ...
    return 0;
}

x 是一个字面常量,它的值在编译时确定,并存储在代码区中。str 是一个字符串常量,也存储在代码区中。

注意:代码区是只读的,不允许直接修改其中的值。如果尝试修改代码区中的数据,将导致不可预测的行为。因此,在C++中,应该遵循对常量的只读访问原则,以确保程序的稳定性和可靠性。

C++内存模型:全局区

C++内存模型中的全局区(Global Area)用于存储全局变量和静态变量。下面是一些关于全局区的使用和举例:

  1. 全局变量(Global Variables):可以在任何函数之外定义的变量被视为全局变量,并在全局区分配内存。全局变量在程序启动时被创建,在整个程序执行期间都存在。
#include <iostream>
int globalVariable = 10;
void func() {
    std::cout << "Global variable: " << globalVariable << std::endl;
}
int main() {
    func();
    return 0;
}

globalVariable 是一个全局变量,它在 func 函数中被访问和使用。全局变量位于全局区,可以被程序中的任何函数访问。

  1. 静态变量(Static Variables):在函数内部定义的静态变量也位于全局区,但只在定义它的函数中可见。静态变量在函数第一次被调用时初始化,并在函数调用结束后不会销毁。
#include <iostream>
void func() {
    static int count = 0;
    count++;
    std::cout << "Count: " << count << std::endl;
}
int main() {
    func(); // 输出 Count: 1
    func(); // 输出 Count: 2
    func(); // 输出 Count: 3
    return 0;
}

count 是一个静态变量,在调用 func 函数时会自动分配内存,并在函数调用结束后不会销毁。每次调用 func 函数,count 的值会递增。

  1. 静态全局变量(Static Global Variables):静态全局变量是声明为 static 的全局变量,它们只能在声明它们的源文件中访问,并且生命周期与整个程序运行期间保持一致。
// File1.cpp
#include <iostream>
static int staticGlobalVariable = 20;
void func() {
    std::cout << "Static global variable: " << staticGlobalVariable << std::endl;
}
// File2.cpp
void func();
int main() {
    func(); // 输出 Static global variable: 20
    return 0;
}

staticGlobalVariable 是一个静态全局变量,它被定义在 File1.cpp 文件中。在另一个文件 File2.cpp 中的 main 函数调用了 func 函数,并访问了静态全局变量。

全局区的变量在程序启动时分配内存,在整个程序执行期间都存在。因此,全局区的变量可以被程序中的任何函数或文件访问,但也要注意全局变量和静态变量的作用域和生命周期,以避免不必要的问题。

C++内存模型:常量区

C++ 中的常量区(Constant Area)用于存储程序中的常量数据,如字符串常量和其他不可修改的数据。常量区是只读的,不允许对其中的数据进行修改。

以下是一些常见的使用常量区的情况以及举例:

  1. 字符串常量:字符串常量通常被存储在常量区,由双引号括起来。这些字符串在程序运行期间保持不变。
const char* str = "Hello, World!"; // 字符串常量存储在常量区
  1. 数字常量:数字常量如整数、浮点数等也可以被视为常量并存储在常量区中。
const int MAX_VALUE = 100; // 常量存储在常量区
  1. 枚举常量:枚举常量也被视为常量,并在常量区中存储。
enum Color {
    RED, // 0
    GREEN, // 1
    BLUE // 2
};
Color c = RED; // 枚举常量存储在常量区
  1. const 修饰的变量:通过使用 const 关键字修饰变量,可以将其视为常量,并在常量区中进行存储。
const int ARRAY_SIZE = 10; // 常量存储在常量区
  1. 字面值常量:C++ 提供了一些字面值常量,如 truefalsenullptr 等,它们也是常量,并存储在常量区。
bool flag = true; // 字面值常量存储在常量区

字符串常量、数字常量、枚举常量、const 变量和字面值常量都是常量,它们在编译时被分配到常量区,无法在运行时修改。使用常量区可以确保这些常量的不可变性,并在需要时提供更好的内存管理和性能。

C++内存模式:动态分配

C++的内存模型中,动态分配内存是一种在程序运行时根据需要分配和释放内存的方式。这种方式可以通过使用 newdelete 运算符来实现。以下是一些关于动态分配内存的常见用法和相应的代码示例:

动态分配单个对象

// 动态分配一个整数
int* ptr = new int;
*ptr = 10;
// 使用动态分配的整数
std::cout << *ptr << std::endl;
// 释放动态分配的内存
delete ptr;

动态分配数组

// 动态分配一个整数数组
int size = 5;
int* arr = new int[size];
// 初始化动态分配的数组
for (int i = 0; i < size; i++) {
    arr[i] = i;
}
// 使用动态分配的数组
for (int i = 0; i < size; i++) {
    std::cout << arr[i] << " ";
}
std::cout << std::endl;
// 释放动态分配的内存
delete[] arr;

动态分配对象

// 定义一个简单的类
class MyClass {
public:
    int data;
    void printData() {
        std::cout << "Data: " << data << std::endl;
    }
};
// 动态分配一个类对象
MyClass* obj = new MyClass;
obj->data = 100;
// 使用动态分配的对象
obj->printData();
// 释放动态分配的内存
delete obj;

注意:使用动态分配内存时,必须在不再需要该内存时显式地使用 delete运算符来释放内存。否则,可能会导致内存泄漏,这会使得程序运行时占用的内存不断增加。

另外还要注意,在动态分配数组时,要使用 delete[] 运算符来释放使用 new[] 运算符分配的内存,以确保正确释放整个数组空间。

动态分配内存可以在需要灵活管理内存资源的情况下很有用,但也需要仔细处理,并避免悬空指针、重复释放等问题。推荐使用智能指针(如 std::shared_ptrstd::unique_ptr)等工具来帮助管理动态分配的内存。

关注我,不迷路,共学习,同进步

关注我,不迷路,共学习,同进步

相关文章
|
1月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
11天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
26天前
|
编译器 C语言 C++
配置C++的学习环境
【10月更文挑战第18天】如果想要学习C++语言,那就需要配置必要的环境和相关的软件,才可以帮助自己更好的掌握语法知识。 一、本地环境设置 如果您想要设置 C++ 语言环境,您需要确保电脑上有以下两款可用的软件,文本编辑器和 C++ 编译器。 二、文本编辑器 通过编辑器创建的文件通常称为源文件,源文件包含程序源代码。 C++ 程序的源文件通常使用扩展名 .cpp、.cp 或 .c。 在开始编程之前,请确保您有一个文本编辑器,且有足够的经验来编写一个计算机程序,然后把它保存在一个文件中,编译并执行它。 Visual Studio Code:虽然它是一个通用的文本编辑器,但它有很多插
|
1月前
|
存储 程序员 编译器
简述 C、C++程序编译的内存分配情况
在C和C++程序编译过程中,内存被划分为几个区域进行分配:代码区存储常量和执行指令;全局/静态变量区存放全局变量及静态变量;栈区管理函数参数、局部变量等;堆区则用于动态分配内存,由程序员控制释放,共同支撑着程序运行时的数据存储与处理需求。
105 21
|
1月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
1月前
|
存储 C语言 C++
【C++打怪之路Lv6】-- 内存管理
【C++打怪之路Lv6】-- 内存管理
38 0
【C++打怪之路Lv6】-- 内存管理
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配