15.内存模型

目录介绍
  • 15.1 内存分区模型
    • 15.1.1 理解代码区
    • 15.1.2 理解全局区
    • 15.1.3 理解栈区
    • 15.1.4 理解堆区
    • 15.1.5 常量存储区
    • 15.1.6 内存模型用例
  • 15.2 理解动态内存
    • 15.2.1 何为动态内存
    • 15.2.2 动态内存特点
    • 15.2.3 new分配内存
    • 15.2.4 delete释放内存
    • 15.2.5 动态内存示例
    • 15.2.6 动态内存最佳实践
  • 15.3 动态内存问题
    • 15.3.1 内存泄漏
    • 15.3.2 野指针
    • 15.3.3 悬空指针问题
  • 15.4 智能指针
    • 15.4.1 unique_ptr
    • 15.4.2 shared_ptr
    • 15.4.3 weak_ptr
  • 15.5 内存管理函数
    • 15.5.1 malloc和free
    • 15.5.2 calloc和realloc
  • 15.6 内存对齐
    • 15.6.1 对齐规则
    • 15.6.2 查看对齐值

15.1 内存分区模型

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

15.1.1 理解代码区

作用: 存储程序的二进制代码(即编译后的机器指令)。

特点: 1.只读,程序运行时不可修改。 2.由操作系统管理,程序结束时释放。

void func() {
// 函数代码存储在代码区
}

15.1.2 理解全局区

全局/静态区(Global/Static Segment)作用:

  1. 存储全局变量、静态变量(包括静态局部变量和静态成员变量)。
  2. 分为两个子区域: 已初始化区:存储已初始化的全局变量和静态变量。 未初始化区(BSS 段):存储未初始化的全局变量和静态变量。

特点:1.在程序启动时分配,程序结束时释放。 2.未初始化的变量会被自动初始化为 0。

int globalVar = 10; // 已初始化全局变量,存储在全局区
static int staticVar; // 未初始化静态变量,存储在 BSS 段
void func() {
    static int localStaticVar = 20; // 静态局部变量,存储在全局区
}

15.1.3 理解栈区

栈区(Stack Segment) 作用:存储局部变量、函数参数、函数返回地址等。

特点:

  1. 由编译器自动管理,函数调用时分配,函数返回时释放。
  2. 内存分配和释放速度快,但空间有限。
  3. 栈的大小通常较小(默认几 MB),如果栈溢出会导致程序崩溃。
void func() {
    int localVar = 30; // 局部变量,存储在栈区
}

15.1.4 理解堆区

堆区(Heap Segment) 作用: 存储动态分配的内存(如 new 和 malloc 分配的内存)。

特点:

  1. 由程序员手动管理,需要显式释放(如 delete 或 free)。
  2. 内存分配和释放速度较慢,但空间较大。
  3. 如果未正确释放内存,会导致内存泄漏。
void func() {
    int* ptr = new int(40); // 动态分配内存,存储在堆区
    delete ptr; // 手动释放内存
}

15.1.5 常量存储区

  • 用于存储常量(如字符串常量)。
  • 通常是只读的。

示例:

const char *str = "Hello, World!"; // 字符串常量存储在常量存储区

15.1.5 内存模型用例

内存分区模型的特点

  1. 代码区:只读,存储程序指令。
  2. 全局/静态区:存储全局和静态变量,生命周期与程序相同。
  3. 栈区:存储局部变量和函数调用信息,自动管理,空间有限。
  4. 堆区:存储动态分配的内存,手动管理,空间较大。
#include <iostream>

int globalVar = 10; // 全局变量,存储在全局区
static int staticVar; // 静态变量,存储在 BSS 段

void func() {
    int localVar = 30; // 局部变量,存储在栈区
    static int localStaticVar = 20; // 静态局部变量,存储在全局区
    int* ptr = new int(40); // 动态分配内存,存储在堆区
    std::cout << "Local Variable: " << localVar << std::endl;
    std::cout << "Dynamic Memory: " << *ptr << std::endl;
    delete ptr; // 释放堆区内存
}

int main() {
    func();
    return 0;
}

15.2 理解动态内存

15.2.1 何为动态内存

在 C++ 中,动态内存 是指在程序运行时(而不是编译时)从堆(Heap)中分配的内存。与栈上的自动内存管理不同,动态内存的分配和释放由程序员显式控制。

通过 new 和 delete 操作符(或 C 风格的 malloc 和 free)来分配和释放内存。

动态内存的使用场景包括:

  • 需要灵活管理内存大小(如动态数组)。
  • 对象的生命周期超出其作用域。
  • 需要共享内存(如多线程环境)。

15.2.2 动态内存特点

  1. 运行时分配: 内存的分配和释放发生在程序运行时,而不是编译时。

  2. 手动管理: 程序员需要显式分配和释放内存,否则会导致内存泄漏。

  3. 堆区存储: 动态内存从堆区分配,堆区的空间通常比栈区大。

  4. 灵活性: 动态内存的大小可以在运行时动态调整,适合处理不确定大小的数据。

15.2.3 new分配内存

C++中利用==new==操作符在堆区开辟数据

堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 ==delete==

语法: new 数据类型

利用new创建的数据,会返回该数据对应的类型的指针

分配单个对象

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called" << std::endl;
    }

    MyClass(int age, string name) {
        this->age = age;
        this->name = name;
        std::cout << "MyClass constructor " << this->age << " , " << this->name << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructor called" << std::endl;
    }

public:
    int age;
    string name;
};

void test1() {
    // 使用new运算符创建MyClass对象
    MyClass* obj = new MyClass();
    // 使用对象指针调用对象的成员函数
    // ...
    // 释放动态分配的内存
    delete obj;
}

void test2() {
    MyClass* obj = new MyClass(30,"yc");
    delete obj;
}

int main() {
//    test1();
    test2();
    return 0;
}

手动管理动态数组

//堆区开辟数组
void test() {
    int* arr = new int[10];
    for (int i = 0; i < 10; i++) {
        arr[i] = i+ 100;
    }
    for (int i = 0; i < 10; i++) {
        cout << arr[i] << endl;
    }
    delete[] arr;
}

int main() {
    test();
    return 0;
}

动态数组,C++ 提供了 std::vector 作为动态数组的替代方案,避免手动管理内存。

#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3}; // 动态数组
    vec.push_back(4); // 添加元素
    for (int i : vec) {
        std::cout << i << " "; // 输出: 1 2 3 4
    }
    return 0;
}

15.2.4 delete释放内存

释放单个对象:

delete ptr; // 释放单个对象
ptr = nullptr; // 将指针置为 nullptr,避免野指针

释放数组:

delete[] arr; // 释放数组
arr = nullptr; // 将指针置为 nullptr

15.2.5 动态内存示例

动态数组

#include <iostream>

int main() {
    int size = 5;
    int* arr = new int[size]; // 动态分配数组

    for (int i = 0; i < size; i++) {
        arr[i] = i * 2; // 初始化数组
    }

    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " "; // 输出数组
    }

    delete[] arr; // 释放数组内存
    return 0;
}

动态对象

#include <iostream>

class MyClass {
public:
    MyClass() { std::cout << "Constructor called!n"; }
    ~MyClass() { std::cout << "Destructor called!n"; }
    void print() { std::cout << "Hello, World!n"; }
};

int main() {
    MyClass* obj = new MyClass; // 动态分配对象
    obj->print();
    delete obj; // 释放对象内存
    return 0;
}

15.2.6 动态内存最佳实践

  1. 优先使用智能指针:避免手动管理内存,减少内存泄漏和悬空指针的风险。
  2. 避免裸指针:尽量使用 std::unique_ptrstd::shared_ptr
  3. 使用容器类:如 std::vectorstd::list 等,避免手动管理动态数组。
  4. 检查内存分配是否成功:在分配大量内存时,检查指针是否为 nullptr
  5. 避免内存泄漏:确保每次 new 都有对应的 delete,或使用智能指针。

15.3 动态内存问题

15.3.1 内存泄漏

如果动态分配的内存没有被释放,会导致内存泄漏。解决方法:确保每次 new 都有对应的 delete。

int* ptr = new int;
// 忘记调用 delete ptr;

15.3.2 野指针

未初始化的指针称为野指针,访问野指针会导致未定义行为。解决方法:始终初始化指针。

int* ptr; // 未初始化
*ptr = 10; // 未定义行为

15.3.3 悬空指针问题

释放内存后,指针仍然指向已释放的内存地址,称为悬空指针。解决方法:释放内存后将指针置为 nullptr。

int* ptr = new int;
delete ptr;
*ptr = 10; // 未定义行为,可能导致崩溃

15.4 智能指针

C++11 引入了智能指针,用于自动管理动态内存,避免内存泄漏和悬空指针。

  • std::unique_ptr:独占所有权的智能指针。
  • std::shared_ptr:共享所有权的智能指针。
  • std::weak_ptr:弱引用,不增加引用计数。

15.4.1 unique_ptr

  • 独占所有权的智能指针。
  • 不能复制,只能移动。
  • 适用于单一所有者场景。

示例:

#include <memory>

int main() {
    std::unique_ptr<int> ptr(new int(10)); // 分配内存
    std::cout << *ptr << std::endl;        // 输出: 10
    // 不需要手动释放内存
    return 0;
}

15.4.2 shared_ptr

  • 共享所有权的智能指针。
  • 使用引用计数管理内存。
  • 适用于多个所有者场景。

示例:

#include <memory>

int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(20); // 分配内存
    std::shared_ptr<int> ptr2 = ptr1; // 共享所有权
    std::cout << *ptr1 << " " << *ptr2 << std::endl; // 输出: 20 20
    // 不需要手动释放内存
    return 0;
}

15.4.3 weak_ptr

  • 弱引用,不增加引用计数。
  • 用于解决 std::shared_ptr 的循环引用问题。

示例:

#include <memory>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(30);
    std::weak_ptr<int> weakPtr = sharedPtr; // 弱引用
    if (auto spt = weakPtr.lock()) { // 尝试提升为 shared_ptr
        std::cout << *spt << std::endl; // 输出: 30
    }
    return 0;
}

15.5 内存管理函数

C++ 提供了底层内存管理函数,如 mallocfreecallocrealloc,但通常不推荐使用。

15.5.1 malloc和free

mallocfree

#include <cstdlib>

int main() {
    int *p = (int *)malloc(sizeof(int)); // 分配内存
    *p = 10;
    std::cout << *p << std::endl; // 输出: 10
    free(p); // 释放内存
    return 0;
}

15.5.2 calloc和realloc

callocrealloc

#include <cstdlib>

int main() {
    int *arr = (int *)calloc(3, sizeof(int)); // 分配并初始化内存
    arr[0] = 1;
    arr = (int *)realloc(arr, 5 * sizeof(int)); // 重新分配内存
    arr[3] = 4;
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " "; // 输出: 1 0 0 4 0
    }
    free(arr); // 释放内存
    return 0;
}

15.6 内存对齐

内存对齐是指数据在内存中的起始地址必须是某个值的整数倍。对齐可以提高内存访问效率。

15.6.1 对齐规则

  • 基本类型的对齐要求通常与其大小相同(如 int 对齐到 4 字节)。
  • 结构体的对齐要求是其成员中对齐要求最大的值。

示例:

struct alignas(16) MyStruct {
    int a;      // 4 字节
    double b;   // 8 字节
}; // 结构体对齐到 16 字节

15.6.2 查看对齐值

使用 alignof 查看类型的对齐值。

std::cout << alignof(int) << std::endl; // 输出: 4
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]