楼主: huihui8714
664 0

[作业] C C++中的内存管理 [推广有奖]

  • 0关注
  • 0粉丝

等待验证会员

学前班

40%

还不是VIP/贵宾

-

威望
0
论坛币
0 个
通用积分
0.0159
学术水平
0 点
热心指数
0 点
信用等级
0 点
经验
20 点
帖子
1
精华
0
在线时间
0 小时
注册时间
2018-9-28
最后登录
2018-9-28

楼主
huihui8714 发表于 2025-12-1 13:41:02 |AI写论文

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

求职就业群
赵安豆老师微信:zhaoandou666

经管之家联合CDA

送您一个全额奖学金名额~ !

感谢您参与论坛问题回答

经管之家送您两个论坛币!

+2 论坛币

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 在哪里?C
  • staticGlobalVar 在哪里?C
  • staticVar 在哪里?C
  • localVar 在哪里?A
  • num1 在哪里?A
  • char2 在哪里?A
  • *char2 在哪里?A
  • pChar3 在哪里?A
  • *pChar3 在哪里?D
  • ptr1 在哪里?A
  • *ptr1 在哪里?B

填空题:

  • sizeof(num1) = 40
  • sizeof(char2) = 5
  • strlen(char2) = 4
  • sizeof(pChar3) = 4 or 8
  • strlen(pChar3) = 4
  • sizeof(ptr1) = 4 or 8

sizeof 和 strlen 的区别?

解答:strlen 是标准库函数,用于计算字符串中字符的数量,直到遇到 '\0' 结束符为止。而 sizeof 是一个编译时运算符,并非函数,其作用是获取数据类型或变量在内存中所占的字节数。

【内存区域说明】

  1. 栈(Stack):又称堆栈,主要用于存放非静态局部变量、函数参数、返回值等,内存由高地址向低地址方向增长。
  2. 内存映射段:一种高效的 I/O 映射机制,通常用于加载共享动态库。用户也可以通过系统调用创建共享内存实现进程间通信。
  3. 堆(Heap):程序运行期间用于动态分配内存的空间,通常从低地址向高地址扩展。
  4. 数据段(静态区):用于存储全局变量和静态变量。
  5. 代码段(常量区):保存可执行指令以及只读常量(如字符串字面量)。

2. C语言中的动态内存管理方式:malloc/calloc/realloc/free

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 都是用于动态内存分配的标准库函数,它们之间存在一些关键差异:

  • malloc:分配指定大小的未初始化连续内存块。
  • calloc:分配指定数量和单位大小的内存块,并将所有位初始化为0。
  • realloc:调整已分配内存块的大小,可以扩大或缩小原有空间。

realloc 扩容的两种常见情况:

  • 情况1:若原内存块后方有足够空间,则直接在原地址后追加内存,原数据保持不变。
  • 情况2:若无法就地扩展,则系统会在堆中寻找一块新的合适空间,复制原数据并释放旧内存,返回新地址。

3. C++ 内存管理方式

虽然C语言的内存管理方法在C++中仍然可用,但在处理复杂类型时显得不够灵活且容易出错。为此,C++引入了更高级的内存控制机制——使用 newdelete 操作符进行动态内存管理。

3.1 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;
}

注意:对于单个元素的申请与释放,应使用 newdelete;而对于连续内存块(数组),则需使用 new[]delete[]。务必成对使用,避免资源泄漏或行为未定义。

3.2 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 则会在释放内存前调用析构函数。相比之下,mallocfree 仅进行原始内存分配与释放,不会触发任何构造或析构过程。

4. operator new 与 operator delete 函数

4.1 operator new 与 operator delete 简介

newdelete 是 C++ 提供的操作符,用于用户层面的动态内存申请与释放。
operator newoperator 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)。

5. new 和 delete 的实现原理

5.1 内置类型

对于基本数据类型(如 int、double 等),new 主要负责调用 operator new 分配足够的内存空间,然后返回对应类型的指针;delete 则调用 operator delete 释放该内存。

5.2 自定义类型

1. new 的原理

执行步骤如下:

  1. 调用 operator new 分配所需大小的内存空间;
  2. 在分配好的内存上构造对象,即调用类的构造函数。

2. delete 的原理

执行流程为:

  1. 先调用对象的析构函数,清理资源;
  2. 再调用 operator delete 释放内存。

3. new T[N] 的原理

用于创建对象数组:

  1. 调用 operator new[](可能内部仍调用 operator new)分配足够容纳 N 个 T 类型对象的空间;
  2. 对每个元素依次调用默认构造函数进行初始化。

4. delete[] 的原理

释放对象数组的过程包括:

  1. 对数组中每一个对象调用析构函数;
  2. 调用 operator delete[] 释放整块内存空间。

6. 定位 new 表达式(placement-new)

定位 new 允许程序员在已分配的内存空间上显式构造对象,语法格式为:new (pointer) Type(args)
它不分配新内存,仅调用构造函数。常用于内存池、嵌入式系统或需要精细控制对象生命周期的场景。

7. 常见面试题

7.1 malloc/free 与 new/delete 的区别

  • 本质不同:malloc/free 是C语言的标准库函数;new/delete 是C++的操作符。
  • 初始化支持:malloc 只分配内存,不初始化;new 在分配后还会调用构造函数。
  • 类型安全:new 返回指定类型的指针,无需强制转换;malloc 返回 void*,需手动转换。
  • 内存失败处理:malloc 失败返回 NULL;new 失败默认抛出异常。
  • 操作对象:new/delete 能正确处理自定义类型的构造与析构;malloc/free 仅处理内存,不具备对象语义。

7.2 内存泄漏

内存泄漏指的是程序动态分配了内存但未能正确释放,导致这部分内存无法再次被使用,长时间运行可能耗尽可用堆空间。
常见原因包括:

  • 忘记调用 free 或 delete;
  • 异常发生时提前退出而未清理资源;
  • 指针丢失(指向动态内存的指针被覆盖或超出作用域)。

预防措施:使用智能指针(如 unique_ptr、shared_ptr)、RAII 技术、确保配对使用分配与释放操作。

operator delete 最终是通过调用 free 函数来完成内存空间的释放操作。

5. new 和 delete 的实现机制

5.1 对于内置类型

当申请的是内置类型(如 int、char 等)的内存时,new 与 malloc、delete 与 free 的行为基本相似。主要区别在于:

  • new 和 delete 用于申请和释放单个元素的内存空间;而 new[] 和 delete[] 则用于申请和释放连续的内存块。
  • 在申请失败的情况下,new 会抛出异常,而 malloc 则返回 NULL 指针。

5.2 针对自定义类型

1. new 的执行流程
  1. 首先调用 operator new 函数来分配所需的内存空间。
  2. 在成功分配的空间上,调用对象的构造函数以完成初始化过程。
2. delete 的执行流程
  1. 先调用对象的析构函数,清理对象内部所持有的资源。
  2. 然后调用 operator delete 函数,将内存空间归还给系统。
3. new T[N] 的工作原理
  1. 调用 operator new[] 函数,在其内部实际仍通过 operator new 来完成 N 个对象所需空间的分配。
  2. 随后在该连续内存区域上依次调用 N 次构造函数,完成每个对象的构造。
4. delete[] 的工作原理
  1. 在待释放的内存区域上,对每一个对象调用析构函数,共执行 N 次,确保所有资源被正确清理。
  2. 接着调用 operator delete[] 函数,该函数内部最终会调用 operator delete 来完成实际的内存释放。

6. 定位 new 表达式(placement-new)

定位 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;
}

7. 常见面试问题解析

7.1 malloc/free 与 new/delete 的区别

两者共同点:均从堆区动态申请内存,且都需要开发者手动释放,否则会造成内存泄漏。

不同之处如下:

  1. malloc 和 free 是标准库函数,而 new 和 delete 是 C++ 中的操作符。
  2. malloc 分配的空间不会进行初始化,而 new 可以自动调用构造函数完成初始化。
  3. 使用 malloc 时需手动计算字节数并传入,而 new 后只需指定类型,若为多个对象,可通过 [N] 指定数量。
  4. malloc 返回 void* 类型,使用时必须强制转换为目标类型的指针;new 则直接返回对应类型的指针,无需转换。
  5. malloc 在分配失败时返回 NULL,因此每次使用都应检查是否为空;new 则通过抛出异常处理失败情况,无需判空但需考虑异常捕获。
  6. 对于自定义类型对象,malloc/free 仅负责内存的分配与释放,不会调用构造函数或析构函数;而 new 在分配后会自动调用构造函数,delete 在释放前会先调用析构函数清理资源。
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;
}

7.2 内存泄漏相关问题

7.2.1 什么是内存泄漏及其危害

定义:
内存泄漏是指程序因设计缺陷或逻辑错误,未能释放不再使用的动态分配内存,导致这部分内存无法被后续使用。

需要注意的是,内存并未物理消失,而是程序失去了对该内存区域的引用或控制权,从而造成资源浪费。

危害:
长时间运行的应用程序一旦发生内存泄漏,后果严重。例如操作系统、后台服务等关键系统组件,随着泄漏积累,可用内存逐渐减少,系统响应速度下降,最终可能导致程序崩溃或系统卡死。

7.2.2 内存泄漏的分类(了解)

在 C/C++ 开发中,主要关注以下两类内存泄漏:

  1. 堆内存泄漏(Heap Leak):指通过 malloc、calloc、realloc 或 new 等方式从堆上分配的内存,在使用完毕后未通过对应的 free 或 delete 进行释放。这种遗漏会导致该段内存永久不可用,形成堆泄漏。
  2. 系统资源泄漏:程序使用了操作系统分配的资源(如文件描述符、套接字、管道、互斥锁等),但未通过相应接口(如 close、fclose、ReleaseSemaphore 等)释放。这类泄漏不仅消耗系统资源,还可能引发性能下降甚至系统不稳定。
7.2.3 如何检测内存泄漏(了解)

在 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 等)进行深入分析与定位。

7.2.4 如何预防内存泄漏

为有效避免内存泄漏,建议采取以下措施:

  • 养成良好的编程习惯,确保每次动态分配都有对应的释放操作。
  • 优先使用智能指针(如 unique_ptr、shared_ptr)和 RAII 技术,由对象生命周期自动管理资源。
  • 使用容器类(如 vector、string)替代原始数组,减少手动内存操作。
  • 在关键路径添加日志或断言,辅助调试内存使用情况。
  • 定期进行静态代码分析和动态内存检测,及时发现潜在问题。

内存泄漏是开发过程中常见的问题,解决方法主要可分为两类:事前预防和事后排查。

在项目初期,建立良好的设计与编码规范至关重要。例如,申请的内存应确保在使用后及时释放,形成资源管理的良好习惯。

然而,即便开发者严格遵循手动释放原则,在异常发生或控制流复杂的情况下,仍可能出现遗漏。因此,仅依赖人工管理并不足以完全避免问题,需结合更可靠的机制。

采用RAII(Resource Acquisition Is Initialization)思想是一种有效的预防手段。通过构造函数获取资源、析构函数自动释放资源,能够保证资源的正确回收。在此基础上,使用智能指针(如shared_ptr、unique_ptr)进行内存管理,可大幅提升程序的安全性与稳定性。

部分企业还会制定内部开发规范,要求使用自研的私有内存管理库。这类库通常集成了内存分配跟踪功能,并提供可启用的泄漏检测选项,有助于在测试阶段发现潜在问题。

当系统已出现内存异常时,可借助专门的内存泄漏检测工具进行分析。但需要注意的是,许多现有工具存在准确率不高、误报频繁的问题,且部分高性能工具价格昂贵,限制了其广泛应用。

综上所述,应对内存泄漏的核心策略包括两种类型:一是以智能指针为代表的“事前预防型”方案;二是以检测工具为主的“事后查错型”手段。结合两者可在不同阶段有效控制风险。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

关键词:delete static Global Public lobal

您需要登录后才可以回帖 登录 | 我要注册

本版微信群
扫码
拉您进交流群
GMT+8, 2026-2-12 16:15