文章目录
1.C/C++内存分布
C/C++程序在内存中的四个主要区域分别是:
- 栈(Stack):栈是一种数据结构,用于存储局部变量和函数调用时的上下文信息。在 C++ 程序中,
每个线程都有一个独立的栈
,位于内存的高地址区域,向低地址方向增长。
栈的大小通常比较有限,因此不应该在其中存储过多的数据。当函数被调用时,它的参数和返回值、以及局部变量都会在栈里分配空间。而在函数执行完毕后,这些由函数申请的空间将自动从栈中释放掉,使得其他操作可以继续使用这段内存空间。
栈具有“先进后出”的特点,也就是说最后入栈的元素会最先被弹出。在函数调用期间,当前函数的返回地址、栈帧指针(以下简称 ebp)、堆栈内容等信息都被压入栈中。以便随着函数调用发生改变,在适当的时候进行恢复。
在编写程序时,需要注意以下几点:
栈的大小通常受到限制,尽量避免在栈中保存过多的数据
尽可能地让局部变量保持在作用域范围之内,避免跨函数使用
避免产生大量的递归调用,以免造成栈溢出
对于需要保存的数据量比较大的变量,可以使用动态内存分配方法,并且在不需要时及时释放这些资源
- 堆(Heap):堆是
动态分配内存
的一种方式,也就是可以在程序运行时按需向操作系统申请并释放任意大小的内存空间。在 C++ 程序中,堆位于内存低地址区域。
堆有着非常广泛的使用场景,例如动态数组、对象构造与析构等。但由于程序员必须手动管理堆上的内存,因此很容易出现内存泄漏和段错误等问题。
在进行堆内存的动态分配时,通常使用 new 关键字来请求内存空间,并将返回值赋给一个指针变量。
需要注意的是,如果忘记调用 delete 或 delete[],或者多次调用它们,则可能会导致内存泄漏或 undefined behavior,即未定义行为。此外,还要避免在栈上保存指向堆内存的指针,以免在函数退出后内存空间已被释放而指针仍然存在,导致程序产生错误。
最后还要注意,由于操作系统的保护机制限制了每个进程可以申请的总内存大小和单块内存大小,在分配堆内存时需要谨慎考虑,避免对系统造成不良影响。 - 数据段(Data):C++程序内存中的数据段(Data Segment)又称为
静态数据区
,主要用于存储已经初始化了的全局变量、静态变量和常量。它是程序在编译时就被分配好并固定下来的一块内存区域。
数据段通常包括以下三个部分:
1:BSS段:BSS (Block Started by Symbol) 主要用于存放未初始化或初值为0的全局变量和静态变量。因为这些变量没有意义的初始值,所以操作系统会将它们全部初始化为0。BSS段通常位于数据段的末尾,并且在可执行文件中占用空间较小。
2:Initialized Data段(也叫做data段): 这个段用于存储特别声明了初始值的全局变量和静态变量。例如int i=5; 就在这里面具体的地址,而不是作为符号存在于符号表中。Initialized Data段紧接着BSS段后面,在可执行文件中占用较大空间。
3: 只读数据(Data readonly): 存储了常量字符串和const修饰的常量, 如 const int A = 100 ,也可以放指针常量等,它们在程序运行过程中是不能被修改的,如果试图去修改只读数据,则会导致Segmentation Fault异常。举例而言,当我们使用字面值字符串时,比如 std::cout << “Hello, world!” ,这个字符串的内容就位于只读数据段。
需要特别注意的是,数据段和代码段(Code Segment)通常是分开的,也就是说它们不在同一个内存区域中。而且数据段中的变量都是静态分配的,因此所占用的空间大小在编译期已经确定下来了,并且在整个程序运行过程中保持不变。
此外还有代码段(Text),其中存储着程序的可执行指令,就是常量区
。
具体的应用看下图说明:
2.C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
2.1 new/delete操作内置类型
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6;
}
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用
new[]和delete[]
,注意:匹配起来使用。
2.2 new和delete操作自定义类型
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与
free不会。例如下面的代码:
#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对于【自定义类型】除了开空间
//还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
// 内置类型是几乎是一样的
int* p3 = (int*)malloc(sizeof(int)); // C
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;
}
编译结果如下:
2.3 operator new与operator delete函数
在C++中,动态分配内存空间的运算符是new运算符,而释放内存空间的运算符是delete运算符。这两个运算符都是C++中非常重要的运算符,但是它们背后的实现却非常复杂。事实上,new运算符和delete运算符只是封装了C++中的operator new函数和operator delete函数而已。
operator new函数是在C++标准库的头文件中被定义的函数,其主要作用是在动态存储区中分配一块指定大小的原始内存空间,并返回指向该空间的指针。operator new函数的语法如下:
void* operator new(size_t size) throw(std::bad_alloc);
其中,参数size表示分配的内存大小,单位是字节;而std::bad_alloc是C++标准异常,当分配内存失败时,operator new函数会抛出该异常。operator new函数的返回值是一个void指针,即指向分配的内存空间。
与operator new函数相对应的是operator delete函数,其主要作用是释放 operator new函数分配的内存空间,其语法如下:
void operator delete(void* ptr) throw();
其中,参数ptr表示需要释放的内存空间指针。需要注意的是,operator delete函数只是完成释放操作,并不是删除该指针对象,因此在operator delete函数释放完内存空间后,该指针仍然是有效的。当然,在实际的程序中,最好还是要将该指针赋值为nullptr,以避免出现悬空指针的情况。
在C++的动态存储区中,operator new函数和operator delete函数是一对重要的函数,它们的实现是基于内存分配和回收的原理。C++中的operator new和operator delete函数是非常有用的函数,可以帮助我们更好地管理动态内存空间,确保程序的稳定性和性能。
另外:operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
2.4 重载operator new与operator delete(了解)
除了标准库中的operator new和operator delete函数,C++还允许在类中重载这两个函数以实现自定义的内存管理。这种重载通常用于实现一些特定的内存管理方式,例如对象池等。
类中重载operator new函数,必须在函数的声明前加上关键字static,同时该函数的返回值类型必须是void*,参数列表必须采用标准形式,即第一个参数为分配的内存大小,第二个参数为调用的new运算符的标志位。一般来说,我们可以将第二个参数忽略,因为这个参数只是为了满足标准需要而设置的。例如:
class MyClass
{
public:
static void* operator new(size_t size);
};
相对应的,类中重载operator delete函数,同样也必须在函数的声明前加上关键字static,其参数列表必须接受一个指向需要释放的内存的指针。例如:
class MyClass
{
public:
static void operator delete(void* ptr);
};
需要特别注意的是,当我们在类中重载operator new函数时,有可能因为内存分配失败而抛出std::bad_alloc异常,因此我们需要在如果在operator new函数中抛出了std::bad_alloc异常,则需要在operator delete函数中进行对应的处理。
当我们在类中重载operator new和operator delete函数时,需要根据实际的程序需要和内存分配的方式来进行实现。一般来说,我们可以通过在operator new函数中将多个对象一次性分配出来,再在operator delete函数中将这些对象一次性释放掉,从而提高程序的效率,并减少了系统开销。
总之,在C++中重载operator new和operator delete函数可以帮助我们实现自定义的内存管理方式,进而提高程序的效率和性能。但需要注意的是,对于这些函数的实现,需要满足一些基本的规则和标准,从而保证程序的正确性和稳定性。
2.5 定位new表达式(placement-new) (了解)
除了常规的new表达式,C++还提供了定位new表达式(也称为placement-new),它可以让我们在已知一块内存地址的情况下,将对象构造到这个地址上。这个内存地址可以是动态分配的,也可以是程序运行时已经分配的。在实际应用中,定位new表达式通常会用于实现自定义的内存池以及一些高性能计算场景。
定位new表达式的语法形式如下所示:
void* operator new(std::size_t size, void* ptr) noexcept;
其中,第一个形参为所需要的内存大小,第二个形参为指向特定内存的指针。两个形参可以有任意顺序。
使用定位new表达式时,需要注意以下几点:
- 指向内存的指针需要是合法的内存地址。
- 使用定位new表达式时,不会进行内存的自动分配。因此,需要手动分配内存,并将指针传递给定位new表达式来进行对象的构造。
- 构造的对象,需要在使用完后,手动调用其析构函数,并释放分配的内存。
下面是一个具体的例子。比如一个类A的定义如下:
class A
{
public:
A(int value)
: m_value(value)
{}
~A()
{
std::cout << "A object destroyed" << std::endl;
}
void PrintValue() const
{
std::cout << "Value: " << m_value << std::endl;
}
private:
int m_value;
};
现在,我们已经手动分配了一块内存并想要将对象构造到这段内存中:
void* pMemory = operator new(sizeof(A)); //手动分配一块内存
A* pA = new (pMemory) A(42); //调用定位new表达式
pA->PrintValue(); //Value: 42
pA->~A(); //手动调用析构函数
operator delete(pMemory); //手动释放内存
在上述代码中,我们首先手动分配了一块内存,然后使用定位new表达式将对象构造到这段内存中,接着我们可以使用对象的成员函数,最后手动释放内存。
定位new表达式虽然比较少用,但是它在内存池、存储器管理等方面有广泛的应用,并且对于提高程序的效率也有一定的作用。
3. 常见面试题
3.1 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在释放空间前会调用析构函数完成空间中资源的清理
3.2 内存泄漏
3.2.1 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。
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;
}
3.2.2 内存泄漏分类(了解)
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak):
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。 - 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
3.2.3 如何检测内存泄漏(了解)
在vs下,可以使用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.
因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜
防,简单的可以采用上述方式快速定位下。如果工程比较大,内存泄漏位置比较多,不太好查时
一般都是借助第三方内存泄漏检测工具处理的。
3.2.4如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:
这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智
能指针来管理才有保证。 - 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总之啊,
内存泄漏非常常见
,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄
漏检测工具。