1. C/C++内存分布
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);
}
选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____
staticGlobalVar在哪里?____
staticVar在哪里?____
localVar在哪里?____
num1 在哪里?____
char2在哪里?____
*char2在哪里?___
pChar3在哪里?____
*pChar3在哪里?____
ptr1在哪里?____
*ptr1在哪里?____
-
globalVar:这是一个全局变量,且没有使用
static
修饰。全局变量存储在数据段(静态区)。
答案:C -
staticGlobalVar:这是一个静态全局变量,存储在数据段(静态区),因为静态全局变量不属于某个函数的局部作用域,且在程序执行期间会一直存在。
答案:C -
staticVar:这是一个函数内部的静态局部变量,存储在数据段(静态区),因为静态局部变量的生命周期是整个程序运行期间,但作用域仅限于所在的函数。
答案:C -
localVar:这是一个普通的局部变量,存储在栈上,局部变量的生命周期只在函数调用期间有效。
答案:A -
num1:这是一个局部的数组,存储在栈上。数组在栈上分配空间,其生命周期与函数调用的作用域一致。
答案:A -
char2:这是一个字符数组,存储在栈上。虽然它初始化了字符串,但它本身是一个局部数组。
答案:A -
*char2:
是一个字符数组char2
中的元素,它表示指向char2
数组的第一个字符的指针。char2
在栈上,而*char2
访问的是这个数组的第一个字符,因此它的存储位置是栈。答案:A(栈)
-
pChar3:这是一个指针,它本身存储在栈上,指向字符串常量
"abcd"
,而字符串常量存储在代码段(常量区)。
答案:A(指针pChar3
存储在栈上)
答案:D("abcd"
存储在代码段) -
ptr1 和 ptr2、ptr3:这三个指针都通过
malloc
、calloc
和realloc
在堆上分配内存,因此它们指向的内存区域是在堆上。
答案:B(ptr1
、ptr2
、ptr3
的内存分配是在堆上)
答案:B(*ptr1
和*ptr2
指向的内容在堆上)
由此引入
内存段及其用途
在程序执行时,操作系统会为程序分配不同的内存区域,这些区域被称为内存段。不同的内存段有不同的作用和特性。以下是常见的内存段及其功能:
1. 栈(Stack)
-
定义:栈用于存储函数的局部变量、函数参数和返回值等。它的内存分配是由系统自动管理的。当函数被调用时,相应的局部变量会被推入栈中;当函数返回时,栈会释放这些变量。
-
特点:
- 自动管理:栈内存由编译器自动分配和释放,无需程序员干预。
- 内存分配与释放速度快:栈的内存分配和释放是按照“先进后出”的顺序进行的,这使得栈操作非常高效。
- 栈的增长方向:栈是向下增长的,即每次分配新的内存时,会从更低的地址开始。
2. 内存映射段(Memory Mapped Segment)
-
定义:内存映射段是用来存放共享的动态库或者文件映射内存的。它用于高效的 I/O 映射,允许多个进程共享同一块内存区域,从而进行进程间通信。
-
特点:
- 共享内存:多个进程可以通过映射相同的内存区域,达到共享数据的目的。
- 进程间通信:在没有显式传输数据的情况下,不同的进程可以直接通过内存共享区域进行数据交换。
3. 堆(Heap)
-
定义:堆用于程序运行时动态内存的分配。堆的内存分配是由程序员手动控制的,需要使用
new
或malloc
等操作来分配内存,而释放内存则使用delete
或free
。 -
特点:
- 手动管理:堆内存需要程序员明确申请和释放,如果不释放内存则可能导致内存泄漏。
- 堆的增长方向:堆是向上增长的,即每次分配新的内存时,会从更高的地址开始。
4. 数据段(Data Segment)
-
定义:数据段用于存储全局变量和静态变量。它包括已初始化的数据和未初始化的数据。
-
特点:
- 已初始化数据:存储程序中定义并初始化的全局变量和静态变量。
- 未初始化数据:通常存储
static
或global
类型的变量,它们的初始值是零。
5. 代码段(Code Segment)
-
定义:代码段存储程序的机器代码,即程序的可执行指令和常量数据。代码段是只读的,避免程序修改自己的代码。常量(如字符串常量)也通常存储在代码段中。
-
特点:
- 只读:为了避免程序修改自己的代码,代码段通常是只读的。
- 存储执行指令:包含程序中所有的执行指令。
2.C语言中动态内存管理方式:malloc/calloc/realloc/free
在C语言中,动态内存管理用于在程序运行时分配和释放内存。主要有四个函数:
-
malloc (memory allocation):分配指定大小的内存块,返回指向该内存块的指针,未初始化,可能包含垃圾值。
void* malloc(size_t size);
- calloc (contiguous allocation):分配指定数量的内存块,并将其初始化为零。它是
malloc
的扩展,通常用于数组分配。
void* calloc(size_t num, size_t size);
- realloc (reallocation):重新调整之前分配的内存块大小。如果扩展内存,则可能会移动原来的内存块到新地址,并返回新地址。
void* realloc(void* ptr, size_t size);
- free (free memory):释放之前通过
malloc
、calloc
或realloc
分配的内存,避免内存泄漏。
void free(void* ptr);
简而言之:
- malloc 和 calloc 用于分配内存,calloc 会初始化为零。
- realloc 用于改变已分配内存的大小。
- free 用于释放内存。
3. C++内存管理方式
3.1 new/delete操作内置类型
void Test(){// 动态申请一个int类型的空间int* ptr4 = new int;// 动态申请一个int类型的空间并初始化为10int* ptr5 = new int(10);// 动态申请10个int类型的空间int* ptr6 = new int[3];delete ptr4;delete ptr5;delete[] ptr6;
}
3.2 new和delete操作自定义类型
#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()
{// new/delete 和 malloc/free 最大区别是 new/delete 对于【自定义类型】除了开空间// 还会调用构造函数和析构函数// 使用 malloc 分配内存并强制转换为 A* 类型A* p1 = (A*)malloc(sizeof(A));// 使用 new 调用 A 的构造函数A* p2 = new A(1);// 释放使用 malloc 分配的内存,不会调用析构函数free(p1);// 释放使用 new 分配的内存,会调用析构函数delete p2;// 内置类型的 malloc 和 new 几乎是一样的int* p3 = (int*)malloc(sizeof(int)); // 使用 malloc 分配内存int* p4 = new int; // 使用 new 分配内存// 释放内存,free 和 delete 区别同样存在free(p3);delete p4;// 使用 malloc 分配多个 A 类型对象的内存A* p5 = (A*)malloc(sizeof(A) * 10);// 使用 new 分配多个 A 类型对象的内存A* p6 = new A[10];// 释放内存free(p5);delete[] p6;return 0;
}
4. operator new与operator delete函数
4.1 operator new与operator delete函数
/*
operator new:该函数实际通过 malloc 来申请空间,当 malloc 申请空间成功时直接返回;
申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,
否则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid *p;// 循环申请空间,直到成功或无法分配while ((p = malloc(size)) == 0){// 如果 malloc 失败,再尝试用户设置的应对措施if (_callnewh(size) == 0){// 如果申请内存失败了,抛出 bad_alloc 异常static const std::bad_alloc nomem;_RAISE(nomem);}}return p;
}/*
operator delete: 该函数最终是通过 free 来释放空间的
*/
void operator delete(void *pUserData)
{_CrtMemBlockHeader *pHead;// 调用用户定义的回调函数RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));// 如果传入指针为 NULL,直接返回if (pUserData == NULL)return;_mlock(_HEAP_LOCK); /* 阻塞其他线程 */__TRY/* 获取内存块头指针 */pHead = pHdr(pUserData);/* 验证块类型 */_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));/* 释放内存块 */_free_dbg(pUserData, pHead->nBlockUse);__FINALLY_munlock(_HEAP_LOCK); /* 释放其他线程 */__END_TRY_FINALLYreturn;
}/*
free 的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
operator new
:这个版本的operator new
使用malloc
来申请内存。如果malloc
申请失败,它会尝试调用用户自定义的内存分配处理函数(_callnewh
)。如果该处理函数也无法解决问题,则抛出std::bad_alloc
异常。
operator delete
:该函数通过_free_dbg
来释放内存,并且在释放之前,会获取内存块的头指针,验证内存块的类型,确保其有效性。_mlock
和_munlock
用于保证在多线程环境下操作的原子性,避免多个线程同时释放内存。
free
宏:重定义了free
为_free_dbg
,用于调试模式下释放内存,提供更多的调试信息。
5. new和delete的实现原理
5.1 内置类型
如果申请的是内置类型的空间,new
和 malloc
,delete
和 free
基本类似,不同的地方是:
new/delete
申请和释放的是单个元素的空间,new[]
和delete[]
申请的是连续空间。而new
在申请空间失败时会抛出异常(std::bad_alloc
),malloc
只会返回NULL
。
例如
int* p1 = (int*)malloc(sizeof(int)); // C
int* p2 = new int; // C++
free(p1); // C
delete p2; // C++
5.2 自定义类型
new的原理
- 申请空间:调用
operator new
来申请内存,实际过程是通过malloc
或平台特定的内存分配机制分配空间。 - 构造对象:在已分配的内存空间上调用构造函数,完成对象的初始化。只有这样,对象的成员变量才会被正确初始化。
void* operator new(size_t size) {void* p = malloc(size);if (!p) throw std::bad_alloc();return p; }
delete的原理
- 析构对象:在对象被销毁时,首先会调用析构函数,清理对象的资源(例如释放内存、关闭文件等)。
- 释放内存:调用
operator delete
来释放内存空间。这里的operator delete
通常通过free
来释放内存。void operator delete(void* p) {if (p) {free(p); // 释放内存} }
new[]的原理
new[]
用来分配数组类型的内存:
- 申请空间:调用
operator new[]
来分配一个连续的内存块。实际上,它会多分配一些额外空间,用来存储数组的大小信息(此过程由编译器实现)。 - 构造对象:在分配的空间上,调用每个元素的构造函数。这样,数组中的每个对象都被正确地初始化。
void* operator new[](size_t size) {void* p = malloc(size);if (!p) throw std::bad_alloc();return p; }
delete[]的原理
delete[]
用来释放数组类型的内存:
- 析构对象:在释放内存之前,会先调用数组中每个对象的析构函数。注意,析构函数会被调用
N
次,其中N
是数组的大小。 - 释放内存:调用
operator delete[]
来释放内存。这实际上会调用operator delete
来释放内存。void operator delete[](void* p) {if (p) {free(p); // 释放内存} }
5.3 内存管理与异常处理
-
异常处理: 如果在调用
new
进行内存分配时发生了异常(如std::bad_alloc
),内存分配失败将导致程序终止。如果使用new[]
,每个数组元素的构造函数也有可能抛出异常,因此需要合理地处理异常,确保资源不会泄漏。 -
内存泄漏: 如果
new
分配的内存没有正确的释放,程序会发生内存泄漏。为了避免这种情况,可以使用智能指针(如std::unique_ptr
或std::shared_ptr
)来自动管理资源。 -
内存对齐:
new
和delete
运算符通常会处理内存对齐问题(如 8 字节对齐)。而malloc
和free
在一些平台上可能需要特别的对齐处理。
6. 定位new表达式(placement-new)
new
表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。其格式如下: new (place_address) type;
或者:
new (place_address) type(initializer-list);
place_address
必须是一个指针,指向已分配的内存区域。initializer-list
是类型的初始化列表,用于给构造函数传递参数。
使用场景:
定位 new
表达式在实际中一般配合 内存池 使用。内存池分配出的内存没有初始化,如果要在其上创建自定义类型的对象,必须显式调用构造函数,因此可以使用 placement-new
来初始化对象。
#include <iostream>
using namespace std;class A {
public:A(int a) : _a(a) {cout << "A constructed with " << _a << endl;}~A() {cout << "A destructed" << endl;}
private:int _a;
};int main() {// 手动分配内存空间void* rawMemory = operator new(sizeof(A));// 使用 placement-new 在已分配的内存上构造对象A* p = new (rawMemory) A(10);// 手动调用析构函数并释放内存p->~A();operator delete(rawMemory);return 0;
}
- 定位
new
与常规new
的区别: 定位new
只是将对象构造在已经分配的内存上,不负责分配内存空间;而常规的new
会先分配内存然后再调用构造函数。- 内存池配合使用: 在内存池的管理中,通常我们会预先分配一定的内存块,这些内存块可能并不包含对象的构造。此时需要使用定位
new
来显式地构造对象,而不是通过new
来直接申请内存。- 手动管理内存: 使用定位
new
时,内存管理更加精细。对象的析构必须通过显式调用析构函数p->~A()
,然后再手动调用operator delete
来释放内存空间。
注意事项:
- 内存空间已经分配: 使用定位
new
时,目标内存空间必须已经分配好,并且足够容纳对象的内存,否则会导致未定义行为。 - 没有自动调用析构函数: 定位
new
不会自动调用析构函数,因此在销毁对象之前需要手动调用析构函数。 - 适用场景: 定位
new
主要用于特定场景,如自定义内存池、优化内存管理、构造大型对象时需要复用内存块等。
#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;
};// 定位new/replacement new
int main()
{// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行A* p1 = (A*)malloc(sizeof(A)); // malloc分配内存new(p1) A; // 在p1指向的内存上调用构造函数A()p1->~A(); // 手动调用析构函数free(p1); // 释放内存A* p2 = (A*)operator new(sizeof(A)); // 使用operator new分配内存new(p2) A(10); // 在p2指向的内存上调用带参数的构造函数A(10)p2->~A(); // 手动调用析构函数operator delete(p2); // 释放内存return 0;
}
malloc(sizeof(A))
和operator new(sizeof(A))
:
- 这两者都分配了足够的内存空间,但并不会调用
A
类的构造函数。
new(p1) A
和new(p2) A(10)
:
- 这些语句在
p1
和p2
指向的内存空间中调用了构造函数。- 在
p1
上调用的是默认构造函数,而在p2
上调用的是带参数的构造函数A(10)
。
p1->~A()
和p2->~A()
:
- 这些语句手动调用析构函数,释放
p1
和p2
上对象的资源。
free(p1)
和operator delete(p2)
:
free
用来释放malloc
分配的内存,operator delete
用来释放operator new
分配的内存。
其中 0x12345678
和 0x23456789
会是实际的内存地址。
7. malloc/free
和 new/delete
的区别
共同点:
- 都是从堆上申请内存,并且都需要手动释放内存。
区别:
-
malloc
和free
是函数,new
和delete
是操作符malloc
和free
是库函数,需要包含<cstdlib>
头文件。new
和delete
是 C++ 中的操作符,不需要头文件,且具有更强的类型安全性。
-
malloc
申请的空间不会初始化,new
会初始化malloc
分配的内存是未初始化的(内容是随机的)。new
会初始化内存,对于内置类型会将其默认初始化为零,对于类类型会调用构造函数。
-
malloc
申请空间时,需要手动计算空间大小并传递,new
只需在其后跟上空间的类型malloc
需要传递内存大小(sizeof
)。例如,malloc(sizeof(A))
申请一个A
类型对象的空间。new
自动计算所需内存大小。例如,new A()
会分配一个A
类型对象所需的空间。
-
malloc
的返回值为void*
,在使用时必须强制类型转换,new
返回指定类型的指针malloc
返回void*
,必须显式地强制转换为目标类型指针。new
返回的是具体类型的指针,无需强制类型转换,确保类型安全。
-
malloc
申请空间失败时返回NULL
,new
会抛出异常malloc
如果无法分配内存,会返回NULL
,因此需要手动检查返回值。new
如果无法分配内存,会抛出std::bad_alloc
异常,除非在new
后加上nothrow
,否则默认会抛异常。
-
malloc/free
只会分配和释放内存,不会调用构造和析构函数,new/delete
会调用构造和析构函数malloc
和free
只进行内存的分配和释放,不会处理对象的构造和析构。new
会调用类的构造函数初始化对象,delete
会调用类的析构函数,释放对象时清理资源。
-
malloc
和free
是 C 语言中的内存管理函数,new
和delete
是 C++ 提供的内存管理操作符malloc
和free
是为 C 语言设计的,并且支持 C 风格的内存管理。new
和delete
是 C++ 中的一部分,能够更好地与面向对象编程结合,支持对象的构造和析构。
-
内存分配的灵活性
malloc
和free
只负责内存的分配和释放,不会涉及到对象的生命周期管理,适用于简单的内存操作。new
和delete
可以与 C++ 中的对象模型紧密结合,不仅负责内存分配,还能确保在构造和析构期间正确管理资源,适用于复杂的对象生命周期。
-
多重分配
new[]
和delete[]
用于处理数组类型的内存分配和释放,能够调用每个对象的构造函数和析构函数,而malloc
和free
不能直接处理对象数组的构造和析构。
进一步补充:
malloc/free
无法处理 C++ 类中重载的new
和delete
操作符,而new/delete
支持自定义内存管理(重载new
和delete
)。在使用
malloc
和free
时,如果程序员没有注意类型匹配或内存大小,容易导致潜在的内存错误或未定义行为。而new/delete
操作符更能提供类型安全,且会自动处理内存的分配与清理过程。
malloc
和free
仅在 C 语言和一些老的 C++ 代码中使用较多,C++ 现代编程中推荐使用new
和delete
,因其与 C++ 面向对象特性更兼容。