1 内存管理基础

1.1 什么是内存管理

在 C++ 中,内存管理是一个核心概念,它涉及到如何在程序执行过程中分配、使用和释放内存。由于 C++ 允许程序员直接管理内存,因此内存管理在 C++ 中显得尤为重要。合理的内存管理可以确保程序的正确运行,避免内存泄漏、野指针等问题,提高程序的稳定性和性能。

(1)内存分配

在 C++ 中,内存分配主要分为两种:静态内存分配和动态内存分配。

  • 静态内存分配:在程序编译时就已经确定了所需内存的大小,并由编译器自动分配。这通常用于定义变量、数组和对象等。静态内存分配的内存生命周期与程序的执行期相同,即程序开始运行时分配内存,程序结束时释放内存。
  • 动态内存分配:在程序运行时根据需要动态地分配和释放内存。这通常通过 new 和 delete 操作符实现。动态内存分配允许程序员根据实际需要灵活地调整内存的使用,但也需要程序员手动管理内存的分配和释放,否则可能导致内存泄漏等问题。

(2)堆与栈

在 C++ 中,内存主要分为堆和栈两部分。

  • 栈内存:主要用于存储局部变量和函数调用的信息。栈内存由编译器自动管理,遵循 LIFO(后进先出)的原则。当函数被调用时,其参数和局部变量会被压入栈中;当函数返回时,这些数据会被从栈中弹出并自动释放。
  • 堆内存:用于动态内存分配。通过 new 操作符在堆上分配的内存需要程序员显式地使用 delete 操作符释放。堆内存的生命周期不受作用域的限制,只要程序员不释放它,它就会一直存在。

(3)内存泄漏

内存泄漏是指程序在申请内存后,未能释放该内存空间,从而造成系统内存的浪费,严重时会导致系统运行缓慢、甚至崩溃。在 C++ 中,内存泄漏通常是由于程序员忘记释放动态分配的内存或者错误地管理内存导致的。为了避免内存泄漏,程序员需要仔细跟踪内存的使用情况,确保在不再需要内存时及时释放它。

(4)野指针

野指针是指已经被释放的内存空间,但是指针仍然指向它。这种情况下,如果程序员尝试通过该指针访问内存,将会导致不可预知的结果,甚至可能导致程序崩溃。为了避免野指针的问题,程序员在释放内存后应立即将指针设置为 nullptr。

(5)内存对齐

内存对齐是为了提高数据访问效率而采取的一种策略。由于计算机硬件的原因,访问对齐的内存地址通常比访问未对齐的内存地址更快。因此,在 C++ 中,有时需要手动调整数据结构或对象的内存布局,以确保其按特定的对齐方式进行存储。

1.2 动态内存分配

C++动态内存分配是指在程序运行时根据实际需求分配内存空间的过程。这种分配方式允许程序员在程序执行期间根据需要动态地创建和销毁对象,从而更加灵活地管理内存资源。

(1)new 和 delete 操作符

在 C++ 中,动态内存分配主要通过 new 和 delete 操作符实现。

new操作符

new操作符用于在堆上动态分配内存,并返回指向该内存空间的指针。其一般语法为:

pointer = new type;

或者,对于带有构造函数的类对象:

pointer = new type(initializer);

例如:

int* pInt = new int;       // 分配一个int大小的内存空间  
double* pDouble = new double(3.14); // 分配一个double大小的内存空间,并初始化为3.14

使用 new 操作符时,如果内存分配成功,则返回指向新分配内存的指针;如果内存分配失败(例如,由于内存不足),则抛出 std::bad_alloc 异常。

delete操作符

delete 操作符用于释放通过 new 操作符分配的内存空间。其一般语法为:

delete pointer;

对于动态分配的数组,需要使用delete[]来释放:

delete[] pointer;

例如:

delete pInt;       // 释放之前通过new分配的int内存空间  
delete[] pDoubleArray; // 释放之前通过new[]分配的double数组内存空间

(2)动态内存分配的特点

  • 动态性:内存空间在运行时根据需要分配和释放,而不是在编译时确定。
  • 灵活性:程序员可以根据实际情况动态调整内存大小,以满足不同的需求。
  • 手动管理:使用 new 和 delete 操作符进行动态内存分配时,需要程序员显式地管理内存的分配和释放。如果忘记释放内存,可能导致内存泄漏;如果释放了未分配的内存或重复释放同一块内存,可能导致程序崩溃。

(3)注意事项

  • 避免内存泄漏:务必确保每个通过 new 分配的内存块最终都被 delete 释放。可以使用智能指针(如 std::unique_ptr 和 std::shared_ptr)来自动管理内存,减少内存泄漏的风险。
  • 避免野指针:释放内存后,应将指针设为nullptr,以防止成为野指针。野指针是指向已被释放内存的指针,继续使用野指针可能导致不可预测的行为。
    异常安全性:在构造函数中分配内存时,应确保在构造函数抛出异常时能够正确释放已分配的内存。这通常可以通过在析构函数中释放内存或使用智能指针来实现。
  • 内存对齐:虽然现代编译器通常会自动处理内存对齐问题,但在某些特殊情况下(如硬件接口、性能优化等),程序员可能需要手动处理内存对齐。

(4)其他动态内存分配函数

除了 new 和 delete 操作符外,C++ 标准库还提供了其他一些函数用于动态内存分配,如 malloc、calloc、realloc 和 free。然而,这些函数是 C 语言的遗留,并且在 C++ 中通常不推荐使用,因为它们不提供类型安全,并且不调用对象的构造函数或析构函数。在 C++ 中,应优先使用 new 和 delete 操作符以及智能指针进行动态内存分配。

总体而言,C++ 动态内存分配是程序员在运行时根据实际需求灵活管理内存的重要手段。使用 new 和 delete 操作符进行动态内存分配时,需要注意避免内存泄漏和野指针等问题,并确保异常安全性。同时,应优先使用 C++ 提供的特性(如智能指针)来简化内存管理任务。

1.3 手动内存管理的挑战

C++ 手动内存管理是一个复杂且容易出错的过程,它带来了多个挑战,需要程序员仔细处理以确保程序的正确性和稳定性。以下是 C++ 手动内存管理面临的主要挑战:

(1)内存泄漏
内存泄漏是手动内存管理中最常见的问题之一。当程序员使用 new 操作符分配内存后,如果忘记使用 delete 操作符释放该内存,就会导致内存泄漏。随着时间的推移,泄漏的内存会逐渐累积,最终可能耗尽系统资源,导致程序运行缓慢或崩溃。

(2)野指针
野指针是指那些指向已经被释放内存或无效内存地址的指针。如果程序员在释放内存后没有将指针置为 nullptr,或者错误地操作了指针(如越界访问),就可能产生野指针。使用野指针进行操作可能导致程序崩溃或不可预测的行为。

(3)重复释放
尝试释放同一块内存多次也是一个常见的错误。这通常发生在复杂的程序中,当多个代码路径可能释放同一块内存时。重复释放同一块内存会导致程序崩溃或未定义行为。

(4)内存碎片
随着程序的运行,内存分配和释放可能会导致内存碎片。碎片化的内存使得难以找到连续的大块内存,可能导致即使有足够的总内存也无法满足分配请求。这可能会限制程序的扩展性和性能。

(5)构造函数和析构函数的调用
C++ 中的对象在动态分配内存时需要调用构造函数进行初始化,而在释放内存时则需要调用析构函数进行清理。手动管理内存时,程序员需要确保正确调用这些函数,以避免资源泄漏或未定义行为。

(6)异常安全性
在构造函数中分配内存时,如果构造函数抛出异常,必须确保已分配的内存得到正确释放,以避免资源泄漏。这需要程序员仔细处理异常路径,并确保资源管理的鲁棒性。

(7)复杂性和易错性
手动内存管理增加了程序的复杂性,使得代码更难理解和维护。程序员需要仔细跟踪每个内存块的分配和释放,确保没有遗漏或错误。这种复杂性增加了出错的可能性,尤其是在大型项目中。

解决方案
为了减轻手动内存管理的挑战,C++ 提供了一些工具和技术,如智能指针(如 std::unique_ptr 和 std::shared_ptr)、RAII(资源获取即初始化)技术和内存管理工具库。这些工具和技术可以帮助程序员自动管理内存,减少内存泄漏和野指针等问题,提高程序的稳定性和可靠性。然而,即使有了这些工具,程序员仍然需要谨慎处理内存管理问题,以确保程序的正确性。

2 智能指针的引入

2.1 为什么需要智能指针

C++ 需要智能指针的原因主要涉及到内存管理的挑战,特别是手动内存管理带来的问题。智能指针作为一种内存管理工具,可以显著减少手动内存管理中的错误和复杂性。

首先,手动内存管理常常面临内存泄漏的风险。当使用 new 操作符分配内存后,如果程序员忘记使用 delete 操作符释放该内存,那么这部分内存就无法被系统回收,从而导致内存泄漏。随着程序运行时间的增长,内存泄漏会逐渐累积,最终可能导致系统资源耗尽,程序运行缓慢或崩溃。

其次,野指针也是一个需要关注的问题。野指针是指那些指向已经被释放内存或无效内存地址的指针。如果程序员在释放内存后没有将指针置为 nullptr,或者错误地操作了指针(如越界访问),就可能产生野指针。使用野指针进行操作可能导致程序崩溃或不可预测的行为。

智能指针就是为了解决这些问题而设计的。智能指针实质上是普通指针抽象封装后的类,在超出智能指针作用域范围后,将会自动通过析构函数释放内存,实现内存的有效管理。这意味着,当智能指针对象不再需要时(例如,当智能指针对象离开其作用域或被重新赋值时),其析构函数会被自动调用,从而释放其所指向的内存。这样,程序员就无需显式地调用 delete 来释放内存,从而减少了内存泄漏和野指针的风险。

C++11 标准中引入了多种智能指针,如 std::unique_ptr、std::shared_ptr 和 std::weak_ptr 等,它们各自具有不同的特性和适用场景。例如,std::unique_ptr 用于表示独占所有权的智能指针,同一时间只能有一个 unique_ptr 指向某个对象;而 std::shared_ptr 则用于表示共享所有权的智能指针,多个 shared_ptr 可以指向同一个对象,当最后一个指向该对象的 shared_ptr 被销毁时,对象才会被删除。

总体而言,智能指针通过自动管理内存的生命周期,显著简化了 C++ 的内存管理任务,并降低了内存泄漏和野指针等问题的风险,提高了程序的稳定性和可靠性。因此,C++ 需要智能指针来更好地处理内存管理问题。

2.2 智能指针的基本概念

C++ 智能指针是标准库提供的一类用于自动管理动态分配内存的对象。它们封装了原始指针,并在适当的时候自动释放所指向的内存,从而避免了手动管理内存时可能出现的内存泄漏、野指针等问题。智能指针通过确保资源的适时释放,提高了程序的健壮性和可靠性。

(1)基本概念:

  • 所有权概念:智能指针表示对动态分配对象的所有权。当智能指针离开其作用域或被重新赋值时,它会自动释放所指向的内存。
  • 引用计数:某些智能指针(如 std::shared_ptr)使用引用计数来管理共享所有权。当引用计数降至零时,对象会被删除。
  • 独占与共享:智能指针可以是独占的(如 std::unique_ptr),也可以是共享的(如 std::shared_ptr)。独占智能指针保证同一时间只有一个智能指针可以指向某个对象。

(2)智能指针类型:

  • std::unique_ptr:独占所有权的智能指针。
  • std::shared_ptr:共享所有权的智能指针,通过引用计数管理内存。
  • std::weak_ptr:不控制生命周期的智能指针,用于解决 shared_ptr 之间的循环引用问题。

(3)使用 std::unique_ptr 的示例:

#include <iostream>  
#include <memory>  

class MyClass {
public:
	MyClass(int value) : m_value(value) {}
	~MyClass() { std::cout << "Deleting MyClass with value " << m_value << std::endl; }
	void printValue() const { std::cout << "Value: " << m_value << std::endl; }

private:
	int m_value;
};

int main() 
{
	{
		// 使用 std::make_unique 创建 unique_ptr  
		std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>(12);
		ptr->printValue(); // 输出:Value: 12  
	}
	// 当 unique_ptr 离开作用域时,会自动释放 MyClass 的内存  
	// 输出:Deleting MyClass with value 12  

	return 0;
}

上面代码的输出为:

Value: 12
Deleting MyClass with value 12

(4)使用 std::shared_ptr 的示例:

#include <iostream>  
#include <memory>  

class MyClass {
public:
	MyClass(int value) : m_value(value) {}
	~MyClass() { std::cout << "Deleting MyClass with value " << m_value << std::endl; }
	void printValue() const { std::cout << "Value: " << m_value << std::endl; }

private:
	int m_value;
};

int main() 
{
	{
		// 使用 std::make_shared 创建 shared_ptr  
		std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(12);
		ptr1->printValue(); // 输出:Value: 12  

		// 创建另一个指向相同对象的 shared_ptr  
		std::shared_ptr<MyClass> ptr2 = ptr1;
	}
	// 当最后一个指向 MyClass 的 shared_ptr 离开作用域时,内存会被释放  
	// 输出:Deleting MyClass with value 12  

	return 0;
}

上面代码的输出为:

Value: 12
Deleting MyClass with value 12

(5)使用 std::weak_ptr 解决循环引用的示例:

#include <iostream>  
#include <memory>  

class Parent;

class Child {
public:
	~Child() { std::cout << "Deleting Child\n"; }
public:
	std::weak_ptr<Parent> parent;
};

class Parent {
public:
	~Parent() { std::cout << "Deleting Parent\n"; }
public:
	std::shared_ptr<Child> child;
};

int main() 
{
	{
		auto child = std::make_shared<Child>();
		auto parent = std::make_shared<Parent>();

		child->parent = parent;
		parent->child = child;
	}
	// 由于 parent 和 child 是通过 weak_ptr 和 shared_ptr 相互引用的,  
	// 因此不存在循环引用,当它们离开作用域时,内存会被正确释放。  
	// 输出:Deleting Child  
	// 输出:Deleting Parent 
	// 注意:如果 Child 的成员变量 parent 的类型改为 std::shared_ptr<Parent> ,则此处不会有任何输出(意味着对象没有被销毁)

	return 0;
}

上面代码的输出为:

Deleting Parent
Deleting Child
03-17 17:32