我们先分析以下代码的内存分布情况:
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);
}
问题与答案:
解答:strlen 是标准库函数,用于计算字符串中字符的数量,直到遇到 '\0' 结束符为止。而 sizeof 是一个编译时运算符,并非函数,其作用是获取数据类型或变量在内存中所占的字节数。
void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?---不需要,可以p2=NULL;
free(p3 );
}
在C语言中,malloc、calloc 和 realloc 都是用于动态内存分配的标准库函数,它们之间存在一些关键差异:
虽然C语言的内存管理方法在C++中仍然可用,但在处理复杂类型时显得不够灵活且容易出错。为此,C++引入了更高级的内存控制机制——使用 new 和 delete 操作符进行动态内存管理。
void Test()
{
//动态申请一个int类型的空间
int* ptr4 = new int;
//动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
//动态申请10个int类型的空间
int* ptr6 = new int[10]{1,2,3};//申请多个空间,可以在后面初始化。
delete ptr4;
delete ptr5;
delete [] ptr6;
}
注意:对于单个元素的申请与释放,应使用 new 和 delete;而对于连续内存块(数组),则需使用 new[] 和 delete[]。务必成对使用,避免资源泄漏或行为未定义。
class A
{
public:
A(int a=0) : _a(a)
{
std::cout << "A():" <<this<< std::endl;
}
~A()
{
std::cout << "~A():" <<this<< std::endl;
}
private:
int _a;
};
int main()
{
//new/delete 和malloc/free最大区别就是 new/delete对于【自定义类型】除了开空间
//还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
//内置类型是几乎一样的
int* p3 = (int*)malloc(sizeof(int));
int* p4 = new int;
free(p3);
delete p4;
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}
重要区别:当使用 new 创建自定义类型的对象时,不仅会分配内存,还会自动调用构造函数;而 delete 则会在释放内存前调用析构函数。相比之下,malloc 和 free 仅进行原始内存分配与释放,不会触发任何构造或析构过程。
new 和 delete 是 C++ 提供的操作符,用于用户层面的动态内存申请与释放。
而 operator new 和 operator delete 是系统提供的全局函数,new 操作符在底层实际是通过调用 operator new 来完成内存分配的,同理,delete 也是通过调用 operator delete 来释放内存。
/*
* operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回,
* 申请失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出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));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
* free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
从实现角度看,operator new 内部通常依赖于 malloc 进行实际的内存请求。如果分配成功则直接返回指针;若失败,则尝试执行用户设置的错误处理例程,若无响应则抛出异常(如 std::bad_alloc)。
对于基本数据类型(如 int、double 等),new 主要负责调用 operator new 分配足够的内存空间,然后返回对应类型的指针;delete 则调用 operator delete 释放该内存。
执行步骤如下:
operator new 分配所需大小的内存空间;执行流程为:
operator delete 释放内存。用于创建对象数组:
operator new[](可能内部仍调用 operator new)分配足够容纳 N 个 T 类型对象的空间;释放对象数组的过程包括:
operator delete[] 释放整块内存空间。定位 new 允许程序员在已分配的内存空间上显式构造对象,语法格式为:new (pointer) Type(args)。
它不分配新内存,仅调用构造函数。常用于内存池、嵌入式系统或需要精细控制对象生命周期的场景。
内存泄漏指的是程序动态分配了内存但未能正确释放,导致这部分内存无法再次被使用,长时间运行可能耗尽可用堆空间。
常见原因包括:
预防措施:使用智能指针(如 unique_ptr、shared_ptr)、RAII 技术、确保配对使用分配与释放操作。
operator delete 最终是通过调用 free 函数来完成内存空间的释放操作。
当申请的是内置类型(如 int、char 等)的内存时,new 与 malloc、delete 与 free 的行为基本相似。主要区别在于:
定位 new 表达式的作用是在已经分配好的原始内存中,显式地调用构造函数来初始化一个对象。
使用语法格式:
new (place_address) type
或
new (place_address) type(initializer-list)
其中,place_address 必须是一个有效的指针,指向已分配但未初始化的内存区域;initializer-list 是可选的初始化参数列表。
典型应用场景:
该特性通常与内存池技术结合使用。由于内存池分配出的内存块并未自动初始化,因此对于自定义类型的对象,必须使用 placement-new 显式调用构造函数,以保证对象状态的正确性。
class A
{
public:
A(int a=0):_a(a)
{
std::cout << "A()" << this << std::endl;
}
~A()
{
std::cout << "~A()" << this << std::endl;
}
private:
int _a;
};
//定位new/replacement new
int main()
{
//p1现在指向的只不过是与A对象相同大小的一段空间,
//还不能算是一个对象,因为构造函数没有执行。
A* p1 = (A*)malloc(sizeof(A));
new (p1)A;//注意:如果A类的构造函数有参数时,此处还需要传参。
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new (p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
两者共同点:均从堆区动态申请内存,且都需要开发者手动释放,否则会造成内存泄漏。
不同之处如下:
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
定义:
内存泄漏是指程序因设计缺陷或逻辑错误,未能释放不再使用的动态分配内存,导致这部分内存无法被后续使用。
需要注意的是,内存并未物理消失,而是程序失去了对该内存区域的引用或控制权,从而造成资源浪费。
危害:
长时间运行的应用程序一旦发生内存泄漏,后果严重。例如操作系统、后台服务等关键系统组件,随着泄漏积累,可用内存逐渐减少,系统响应速度下降,最终可能导致程序崩溃或系统卡死。
在 C/C++ 开发中,主要关注以下两类内存泄漏:
在 Visual Studio 环境下,可以利用 Windows 提供的 _CrtDumpMemoryLeaks() 函数进行初步检测。该函数能够报告当前是否存在内存泄漏以及大致泄漏的字节数,但缺乏精确的位置信息。
int main()
{
int* p = new int[10];
// 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
_CrtDumpMemoryLeaks();
return 0;
}
////////////////////////////////////////////////////////
// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
Detected memory leaks!
Dumping objects ->
{79} normal block at 0x00EC5FB8, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
尽管编码时应格外注意内存管理,但在复杂项目中仍难以完全避免疏漏。简单场景可通过上述方法快速排查,而对于大型工程或存在多处泄漏的情况,通常需要借助专业的第三方内存检测工具(如 Valgrind、Visual Leak Detector 等)进行深入分析与定位。
为有效避免内存泄漏,建议采取以下措施:
内存泄漏是开发过程中常见的问题,解决方法主要可分为两类:事前预防和事后排查。
在项目初期,建立良好的设计与编码规范至关重要。例如,申请的内存应确保在使用后及时释放,形成资源管理的良好习惯。
然而,即便开发者严格遵循手动释放原则,在异常发生或控制流复杂的情况下,仍可能出现遗漏。因此,仅依赖人工管理并不足以完全避免问题,需结合更可靠的机制。
采用RAII(Resource Acquisition Is Initialization)思想是一种有效的预防手段。通过构造函数获取资源、析构函数自动释放资源,能够保证资源的正确回收。在此基础上,使用智能指针(如shared_ptr、unique_ptr)进行内存管理,可大幅提升程序的安全性与稳定性。
部分企业还会制定内部开发规范,要求使用自研的私有内存管理库。这类库通常集成了内存分配跟踪功能,并提供可启用的泄漏检测选项,有助于在测试阶段发现潜在问题。
当系统已出现内存异常时,可借助专门的内存泄漏检测工具进行分析。但需要注意的是,许多现有工具存在准确率不高、误报频繁的问题,且部分高性能工具价格昂贵,限制了其广泛应用。
综上所述,应对内存泄漏的核心策略包括两种类型:一是以智能指针为代表的“事前预防型”方案;二是以检测工具为主的“事后查错型”手段。结合两者可在不同阶段有效控制风险。
扫码加好友,拉您进群



收藏
