跳转至

C++内存管理

约 3846 个字 591 行代码 3 张图片 预计阅读时间 20 分钟

C/C++内存分配

在C语言动态内存管理章节已经了解到内存的分类,包括下面四个区域:

  1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段--高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
  3. 堆--用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段--存储全局数据和静态数据。
  5. 代码段--可执行的代码/只读常量
C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
    static int staticVar = 1;
    int localVar = 1;
    int num1[10] = { 1, 2, 3, 4 };
    char char2[] = "abcd";// 需要注意
    const char* pChar3 = "abcd";
    int* ptr1 = (int*)malloc(sizeof(int) * 4);
    int* ptr2 = (int*)calloc(4, sizeof(int));
    int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
    free(ptr1);
    free(ptr3);
}

在上面代码中,globalVar为全局变量,与静态变量staticGlobalVarstaticGlobalVar,存放在数据段(静态区),num1char2均为局部数组,所以均放在内存的栈区。对于指针类型pChar3ptr1ptr2ptr3均为局部变量,所以均存放在栈区,但是pChar3指向的内容是字符串的第一个元素的地址,该地址在内存的代码段(常量区),其余三个指针均指向在内存堆区开辟的空间

Note

注意尽管char2中存储的是字符串,但是该字符串只是存放在代码段区域的字符串的拷贝,所以依旧在栈区

内部链接属性和外部链接属性

在C++和C语言中都存在外部链接属性和内部属性,这两个属性用于判断某一个变量或者函数是否在多文件情况下可以在其他文件被访问

内部链接属性:简单理解就是这个变量只在当前文件是可见的,如果其他文件想使用定义在某一个源文件中具有内部链接属性的变量,此时在定义内部链接属性的变量时需要保证该对应变量被extern修饰,再在需要使用到该变量的其他文件中使用extern关键字引入

外部链接属性:简单理解就是这个变量在所有文件中是可见的,如果其他文件想使用定义在某一个源文件中具有外部链接属性的变量,只需要使用extern关键字引入具有外部链接属性的变量到需要使用的文件中即可

C语言中的const的全局变量和普通的全局变量都具有外部链接属性,而C++中,被const修饰的全局变量是具有内部链接属性,而普通的全局变量则是具有内部链接属性,利用这两个特点就可以分别演示具有外部链接属性的变量和内部链接属性的变量如何引入

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// const.h
#ifndef CONST_H
#define CONST_H
#include <stdio.h>

void print();
#endif //CONST_H

// const.c
#include ".h"
const int num = 100; // 具有外部链接属性
void print()
{
    printf("%d\n", num);
}

// test.c
#include "const.h"
extern int num; // 直接引入即可
int main()
{
    print();
    printf("%d\n", num);
    return 0;
}

输出结果
100
100
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// const.h
#ifndef CONST_H
#define CONST_H
#include <iostream>

void print();
#endif //CONST_H  

// const.cpp
#include "const.h"
extern const int num = 100; // const修饰的全局变量本身具有内部链接属性,使用extern打破内部链接属性,使其具有内部链接属性

void print()
{
    std::cout << num << std::endl;
    // std::cout << num1 << std::endl;
}

// test.cpp
#include "const.h"
extern const int num; // 再使用extern引入
int main()
{
    print();
    std::cout << num << std::endl;
    return 0;
}

输出结果
100
100
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// const.h
#ifndef CONST_H
#define CONST_H
#include <iostream>

void print();
#endif //CONST_H

// const.cpp
#include "const.h"
int num1 = 200; // 直接创建的全局变量具有外部链接属性

void print()
{
    std::cout << num1 << std::endl;
    // std::cout << num1 << std::endl;
}

// test.cpp
#include "const.h"
extern int num1; // 直接使用extern引入即可
int main()
{
    print();
    std::cout << num1 << std::endl;
    return 0;
}

输出结果
200
200

在实际的项目管理中,一般更喜欢将常量等定义在头文件中,此时C++中具有内部链接属性的const全局变量就可以发挥其内部链接属性的作用,因为具有内部链接属性,所以当头文件在被包含的源文件中展开时可以保证const全局变量在当前所处的源文件中是唯一的,但是C语言中的const修饰的全局变量具有外部链接属性,所以C语言要是定义常量更多还是使用宏,但是宏进行替换还是有一些细节问题,这也是为什么C++希望将const修饰的全局变量更改为具有内部链接属性而不是具有外部链接属性的一大原因,所以C++就可以通过const定义一个真正意义上的常量,并且可以显式指定类型,例如下面的代码:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// const.h
#ifndef CONST_H
#define CONST_H
#include <iostream>
const int num = 100;

void print();
#endif //CONST_H

// const.cpp
#include "const.h"

void print()
{
    std::cout << num << std::endl;
}

// test.cpp
#include "const.h"

int main()
{
    print();
    std::cout << num << std::endl;
    return 0;
}

输出结果
100
100
有前面的铺垫,现在思考为什么C++中的const全局变量叫常量,但是C语言很少这么说?

其实在C语言中,如果声明了一个const变量,并且这个变量有外部链接(external linkage),那么它会在链接阶段进行符号解析。这意味着,如果你在一个源文件中声明了一个const变量,并在另一个源文件中引用它,那么编译器会在每个源文件中分别处理这些声明,然后链接器会确保它们指向相同的内存位置。因此,虽然它的值在编译期间是已知的,但是它的地址可能不是,直到链接期间才能确定。而在C++中,声明具有内部链接(internal linkage)的const变量,这种情况下,变量通常是在编译期就确定的,而且不会出现在符号表中供链接器处理。这使得这样的const变量更像一个真正的常量,因为它不需要在多个源文件之间共享

所以,C语言的const修饰的全局变量严格来说不能算编译时期可确定的变量,但是C++中的const修饰的全局变量如果在其值为字面量或者在编译期可以确定的值可以算

C++内存管理

C++内存管理介绍

在C语言中,使用malloc/calloc/realloc进行动态内存空间的开辟,但是这三种方式中只有calloc会对数据区域进行初始化,并且初始化值为0,并且使用free宏对动态分配的空间进行释放

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
    int* ptr = (int*)malloc(sizeof(int) * 2);
    assert(ptr);
    int* ptr1 = (int*)calloc(2, sizeof(int));
    assert(ptr1);
    int* tmp = (int*)realloc(ptr, sizeof(int) * 20);
    assert(tmp);
    //此处不需要释放ptr原来指向的空间
    ptr = tmp;
    free(ptr);
    free(ptr1);

    return 0;
}

在C++中,仍然可以使用malloc/calloc/realloc进行动态内存分配和free进行空间释放,但是更推荐使用new进行动态内存分配以及delete进行空间释放,所以上面的代码可以转化为下面的代码

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>
using namespace std;

int main()
{
    int* ptr = new int[2];// 对应malloc
    int* ptr1 = new int[2] {0};// 对应calloc

    delete[] ptr;
    delete[] ptr1;

    return 0;
}

C++内存管理使用

C++内存管理基本语法

在C++中,使用new关键字进行内存空间的申请

C++
1
2
3
4
5
6
//单个空间开辟
指定类型的指针 = new 需要开辟的空间中的数据类型;
指定类型的指针 = new 需要开辟的空间中的数据类型(初始值);
//连续空间开辟
指定类型的指针 = new 需要开辟的空间中的数据类型[需要开辟的空间个数];
指定类型的指针 = new 需要开辟的空间中的数据类型[需要开辟的空间个数]{每一个空间初始化值};

在C++中,使用delete关键字进行内存空间的释放

C++
1
2
3
4
//单个空间释放
delete 指向需要释放的空间的指针;
//连续空间释放
delete[] 指向需要释放的空间的指针;

Note

注意,newdelete一定要匹配使用,单个空间开辟就使用单个空间的释放,连续空间的开辟就使用连续空间的释放,更不能newfree等交叉使用

对于内置类型来说,如下面代码

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;

int main()
{
    //单个空间开辟和释放
    int* ptr = new int;//不初始化
    cout << *ptr << endl;
    int* ptr1 = new int(10);//初始化为10
    cout << *ptr1 << endl;
    delete ptr;
    delete ptr1;

    //连续空间开辟和释放
    int* ptr2 = new int[5];//不初始化
    int* ptr3 = new int[5] {1, 2, 3, 4, 5};//初始化
    for (int i = 0; i < 5; i++)
    {
        cout << ptr2[2] << " ";
    }
    cout << endl;
    for (int i = 0; i < 5; i++)
    {
        cout << ptr3[i] << " ";
    }

    delete[] ptr2;
    delete[] ptr3;

    return 0;
}
输出结果
-842150451
10
-842150451 -842150451 -842150451 -842150451 -842150451
1 2 3 4 5

对于内置类型来说,如果使用free进行释放时,并不会出现报错

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;

int main()
{
    //单个空间开辟和释放
    int* ptr = new int;//不初始化
    cout << *ptr << endl;
    int* ptr1 = new int(10);//初始化为10
    cout << *ptr1 << endl;
    free(ptr);
    free(ptr1);

    //连续空间开辟和释放
    int* ptr2 = new int[5];//不初始化
    int* ptr3 = new int[5] {1, 2, 3, 4, 5};//初始化
    for (int i = 0; i < 5; i++)
    {
        cout << ptr2[2] << " ";
    }
    cout << endl;
    for (int i = 0; i < 5; i++)
    {
        cout << ptr3[i] << " ";
    }

    free(ptr2);
    free(ptr3);

    return 0;
}
输出结果
-842150451
10
-842150451 -842150451 -842150451 -842150451 -842150451
1 2 3 4 5

尽管也正常输出,但是依旧不建议使用freenew开辟的空间进行释放

对于自定义类型来说,分两种情况:

  1. 自定义类型中的成员变量仅为内置类型
  2. 自定义类型中的成员变量有指针指向的动态内存分配的空间
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
using namespace std;

//成员变量仅为内置类型
class test
{
private:
    int _num;
public:
    test(int num = 1)
        :_num(num)
    {
        cout << "test()" << endl;
    }
    ~test()
    {
        cout << "~test()" << endl;
    }
};

int main()
{
    test* ptr = new test;
    cout << endl;
    test* ptr1 = new test(2);
    cout << endl;
    test* ptr2 = new test[5]{ 1,2,3,4,5 };
    cout << endl;
    delete ptr;
    cout << endl;
    delete ptr1;
    cout << endl;
    delete[] ptr2;

    return 0;
}
输出结果
test()

test()

test()
test()
test()
test()
test()

~test()

~test()

~test()
~test()
~test()
~test()
~test()

在上面的代码中,对于单个对象空间的分配时,会调用一次构造函数,而对连续空间的对象空间分配时,会调用对象个数次构造函数。对于空间释放,当只有一个对象时,只调用一次析构函数,当有连续空间的对象时,则会调用对象个数次析构函数

如果将上面代码中的delete改为free,则不会调用析构函数

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
using namespace std;

//成员变量仅为内置类型
class test
{
private:
    int _num;
public:
    test(int num = 1)
        :_num(num)
    {
        cout << "test()" << endl;
    }
    //~test()
    //{
    //    cout << "~test()" << endl;
    //}
};

int main()
{
    test* ptr = new test;
    cout << endl;
    test* ptr1 = new test(2);
    cout << endl;
    test* ptr2 = new test[5]{ 1,2,3,4,5 };
    cout << endl;
    free(ptr);
    cout << endl;
    free(ptr1);
    cout << endl;
    free(ptr2);

    return 0;
}
输出结果
test()

test()

test()
test()
test()
test()
test()
但是此时编译器会报警告
ptr使用new分配但使用free删除
ptr2使用new []分配但使用free删除
ptr1使用new分配但使用free删除

在上面的代码中,使用new为自定义类型的对象开辟空间时会调用构造函数,但是释放空间时使用free编译器会给出警告,如果没有删除析构函数,那么程序将运行终止,所以不要使用freedelete交叉使用

另外对于下面的代码来说

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;

//成员变量仅为内置类型
class test
{
private:
    int _num;
public:
    test(int num = 1)
        :_num(num)
    {
        cout << "test()" << endl;
    }
    ~test()
    {
        cout << "~test()" << endl;
    }
};

int main()
{
    test* ptr = new test;
    cout << endl;
    test* ptr1 = new test(2);
    cout << endl;
    test* ptr2 = new test[5]{ 1,2,3,4,5 };
    cout << endl;
    delete ptr;
    cout << endl;
    delete ptr1;
    cout << endl;
    delete ptr2;

    return 0;
}

因为ptr2指向的空间是连续的对象空间,此时使用对单个空间释放形式的delete时,程序也会终止运行,所以单个空间开辟使用单个空间释放,多个空间开辟使用多个空间释放

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
using namespace std;

//对应有资源申请的内置类型来说
class test
{
private:
    int* _a;
public:
    test()
        :_a(nullptr)
    {
        _a = new int[5];
        cout << "test()" << endl;
    }

    ~test()
    {
        delete[] _a;
        cout << "~test()" << endl;
    }

};

int main()
{
    //单个空间开辟和释放
    test* ptr = new test;
    cout << endl;
    delete ptr;
    cout << endl;

    //多个空间开辟和释放
    test* ptr1 = new test[5];
    cout << endl;
    delete[] ptr1;
    cout << endl;

    return 0;
}
输出结果
test()

~test()

test()
test()
test()
test()
test()

~test()
~test()
~test()
~test()
~test()

在上面的代码中,类成员变量的类型为指针类型,在构造函数中,为该指针分配了5个连续的int类型空间,在析构函数中,释放这5个连续的空间。当类实例化对象时,对于单个空间的开辟和释放会调用一次构造函数和一次析构函数,而对于连续空间的开辟和释放会调用连续空间个数次的构造函数和析构函数

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;

//对应有资源申请的内置类型来说
class test
{
private:
    int* _a;
public:
    test()
        :_a(nullptr)
    {
        _a = new int[5];
        cout << "test()" << endl;
    }

    ~test()
    {
        delete[] _a;
        cout << "~test()" << endl;
    }

};

int main()
{
    //单个空间开辟和释放
    test* ptr = new test;
    cout << endl;
    free(ptr);
    cout << endl;

    return 0;
}

对于单个空间的释放来说,不会出现程序崩溃,尽管没有调用析构函数释放类对象成员变量指向的内存空间,但是当程序结束时,该空间会得到释放,此时也即内存泄漏

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;

//对应有资源申请的内置类型来说
class test
{
private:
    int* _a;
public:
    test()
        :_a(nullptr)
    {
        _a = new int[5];
        cout << "test()" << endl;
    }

    ~test()
    {
        delete[] _a;
        cout << "~test()" << endl;
    }

};

int main()
{
    //多个空间开辟和释放
    test* ptr1 = new test[5];
    cout << endl;
    free(ptr1);
    cout << endl;

    return 0;
}

对于多个连续空间的释放来说,因为free只能释放一个空间,如果直接理解会认为释放掉了连续空间中的第一个空间而并未释放后面的空间导致程序内存泄漏错误。但是实际上并不是,因为内存泄漏只有在没有足够内存时才会导致程序崩溃,编译器是不会检查内存泄露的。而真正导致程序崩溃的原因是VS在此处做出的优化

而之所以free释放空间时会报错,就是因为返回的起始位置并不是开辟的空间的真正起始位置从而导致程序崩溃,free释放空间时必须从开辟的空间的实际起始位置开始,而不能释放部分空间,而delete(不是delete[])的底层设计也是free(只是做了一些异常检查),所以调用delete而不是delete[]也会导致程序崩溃,而delete[]不崩溃是因为在释放连续空间时会先向前移动找到存储空间个数的值,再通过该值确定调用析构函数的次数,最后销毁指向连续空间的指针

总结:

对于内置类型来说,因为内置类型开辟的空间如果使用free也是直接释放指针指向的空间,只是不会调用析构函数,一般情况下不会出现问题,但是不推荐使用free。建议对单个空间用delete,连续空间用delete[]

对于自定义类型来说,使用free比较容易产生内存泄漏,所以不论是成员变量是内置类型,还是成员变量是涉及资源申请的,都建议使用deletedelete[]

operator newoperator delete函数

newdelete是用户进行动态内存申请和释放的操作符,operator newoperator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间

operator new 实际也是通过malloc来申请空间,operator delete 最终是通过free来释放空间的

Note

注意operator newoperator delete函数不是运算符重载

内置类型:

对于operator new函数来说

如果是单个空间,那么在申请空间时使用new,底层会调用malloc函数开辟单个空间,但是不同于malloc函数,operator new如果空间申请失败会抛出异常,而不是返回空指针

如果是连续空间,则在申请空间时new[],底层会调用malloc函数开辟连续的空间

对于operator delete函数来说

如果是单个空间,那么在释放空间时使用delete,底层会调用free函数释放单个空间,但是不同于free宏,operator delete会在释放时检查一些可能存在的问题

如果是连续空间,则在申请空间时delete[],底层会调用free函数开辟连续的空间

自定义类型:

对于operator new函数来说

如果是单个空间,则在申请空间时使用new,会调用operator new申请空间,再在申请的空间上调用构造函数创建对象

如果是多个连续空间,则在申请空间时使用new[],会调用operator new申请连续的空间,再在申请的连续空间上调用申请空间个数次的构造函数创建对象

对于operator delete函数来说

如果是单个空间,则在释放空间时使用delete,释放空间时先调用类的析构函数,再调用operator delete函数释放指向单个存储对象空间的指针

如果是连续空间,则在释放空间时使用delete[],释放空间时先调用类的析构函数,调用次数为创建的对象的个数,最后调用operator delete函数释放指向存储对象的连续空间的指针

定位new表达式(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

基本语法

C++
1
new (开辟空间的起始地址) 类型或者 new (开辟空间的起始地址) 类型(初始值)

定位new最大的特点就是将开辟位置交给了程序员自己,例如下面的代码:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

const int size = 1024;
const int n = 10;
char arr[size];

int main()
{
    int *p1 = new int[n];
    int *p2 = new (arr) int(n);

    printf("%p\n", arr);
    printf("%p\n", p1);
    printf("%p\n", p2);

    std::cout << std::endl;

    int *p3 = new int[n];
    int *p4 = new (arr) int(n);

    printf("%p\n", arr);
    printf("%p\n", p3);
    printf("%p\n", p4);

    delete[] p1;
    delete[] p3;

    return 0;
}

输出结果
00007ff71494c040
00000000006786a0
00007ff71494c040

00007ff71494c040
000000000067e330
00007ff71494c040
在上面的代码中,可以看到使用定位new在数组空间中开辟空间,此时只要定位new起始地址是数组的起始地址,就会出现地址一直与数组的起始地址相同,而普通的new开辟的空间起始地址则有操作系统分配

Note

需要注意,上面的代码中,由于定位new开辟的空间是在栈数组,该数组开辟在栈上,出了main函数作用域就会销毁,所以此时定位new在此基础上开辟的空间就不需要单独使用deleter[]释放

使用场景

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显式调构造函数进行初始化

Note

内存池的内存申请本质上还是在堆上,但是不同于向操作系统申请,内存池申请的空间一般会大于用户需要的空间,从而减少申请的次数

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;

class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        cout << "A():" << this << endl;
    }
    ~A()
    {
        cout << "~A():" << this << endl;
    }
private:
    int _a;
};

int main()
{
    // p1现在指向的只不过是A类大小的一段空间,还不能算是一个对象,因为构造函数没有执行
    A* p1 = (A*)malloc(sizeof(A));
    // 在空间中调用构造函数创建对象
    new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
    p1->~A();// 析构函数可以显式调用
    free(p1);
    A* p2 = (A*)operator new(sizeof(A));// 也可以直接使用operator new函数开辟空间
    // 在空间中调用构造函数创建对象
    new(p2)A(10);
    p2->~A();
    delete p2;
    return 0;
}

malloc/freenew/delete

相同点

malloc/freenew/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放

不同点

  1. mallocfree是函数,newdelete是操作符
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理