Skip to content

动态对象创建

当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了浪费空间,也许空间不足不能满足程序需要。所以对于数组来讲,如果能根据需要来分配空间大小再好不过。

所以动态的意思意味着不确定性

为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然C语言早就提供了动态内存分配(dynamic memory allocation),函数mallocfree可以在运行时从堆中分配和释放存储单元。

C动态分配内存方法

为了在运行时动态分配内存,C在他的标准库中提供了一些函数malloc以及它的变种callocrealloc,释放内存的free。这些函数是有效的、但是原始的,需要程序员理解和小心使用。为了使用C的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:

点击查看代码
cpp
#include <iostream>
#include <cstring>
using namespace std;

class Person {
public:
    Person() {
        mAge = 20;
        pName = (char*)malloc(strlen("john") + 1);
        strcpy(pName, "john");
    }
    void Init() {
        mAge = 20;
        pName = (char*)malloc(strlen("john") + 1);
        strcpy(pName, "john");
    }
    void Clean() {
        if (pName != NULL) {
            free(pName);
        }
    }

public:
    int mAge;
    char* pName;
};

int main(int argc, char *argv[]) {
    //分配内存
    Person* person = (Person*)malloc(sizeof(Person));
    if (person == NULL) {
        return 0;
    }
    //调用初始化函数
    person->Init();

    cout << person->pName << "  " << person->mAge << endl;
    //清理对象
    person->Clean();
    //释放person对象
    free(person);

    return 0;
}

程序输出:

shell
john  20

然而这些函数在C++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。这种方式所存在的问题:

  • 程序员必须确定对象的长度。
  • malloc返回一个void*指针,C++不允许将void*赋值给其他任何指针,必须强转。
  • malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。
  • 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数。

C的动态内存分配函数太复杂,容易令人混淆,是不可接受的,C++中我们推荐使用运算符 newdelete

对象创建

当创建一个C++对象时会发生两件事:

  1. 为对象分配内存

  2. 调用构造函数来初始化那块内存

第一步我们能保证实现,需要我们确保第二步一定能发生。C++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。

new和delete关键字

C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。

cpp
Person* person = new Person;
// 相当于:
Person* person = (Person*)malloc(sizeof(Person));
if (person == NULL) {
    return 0;
}
person->Init(); //构造函数

new操作符能确定在调用构造函数初始化之前内存分配是成功的,所以不用显式确定调用是否成功。

现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。

注意

不要使用void *类型的变量来接收new出来的对象(会导致delete该变量时无法释放内存,需要强转成new时的类型才能正确释放内存)

new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。

如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。

如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。

代码示例
cpp
#include <iostream>
#include <cstring>
using namespace std;

class Person {
public:
    Person() {
        cout << "无参构造函数!" << endl;
        pName = (char*)malloc(strlen("undefined") + 1);
        strcpy(pName, "undefined");
        mAge = 0;
    }
    Person(char* name, int age) {
        cout << "有参构造函数!" << endl;
        pName = (char*)malloc(strlen(name) + 1);
        strcpy(pName, name);
        mAge = age;
    }
    void ShowPerson() {
        cout << "Name:" << pName << " Age:" << mAge << endl;
    }
    ~Person() {
        cout << "析构函数!" << endl;
        if (pName != NULL) {
            delete pName;
            pName = NULL;
        }
    }

public:
    char* pName;
    int mAge;
};

void test() {
    // 使用new创建对象需要的空间
    Person* person1 = new Person;
    char name[] = "John";
    Person* person2 = new Person(name, 33);
    person1->ShowPerson();
    person2->ShowPerson();

    // 对应的使用delete释放new出来的空间
    delete person1;
    delete person2;
}

int main(int argc, char *argv[]) {
    test();

    return 0;
}

程序输出:

shell
无参构造函数!
有参构造函数!
Name:undefined Age:0
Name:John Age:33
析构函数!
析构函数!

operator new()

operator new()是c++提供的申请内存空间的函数,如果是复杂类型 new运算符通过调用operator new()来申请空间 然后调用对象的构造函数。

我们可以通过自己实现的operator new()函数来重写C++底层的operator new()函数。

new和operator new()的关系

newdelete是C++中用于动态分配和释放内存的运算符,而operator new()operator delete()是全局函数,它们之间是底层调用的关系。new在底层调用operator new()全局函数来分配内存,然后调用构造函数来初始化对象。如果分配内存失败,它会抛出异常。

相反,当您使用delete运算符来释放内存时,它会先调用析构函数来销毁对象,然后调用operator delete()全局函数来释放内存。

总的来说new/delete在运行的时间分别做了两件事:

  • new在运行的时间首先调用了operator new()进行了内存的分配,然后调用了对象的构造函数;
  • delete在运行的时间首先调用了对象的析构函数,然后调用operator delete()进行分配空间的回收。

重写operator new()函数

在一些特定的场景下我们也必须重写operator new()函数来实现我们的功能,比如实现对象池、自己实现检查内存泄漏等。一般情况下,我们直接使用默认提供的operator new()函数即可,除非你写的比默认提供的更好。

简单测试重写operator new()函数

cpp
#include <iostream>

// 重写 operator new()
void *operator new(size_t size) {
    if (size < 0) {
        throw std::bad_alloc();
    }

    void *p = malloc(size);
    std::cout << "call operator new(), address is " << p << std::endl;

    return p;
}

// 重写 operator delete()
void operator delete(void *p) {
    if (p == nullptr) {
        return;
    }

    std::cout << "call operator delete(), address is " << p << std::endl;

    free(p);
    p = nullptr;
}

int main() {
    int *pi = new int(10);
    std::cout << "pi address is " << pi << std::endl;
    delete pi;

    return 0;
}

运行结果

bash
call operator new(), address is 0x59dba338e2b0
pi address is 0x59dba338e2b0
call operator delete(), address is 0x59dba338e2b0

内存泄漏检测

需要重写的函数

简单实现

cpp
#pragma once

// https://zh.cppreference.com/w/cpp/memory/new/operator_new

#ifdef DEBUG

#include <iostream>
#include <string>
#include <unordered_map>
#include <memory>

static std::unordered_map<std::string, std::string> new_ptr_map;

void *operator new(std::size_t count, const char *file, int line) {
    void *p = malloc(count);
    std::string s;
    s = s + "file:" + file + " line:" + std::to_string(line) + " leak " + std::to_string(count) + " bytes.";
    char addr[64] = {0};
    sprintf(addr, "%p", p);

    new_ptr_map.insert(std::make_pair(addr, s));

    return p;
}
void operator delete(void *p) {
    if (p == nullptr) {
        return;
    }

    char addr[64] = {0};
    sprintf(addr, "%p", p);
    new_ptr_map.erase(addr);
}

void *operator new[](std::size_t count, const char *file, int line) {
    void *p = malloc(count);
    std::string s;
    s = s + "file:" + file + " line:" + std::to_string(line) + " leak " + std::to_string(count) + " Bytes.";
    char addr[64] = {0};
    sprintf(addr, "%p", p);

    new_ptr_map.insert(std::make_pair(addr, s));

    return p;
}

void check_leak() {
    if (new_ptr_map.empty()) {
        return;
    }

    for (auto it : new_ptr_map) {
        std::cout << it.second << std::endl;
    }
}

#define new new (__FILE__, __LINE__)

#endif // DEBUG
cpp
#define DEBUG

#include "debug-new.h"


int main() {
    int *p = new int(10);
    int *p1 = new int[10];
    double *d = new double(20.0);

    // delete p;
    // delete[] p1;
    // delete d;

    check_leak();
    return 0;
}
bash
valgrind --tool=memcheck --leak-check=full  ./main 
==49512== Memcheck, a memory error detector
==49512== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==49512== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info
==49512== Command: ./main
==49512== 
file:test-debug-new.cpp line:6 leak 8 Bytes.
file:test-debug-new.cpp line:5 leak 40 Bytes.
file:test-debug-new.cpp line:4 leak 4 Bytes.
==49512== 
==49512== HEAP SUMMARY:
==49512==     in use at exit: 52 bytes in 3 blocks
==49512==   total heap usage: 21 allocs, 18 frees, 75,696 bytes allocated
==49512== 
==49512== 4 bytes in 1 blocks are definitely lost in loss record 1 of 3
==49512==    at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==49512==    by 0x10A76A: operator new(unsigned long, char const*, int) (debug-new.h:17)
==49512==    by 0x10B1AD: main (test-debug-new.cpp:4)
==49512== 
==49512== 8 bytes in 1 blocks are definitely lost in loss record 2 of 3
==49512==    at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==49512==    by 0x10A76A: operator new(unsigned long, char const*, int) (debug-new.h:17)
==49512==    by 0x10B1ED: main (test-debug-new.cpp:6)
==49512== 
==49512== 40 bytes in 1 blocks are definitely lost in loss record 3 of 3
==49512==    at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==49512==    by 0x10ACB3: operator new[](unsigned long, char const*, int) (debug-new.h:38)
==49512==    by 0x10B1D0: main (test-debug-new.cpp:5)
==49512== 
==49512== LEAK SUMMARY:
==49512==    definitely lost: 52 bytes in 3 blocks
==49512==    indirectly lost: 0 bytes in 0 blocks
==49512==      possibly lost: 0 bytes in 0 blocks
==49512==    still reachable: 0 bytes in 0 blocks
==49512==         suppressed: 0 bytes in 0 blocks
==49512== 
==49512== For lists of detected and suppressed errors, rerun with: -s
==49512== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)

参考资料

用于数组的new和delete

使用new和delete在堆上创建数组非常容易。

cpp
//创建字符数组
char* pStr = new char[100];

//创建整型数组
int* pArr1 = new int[100];

//创建整型数组并初始化
int* pArr2 = new int[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

//释放数组内存
delete[] pStr;
delete[] pArr1;
delete[] pArr2;

释放数组的时间必须加上中括号(不加中括号而引发的异常在有些编译器中不会显式报错)

代码示例
cpp
#include <iostream>
using namespace std;

class Person {
public:
    Person() {
        //不能使用sizeof
        const int len = strlen("undefined")+1;
        pName = (char *)malloc(len);
        strcpy(pName, "undefined");
        mAge = 0;
    }
    Person(char* name, int age) {
        //不能使用sizeof
        const int len = strlen(name)+1;
        pName = (char*)malloc(len);
        strcpy(pName, name);
        mAge = age;
    }
    ~Person() {
        if (this->pName != NULL) {
            free(pName);
        }
    }

public:
    char* pName;
    int mAge;
};

void test() {
    char name1[] = "john";
    char name2[] = "Smith";

    //栈聚合初始化
    Person person[] = { Person(name1, 20), Person(name2, 22) };
    cout << person[0].pName << endl;
    cout << person[1].pName << endl;

    //创建堆上对象数组必须提供构造函数
    Person* workers = new Person[20];
}

int main(int argc, char *argv[]) {
    test();

    return 0;
}

程序输出:

shell
john
Smith

delete void*可能会出错

如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数。以下代码未调用析构函数,导致可用内存减少。

代码示例
cpp
#include <iostream>
using namespace std;

class Person {
public:
    Person(char* name, int age) {
        pName = (char*)malloc(sizeof(name));
        strcpy(pName, name);
        mAge = age;
    }
    ~Person() {
        if (pName != NULL) {
            delete pName;
        }
    }

public:
    char* pName;
    int mAge;
};

void test() {
    char name[] = "john";
    void* person = new Person(name, 20);
    delete person;
}

int main(int argc, char *argv[]) {
    test();

    return 0;
}

malloc、free和new、delete可以混搭使用吗?也就是说malloc分配的内存,可以调用delete吗?通过new创建的对象,可以调用free来释放吗?

回答:不可以,因为new和malloc分配内存的区域不同。new申请的内存开辟在自由储存区free store;malloc申请的内存开辟在堆区heap。虽然有些编译器并没有正确的执行这一规则,但是为了能够使程序稳定运行,还是不要混搭使用。

使用new和delete采用相同形式

cpp
Person* person = new Person[10];
delete person;

以上代码有什么问题吗?(vs下直接中断、qt下析构函数调用一次)

使用了new也搭配使用了delete,问题在于Person有10个对象,那么其他9个对象可能没有调用析构函数,也就是说其他9个对象可能删除不完全,因为它们的析构函数没有被调用。

我们现在清楚使用new的时候发生了两件事:

  • 分配内存
  • 调用构造函数,那么调用delete的时候也有两件事
    • 析构函数
    • 释放内存

那么刚才我们那段代码最大的问题在于:person指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。换句话说,person指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。更明确的说,数组所用的内存通常还包括“数组大小记录”,使得delete的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。单一对象和数组对象的内存布局可理解为下图:

本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的。

当我们使用一个delete的时候,我们必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。当我们使用delete[],那么delete就知道是一个对象数组,从而清楚应该调用几次析构函数。

结论:

如果在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果在new表达式中不使用[], 一定不要在相应的delete表达式中使用[]

不同形式的new

  1. 普通形式的new
  2. 不抛出异常的new
  3. 在堆上申请常量的new
  4. 定位new

代码演示

cpp
#include <iostream>
using std::cout;
using std::endl;

int main() {
    // 1. 普通形式的new
    int *p = new int(10);
    cout << "*p=" << *p << endl;
    delete p;

    // 2. 不抛出异常的new
    p = new (std::nothrow) int(20);
    cout << "*p=" << *p << endl;
    delete p;

    // 3. 在堆上申请常量的new
    const int *p1 = new const int(30);
    cout << "*p1=" << *p1 << endl;
    delete p1;

    // 4. 定位new
    int a = 5;
    p = new (&a) int(40);
    cout << "a=" << a << endl;

    return 0;
}
bash
$ g++ main.cc -o main -Wall
$ ./main 
*p=10
*p=20
*p1=30
a=40
$ valgrind --tool=memcheck --leak-check=full ./main 
==29400== Memcheck, a memory error detector
==29400== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==29400== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info
==29400== Command: ./main
==29400== 
*p=10
*p=20
*p1=30
a=40
==29400== 
==29400== HEAP SUMMARY:
==29400==     in use at exit: 0 bytes in 0 blocks
==29400==   total heap usage: 5 allocs, 5 frees, 74,764 bytes allocated
==29400== 
==29400== All heap blocks were freed -- no leaks are possible
==29400== 
==29400== For lists of detected and suppressed errors, rerun with: -s
==29400== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
$

面试题

C语言中的malloc/free和C++中的new/delete有什么区别?

最主要的区别是new/delete都是执行了两步操作,一个是先申请空间然后调用构造函数,一个是先调用析构函数然后释放空间;但是malloc/free只能申请/释放空间,如果要使用malloc/free来为对象创建/释放空间,那么就要程序员手动调用对应的构造和析构函数。

其次它们的区别还有

  • malloc必须确定申请空间的长度;new自动根据类型计算大小。
  • malloc返回void*需强制类型转换(可能引发类型错误),new直接返回类型化指针(编译时类型检查)。举例:int* p = malloc(sizeof(int))需要强转,而int* p = new int无类型风险。
  • malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功;new失败抛出std::bad_alloc异常。
  • malloc无法跟踪数组对象个数,需手动管理;new[]/delete[]用于对象数组,自动调用每个元素的构造/析构函数。
  • new/delete是C++运算符(可重载),而malloc/free是C标准库函数(不可重载)。这个特性差异在自定义内存池开发中至关重要(例如实现内存泄漏检测工具时需要重载new),通过重载operator new还可以实现内存使用统计功能。

另外new和malloc分配内存的区域不同。C++标准将new分配区域称为自由存储区(free store),malloc使用堆区(heap),但多数编译器实现中二者共用同一内存池。为了能够使程序稳定运行,建议不要混搭使用。

最终总结

二者核心区别在于new/delete管理对象生命周期(构造/析构+内存),而malloc/free仅处理原始内存。此外在类型安全、错误处理、运算符重载等方面也存在本质差异。尽管内存区域的理论划分存在,但实践中更应关注它们的行为差异而非实现细节。