C++ 中new/delete与malloc/free详解


前言

对C++学习感兴趣的可以看看这篇文章哦:C/C++教程

一、new/delete

1. 序言

在 C++ 的程序设计中,动态内存分配是非常常见的操作。new 和 delete 是 C++ 中提供的动态内存分配运算符,它们可以用于动态分配任意类型的内存,并且不需要显式地指定内存块的大小。

2. 使用方法

2.1. new 和 delete 基本语法

new 和 delete 是 C++ 中的关键字,用于动态分配和释放内存。下面是 new 和 delete 的基本语法:

// 动态分配一个对象
Type* p = new Type;

// 释放已经分配的对象
delete p;

其中 Type 表示要动态分配的数据类型,p 是一个指向该数据类型的指针。当执行 new Type 操作时,系统会为该类型的对象分配一块内存,并返回指向这个内存块的指针。当不再需要这个对象时,可以通过 delete 操作来释放这块内存。

如果需要同时分配多个对象,可以使用如下语法:

// 动态分配一个数组
Type* p = new Type[n];

// 释放已经分配的数组
delete [] p;

其中 n 表示要分配的对象个数,p 是一个指向 Type 类型数组的指针。

需要注意的是,在使用 new 进行动态内存分配时,如果发生内存不足的情况,则会抛出 std::bad_alloc 异常。因此,在程序中应该对这种异常进行处理。

2.2. new 和 delete 的底层实现原理

new 和 delete 在底层实现上也有一些细节需要注意。在执行 new Type 操作时,实际上是依次进行了以下几个步骤:

  1. 调用 operator new 函数来申请一块大小为 sizeof(Type) 的内存。
  2. 调用类的构造函数来初始化申请到的内存空间。
  3. 返回指向申请到的内存空间的指针。

而在执行 delete p 操作时,实际上是依次进行了以下几个步骤:

  1. 调用类的析构函数来释放对象占用的资源。
  2. 调用 operator delete 函数来释放对象所占用的内存。

其中 operator new 和 operator delete 都是 C++ 中提供的关键字。operator new 函数负责申请内存,而 operator delete 函数负责释放内存。

需要注意的是,和 malloc/free 不同的是,new/delete 能够调用类的构造和析构函数,并自动计算所需的内存空间大小。这也是使用 new/delete 的一大优势。

3. 底层原理

3.1. operator new 和 operator delete

C++ 中的 operator new 函数和 operator delete 函数是用来动态分配和释放内存的。operator new 函数负责申请内存,而 operator delete 函数负责释放内存。

下面是 operator new 的一种实现方式:

void* operator new(size_t size) {
    
    
    if (size == 0) {
    
    
        size = 1;
    }
    void* ptr = malloc(size);
    if (!ptr) {
    
    
        throw std::bad_alloc();
    }
    return ptr;
}

其中 size 表示要申请的内存空间大小。如果该值为 0,则将其设置为 1。然后调用 malloc 函数来申请指定大小的内存空间,如果申请失败,则抛出 std::bad_alloc 异常。

下面是 operator delete 的一种实现方式:

void operator delete(void ptr) noexcept {
    
    
    free(ptr); 
}

其中 ptr 是要释放的内存空间指针。这里使用了 noexcept 关键字来表明该函数不会抛出任何异常。

注意,在使用 operator new 和 operator delete 函数时,需要自己负责调用类的构造函数和析构函数。

3.2. new 和 delete 的底层实现原理

new 和 delete 关键字在底层实现上实际上是对 operator new 和 operator delete 函数进行了封装和重载,以方便程序员使用。下面是 new 和 delete 的一种实现方式:

void* operator new(size_t size) {
    
    
    if (size == 0) {
    
    
        size = 1;
    }
    void* ptr = malloc(size);
    if (!ptr) {
    
    
        throw std::bad_alloc();
    }
    return ptr;
}

void operator delete(void* ptr) noexcept {
    
    
    free(ptr);
}

void* operator new[](size_t size) {
    
    
    if (size == 0) {
    
    
        size = 1;
    }
    void* ptr = malloc(size);
    if (!ptr) {
    
    
        throw std::bad_alloc();
    }
    return ptr;
}

void operator delete[](void* ptr) noexcept {
    
    
    free(ptr);
}

可以看到,new 和 delete 实际上是对 operator new 和 operator delete 进行了重载。

注意,对于复杂数据结构,使用new[] 申请多个内存时,会多申请一块4字节的内存,用于存储当前申请的数量,用于delete[]知道调用几次对象的析构函数,但这个数据对外不可见

4. 注意事项

在使用 new/delete 进行动态内存分配时,需要注意以下几点:

  • 内存泄漏:必须及时释放不再使用的内存,否则可能会导致内存泄漏。
  • 悬空指针:已经释放的内存块指针不能再被访问,否则可能会导致程序崩溃或出现其他的不可预测的错误。
  • 不要重复释放内存:同一个内存块只能被释放一次,否则可能会导致程序崩溃或出现其他的不可预测的错误。
  • 多线程环境:在多个线程同时访问同一块内存时,需要采用适当的同步机制来保证线程安全。

5. 总结

在 C++ 编程中,应该根据具体情况选择适当的动态内存分配方式,在使用动态内存分配时应该遵循良好的编程习惯,确保程序的正确性和稳定性。同时,也需要注意避免内存泄漏、悬空指针、重复释放内存等问题,保证程序的健壮性和稳定性。

二、malloc/free

1. 序言

malloc 和 free 是 C 中提供的动态内存分配函数,它们可以用于动态分配任意类型的内存,并且不需要显式地指定内存块的大小

2. 使用方法

2.1. malloc 和 free 基本语法

malloc 和 free 是 C中的函数,用于动态分配和释放内存。下面是 malloc 和 free 的基本语法:

// 动态分配一块内存
Type* p = (Type*)malloc(sizeof(Type));

// 释放已经分配的内存
free(p);

其中 Type 表示要动态分配的数据类型,p 是一个指向该数据类型的指针。当执行 malloc(sizeof(Type)) 操作时,系统会为该类型的对象分配一块内存,并返回指向这个内存块的指针。当不再需要这个对象时,可以通过 free 操作来释放这块内存。

需要注意的是,在使用 malloc 进行动态内存分配时,如果发生内存不足的情况,则会返回空指针。因此,在程序中应该对这种情况进行处理。

2.2. malloc 和 free 的底层实现原理

malloc 和 free 在底层实现上也有一些细节需要注意。在执行 malloc(sizeof(Type)) 操作时,实际上是依次进行了以下几个步骤:

  1. 调用系统函数 sbrk 来扩展程序的数据段。(windows则会调用win API来实现这一功能)
  2. 将申请到的内存块与已经使用的内存块链接起来。
  3. 返回指向申请到的内存空间的指针。

而在执行 free(p) 操作时,实际上是依次进行了以下几个步骤:

  1. 将 p 所指向的内存块标记为未使用。
  2. 将 p 所指向的内存块与其他未使用的内存块合并起来。
  3. 如果合并后的内存块没有被占用,则释放该内存块。

需要注意的是,在使用 malloc/free 进行动态内存分配时,需要自己负责调用类的构造函数和析构函数,并且无法计算所需的内存空间大小。

3. 底层原理

3.1. sbrk 函数

sbrk 函数是一个系统调用,用于扩展程序的数据段。在 Linux 系统中,sbrk 函数可以返回当前堆顶部地址,并且可以将堆顶部地址向上或向下移动指定的字节数。

下面是一个简单的示例,演示了如何使用 sbrk 函数来获取当前堆顶部地址:

#include <unistd.h>
#include <iostream>

int main() {
    
    
    void* p1 = sbrk(0);  // 获取当前堆顶部地址
    std::cout << "p1 = " << p1 << std::endl;

    void* p2 = sbrk(1024);  // 将堆顶部地址向上移动 1024 字节
    std::cout << "p2 = " << p2 << std::endl;

    void* p3 = sbrk(-512);  // 将堆顶部地址向下移动 512 字节
    std::cout << "p3 = " << p3 << std::endl;

    void* p4 = sbrk(0);  // 再次获取当前堆顶部地址
    std::cout << "p4 = " << p4 << std::endl;

    return 0;
}

而window系统也有自己的win API可以用于分配堆内存

3.2. 内存块管理

malloc 和 free 在底层实现上还需要进行内存块的管理。在使用 malloc 进行动态内存分配时,系统会为申请的内存块添加一些额外的信息

例如内存块的大小、指向下一个内存块的指针等。这些信息会被保存在内存块的开始位置,并且不会影响用户程序对内存空间的访问。

下面是一个简单的示例,演示了如何通过 malloc 函数获取一块内存,以及内存块中包含的信息:

#include <iostream>
#include <cstdlib>

int main() {
    
    
    int* p = (int*)malloc(sizeof(int) * 10);  // 动态分配一块内存,可以存放 10 个 int 类型的变量
    if (!p) {
    
    
        std::cout << "Memory allocation failed" << std::endl;
        return -1;
    }

    std::cout << "Allocate memory at address " << p << std::endl;
    std::cout << "The size of the memory block is " << sizeof(int) * 10 << " bytes" << std::endl;

    int* next_p = (int*)(((char*)p) + sizeof(int) * 10);
    std::cout << "The pointer to the next memory block is " << next_p << std::endl;

    free(p);  // 释放内存

    return 0;
}

3.3. 内存对齐

在使用 malloc 进行动态内存分配时,需要考虑内存对齐的问题。

所谓内存对齐,就是指将数据类型放置在内存中的地址必须满足一定的条件。

具体来说,每个数据类型都有一个与之相关联的对齐值,这个对齐值通常等于该数据类型的大小(例如 int 的对齐值为 4 字节)。在进行内存分配时,系统会确保申请到的内存块的起始地址是对齐值的整数倍。

下面是一个简单的示例,演示了如何通过 malloc 函数获取一块内存,并展示内存对齐的效果:

#include <iostream>
#include <cstdlib>

struct MyStruct {
    
    
    double x;
    char c;
    int i;
};

int main() {
    
    
    std::cout << "Size of MyStruct is " << sizeof(MyStruct) << " bytes" << std::endl;

    MyStruct* p1 = (MyStruct*)malloc(sizeof(MyStruct));
    if (!p1) {
    
    
        std::cout << "Memory allocation failed" << std::endl;
        return -1;
    }

    std::cout << "Allocate memory at address " << p1 << std::endl;

    char* p2 = (char*)p1;
    for (int i = 0; i < sizeof(MyStruct); ++i) {
    
    
        std::cout << (int)p2[i] << " ";
    }
    std::cout << std::endl;

    free(p1);  // 释放内存

    return 0;
}

可以看到,在分配 MyStruct 类型的内存时,系统会确保返回的起始地址是 double 类型大小的整数倍。

4. 注意事项

在使用 malloc/free 进行动态内存分配时,需要注意以下几点:

  • 内存泄漏:必须及时释放不再使用的内存,否则可能会导致内存泄漏。
  • 悬空指针:已经释放的内存块指针不能再被访问,否则可能会导致程序崩溃或出现其他的不可预测的错误。
  • 不要重复释放内存:同一个内存块只能被释放一次,否则可能会导致程序崩溃或出现

其他的不可预测的错误。

  • 内存越界:访问已经释放的内存块,或者访问超出分配的内存块范围的地址,都可能会导致程序崩溃或出现其他的不可预测的错误。
  • 不要混用 malloc/free 和 new/delete:在同一个程序中应该统一使用一种内存分配方式,不要混用 malloc/free 和 new/delete,否则可能会导致内存管理出现问题。

5. new/delete 与 malloc/free 的区别

new 和 delete 是 C++ 中提供的动态内存分配运算符,它们和 malloc/free 在功能上是类似的。

new/delete 的使用方法比 malloc/free 更简单直观。另外,new/delete 还有以下几个优点:

  • 类型安全:new/delete 可以根据类型自动计算所需的内存空间大小,无需手动指定。
  • 自动调用构造函数和析构函数:new 可以自动调用类的构造函数,delete 可以自动调用类的析构函数,更方便地管理对象的生命周期。
  • 支持重载:new/delete 运算符可以被重载,从而可以实现一些特殊的功能。

需要注意的是,在使用 new/delete 进行动态内存分配时,同样可能会出现内存泄漏、悬空指针、内存越界等问题。因此,在编写程序时,必须仔细处理动态内存分配和释放的问题,避免出现以上问题。

6. 总结

在使用动态内存分配的过程中,需要注意内存泄漏、悬空指针、内存越界等问题,同时还需要根据具体情况选择合适的内存分配方式。

猜你喜欢

转载自blog.csdn.net/weixin_50964512/article/details/130075826