第17章 类的内存管理

动态内存分配概述

动态内存分配是指在程序运行时根据需要分配和释放内存的过程。在C++中,使用newdelete运算符进行动态内存管理。

基本概念

  • 静态内存:在编译时分配的内存,如全局变量、静态变量
  • 栈内存:在函数调用时自动分配的内存,函数返回时自动释放
  • 堆内存:在运行时动态分配的内存,需要手动释放

动态内存分配的优点

  1. 灵活性:可以根据运行时的需要分配内存
  2. 持久性:堆内存的生命周期由程序员控制,不受函数作用域的限制
  3. 大数据结构:可以分配大型数据结构,如大型数组、复杂对象等

new 和 delete 运算符

分配单个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 分配单个int
int* pInt = new int;
*pInt = 100;
std::cout << *pInt << std::endl;
delete pInt;
pInt = nullptr;

// 分配并初始化
int* pInt2 = new int(200);
std::cout << *pInt2 << std::endl;
delete pInt2;
pInt2 = nullptr;

// 分配对象
class MyClass {
private:
int value;

public:
MyClass(int v) : value(v) {
std::cout << "Constructor called" << std::endl;
}

~MyClass() {
std::cout << "Destructor called" << std::endl;
}

int getValue() const {
return value;
}
};

MyClass* pObj = new MyClass(300);
std::cout << pObj->getValue() << std::endl;
delete pObj;
pObj = nullptr;

分配数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 分配数组
int* pArray = new int[5];
for (int i = 0; i < 5; i++) {
pArray[i] = i;
}
for (int i = 0; i < 5; i++) {
std::cout << pArray[i] << " ";
}
std::cout << std::endl;
delete[] pArray;
pArray = nullptr;

// 分配并初始化数组(C++11+)
int* pArray2 = new int[5]{1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
std::cout << pArray2[i] << " ";
}
std::cout << std::endl;
delete[] pArray2;
pArray2 = nullptr;

// 分配对象数组
MyClass* pObjArray = new MyClass[3]{10, 20, 30};
for (int i = 0; i < 3; i++) {
std::cout << pObjArray[i].getValue() << " ";
}
std::cout << std::endl;
delete[] pObjArray;
pObjArray = nullptr;

分配失败

1
2
3
4
5
6
7
8
9
// 使用new(nothrow)处理分配失败
int* pInt = new (std::nothrow) int[1000000000];
if (pInt == nullptr) {
std::cout << "Memory allocation failed" << std::endl;
} else {
std::cout << "Memory allocation successful" << std::endl;
delete[] pInt;
pInt = nullptr;
}

类中的动态内存管理

包含指针成员的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class String {
private:
char* data;
size_t length;

public:
// 构造函数
String(const char* str) {
if (str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
} else {
length = 0;
data = new char[1];
data[0] = '\0';
}
std::cout << "Constructor called" << std::endl;
}

// 析构函数
~String() {
delete[] data;
std::cout << "Destructor called" << std::endl;
}

// 访问器
const char* c_str() const {
return data;
}

size_t size() const {
return length;
}
};

int main() {
String s1("Hello");
std::cout << "s1: " << s1.c_str() << ", size: " << s1.size() << std::endl;

String s2("World");
std::cout << "s2: " << s2.c_str() << ", size: " << s2.size() << std::endl;

return 0;
}

拷贝构造函数

当类包含指针成员时,需要实现拷贝构造函数以避免浅拷贝问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class String {
private:
char* data;
size_t length;

public:
// 构造函数
String(const char* str) {
if (str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
} else {
length = 0;
data = new char[1];
data[0] = '\0';
}
std::cout << "Constructor called" << std::endl;
}

// 拷贝构造函数
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "Copy constructor called" << std::endl;
}

// 析构函数
~String() {
delete[] data;
std::cout << "Destructor called" << std::endl;
}

// 访问器
const char* c_str() const {
return data;
}

size_t size() const {
return length;
}
};

int main() {
String s1("Hello");
String s2 = s1; // 调用拷贝构造函数

std::cout << "s1: " << s1.c_str() << std::endl;
std::cout << "s2: " << s2.c_str() << std::endl;

return 0;
}

赋值运算符

当类包含指针成员时,需要重载赋值运算符以避免浅拷贝问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class String {
private:
char* data;
size_t length;

public:
// 构造函数
String(const char* str) {
if (str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
} else {
length = 0;
data = new char[1];
data[0] = '\0';
}
std::cout << "Constructor called" << std::endl;
}

// 拷贝构造函数
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "Copy constructor called" << std::endl;
}

// 赋值运算符
String& operator=(const String& other) {
if (this != &other) { // 避免自赋值
// 释放旧内存
delete[] data;

// 分配新内存并拷贝数据
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "Assignment operator called" << std::endl;
}
return *this;
}

// 析构函数
~String() {
delete[] data;
std::cout << "Destructor called" << std::endl;
}

// 访问器
const char* c_str() const {
return data;
}

size_t size() const {
return length;
}
};

int main() {
String s1("Hello");
String s2("World");

std::cout << "Before assignment:" << std::endl;
std::cout << "s1: " << s1.c_str() << std::endl;
std::cout << "s2: " << s2.c_str() << std::endl;

s2 = s1; // 调用赋值运算符

std::cout << "After assignment:" << std::endl;
std::cout << "s1: " << s1.c_str() << std::endl;
std::cout << "s2: " << s2.c_str() << std::endl;

return 0;
}

移动构造函数和移动赋值运算符(C++11+)

为了提高性能,对于包含动态内存的类,应该实现移动构造函数和移动赋值运算符。

移动构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class String {
private:
char* data;
size_t length;

public:
// 构造函数
String(const char* str) {
if (str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
} else {
length = 0;
data = new char[1];
data[0] = '\0';
}
std::cout << "Constructor called" << std::endl;
}

// 拷贝构造函数
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "Copy constructor called" << std::endl;
}

// 移动构造函数
String(String&& other) noexcept : data(other.data), length(other.length) {
other.data = nullptr;
other.length = 0;
std::cout << "Move constructor called" << std::endl;
}

// 赋值运算符
String& operator=(const String& other) {
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "Assignment operator called" << std::endl;
}
return *this;
}

// 移动赋值运算符
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
std::cout << "Move assignment operator called" << std::endl;
}
return *this;
}

// 析构函数
~String() {
delete[] data;
std::cout << "Destructor called" << std::endl;
}

// 访问器
const char* c_str() const {
return data;
}

size_t size() const {
return length;
}
};

// 工厂函数,返回临时对象
String createString() {
return String("Temporary string");
}

int main() {
String s1("Hello");

// 移动构造
String s2 = std::move(s1);
std::cout << "s2: " << s2.c_str() << std::endl;

// 移动赋值
String s3("World");
s3 = std::move(s2);
std::cout << "s3: " << s3.c_str() << std::endl;

// 从临时对象移动
String s4 = createString();
std::cout << "s4: " << s4.c_str() << std::endl;

return 0;
}

C++20新特性:std::span

C++20引入了std::span,用于安全地访问数组和其他序列,而不需要拥有它们的所有权:

std::span的基本概念

  • span:一个非拥有的序列视图,包含指针和大小
  • 灵活性:可以指向任何连续的内存区域,如数组、vector、string等
  • 安全性:提供边界检查,避免越界访问
  • 零开销:span本身不分配内存,只是对现有内存的视图

std::span的使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <span>
#include <iostream>
#include <vector>
#include <array>

// 使用span处理数组
void processArray(std::span<int> numbers) {
for (size_t i = 0; i < numbers.size(); i++) {
numbers[i] *= 2; // 可以修改元素
}
}

// 使用span处理常量数据
void printData(std::span<const int> data) {
for (int value : data) {
std::cout << value << " ";
}
std::cout << std::endl;
}

int main() {
// 处理数组
int arr[] = {1, 2, 3, 4, 5};
std::span<int> arrSpan(arr);
processArray(arrSpan);
printData(arrSpan); // 输出:2 4 6 8 10

// 处理vector
std::vector<int> vec = {10, 20, 30, 40, 50};
std::span<int> vecSpan(vec);
processArray(vecSpan);
printData(vecSpan); // 输出:20 40 60 80 100

// 处理array
std::array<int, 3> arr3 = {100, 200, 300};
std::span<int> arr3Span(arr3);
processArray(arr3Span);
printData(arr3Span); // 输出:200 400 600

// 创建子span
std::span<int> subSpan = arrSpan.subspan(1, 3); // 从索引1开始,长度为3
printData(subSpan); // 输出:4 6 8

return 0;
}

std::span的优点

  1. 安全性:提供边界检查,避免越界访问
  2. 灵活性:可以指向任何连续的内存区域
  3. 零开销:不分配内存,只是对现有内存的视图
  4. 简洁性:简化了函数参数,不再需要同时传递指针和大小
  5. 兼容性:与现有的数组和容器无缝集成

智能指针

智能指针是C++11引入的一种自动管理动态内存的工具,它们会在适当的时候自动释放所管理的内存。

unique_ptr

unique_ptr是一种独占所有权的智能指针,同一时间只能有一个unique_ptr指向同一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <memory>

class MyClass {
private:
int value;

public:
MyClass(int v) : value(v) {
std::cout << "Constructor called with value: " << value << std::endl;
}

~MyClass() {
std::cout << "Destructor called with value: " << value << std::endl;
}

int getValue() const {
return value;
}
};

int main() {
// 创建unique_ptr
std::unique_ptr<MyClass> p1(new MyClass(100));
std::cout << "p1: " << p1->getValue() << std::endl;

// 使用make_unique(C++14+)
auto p2 = std::make_unique<MyClass>(200);
std::cout << "p2: " << p2->getValue() << std::endl;

// 转移所有权
std::unique_ptr<MyClass> p3 = std::move(p1);
// p1现在为空
if (p1) {
std::cout << "p1 is not null" << std::endl;
} else {
std::cout << "p1 is null" << std::endl;
}
std::cout << "p3: " << p3->getValue() << std::endl;

// 数组版本
auto pArray = std::make_unique<MyClass[]>(3);

return 0; // 智能指针自动释放内存
}

shared_ptr

shared_ptr是一种共享所有权的智能指针,多个shared_ptr可以指向同一个对象,当最后一个shared_ptr被销毁时,对象才会被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <memory>

class MyClass {
private:
int value;

public:
MyClass(int v) : value(v) {
std::cout << "Constructor called with value: " << value << std::endl;
}

~MyClass() {
std::cout << "Destructor called with value: " << value << std::endl;
}

int getValue() const {
return value;
}
};

int main() {
// 创建shared_ptr
std::shared_ptr<MyClass> p1(new MyClass(100));
std::cout << "p1 use count: " << p1.use_count() << std::endl;

// 使用make_shared
auto p2 = std::make_shared<MyClass>(200);
std::cout << "p2 use count: " << p2.use_count() << std::endl;

// 共享所有权
std::shared_ptr<MyClass> p3 = p1;
std::cout << "p1 use count after p3 assignment: " << p1.use_count() << std::endl;
std::cout << "p3 use count: " << p3.use_count() << std::endl;

// 重置
p1.reset();
std::cout << "p1 use count after reset: " << (p1 ? p1.use_count() : 0) << std::endl;
std::cout << "p3 use count after p1 reset: " << p3.use_count() << std::endl;

return 0; // 最后一个shared_ptr销毁时释放内存
}

weak_ptr

weak_ptr是一种不增加引用计数的智能指针,用于解决shared_ptr的循环引用问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <memory>

class B;

class A {
private:
std::shared_ptr<B> bPtr;

public:
A() {
std::cout << "A constructor called" << std::endl;
}

~A() {
std::cout << "A destructor called" << std::endl;
}

void setB(const std::shared_ptr<B>& b) {
bPtr = b;
}
};

class B {
private:
std::weak_ptr<A> aPtr; // 使用weak_ptr避免循环引用

public:
B() {
std::cout << "B constructor called" << std::endl;
}

~B() {
std::cout << "B destructor called" << std::endl;
}

void setA(const std::shared_ptr<A>& a) {
aPtr = a;
}
};

int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();

a->setB(b);
b->setA(a);

std::cout << "a use count: " << a.use_count() << std::endl;
std::cout << "b use count: " << b.use_count() << std::endl;

// 检查weak_ptr是否有效
if (auto aLocked = b->aPtr.lock()) {
std::cout << "a is still valid" << std::endl;
} else {
std::cout << "a is no longer valid" << std::endl;
}

return 0; // 没有循环引用,a和b都会被正确销毁
}

异常安全

异常安全的概念

异常安全是指当程序抛出异常时,系统能够保持一致的状态,不会泄漏资源,也不会破坏数据结构。

异常安全级别

  1. 无抛出保证(No-throw guarantee):函数不会抛出异常
  2. 强异常安全保证(Strong exception safety):如果函数抛出异常,程序状态不变
  3. 基本异常安全保证(Basic exception safety):如果函数抛出异常,程序状态保持有效,但可能与之前不同
  4. 无异常安全保证(No exception safety):函数抛出异常时,程序可能处于无效状态

实现异常安全的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class String {
private:
char* data;
size_t length;

public:
// 构造函数
String(const char* str) : data(nullptr), length(0) {
if (str) {
length = strlen(str);
char* temp = new char[length + 1]; // 可能抛出异常
strcpy(temp, str);
data = temp; // 不抛出异常
} else {
char* temp = new char[1]; // 可能抛出异常
temp[0] = '\0';
data = temp; // 不抛出异常
}
std::cout << "Constructor called" << std::endl;
}

// 拷贝构造函数
String(const String& other) : data(nullptr), length(0) {
char* temp = new char[other.length + 1]; // 可能抛出异常
strcpy(temp, other.data);
data = temp; // 不抛出异常
length = other.length; // 不抛出异常
std::cout << "Copy constructor called" << std::endl;
}

// 赋值运算符(使用拷贝交换技术)
String& operator=(String other) { // 传值,可能抛出异常
swap(other); // 不抛出异常
std::cout << "Assignment operator called" << std::endl;
return *this;
}

// 交换函数
void swap(String& other) noexcept {
std::swap(data, other.data);
std::swap(length, other.length);
}

// 析构函数
~String() {
delete[] data;
std::cout << "Destructor called" << std::endl;
}

// 访问器
const char* c_str() const {
return data;
}

size_t size() const {
return length;
}
};

动态内存管理的最佳实践

1. 使用智能指针

优先使用std::unique_ptrstd::shared_ptr管理动态内存,避免手动使用newdelete

2. 避免内存泄漏

  • 确保每个new都有对应的delete
  • 确保每个new[]都有对应的delete[]
  • 使用RAII(资源获取即初始化)原则管理资源

3. 避免悬空指针

  • 释放内存后将指针设置为nullptr
  • 使用智能指针自动管理指针的生命周期

4. 避免重复释放

  • 释放内存后将指针设置为nullptrdelete nullptr是安全的)
  • 使用智能指针自动管理释放

5. 处理内存分配失败

  • 使用new(nothrow)处理分配失败
  • 捕获std::bad_alloc异常

6. 优化内存使用

  • 合理估计内存需求
  • 避免频繁的内存分配和释放
  • 使用内存池技术管理频繁分配的小对象

常见错误和陷阱

1. 内存泄漏

1
2
3
4
5
6
7
8
9
10
11
// 错误:内存泄漏
void function() {
int* p = new int(100);
// 没有delete p;
}

// 正确:使用智能指针
void function() {
std::unique_ptr<int> p(new int(100));
// 智能指针自动释放内存
}

2. 悬空指针

1
2
3
4
5
6
7
8
9
10
11
12
// 错误:悬空指针
int* p = new int(100);
delete p;
std::cout << *p << std::endl; // 未定义行为

// 正确:释放后设置为nullptr
int* p = new int(100);
delete p;
p = nullptr;
if (p != nullptr) {
std::cout << *p << std::endl; // 不会执行
}

3. 重复释放

1
2
3
4
5
6
7
8
9
// 错误:重复释放
int* p = new int(100);
delete p;
delete p; // 未定义行为

// 正确:使用智能指针
std::unique_ptr<int> p(new int(100));
p.reset(); // 释放内存
p.reset(); // 安全,reset(nullptr)是安全的

4. 数组和单个对象释放混淆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 错误:使用delete释放数组
int* p = new int[5];
delete p; // 未定义行为

// 错误:使用delete[]释放单个对象
int* p = new int;
delete[] p; // 未定义行为

// 正确:匹配使用
int* pArray = new int[5];
delete[] pArray; // 正确

int* pObj = new int;
delete pObj; // 正确

5. 浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 错误:浅拷贝
class ShallowCopy {
private:
int* data;

public:
ShallowCopy(int value) {
data = new int(value);
}

// 默认拷贝构造函数是浅拷贝

~ShallowCopy() {
delete data;
}
};

// 正确:深拷贝
class DeepCopy {
private:
int* data;

public:
DeepCopy(int value) {
data = new int(value);
}

DeepCopy(const DeepCopy& other) {
data = new int(*other.data);
}

DeepCopy& operator=(const DeepCopy& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
}
return *this;
}

~DeepCopy() {
delete data;
}
};

6. 循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 错误:循环引用导致内存泄漏
class A {
private:
std::shared_ptr<B> bPtr;

public:
void setB(const std::shared_ptr<B>& b) {
bPtr = b;
}
};

class B {
private:
std::shared_ptr<A> aPtr;

public:
void setA(const std::shared_ptr<A>& a) {
aPtr = a;
}
};

// 正确:使用weak_ptr避免循环引用
class A {
private:
std::shared_ptr<B> bPtr;

public:
void setB(const std::shared_ptr<B>& b) {
bPtr = b;
}
};

class B {
private:
std::weak_ptr<A> aPtr;

public:
void setA(const std::shared_ptr<A>& a) {
aPtr = a;
}
};

小结

本章介绍了C++中类和动态内存分配的相关内容,包括:

  1. 动态内存分配概述:静态内存、栈内存、堆内存的区别
  2. new 和 delete 运算符:分配和释放单个对象、数组
  3. 类中的动态内存管理:包含指针成员的类、拷贝构造函数、赋值运算符
  4. 移动语义:移动构造函数和移动赋值运算符
  5. 智能指针:unique_ptr、shared_ptr、weak_ptr
  6. 异常安全:异常安全的概念和实现
  7. 动态内存管理的最佳实践:使用智能指针、避免内存泄漏、悬空指针等
  8. 常见错误和陷阱:内存泄漏、悬空指针、重复释放、数组和单个对象释放混淆、浅拷贝、循环引用

动态内存管理是C++编程中的重要部分,掌握好动态内存管理对于编写高效、可靠的程序至关重要。通过合理使用智能指针、遵循RAII原则、注意异常安全等最佳实践,可以避免许多常见的内存管理错误,提高程序的质量和可靠性。

在后续章节中,我们将学习类的继承、多态、模板等更高级的C++特性,这些特性将与动态内存管理结合使用,帮助我们构建更复杂、更强大的程序。