C++:C/C++内存管理

    • C语言
      • C语言内存分配回顾
      • malloc & calloc & realloc & free
    • C++
      • new & delete
      • new[ ] & delete[ ]
      • 定位new
      • new & delete原理
    • malloc / free 与 new / delete对比

C语言

C语言内存分配回顾

我们先回顾一下C语言的内存分配:

解析:

int globalVar = 1;
在main外部,将globalVar定义在了全局,放在了C静态区


static int staticGlobalVar = 1;
在main外部,也是将staticglobalVar定义在了全局,也存储在C静态区


两者区别:
没有被static修饰的全局变量在整个程序中都是可见的,可以被其他文件访问和修改;而被static修饰的全局变量只能在定义该变量的文件内部可见,无法被其他文件访问。


static int staticVar = 1;
static修饰的局部变量会在整个程序执行过程中保留其值,直到程序结束。而staticVar也被存到的C静态区


int localVar = 1;
一个普通的局部变量定义,存储在A栈区


int num1[10] = { 1, 2, 3, 4 };
是一个数组定义过程,也是一个普通变量,被存储在A栈区


char char2[] = "abcd";
是一个数组定义过程,不过是在数组内部承载了一个字符串,此时会将字符串一个一个字符拷贝进数组中,所以最后char2是一个指针,而*char2是一个数组或者是首元素。都被存在A栈区


const char* pChar3 = "abcd";
用一个const指针接收了字符串 "abcd"的地址,对于pChar3本身,是一个指针,存储在A栈区。对于*pChar3,则是这个常量字符串 "abcd",存储在D常量区


int* ptr1 = (int*)malloc(sizeof(int) * 4);
是一个动态内存分配过程,ptr1本身是一个指针,存储在A栈区。而*ptr1则是指向一块动态内存,存放在B堆区

sizeof(num1)
num1是一个数组,此时得到的是数组的大小4*10 = 40


sizeof(char2)
char2是一个数组,内部存储着”abcd“被一个一个字符拷贝后的结果,由于字符串末尾有一个’\0‘,所以数组长度比实际长1。最后大小为1 * (4 + 1)=5


strlen(char2)
strlen用于统计字符串的长度,遇到’\0‘时停止同统计,所以结果为4


sizeof(pChar3)
pChar3是一个指针,在32位计算机中指针大小为4,64位计算机中指针大小为8。所以答案为4/8


strlen(pChar3)
pChar3指向字符串”abcd”,长度为4,结果为4


sizeof(ptr1)
ptr1是一个指针,在32位计算机中指针大小为4,64位计算机中指针大小为8。所以答案为4/8

答案:



malloc & calloc & realloc & free

在C语言中,动态内存管理主要是通过malloccallocreallocfree四个函数完成的。
我们简单回顾一下它们的作用与区别:
malloc,calloc和realloc是C语言中用于动态内存分配的函数。

  1. malloc函数:

    • 作用:malloc函数用于在程序运行时动态分配指定大小的内存空间。
    • 使用方法:malloc函数的原型为void *malloc(size_t size),其中size参数表示需要分配的内存空间大小(以字节为单位)。函数返回一个void指针,指向分配的内存空间的起始地址。
    • 示例:
      int *ptr;ptr = (int *)malloc(10 * sizeof(int));
  2. calloc函数:

    • 作用:calloc函数用于在程序运行时动态分配指定数量、指定大小的内存空间,并将分配的内存空间初始化为零。
    • 使用方法:calloc函数的原型为void *calloc(size_t num, size_t size),其中num参数表示需要分配的元素个数,size参数表示每个元素的大小(以字节为单位)。函数返回一个void指针,指向分配的内存空间的起始地址。
    • 示例:
      int *ptr;ptr = (int *)calloc(10, sizeof(int));
  3. realloc函数:

    • 作用:realloc函数用于修改之前动态分配的内存空间的大小。
    • 使用方法:realloc函数的原型为void *realloc(void *ptr, size_t size),其中ptr参数是之前由malloc或calloc分配的内存空间的指针,size参数表示新的内存空间大小。函数返回一个void指针,指向修改后的内存空间的起始地址。如果返回空指针,则表示内存分配失败。
    • 示例:
      int *ptr;ptr = (int *)realloc(ptr, 20 * sizeof(int));

三者的异同点如下:

  • malloccalloc都用于动态分配内存空间,而realloc用于调整动态分配的内存空间的大小。
  • malloccalloc都返回分配的内存空间的起始地址,而realloc返回修改后的内存空间的起始地址。
  • malloccalloc分配的内存空间不会被初始化,而realloc可能会保留之前分配的内存内容。
  • realloc函数可能会将之前分配的内存空间内容复制到新的内存空间中,所以在使用realloc时要小心,以免丢失之前分配的内存中的数据。
  • realloc函数还可以用于分配新的内存空间,如果之前的指针是空指针,则realloc的操作相当于malloc

C++

new & delete

在C++中,newdelete是用于动态分配和释放内存的关键字。动态分配内存是指在程序运行时按需分配所需的内存,而不是在编译时固定分配内存。

new关键字用于动态分配单个对象的内存,并返回指向该对象的指针。其语法如下:

pointer = new type;

其中,pointer是一个指针,用于存储分配的内存地址,type是要分配内存的对象类型。

例如,以下代码分配了一个整数的内存,并将地址存储在ptr指针中:

int* ptr = new int;

此时,ptr指向一个未初始化的整数对象。

此外,如果希望分配的内存被初始化,可以用以下语法:

pointer = new type();

比如以下代码:

int* ptr = new int(10);

就是开辟了一个int的空间,并赋值为10。


要释放动态分配的内存,可以使用delete关键字。其语法如下:

int* pointer = new int;delete pointer;

需要注意的是,使用delete释放指针指向的内存后,该指针将不再有效,因为内存已经被释放。为了避免悬挂指针的问题,可以在释放内存后将指针设置为nullptr,以防止后续误用。

delete ptr;ptr = nullptr;

但是到此为止,好像C++的动态内存和C语言的功能没什么区别。其实C++的new和delete与C语言的malloc和free的区别体现在类上。

当使用new关键字来创建对象时,会调用对象的构造函数,而malloc不会。
比如以下代码:

class A{public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}private:int _a;};int main(){A* p1 = (A*)malloc(sizeof(A));A* p2 = new A;free(p1);delete p2;return 0;}

我们定义了一个A的类,然后分别用mallocnew的方式开辟了内存,来存放一个A的对象。

对于p1而言,只是开辟了一个A类需要的大小,并把void*指针转化为了A*的指针。
而对于p2,不仅开辟了空间,而且调用了A类的构造函数,把a初始化为0。

所以我们在开辟类的动态内存时,最好使用new,来调用构造函数。

此外,delete也会调用类的析构函数,而free不会。


new[ ] & delete[ ]

new[ ]
如果需要动态分配一个数组,可以使用以下语法:

pointer = new type[size];

其中,size是要分配内存的数组的大小。

例如,以下代码分配了一个包含10个整数的数组的内存,并将地址存储在ptr指针中:

int* ptr = new int[10];

此时,ptr指向一个包含10个未初始化的整数对象的数组。

此外,你还可以对这个数组进行初始化,用大括号即可:

int* ptr = new int[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

这样就可以将数组初始化为{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

delete[ ]

同样地,如果之前使用new分配了一个数组,可以使用以下语法释放内存:

delete[] pointer;

方括号内无需填入任何值。

比如:

int* ptr = new int[10];delete[] ptr;

同样的new[ ] & delete[ ]如果作用与于类,那么会调用相应的构造函数与析构函数。


定位new

那么假如我们用malloc开辟了一个类的空间,还能不能对这个类初始化,调用其构造函数?
是可以的,这就需要定位new了。

在C++中,定位new是一种特殊的new表达式,它允许我们在指定的内存位置上创建对象。通常情况下,new表达式会自动分配内存,并在该内存上创建对象。但是,有时我们希望将对象放置在已经分配的内存中,这就需要使用定位new。

定位new的语法如下:

new (指针) 类型(参数列表);

其中,指针是一个指向已经分配的内存的指针,类型是要创建的对象的类型,参数列表是对象构造函数的参数。

示例:

class MyClass {public:int value;MyClass(int v) : value(v) {std::cout << "构造函数被调用了" << value << std::endl;}};int main() {// 分配内存void* memory = malloc(sizeof( MyClass));// 在已分配的内存上创建对象MyClass* obj = new (memory) MyClass(10);//销毁对象obj->~MyClass();return 0;}

在上面的示例中,首先我们使用malloc分配了足够的内存以容纳一个MyClass对象。然后,我们使用定位new在已分配的内存中创建了一个MyClass对象,并传递了一个参数值10给构造函数。我们可以看到构造函数被调用,并输出了相应的消息。

需要注意的是,使用定位new创建的对象必须手动调用析构函数进行销毁,并手动释放相应的内存。

定位new在一些特定的情况下非常有用,例如在实现自定义的内存管理时,或者在某些嵌入式系统中,需要将对象放置在特定的内存地址上。但是在一般的编程中,几乎用不上。


new & delete原理

其实new和delete本质上还是malloc和free,但是C++在两者基础上做了很多优化,最后才得到的new和delete,接下来我将对new和delete进行拆解,带大家看清两者的原理。

对于new来说,其要完成的工作有:

  1. 开辟指定大小的空间
  2. 如果开辟空间失败,抛出异常(对malloc而言是返回空指针)
  3. 如果开辟的空间用于存放对象,那么调用对应的析构函数

我们看看以上三个步骤中,谁是可以通过malloc完成的?
当然是第一步,malloc就可以完成指定的空间的开辟。

抛出异常是C++相比于C语言特有的步骤,可以简单理解为报错。那么C++要如何检测开辟内存失败?
malloc开辟内存失败就会返回空指针,所以我们可以通过malloc的返回值确定是否要抛出异常。所以抛出异常这个步骤也与malloc紧密关联,于是C++将第一步与第二步封装成了一个函数operator new ,它可以同时完成内存开辟和抛出异常,而两者都基于malloc实现。

而我们平常使用的new就是operator new函数+构造函数。
这也就是new的底层原理:将malloc进行封装,完成内存开辟与抛出异常,再额外调用构造函数,完成对象的初始化

搞懂了new的原理,那么delete的原理也就差不多了:
delete也将free进行了一个封装,构成了一个operator delete函数,其完成内存的释放,此外还会调用析构函数,完成对象调用的资源的释放。

不过要注意,delete是先调用析构函数释放对象调用的资源,再调用operator delete完成内存释放。

我们再加上new[]与delete[]不额外讲解了,我们现在对四个操作符进行一次总结:

new的原理

  1. 调用operator new函数申请空间
  2. 在申请的空间上执行构造函数,完成对象的构造

delete的原理

  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间

new []的原理

  1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
  2. 在申请的空间上执行N次构造函数

delete[]的原理

  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

malloc / free 与 new / 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在释放空间前会调用析构函数完成空间中资源的清理