文章目录
1. C/C++内存分布
以下是一些简单的C++代码示例:
int globalvar = 1;
static int staticglobalval = 1;
int main(){
static int staticvar = 1;
int localvar = 1;
int num[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);
return 0;
}
/*
1. 选择题:
选项 A:栈 B:堆 C:数据段(静态区) D:代码段(常量区)
globalvar在哪里?_C_ staticglobalvar在哪里?_C_
staticvar在哪里?_C_ localvar在哪里?_A_
num1在哪里?_A_
char2在哪里?_A_ *char2在哪里?_A_
pchar3在哪里?_A_ *pchar3在哪里?_D_
ptr1在哪里?_A_ *ptr1在哪里?_B_
2. 填空题:
sizeof(num1) = _40_;
sizeof(char2) = _5_; strlen(char2) = _4_;
sizeof(pchar3) = _4 or 8_; strlen(pchar3) = _4_;
sizeof(ptr1) = _4 or 8_;
*/
【注意】
- 代码区(Text Segment):
- 存放函数体的二进制代码,这部分内存的内容在程序运行前就已经确定下来,通常是只读的,以防止程序意外修改其指令。
- 全局数据区(Global Data Segment):
- 包括初始化的全局变量和静态变量(
static
)。程序结束时由操作系统回收。
- 堆区(Heap):
- 是由
malloc
、calloc
、realloc
和free
这样的 C 标准库函数管理的一块内存区域。程序运行时,如果需要额外的内存空间,可以从堆区动态地分配出来(通过malloc
等函数),用完后需要手动释放(通过free
等函数)。堆区的内存分配和释放是不确定的,由程序员控制,容易产生内存泄露等问题。
- 栈区(Stack):
- 用于存放函数的参数值、返回地址和局部变量等。这部分内存的分配和释放是自动进行的,每进入一个函数调用,栈就会自动分配一块空间用于这个函数的局部变量和返回地址等,当函数返回时,相应的栈空间自动被回收。栈区的大小通常是有限的,过度使用会导致栈溢出错误。
- 内存映射区(Memory Mapped Region):
- 例如共享库、动态链接库以及一些特殊的硬件接口对应的内存区域,如显存等。这部分内存通常用于特定的系统级操作或硬件交互。
2. C语言中动态内存管理方式
在C语言中,动态内存管理主要通过malloc
、calloc
、realloc
和free
这四个函数进行。以下是一个简化的代码示例,展示了如何在C语言中使用这些函数来动态分配、使用和释放内存:
#include <stdio.h>
#include <stdlib.h>
int main() {
// 动态分配一个整数的内存
int *pInt = (int *)malloc(sizeof(int));
if (pInt == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return 1;
}
// 使用分配的内存
*pInt = 42;
printf("Value of pInt: %d\n", *pInt);
// 动态分配一个包含10个整数的数组的内存
int *pArray = (int *)malloc(10 * sizeof(int));
if (pArray == NULL) {
fprintf(stderr, "Memory allocation failed\n");
free(pInt); // 在失败时释放之前分配的内存
return 1;
}
// 初始化数组
for (int i = 0; i < 10; i++) {
pArray[i] = i * 2;
}
// 打印数组
printf("Array values: ");
for (int i = 0; i < 10; i++) {
printf("%d ", pArray[i]);
}
printf("\n");
// 调整之前分配的整数内存大小(例如,扩大为原来大小的2倍)
int *pResizedInt = (int *)realloc(pInt, 2 * sizeof(int));
if (pResizedInt == NULL) {
fprintf(stderr, "Memory reallocation failed\n");
free(pInt); // 在失败时释放原始内存
free(pArray);
return 1;
}
// 使用重新分配的内存(注意,pInt可能已经被realloc移动到新位置,所以使用pResizedInt)
pResizedInt[0] = 42; // 保留原始值
pResizedInt[1] = 84; // 新分配的空间
// 打印重新分配后的整数值
printf("Resized int values: %d, %d\n", pResizedInt[0], pResizedInt[1]);
// 释放分配的内存
free(pResizedInt);
free(pArray);
return 0;
}
在这个示例中,我们首先使用
malloc
分配了一个整数的内存和一个包含10个整数的数组的内存。然后,我们初始化了这些内存区域,并打印了它们的值。接下来,我们使用realloc
调整了之前分配的整数内存的大小,并验证了调整后的内存内容。最后,我们使用free
释放了所有分配的内存。请注意,在实际应用中,您应该始终检查
malloc
、calloc
和realloc
的返回值,以确保内存分配成功。如果分配失败,这些函数会返回NULL
,并且程序应该适当地处理这种情况,通常是通过释放之前分配的内存(如果有的话)和退出程序或返回错误代码。
3. C++中动态内存管理
在C++中,动态内存管理是一个至关重要的特性,它允许程序在运行时根据需要分配和释放内存。这与静态内存分配(如全局变量或局部变量)形成鲜明对比,静态内存的大小在编译时就已经确定,并且在程序的整个生命周期内保持不变。
动态内存管理主要通过以下几个C++库函数实现:
-
new
和delete
:new
用于分配内存,并调用类的构造函数(如果是类类型对象)。delete
用于释放内存,并调用类的析构函数(如果是类类型对象)。
-
new[]
和delete[]
:new[]
用于分配数组类型的内存,并调用每个数组元素的构造函数(如果是类类型对象)。delete[]
用于释放数组类型的内存,并调用每个数组元素的析构函数(如果是类类型对象)。
-
malloc
,calloc
,realloc
和free
:(来自C标准库):
- 这些函数在C++中也可以使用,但它们不会调用类的构造函数和析构函数。因此,在处理类类型对象时,通常不推荐使用这些函数,除非确实需要与C代码进行交互。
下面是一个简单的C++代码示例,展示了如何使用new
和delete
进行动态内存管理:
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
void display() {
std::cout << "MyClass object displayed" << std::endl;
}
};
int main() {
// 动态分配一个MyClass对象
MyClass* pMyClass = new MyClass;
pMyClass->display();
// 释放动态分配的MyClass对象
delete pMyClass;
// 动态分配一个包含5个MyClass对象的数组
MyClass* pMyClassArray = new MyClass[5];
for (int i = 0; i < 5; ++i) {
pMyClassArray[i].display();
}
// 释放动态分配的MyClass对象数组
delete[] pMyClassArray;
return 0;
}
在这个示例中,我们首先使用
new
动态分配了一个MyClass
对象,并调用了它的display
方法。然后,我们使用delete
释放了这个对象,从而确保了它的析构函数被调用。接下来,我们使用new[]
动态分配了一个包含5个MyClass
对象的数组,并同样调用了每个对象的display
方法。最后,我们使用delete[]
释放了这个对象数组。请注意,在使用
new
和delete
时,必须确保指针类型与分配的对象类型匹配,并且不要对同一个指针进行多次delete
操作,这会导致未定义行为。同样地,对于使用new[]
分配的数组,必须使用delete[]
进行释放,而不是delete
。
4. operator new与operator delete函数
operator new
和 operator delete
是C++中用于动态内存管理的关键函数。它们与C语言中的malloc
和free
有相似之处,但专为C++对象设计,能够与构造函数和析构函数协同工作。下面是对这两个函数的详细讲解:
4.1 operator new
函数
operator new
函数用于分配内存。当使用new
运算符创建对象时,会调用这个函数。它有以下几个特点:
-
全局与类作用域:
- 全局作用域的
operator new
函数为所有类型提供默认的内存分配机制。 - 类作用域的
operator new
函数允许类自定义其对象的内存分配方式。
- 全局作用域的
-
重载:
operator new
可以被重载以提供不同的内存分配策略。- 重载时可以指定不同的参数列表,包括分配大小、对齐方式、内存池等。
-
分配策略:
- 默认情况下,
operator new
使用全局的堆内存进行分配。 - 可以定制以使用特定的内存池、栈内存、或其他内存源。
- 默认情况下,
-
异常处理:
- 如果内存分配失败,默认的
operator new
会抛出std::bad_alloc
异常。 - 可以定制以返回
nullptr
或其他错误处理机制。
- 如果内存分配失败,默认的
-
语法:
void* operator new(std::size_t size); // 全局作用域 void* MyClass::operator new(std::size_t size); // 类作用域
-
示例:
class MyClass { public: void* operator new(std::size_t size) { // 自定义内存分配逻辑 return ::operator new(size); // 调用全局operator new } };
4.2 operator delete
函数
operator delete
函数用于释放内存。当使用delete
运算符销毁对象时,会调用这个函数。它也有以下几个特点:
-
全局与类作用域:
- 与
operator new
相似,operator delete
也可以在全局或类作用域中定义。
- 与
-
重载:
operator delete
也可以被重载以提供不同的内存释放策略。- 通常与对应的
operator new
重载相匹配。
-
释放策略:
- 默认情况下,
operator delete
将内存释放回全局的堆内存。 - 可以定制以将内存返回特定的内存池、栈内存、或其他内存源。
- 默认情况下,
-
异常处理:
operator delete
通常不抛出异常,但在某些定制实现中可能会进行错误处理。
-
语法:
void operator delete(void* ptr) noexcept; // 全局作用域 void MyClass::operator delete(void* ptr) noexcept; // 类作用域
-
示例:
class MyClass { public: void operator delete(void* ptr) noexcept { // 自定义内存释放逻辑 ::operator delete(ptr); // 调用全局operator delete } };
下面是一个使用自定义operator new
和operator delete
的完整示例:
#include <iostream>
#include <cstdlib>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }
~MyClass() { std::cout << "MyClass destructor\n"; }
void* operator new(std::size_t size) {
std::cout << "Custom operator new\n";
return std::malloc(size);
}
void operator delete(void* ptr) noexcept {
std::cout << "Custom operator delete\n";
std::free(ptr);
}
};
int main() {
MyClass* obj = new MyClass();
delete obj;
return 0;
}
输出:
Custom operator new
MyClass constructor
MyClass destructor
Custom operator delete
在这个示例中,我们定义了
MyClass
的自定义operator new
和operator delete
函数。当我们使用new
运算符创建MyClass
对象时,会调用自定义的operator new
函数进行内存分配。同样,当我们使用delete
运算符销毁对象时,会调用自定义的operator delete
函数进行内存释放。
5. new
和delete
的实现原理
new
和 delete
是 C++ 中用于动态内存分配和释放的运算符。它们的实现原理涉及到底层的内存管理机制,以及 C++ 构造函数和析构函数的调用。下面是对 new
和 delete
实现原理的详细解释:
5.1 new
的实现原理
- 内存分配:
- 当使用
new
运算符创建对象时,首先会调用operator new
函数来分配足够的内存来存储对象。 operator new
通常是一个全局函数,但也可以被重载为类成员函数或全局的模板函数。- 默认情况下,
operator new
使用malloc
或类似的底层系统调用来分配内存。
- 当使用
- 构造函数调用:
- 一旦内存分配成功,
new
运算符会在分配的内存上调用对象的构造函数来初始化对象。 - 对于类类型的对象,这意味着会调用类的构造函数,并按照构造函数中定义的逻辑来设置对象的初始状态。
- 一旦内存分配成功,
- 返回指针:
- 最后,
new
运算符会返回一个指向新创建对象的指针,这个指针可以用于在程序中访问和操作对象。
- 最后,
5.2 delete
的实现原理
- 析构函数调用:
- 当使用
delete
运算符销毁对象时,首先会调用对象的析构函数来清理对象并释放其占用的资源。 - 对于类类型的对象,这意味着会调用类的析构函数,并执行析构函数中定义的清理逻辑。
- 当使用
- 内存释放:
- 一旦析构函数调用完成,
delete
运算符会调用operator delete
函数来释放之前分配的内存。 operator delete
通常是一个全局函数,但同样可以被重载为类成员函数或全局的模板函数。- 默认情况下,
operator delete
使用free
或类似的底层系统调用来释放内存。
- 一旦析构函数调用完成,
- 指针处理:
- 在释放内存之后,理想情况下应该将指向已删除对象的指针设置为
nullptr
,以避免悬挂指针(dangling pointer)和未定义行为。
- 在释放内存之后,理想情况下应该将指向已删除对象的指针设置为
【注意】:
- 异常处理:在内存分配失败时,
new
运算符会抛出std::bad_alloc
异常(除非使用了nothrow
版本的new
),而delete
运算符则通常不会抛出异常。 - 自定义内存管理:C++ 允许用户重载
operator new
和operator delete
来实现自定义的内存管理策略,这可以用于优化性能、跟踪内存使用或实现特定的内存分配模式。 - 对齐要求:
operator new
和operator delete
还需要考虑内存对齐的要求,以确保分配的内存满足对象的对齐需求。
综上所述,
new
和delete
的实现原理涉及到内存分配、构造函数和析构函数的调用,以及指针的管理。这些机制共同工作,使得 C++ 能够提供灵活且强大的动态内存管理能力。
6. 定位new
表达式(placement-new)(了解)
在C++中,“placement-new” 是一种特殊的 new
表达式,用于在已经分配好的内存区域上构造对象。与普通的 new
表达式不同,placement-new
不分配内存;它仅仅在指定的内存位置上调用对象的构造函数。这在需要精细控制内存布局或进行对象池管理等高级内存管理策略时特别有用。
placement-new
的语法如下:
void* placement_address = /* 内存地址 */;
T* obj = new (placement_address) T(constructor_arguments);
这里,placement_address
是一个指向已分配内存的指针,T
是要构造的对象的类型,constructor_arguments
是构造函数的参数(如果有的话)。
假设我们有一个简单的类 MyClass
:
class MyClass {
public:
MyClass(int x) : x_(x) {
// 构造函数体
}
~MyClass() {
// 析构函数体
}
private:
int x_;
};
我们可以使用 placement-new
在预先分配的内存上构造 MyClass
的实例:
#include <new> // 必须包括这个头文件以使用placement-new
#include <iostream>
int main() {
// 假设我们有一块足够大的已分配内存
char buffer[sizeof(MyClass)];
// 在 buffer 上使用 placement-new 构造 MyClass 对象
MyClass* my_object = new (buffer) MyClass(42);
// ... 使用 my_object ...
// 显式调用析构函数
my_object->~MyClass();
// 注意:不要对 buffer 使用 delete,因为我们没有使用普通的 new 来分配它
return 0;
}
【注意】:
- 显式析构:使用
placement-new
构造的对象,必须显式调用它们的析构函数。在上面的例子中,我们调用了my_object->~MyClass()
。 - 内存管理:由于
placement-new
不分配内存,因此也不负责释放内存。你必须自己管理用于placement-new
的内存区域。 - 对齐:确保用于
placement-new
的内存区域是正确对齐的。否则,可能会导致未定义行为。 - 异常安全:如果构造函数抛出异常,你需要确保能够妥善处理它,因为异常传播不会自动释放用于
placement-new
的内存。
placement-new
是C++中一种强大但低级的特性,主要用于需要高性能或特殊内存管理需求的场景。在大多数情况下,普通的new
和delete
表达式以及智能指针(如std::unique_ptr
和std::shared_ptr
)提供了更安全、更易于管理的内存管理方式。
7.常见面试题
malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
malloc和free是函数,new和delete是操作符。
malloc申请的空间不会初始化,new可以初始化。
malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
malloc的返回值为void*,在使用时必须强转,new不需要,因为new后跟的是空间的类型。
malloc申请空间失败时,这回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初给化,delete在释放空间前会调用析构函数完成空间中资源的清理。