C++的new / delete 与 C语言的malloc/realloc/calloc / free 的讲解

简介: C++的new / delete 与 C语言的malloc/realloc/calloc / free 的讲解

  在C语言中我们通常会使用malloc/realloc/calloc来动态开辟的空间,malloc是只会开辟你提供的空间大小,并不会初始化内容;calloc不但会开辟空间,还会初始化;realloc是专门来扩容的,当你第一次开辟的空间不够用的时候,就要使用realloc;如果你第一次使用realloc的时候,前面没有开辟过空间,那么realloc的行为会跟malloc一样,之后再发挥realloc自己的行为。而我们的C++是面向对象的编程,当开辟空间失败了malloc只会返回一个空指针,我们还需要自己来判断。所以在C++这里就将malloc升级成为了new,new在开辟空间失败的时候会抛出异常,这跟我们面向对象的理念是一致的。


       其实我在C语言阶段学习动态开辟空间的时候,是有点迷糊的,就是不知道如何去开辟,给谁开辟?现在想想其实不难理解,假设我们要开辟10个int类型的空间大小,我们有两种办法,一种就是直接定义一个int类型的数组:int arr[10],但是这样的空间是定长的空间,我们无法对定长数组进行扩容。第二种就是用到动态开辟的空间了,我们肯定是先要malloc10个int类型的空间,但是我们应该如何取到这段空间呢?用指针变量接收是吧,但是为什么是指针变量呢?我们继续讲解,因为我们先开辟了一段空间,正常情况下是无法取到这段空间的,只有拿到这段空间的地址,才可以访问这段空间。这也就是为什么我们会用指针变量来接收malloc开辟的空间了。

       下面是malloc的函数声明:通过malloc的返回值也不难看出要用指针接收。所以我们要开辟空间,一种就是直接定义类型,比如int a、int arr[10]、char ch等等,一种就是用malloc/realloc/calloc开辟空间,用指针接收。

一、介绍

1. malloc、realloc、calloc的介绍

1. malloc只是动态开辟空间,并没有初始化这段空间的内容;

2. realloc是用来扩容的,如果没有预先开辟空间,直接使用realloc,realloc的作用相当于malloc;

3. calloc相当于是malloc的加强版,不仅可以开辟空间,也顺便初始化我们的内容;

4. 面对内置类型可以直接开辟空间,但是对于自定义类型的话,比如我们的栈,则无法一步到位,因为栈里面还有数组空间的开辟,需要两层开辟。


       下面就是栈的举例:因为我们仅仅是用的malloc来开辟一个栈的空间,所以栈内部的数组空间还需要开辟,如果我们要是写一个构造函数去初始化,在主函数中是无法调用的,也就完成不了对数组空间的开辟,不过在了解定位new之后是可以调用的,我们这里使用写一个成员函数Init来完成对数组空间的开辟,这样我们在主函数中也可以调用。然后其实这样的方法有点麻烦,而且在malloc失败的时候只是返回空指针,判断条件也需要我们自己去写,这很不满足面向对象的要求,所以C++基于这样的原因创造了new这个操作符。

#include <iostream>
using namespace std;
class Stack
{
public:
    void Init(int capacity)
    {
        _a = (int*)malloc(sizeof(int) * capacity);
        if(_a == nullptr)
        {
            perror("malloc fail");
            exit(-1);
        }
        _size = 0;
        _capacity = capacity;
    }
private:
    int *_a;
    int _size;
    int _capacity;
};
int main()
{
    Stack *st = (Stack*)malloc(sizeof(Stack));
    st->Init(4);
    return 0;
}

2. free的介绍

       free专门释放动态开辟的空间,如果释放的不是动态开辟的空间,就会报错,所以这里一定要多加注意!!!

       一般就是free( ),括号里面就是指向那段空间的指针就行。

3. new、operator new的介绍(内置类型和自定义类型)

       new是在C++中才有的操作符,因为C++兼容C语言,所以在C++中可以写C,但是在C中写不了C++。new也是用来动态开辟空间的,也可以初始化,即可以开辟内置类型的空间,也可以开辟自定义类型的空间,这两种类型都可以初始化;


1. new对内置类型开辟空间:直接开辟

int *a = new int;      //单纯开辟空间
int *b = new int(3);   //开辟空间并初始化
int *arr1 = new int[5]; //单纯开辟5个整型的空间
int *arr2 = new int[5]{1, 2, 3, 4, 5}; //开辟5个整型空间并初始化

2. new对自定义类型开辟空间:第一步:开辟这个自定义类型需要的空间;

                                                 第二步:调用这个自定义类型的构造函数;


所以我们上面对于栈的开辟,可以改善为下面这样:

#include <iostream>
using namespace std;
class Stack
{
public:
    Stack(int capacity = 4, int size = 0, int num = 0)
        : _a(new int[capacity]{0})
        , _size(size)
        , _capacity(capacity)
    {
        cout << "Stack(int capacity = 4)" << endl; //方便观察确实调用了构造函数
    }
private:
    int *_a;
    int _size;
    int _capacity;
};
int main()
{
    Stack *st = new Stack;
    //Stack *st = new Stack(4, 0, 0);这种就是对于显式构造函数的写法,或者是你想传入的初始化内容
    return 0;
}

3. 那operator new 又是什么呢?


       相信大家对这个并不熟悉,所以这部分知识了解即可,operator new 是一种全局函数,对malloc进行了封装,也就是让malloc函数更加面向对象,而不是面向过程了。因为我们在C语言的时候,malloc开辟空间失败,会返回个空指针,我们会根据空指针去找过程中的错误,然后C++是面向对象编程,所以就必须知道是哪个对象出错了,new出错就报new。所以operator new就是对malloc进行了一个包装,底层还是通过malloc实现,只不过在出错的时候,operator new是抛异常,不是返回空指针了。下面就是operator new这个函数的定义:


所以operator new 还是通过malloc实现的,看不懂下面的也没关系,记住operator new 开辟空间失败抛异常,malloc失败是返回NULL;


void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
    void *p;
    while ((p = malloc(size)) == 0)
    {
         if (_callnewh(size) == 0)
         {
             // report no memory
             // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
             static const std::bad_alloc nomem;
             _RAISE(nomem);
         }
    }
    return (p);
}

4. delete、operator delete的介绍

delete是用来释放动态开辟的空间,如果释放的不是动态开辟的空间会报错;

他跟free的区别是在自定义类型这里,free仅仅释放他括号里指向的空间;

delete在释放自定义类型的空间时候,会做两件事:

1. 先调用自定义类型的析构函数;

2. 再去调用operator delete函数;

    int *a = new int;
    char *ch = new char;
    double *d = new double[5];
    delete a;
    delete ch;
    delete []d;

而operator delete 我们会在new、delete针对数组这里拿出来讲解

二、new、delete的原理

1. new对于自定义类型:

1. new 会先调用 operator new 开自定义类型的空间;

2. 在申请的空间上,再去调用自定义类型的构造函数 ;

这里解答一下为什么new步调用malloc,而是调用的operator new

因为还是C++是面向对象编程,所以我们不使用malloc这套判断错误的方式,而是选择operator new 这个函数去封装一下new,再让new来调用自己,一切都是为了面向对象,所以new的产生原因也是这样的,因为面向对象。

2. delete对于自定义类型

1. 在空间上执行析构函数,完成对象中资源的清理工作

2. 调用operator delete函数释放对象的空间


需要注意的就是为什么delete要先调用析构函数

因为我们要知道,再对于栈这样的数据结构,他里面数组的空间是被栈空间的指针指着的,如果先释放栈的空间,就相当于把数组的地址释放了,然后再去调用析构函数就会找不到要析构的位置了,所以就会出错;

基于这样的原因一定要先析构,再释放自定义类型的空间;

3. new、delete对于数组、自定义类型的数组(重点,难点)

1. 对于自定义类型的数组

#include <iostream>
using namespace std;
class Stack
{
public:
    Stack(int capacity = 4, int size = 0, int num = 0)
        : _a(new int[capacity]{0})
        , _size(size)
        , _capacity(capacity)
    {
        cout << "Stack(int capacity = 4)" << endl; //方便观察确实调用了构造函数
    }
private:
    int *_a;
    int _size;
    int _capacity;
};
int main()
{
    Stack *st = new Stack[5];
    delete []st;
    return 0;
}

我们对于自定义类型的数组,new的时候会多开辟一个4字节的空间,用来存放数组的个数,但是存放这个数组的个数是用来干什么呢?首先对于自定义类型,我们在delete的时候会调用析构函数,但是具体析构多少次呢?我们给delete传了么?因为delete []st;我们是不是没有传入数组的大小,所以并不知道要析构多少次,这时候多开辟的空间就派上用场了。

多开辟的空间存放的是数组的个数,然后他的作用是告诉delete调用多少次析构函数的。

       所以delete []st,他先通过st指针指向的位置,先调用5次析构函数,连续释放完5个_a指针指向的空间之后,返回到a位置处,再用通过调用operator delete释放自定义类型数组的这段空间。

2. 对于内置类型的数组

new不会多开辟4字节的空间,因为这4字节的空间是给析构函数准备的。而内置类型不需要析构函数。所以不会开辟

三、new和delete的底层

1. new --> 内置类型数组


new --> operator new[ ] 函数  --> operator new 函数 --> malloc  


这里要知道的是,上面都是调用了一次,因为开辟的空间是提前算好的,开辟一次就行

2. delete --> 内置类型数组

delete --> operator delete[ ] 函数  --> operator delete 函数 --> free  

3. new --> 自定义类型数组

1. new --> operator new[ ] 函数  --> operator new 函数 --> malloc  

2. 每一个自定义类型再调用构造函数


开辟的空间都是一次性开辟好的,所以只需要new一次,而每个自定义类型都需要调用构造函数,所以是多个。

4. delete --> 自定义类型数组

1. 每一个自定义类型先调用自己的析构函数

2. delete --> operator delete[ ] 函数  --> operator delete 函数 --> free

四、面试题:new/delete 和 malloc/free 的 异同


共同点:

都是从堆上申请空间,并且需要用户手动释放。


不同点:

1. malloc和free是函数,new和delete是操作符;

2. malloc申请的空间不会初始化,new可以初始化;

3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[ ]中指定对象个数即可;

4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型

5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需

要捕获异常;

6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理;

五、练习题

1. C++中,类ClassA的构造函数和析构函数的执行次数分别为( )

ClassA *p = new ClassA[5];
delete p;

A.5,1

B.1,1

C.5,5

D.程序可能崩溃

答案及解析:D

大家可以参考下面这个图:

       delete p这就相当于先调用b位置的析构函数,然后直接在b位置释放了。因为是delete p;并不是delete [ ]p;所以delete并不知道说这是一个数组,并不会跳到最前面来释放,释放的位置不对,导致了程序的崩溃;

六、注意

new和delete一定要匹配,避免未定义行为;

new —— delete

new [ ] —— delete[ ]

malloc —— free


相关文章
|
3月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
133 26
|
12月前
|
程序员 编译器 C语言
C中的 malloc 和C++中的 new 有什么区别
在C语言中,`malloc`函数用于在运行时分配内存,返回指向所分配内存的指针,需显式包含头文件 `&lt;stdlib.h&gt;`。而在C++中,`new`不仅分配内存,还对其进行构造初始化,且直接使用类型声明即可,无需额外包含头文件。`new`还支持数组初始化,能更好地融入C++的面向对象特性,而`malloc`仅作为内存分配工具。使用完毕后,`free`和`delete`分别用于释放`malloc`和`new`分配的内存。
301 21
|
12月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
2090 1
|
存储 编译器 程序员
【C/C++】动态内存管理(C:malloc,realloc,calloc,free)
探索C++与C语言的动态内存管理:从malloc到new/delete,了解内存分布及栈、堆的区别。文章涵盖malloc、realloc、calloc与free在C中的使用,强调内存泄漏的风险。C++引入new和delete,支持对象构造与析构,还包括operator new和placement-new。深入分析内存管理机制,揭示C与C++在内存处理上的异同。别忘了,正确释放内存至关重要!
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
96 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
173 0
|
6月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
186 12
|
7月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
132 16
|
8月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)