C++中malloc和new调用方式对比
C++ 中 malloc
和 new
的基础概念
malloc
函数介绍
malloc
是 C 语言标准库函数,在 <stdlib.h>
头文件中声明。它的主要作用是在堆上分配指定字节数的内存空间,并返回一个指向该内存起始地址的指针。如果分配失败,malloc
返回 NULL
。
其函数原型为:
void* malloc(size_t size);
这里的 size
参数指定要分配的字节数。例如,要分配 10 个 int
类型的空间(假设 int
为 4 字节),可以这样调用:
#include <stdlib.h>
#include <stdio.h>
int main() {
int* ptr = (int*)malloc(10 * sizeof(int));
if (ptr == NULL) {
perror("malloc failed");
return 1;
}
// 使用 ptr 进行操作
free(ptr);
return 0;
}
在上述代码中,首先调用 malloc
分配了 10 * sizeof(int)
字节的内存空间,并将返回的 void*
指针强制转换为 int*
类型。然后检查返回值是否为 NULL
,若为 NULL
则说明分配失败,通过 perror
打印错误信息。最后使用完内存后,调用 free
函数释放内存。
new
运算符介绍
new
是 C++ 的运算符,用于在堆上分配内存并构造对象(如果是对象类型)。当使用 new
分配内存时,它不仅会分配所需的内存空间,还会调用对象的构造函数(对于对象类型)。
对于基本数据类型,new
的使用方式相对简单。例如,分配一个 int
类型的变量:
#include <iostream>
int main() {
int* num = new int;
*num = 10;
std::cout << "Value of num: " << *num << std::endl;
delete num;
return 0;
}
在这段代码中,new int
分配了一个 int
类型的内存空间,并返回一个指向该空间的指针。然后给该内存空间赋值为 10,最后使用 delete
运算符释放内存。
对于自定义类型,new
的行为更为复杂且强大。假设有一个自定义类 MyClass
:
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
};
int main() {
MyClass* obj = new MyClass;
delete obj;
return 0;
}
当执行 new MyClass
时,首先在堆上分配足够存储 MyClass
对象的内存空间,然后调用 MyClass
的构造函数。当执行 delete obj
时,先调用 MyClass
的析构函数,然后释放内存。
内存分配机制的差异
malloc
的内存分配方式
malloc
是基于系统堆内存分配的函数。它从堆中查找一块足够大小的连续内存块。当请求的内存大小小于等于当前堆中最大的空闲块时,malloc
会尝试从堆中分割出一块大小合适的内存块返回给调用者。如果堆中没有足够大的连续空闲块,malloc
可能会尝试扩展堆(通过系统调用,如 brk
或 sbrk
在 Linux 系统上),如果扩展失败,则返回 NULL
。
在 malloc
分配内存时,它只关心内存的大小,不关心所分配内存的具体类型。这就是为什么 malloc
的返回值是 void*
,需要调用者手动进行类型转换。例如:
double* dptr = (double*)malloc(10 * sizeof(double));
这里将 malloc
返回的 void*
指针转换为 double*
指针,以方便后续对 double
类型数据的操作。
new
的内存分配方式
new
运算符的内存分配过程相对复杂一些。当使用 new
分配内存时,首先调用 operator new
函数来分配内存。operator new
函数本质上也是调用系统的内存分配函数(如 malloc
),但在这之前,new
会做一些额外的工作。
对于对象类型,new
会在分配完内存后调用对象的构造函数。例如,对于前面定义的 MyClass
类,new MyClass
的过程如下:
- 调用
operator new
分配足够存储MyClass
对象的内存空间。 - 在分配好的内存空间上调用
MyClass
的构造函数。
当使用 delete
释放内存时,过程则相反:
- 调用对象的析构函数。
- 调用
operator delete
释放内存。
这种机制确保了对象在创建和销毁时,其构造函数和析构函数能够正确调用,从而保证对象的完整性和资源的正确管理。
例如,考虑一个包含动态数组成员的类 MyArrayClass
:
class MyArrayClass {
private:
int* arr;
int size;
public:
MyArrayClass(int s) : size(s) {
arr = new int[size];
for (int i = 0; i < size; ++i) {
arr[i] = i;
}
}
~MyArrayClass() {
delete[] arr;
}
};
int main() {
MyArrayClass* obj = new MyArrayClass(5);
delete obj;
return 0;
}
在 MyArrayClass
的构造函数中,使用 new
分配了一个动态数组 arr
。在析构函数中,使用 delete[]
释放该数组。当执行 new MyArrayClass(5)
时,new
首先为 MyArrayClass
对象分配内存,然后调用其构造函数,在构造函数中又为 arr
分配内存。当执行 delete obj
时,先调用 MyArrayClass
的析构函数,释放 arr
的内存,然后调用 operator delete
释放 MyArrayClass
对象的内存。
错误处理的差异
malloc
的错误处理
malloc
在分配内存失败时,返回 NULL
。调用者需要在使用返回的指针之前检查其是否为 NULL
。例如:
#include <stdlib.h>
#include <stdio.h>
int main() {
char* str = (char*)malloc(1000000000); // 假设系统无法分配这么大内存
if (str == NULL) {
perror("malloc failed");
return 1;
}
// 使用 str 进行操作
free(str);
return 0;
}
在上述代码中,通过检查 str
是否为 NULL
来判断 malloc
是否成功。如果失败,使用 perror
打印错误信息。perror
函数会根据 errno
变量的值打印出相应的错误描述,errno
是一个全局变量,在 malloc
失败时会被设置为相应的错误码。
new
的错误处理
new
运算符在分配内存失败时,默认情况下会抛出 std::bad_alloc
异常。这与 malloc
返回 NULL
的处理方式不同。例如:
#include <iostream>
#include <new>
int main() {
try {
char* str = new char[1000000000]; // 假设系统无法分配这么大内存
// 使用 str 进行操作
delete[] str;
} catch (const std::bad_alloc& e) {
std::cerr << "new failed: " << e.what() << std::endl;
return 1;
}
return 0;
}
在这段代码中,使用 try - catch
块来捕获 new
可能抛出的 std::bad_alloc
异常。如果捕获到异常,通过 e.what()
获取异常信息并打印。
此外,C++ 还提供了一种非抛出版本的 new
,即 nothrow new
。使用 nothrow new
时,如果内存分配失败,它返回 NULL
,而不是抛出异常。例如:
#include <iostream>
#include <new>
int main() {
char* str = new (std::nothrow) char[1000000000];
if (str == NULL) {
std::cerr << "nothrow new failed" << std::endl;
return 1;
}
// 使用 str 进行操作
delete[] str;
return 0;
}
这种方式在一些不适合使用异常处理的场景(如嵌入式系统或对异常处理开销敏感的代码)中很有用。
内存释放的差异
free
与 malloc
的配合
当使用 malloc
分配内存后,需要使用 free
函数来释放内存。free
函数的原型为:
void free(void* ptr);
这里的 ptr
必须是 malloc
、calloc
或 realloc
返回的指针。如果传递一个非上述函数返回的指针,或者已经释放过的指针,行为是未定义的。例如:
#include <stdlib.h>
#include <stdio.h>
int main() {
int* ptr = (int*)malloc(10 * sizeof(int));
if (ptr == NULL) {
perror("malloc failed");
return 1;
}
free(ptr);
// 再次释放 ptr 会导致未定义行为
// free(ptr);
return 0;
}
在上述代码中,释放 ptr
后如果再次释放,程序的行为是未定义的,可能导致程序崩溃或其他不可预测的错误。
delete
与 new
的配合
使用 new
分配内存后,需要使用 delete
运算符来释放内存。对于单个对象,使用 delete
,对于数组对象,使用 delete[]
。例如:
#include <iostream>
int main() {
int* num = new int;
*num = 10;
delete num;
int* arr = new int[5];
for (int i = 0; i < 5; ++i) {
arr[i] = i;
}
delete[] arr;
return 0;
}
在上述代码中,delete num
用于释放单个 int
类型变量的内存,delete[] arr
用于释放 int
数组的内存。如果使用 delete
释放数组内存,会导致只调用数组第一个元素的析构函数(如果是对象数组),并且可能导致内存泄漏。同样,如果使用 delete[]
释放单个对象的内存,行为也是未定义的。
对于自定义类对象,delete
会先调用对象的析构函数,然后释放内存。例如:
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
};
int main() {
MyClass* obj = new MyClass;
delete obj;
return 0;
}
在这段代码中,delete obj
首先调用 MyClass
的析构函数,然后释放 obj
所指向的内存。
对数组的处理差异
malloc
分配数组
使用 malloc
分配数组内存时,只分配指定字节数的连续内存空间,不会调用数组元素的构造函数(如果是对象数组)。例如,分配一个 int
数组:
#include <stdlib.h>
#include <stdio.h>
int main() {
int* arr = (int*)malloc(5 * sizeof(int));
if (arr == NULL) {
perror("malloc failed");
return 1;
}
for (int i = 0; i < 5; ++i) {
arr[i] = i;
}
// 使用 arr 进行操作
free(arr);
return 0;
}
在上述代码中,malloc
分配了 5 * sizeof(int)
字节的内存空间,但数组元素未初始化,需要手动赋值。
如果是对象数组,情况更为复杂。假设有一个类 MyObject
:
class MyObject {
public:
MyObject() {
std::cout << "MyObject constructor called" << std::endl;
}
~MyObject() {
std::cout << "MyObject destructor called" << std::endl;
}
};
使用 malloc
分配 MyObject
数组:
#include <stdlib.h>
#include <iostream>
int main() {
MyObject* arr = (MyObject*)malloc(5 * sizeof(MyObject));
if (arr == NULL) {
std::cerr << "malloc failed" << std::endl;
return 1;
}
// 这里数组元素的构造函数未被调用
// 手动调用构造函数(不推荐这种方式,仅为演示)
for (int i = 0; i < 5; ++i) {
new (&arr[i]) MyObject;
}
// 使用 arr 进行操作
// 手动调用析构函数
for (int i = 0; i < 5; ++i) {
arr[i].~MyObject();
}
free(arr);
return 0;
}
在上述代码中,使用 malloc
分配了 MyObject
数组的内存,但构造函数未被调用。如果要使用数组元素,需要手动调用构造函数(使用 placement new),在释放内存前还需要手动调用析构函数。这种方式很容易出错,并且不符合 C++ 的对象管理原则。
new
分配数组
使用 new[]
分配数组内存时,不仅会分配内存,还会调用数组每个元素的构造函数(如果是对象数组)。例如,分配一个 int
数组:
#include <iostream>
int main() {
int* arr = new int[5];
for (int i = 0; i < 5; ++i) {
arr[i] = i;
}
// 使用 arr 进行操作
delete[] arr;
return 0;
}
在上述代码中,new int[5]
分配了 5
个 int
类型的内存空间,并自动初始化数组元素(对于基本类型,默认初始化)。
对于对象数组,new[]
会依次调用每个对象的构造函数。例如:
class MyObject {
public:
MyObject() {
std::cout << "MyObject constructor called" << std::endl;
}
~MyObject() {
std::cout << "MyObject destructor called" << std::endl;
}
};
int main() {
MyObject* arr = new MyObject[5];
// 使用 arr 进行操作
delete[] arr;
return 0;
}
在这段代码中,new MyObject[5]
分配了 5
个 MyObject
对象的内存空间,并依次调用每个 MyObject
对象的构造函数。当使用 delete[] arr
释放内存时,会依次调用每个 MyObject
对象的析构函数。
内存对齐的差异
malloc
的内存对齐
malloc
分配的内存通常按照系统默认的对齐方式进行对齐。在大多数系统中,malloc
分配的内存至少会按照 sizeof(void*)
的倍数进行对齐。这意味着分配的内存地址能被 sizeof(void*)
整除。例如,在 64 位系统上,sizeof(void*)
通常为 8 字节,malloc
分配的内存地址会是 8 的倍数。
这种对齐方式对于大多数基本数据类型和简单结构体来说是足够的。例如:
#include <stdlib.h>
#include <stdio.h>
int main() {
int* ptr = (int*)malloc(sizeof(int));
printf("Allocated address: %p\n", (void*)ptr);
printf("Alignment: %zu\n", (size_t)ptr % sizeof(void*));
free(ptr);
return 0;
}
在上述代码中,通过打印分配内存地址对 sizeof(void*)
取模的结果,可以验证其对齐情况。
然而,对于一些特殊的结构体,可能需要更严格的对齐要求。例如,有些硬件设备要求特定数据类型必须以特定字节数对齐才能高效访问。在这种情况下,malloc
可能无法满足需求。
new
的内存对齐
new
运算符在分配内存时,会根据对象的类型进行适当的对齐。对于基本数据类型,它遵循系统默认的对齐规则,与 malloc
类似。但对于自定义类型,new
会根据类中成员的最大对齐要求来对齐内存。
例如,假设有一个结构体:
struct MyStruct {
char c;
double d;
};
在 64 位系统上,double
类型通常要求 8 字节对齐。当使用 new
分配 MyStruct
对象时,new
会确保分配的内存地址是 8 字节对齐的,以满足 double
成员的对齐要求。
#include <iostream>
struct MyStruct {
char c;
double d;
};
int main() {
MyStruct* ptr = new MyStruct;
std::cout << "Allocated address: " << ptr << std::endl;
std::cout << "Alignment: " << (size_t)ptr % sizeof(double) << std::endl;
delete ptr;
return 0;
}
在上述代码中,通过打印分配内存地址对 sizeof(double)
取模的结果,可以看到 new
分配的内存地址是 8 字节对齐的。
此外,C++11 引入了 alignas
关键字,可以用于指定自定义类型的对齐要求。例如:
struct alignas(16) MyAlignedStruct {
char c;
double d;
};
这里 MyAlignedStruct
结构体要求 16 字节对齐。当使用 new
分配 MyAlignedStruct
对象时,new
会确保满足 16 字节对齐的要求。
性能方面的差异
malloc
的性能特点
malloc
是 C 语言标准库函数,其实现相对简单直接。在一些简单的内存分配场景中,malloc
的性能表现良好。由于 malloc
只负责分配内存,不涉及对象的构造和析构,对于基本数据类型的大量内存分配,malloc
的开销相对较小。
然而,malloc
在处理复杂的内存管理需求时,性能可能会受到影响。例如,在频繁分配和释放小块内存的场景下,malloc
可能会导致内存碎片问题。内存碎片是指由于多次分配和释放内存,使得堆中出现大量不连续的小块空闲内存,这些小块内存无法满足较大的内存分配请求,从而降低了内存利用率和分配效率。
为了减少内存碎片问题,一些 malloc
的实现采用了诸如伙伴系统(Buddy System)、堆合并等技术。但即使如此,在极端情况下,内存碎片仍然可能成为性能瓶颈。
new
的性能特点
new
运算符由于涉及对象的构造和析构,其性能开销相对 malloc
要大一些。每次使用 new
分配对象内存时,除了分配内存本身的开销,还需要调用对象的构造函数,这在对象构造函数复杂时会带来显著的性能开销。同样,在使用 delete
释放对象时,需要调用析构函数。
对于数组分配,new[]
不仅要分配内存,还要依次调用每个元素的构造函数,这也会增加性能开销。不过,new
在处理对象的内存管理方面具有更好的语义和安全性,能够确保对象的正确构造和析构,减少内存泄漏和悬空指针等问题。
在一些性能敏感的场景中,如果只需要分配基本数据类型的内存,并且对对象构造和析构没有需求,可以考虑使用 malloc
来提高性能。但在大多数 C++ 程序中,由于对象的存在,new
是更常用的内存分配方式,尽管其性能开销相对较高,但带来的代码安全性和可维护性提升往往更为重要。
例如,在一个简单的基准测试中,对比 malloc
和 new
分配大量 int
数组的性能:
#include <iostream>
#include <stdlib.h>
#include <chrono>
const int num_elements = 1000000;
void testMalloc() {
auto start = std::chrono::high_resolution_clock::now();
int* arr = (int*)malloc(num_elements * sizeof(int));
if (arr != NULL) {
for (int i = 0; i < num_elements; ++i) {
arr[i] = i;
}
free(arr);
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "malloc time: " << duration << " ms" << std::endl;
}
void testNew() {
auto start = std::chrono::high_resolution_clock::now();
int* arr = new int[num_elements];
for (int i = 0; i < num_elements; ++i) {
arr[i] = i;
}
delete[] arr;
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "new time: " << duration << " ms" << std::endl;
}
int main() {
testMalloc();
testNew();
return 0;
}
在上述代码中,testMalloc
函数使用 malloc
分配和初始化 int
数组,testNew
函数使用 new[]
进行相同操作。通过测量时间,可以看到在这种简单的基本数据类型数组分配场景下,malloc
的性能略优于 new[]
。但如果数组元素是复杂对象,new[]
的构造函数调用开销会使性能差异更加明显。
代码可维护性和可读性的差异
malloc
对代码可维护性和可读性的影响
使用 malloc
分配内存时,由于其返回 void*
指针,需要手动进行类型转换,这在一定程度上降低了代码的可读性。例如:
double* dptr = (double*)malloc(10 * sizeof(double));
从这段代码中,读者需要仔细查看类型转换部分才能确定所分配内存的实际类型。
在处理对象类型时,malloc
不调用构造函数,需要手动管理对象的初始化,这增加了代码的复杂性和出错的可能性。例如,对于前面的 MyObject
类,使用 malloc
分配对象数组时,不仅要手动调用构造函数,还要手动调用析构函数,代码变得冗长且难以理解:
MyObject* arr = (MyObject*)malloc(5 * sizeof(MyObject));
if (arr != NULL) {
for (int i = 0; i < 5; ++i) {
new (&arr[i]) MyObject;
}
// 使用 arr 进行操作
for (int i = 0; i < 5; ++i) {
arr[i].~MyObject();
}
free(arr);
}
这种手动管理对象生命周期的方式在代码维护时容易出现遗漏,导致内存泄漏或对象状态不一致等问题。
new
对代码可维护性和可读性的影响
new
运算符在代码可维护性和可读性方面具有明显优势。对于对象类型,new
会自动调用构造函数,delete
会自动调用析构函数,代码逻辑更加清晰。例如:
MyObject* obj = new MyObject;
// 使用 obj 进行操作
delete obj;
从这段代码中,很容易看出 MyObject
对象的创建和销毁过程,代码简洁明了。
对于数组分配,new[]
同样会自动调用每个元素的构造函数,delete[]
会自动调用析构函数,进一步简化了代码。例如:
MyObject* arr = new MyObject[5];
// 使用 arr 进行操作
delete[] arr;
这种方式使得代码更符合面向对象编程的思想,易于理解和维护。同时,new
的异常处理机制(默认抛出 std::bad_alloc
异常)也使得错误处理代码更加清晰,与 C++ 的异常处理机制相融合,提高了代码的健壮性。
综上所述,在 C++ 编程中,new
运算符在代码可维护性和可读性方面通常优于 malloc
,尤其在处理对象类型和复杂数据结构时表现得更为突出。但在一些特定场景下,如对性能要求极高且只涉及基本数据类型的内存分配,malloc
可能仍然是一个不错的选择。开发者需要根据具体的需求和场景,权衡两者的优缺点,选择合适的内存分配方式。