C++ -内存管理

博客主页:【夜泉_ly
本文专栏:【C++
欢迎点赞👍收藏⭐关注❤️

在这里插入图片描述

C/C++ -内存管理的深入探讨

内存管理是编程语言中一个重要的主题,尤其在 C 和 C++ 中,开发者需要手动管理内存,以确保程序的高效运行。本文将对 C/C++ 的内存管理进行详细分析,涵盖数据存储分类、内存区域划分、动态内存管理等内容。

1. 数据存储分类

在程序中,需要存储多种数据,根据其用途和存储方式,可以将其分为以下几类:

1.1 局部数据

局部数据通常在函数内部定义,这些变量的生命周期与函数调用的时长直接相关。函数结束时,局部数据将自动被销毁,释放相应的内存空间。
局部数据的特点在于它们的存储位置在栈区,内存的分配和释放由操作系统自动管理,避免了内存泄漏的风险。

1.2 静态数据

静态数据是指在程序的整个运行期间都保持存在的数据。这类数据在程序加载时分配内存,并在程序结束时释放。
静态数据的存储位置在静态区,适合那些需要在多个函数之间共享的信息。

1.3 常量数据

常量数据是指在定义后不允许修改的变量。这类数据通常存储在常量区,用于保护程序中不应被更改的值,如数学常数或配置参数。
使用常量有助于提高代码的可读性和安全性。

1.4 动态申请的数据

动态申请的数据是指在运行时根据需求分配的内存。这些数据存储在堆区,使用后需要手动释放。
动态内存管理提供了灵活性,使程序能够根据实际需求调整内存使用。

2. 内存区域划分

在运行时,操作系统对每个进程的地址空间进行了划分,以满足不同数据的存储需求。具体的内存区域包括:
在这里插入图片描述

2.1 栈区

栈区是用于存储局部变量和函数调用信息的区域,内存的分配和释放是自动进行的。
栈的生长方向是向下的,每次函数调用时会创建一个栈帧,存储函数参数、局部变量以及返回地址。
当函数返回时,栈帧被销毁,所占用的内存被自动释放。
这种机制使得局部变量的管理高效而简便,但也意味着局部数据的生存期受到函数调用的限制。

2.2 堆区

堆区用于动态内存分配,其内存的分配和释放需要开发者手动管理。
与栈区不同,堆的生长方向是向上的,允许程序在运行时根据需要动态地申请内存。
动态内存管理的灵活性是其最大的优点,但同时也需要注意内存的释放,以防止内存泄漏和碎片化。

2.3 静态区/数据段

静态区用于存储静态变量和全局变量,这些变量在程序整个生命周期内保持存在。
由于其在程序启动时分配内存,因此不需要手动管理。

2.4 常量区/代码段

常量区用于存储程序的代码和只读数据。
此区域的数据在程序执行期间不会被修改,确保了程序的稳定性。

2.5 相关问题

void test()
{
    char charArray[] = "aaaa"; 
    const char* ptr1 = "aaaa";
    int* ptr2 = (int*)malloc(sizeof(int) * 4);
}
问题:
  1. charArray 在哪儿?
  2. *charArray 在哪儿?
  3. ptr1 在哪儿?
  4. *ptr1 在哪儿?
  5. ptr2 在哪儿?
  6. *ptr2 在哪儿?
答案及详解:
  1. charArray 在哪儿?

    • 答案: 栈
    • 详解: charArray 是一个局部数组,定义在 test 函数内部。局部变量(包括数组)存储在栈区,随着函数调用的开始而创建,在函数结束时自动释放。
  2. *charArray 在哪儿?

    • 答案: 栈
    • 详解: *charArray 表示数组的首元素的地址,它也是一个局部变量。数组的元素存储在栈上,因此 *charArray 的值(即首元素的地址)同样位于栈中。
  3. ptr1 在哪儿?

    • 答案: 栈
    • 详解: ptr1 是一个指向常量字符串的指针,它在 test 函数中定义,因此其存储在栈区。虽然指针 ptr1 指向常量字符串,但指针本身仍然是局部变量,存放在栈上。
  4. *ptr1 在哪儿?

    • 答案: 常量区
    • 详解: *ptr1 是指向常量字符串 “aaaa” 的指针,常量字符串存储在常量区(或代码段)。因此 *ptr1 的值(即指向的字符串)位于常量区。
  5. ptr2 在哪儿?

    • 答案: 栈
    • 详解: ptr2 是一个指向动态分配内存的指针,定义在 test 函数内部,因此其存储在栈区。指针本身是局部变量,存储在栈中。
  6. *ptr2 在哪儿?

    • 答案: 堆
    • 详解: *ptr2 指向通过 malloc 动态分配的内存区域。malloc 函数分配的内存位于堆区,因此 *ptr2 的值(即指向的内存)存储在堆中。
小结

通过上述问题及解析,可以清楚地看到 C/C++ 中不同类型的变量和指针所存储的内存区域。
理解这些概念有助于开发者在进行内存管理时,选择合适的策略,确保程序的高效性和安全性。

补充一点:const修饰的变量只是具有常性(常变量),不代表存储在常量区。
可简单验证:

int main()
{
	int a;
	const int b = 0;
	cout << &a << endl;
	cout << &b << endl;
	return 0;
}

运行结果如下图:
在这里插入图片描述
可以发现,变量a、变量b的地址是相邻的,a肯定在栈区,因此可知b也在栈区。

3. 动态内存管理

在 C 语言中,动态内存管理主要依赖于 malloccallocreallocfree 等函数。
而 C++ 在此基础上进行了扩展,提供了 newdelete 操作符,简化了内存管理的复杂性。

3.1 使用 malloc 与 new 的对比

C 语言的动态内存分配需要手动计算所需内存字节数,并强制类型转换:

int* p1 = (int*)malloc(sizeof(int));
free(p1);

而在 C++ 中,使用 new 进行内存分配时,类型转换是自动的,并且会调用构造函数

int* p2 = new int; // 自动处理类型
delete p2; // 释放内存

这种设计简化了内存管理,降低了出错的可能性。

3.2 数组的动态分配

对于需要分配多个元素的情况,C 和 C++ 也有不同的处理方式:

// C
int* p1 = (int*)malloc(sizeof(int) * 10);
free(p1);
// C++
int* p2 = new int[10]; // 直接使用 new
delete[] p2; // 释放数组内存

使用 new 时,C++ 可以调用构造函数并初始化元素,提高了灵活性和安全性。

3.3 自定义类型的动态分配

在 C++ 中,可以使用 new 运算符为自定义类型(如类或结构体)动态分配内存。以下是一个简单的类 Person 示例:

class Person 
{
public:
    std::string name;
    int age;
    Person(std::string n, int a) : 
    name(n), 
    age(a) 
    {
        std::cout << "Person created: " << name << ", Age: " << age << std::endl;
    }
    ~Person() 
    {
        std::cout << "Person destroyed: " << name << std::endl;
    }
};

可以使用 new 来动态分配一个 Person 对象:

void createPerson() 
{
    Person* p1 = new Person("Alice", 30); // 动态分配 Person 对象
    delete p1; // 释放内存
}

对于需要动态分配多个对象的情况,可以使用 new[] 进行数组的动态分配:

void createPersonArray() 
{
    int size = 3; // 指定 Person 数组的大小

    // 使用聚合初始化动态分配 Person 对象数组
    Person* people = new Person[size] {
        {"Alice", 30},   // 第一个对象
        {"Bob", 25},     // 第二个对象
        {"Charlie", 35}  // 第三个对象
    };

    // 也可以使用匿名对象初始化
    // Person* people = new Person[size] {
    //     Person("Alice", 30), 
    //     Person("Bob", 25), 
    //     Person("Charlie", 35)
    // };

    // 输出每个 Person 对象的信息
    for (int i = 0; i < size; i++) {
        std::cout << "Person " << i + 1 << ": " << people[i].name << ", Age: " << people[i].age << std::endl;
    }

    delete[] people; // 释放动态分配的内存
}
说明
  • 聚合初始化:这种方式可以直接使用初始化列表来为数组中的每个对象赋值。在这个例子中,数组的每个元素都是通过大括号 {} 中的初始化值构造的。
  • 匿名对象初始化:在第二个注释的例子中,使用了匿名对象的方式来初始化,这在某些情况下可能更为清晰,尤其是当需要调用非默认构造函数时。
  • 默认构造函数的注意事项:如果 Person 类没有定义默认构造函数,则在使用数组初始化时,必须确保所有元素都能被完全初始化。否则,编译器会产生错误。

小结

动态内存管理是 C/C++ 编程中的重要主题。通过使用 newdelete,开发者能够灵活地创建和管理对象。合理使用动态分配不仅可以提高程序的性能,还可以避免内存泄漏等问题。

4. operator newoperator delete

在 C++ 中,operator newoperator delete 是内存管理的底层机制,作为全局函数,它们用于动态内存的分配和释放。

4.1 基本用法

使用 operator newoperator delete 的基本语法如下:

int* p1 = (int*)operator new(sizeof(int)); // 使用 operator new
operator delete(p1); // 释放内存
//C:
//int* p1 = (int*)malloc(sizeof(int));
//free(p1);

可以看见,其用法上与mallocfree是一样的。
但是,这两者不仅仅是 mallocfree 的简单替代,而是提供了更复杂的功能,满足面向对象编程的需求。

4.2 operator newnewmalloc的关系

错误处理

面向对象语言错误处理,不喜欢用返回值,更喜欢用抛异常,例如当内存分配失败时, malloc 返回 nullptr,而 new 则会抛出 std::bad_alloc 异常:

int* p1 = (int*)malloc(1024*1024*1024);;
while(p1 != nullptr)
	p1 = (int*)malloc(1024*1024*1024);
cout << p1 << endl;//malloc失败返回空

运行结果:

00000000

while(1)
	p1 = new int[1024 * 1024];// 内存分配失败,抛出异常

在这里插入图片描述
捕获异常:

try 
{
    p1 = new int[10000000000]; // 内存分配失败,抛出异常
} 
catch (const std::exception& e) 
{
    std::cout << e.what() << std::endl; // 捕获异常并处理
}

运行结果:
在这里插入图片描述

关系

在前面我说明了newmalloc的主要区别是new最后会调用构造函数。
但是,new的第一步还是开空间,而开空间时则用到了malloc
又因为,malloc的错误处理不能满足需求,因此,operator new对其进行了封装:

int* p1 = (int*)operator new(sizeof(int)); // 使用 operator new
operator delete(p1); // 释放内存

在这里插入图片描述
可以看见,operator newmalloc的基础上,添加了抛出异常的功能。

而在new中,直接调用的是operator new

int* ptr = new int;

在这里插入图片描述

对于自定义类型,各函数调用的顺序

自定义一个List类:

class List
{
public:
	List() :
	size(0),
	capacity(0)
	{
		ptrArray = (int*)malloc(sizeof(int));
	}
	~List()
	{
		free(ptrArray);
		ptrArray = NULL;
	}
private:
	int* ptrArray;
	int size;
	int capacity;
};

然后new/delete

List* ptrlist = new List;
delete ptrlist;

调用顺序如下图:
在这里插入图片描述
在这里插入图片描述

5. 定位 new 表达式

在C++中,new 表达式不仅用于动态分配内存,还允许开发者在特定内存区域内显式调用构造函数。这种用法在需要更细粒度的内存控制时非常有用。以下是相关示例及其解释:

5.1 使用 mallocnew 的结合

在某些情况下,开发者可能会首先使用 malloc 函数分配一块内存,然后使用 new 表达式在这块内存上显式构造对象。下面是如何实现这一点的示例:

A* p1 = (A*)malloc(sizeof(A)); // 使用 malloc 分配内存
new(p1) A; // 在已分配的内存上调用 A 类的默认构造函数

这里,malloc 函数只分配了足够的内存来存放一个 A 类型的对象,但并未调用其构造函数。通过 new(p1) A;,我们显式地在 p1 指向的内存地址上构造了一个 A 类的对象。这种方式可以让开发者控制对象的构造过程,尤其是在处理特定的内存对齐或预分配内存块时非常有用。

5.2 带参数的构造函数

如果 A 类的构造函数需要参数,也可以在 new 表达式中传递这些参数。例如:

new(p1) A(1); // 调用 A 类的构造函数,并传递参数 1

这种写法允许开发者灵活地创建对象,并根据需要初始化其状态。

5.3 手动调用析构函数

需要注意的是,当使用这种方式构造对象时,C++不会自动调用析构函数。因此,在对象不再使用时,必须手动调用析构函数以释放资源:

p1->~A(); // 手动调用 A 类的析构函数

这一步骤是必要的,因为如果不调用析构函数,可能会导致资源泄漏,尤其是当对象管理动态分配的内存或其他系统资源时。

5.4 释放内存

最后,使用 free 函数来释放之前用 malloc 分配的内存:

free(p1); // 释放分配的内存

这种方法确保了在不再需要对象后,能够适当地回收内存资源。

5.5 适用场景

这种使用模式通常用于池化技术:

内存管理与性能优化

在 C/C++ 的内存管理中,动态内存分配的频繁调用可能导致内存碎片化,从而影响程序的运行效率。为了避免这一问题,开发者可以采用对象池(Object Pooling)技术,将相同类型的对象集中管理,从而减少内存的分配和释放次数。

对象池可以预先分配一定数量的对象,并在需要时从池中取出,使用完后再返回到池中。这样不仅减少了内存分配带来的开销,还提高了内存利用率,避免了频繁的堆内存申请。

小结

通过理解和使用 new 表达式的这些特性,开发者可以在C++中更灵活地管理对象的生命周期,从而提高程序的性能和效率。

总结

C/C++ 的内存管理是一个复杂而重要的主题,理解其内存区域的划分、动态内存管理的机制以及异常处理对于编写高效且安全的代码至关重要。通过合理运用内存管理技术和优化策略,开发者可以提升程序的性能,减少内存泄漏和碎片化问题。随着编程语言的发展,内存管理的方式也在不断演进,未来可能会出现更加智能和自动化的内存管理技术。
在这里插入图片描述


希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值