12.1 动态内存与智能指针
动态内存管理是C++编程中的重要部分。使用动态内存时,程序员需要手动分配和释放内存,这可能导致内存泄漏和其他问题。为了解决这些问题,C++11引入了智能指针(smart pointers),它们自动管理动态内存的生命周期。本节将介绍动态内存管理的基本概念以及智能指针的使用。
12.1.1 直接管理内存
在C++中,可以使用new
和delete
操作符来分配和释放动态内存。
new
:分配动态内存并返回指向该内存的指针。delete
:释放先前使用new
分配的动态内存。
示例代码
#include <iostream>
int main() {
// 使用new分配动态内存
int* p = new int(42);
// 输出动态内存中的值
std::cout << "Value: " << *p << std::endl;
// 使用delete释放动态内存
delete p;
return 0;
}
直接管理内存的缺点包括需要手动释放内存,容易导致内存泄漏和悬空指针(dangling pointer)的问题。因此,使用智能指针来管理动态内存是一个更好的选择。
12.1.2 shared_ptr
shared_ptr
是C++标准库中的智能指针类型之一,用于共享所有权。多个shared_ptr
实例可以共享同一个对象,当最后一个shared_ptr
被销毁时,对象的内存会被自动释放。
示例代码
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::cout << "Shared_ptr value: " << *p1 << std::endl;
std::shared_ptr<int> p2 = p1; // p1和p2共享同一个对象
std::cout << "Use count: " << p1.use_count() << std::endl;
return 0;
}
12.1.3 shared_ptr
与new
结合使用
虽然推荐使用std::make_shared
来创建shared_ptr
,但也可以直接与new
结合使用。这种方式需要注意,在创建shared_ptr
时要确保对象的内存被正确管理。
示例代码
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> p1(new int(42));
std::cout << "Shared_ptr value: " << *p1 << std::endl;
std::shared_ptr<int> p2 = p1; // p1和p2共享同一个对象
std::cout << "Use count: " << p1.use_count() << std::endl;
return 0;
}
12.1.4 unique_ptr
unique_ptr
是独占所有权的智能指针,不能被复制,只能被移动。这意味着在同一时间只能有一个unique_ptr
指向特定对象。使用unique_ptr
可以确保动态内存不会被多个指针同时管理,从而避免了双重释放的问题。
示例代码
#include <iostream>
#include <memory>
void useUniquePtr() {
std::unique_ptr<int> p1(new int(42));
std::cout << "Value: " << *p1 << std::endl;
// 将所有权转移给另一个unique_ptr
std::unique_ptr<int> p2 = std::move(p1);
if (!p1) {
std::cout << "p1 is null after move" << std::endl;
}
std::cout << "Value after move: " << *p2 << std::endl;
}
int main() {
useUniquePtr();
return 0;
}
12.1.5 weak_ptr
weak_ptr
是一种弱引用智能指针,不影响shared_ptr
的引用计数。它通常与shared_ptr
一起使用,以避免循环引用导致的内存泄漏。weak_ptr
不能直接访问对象,需要先将其转换为shared_ptr
。
示例代码
#include <iostream>
#include <memory>
void useWeakPtr() {
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // wp是sp的弱引用
std::cout << "Use count before reset: " << sp.use_count() << std::endl;
sp.reset(); // 释放sp持有的对象
if (wp.expired()) {
std::cout << "Weak pointer is expired" << std::endl;
} else {
std::shared_ptr<int> sp2 = wp.lock(); // 将wp转换为shared_ptr
std::cout << "Value: " << *sp2 << std::endl;
}
}
int main() {
useWeakPtr();
return 0;
}
12.1.6 智能指针与异常
智能指针在异常处理时尤为有用。由于智能指针会自动释放所管理的内存,因此可以防止因异常抛出导致的内存泄漏。
示例代码
#include <iostream>
#include <memory>
void mayThrow() {
throw std::runtime_error("An error occurred");
}
void useSmartPointerWithException() {
std::shared_ptr<int> sp = std::make_shared<int>(42);
try {
mayThrow();
} catch (const std::exception& e) {
std::cout << "Exception caught: " << e.what() << std::endl;
}
std::cout << "Value: " << *sp << std::endl;
}
int main() {
useSmartPointerWithException();
return 0;
}
在上述代码中,即使mayThrow
函数抛出异常,智能指针sp
也会确保在其作用域结束时正确释放内存。
重点与难点分析
重点:
- 理解动态内存分配与释放:掌握
new
和delete
操作符的使用。 - 掌握智能指针的基本概念和使用方法:了解
std::unique_ptr
、std::shared_ptr
和std::weak_ptr
的特点和用法。 - 避免内存泄漏和双重释放:通过使用智能指针自动管理内存,减少内存管理错误。
难点:
- 智能指针的所有权管理:理解
std::unique_ptr
的独占所有权和std::shared_ptr
的共享所有权。 - 循环引用问题的解决:了解如何使用
std::weak_ptr
避免std::shared_ptr
之间的循环引用。
练习题解析
- 练习12.1:编写一个程序,使用
std::unique_ptr
管理动态分配的整数,并输出其值。
-
- 示例代码:
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> p(new int(42));
std::cout << "Value: " << *p << std::endl;
return 0;
}
- 练习12.2:编写一个程序,使用
std::shared_ptr
管理动态分配的字符串,并输出其值和引用计数。
-
- 示例代码:
#include <iostream>
#include <memory>
#include <string>
int main() {
std::shared_ptr<std::string> p1 = std::make_shared<std::string>("Hello, World!");
std::shared_ptr<std::string> p2 = p1; // p1和p2共享同一个字符串
std::cout << "Value: " << *p1 << std::endl;
std::cout << "Use count: " << p1.use_count() << std::endl;
return 0;
}
- 练习12.3:编写一个程序,使用
std::weak_ptr
和std::shared_ptr
避免循环引用,验证std::weak_ptr
的有效性。
-
- 示例代码:
#include <iostream>
#include <memory>
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev;
};
int main() {
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1;
std::cout << "Node1 use count: " << node1.use_count() << std::endl;
std::cout << "Node2 use count: " << node2.use_count() << std::endl;
node1.reset();
node2.reset();
return 0;
}
总结与提高
本节总结:
- 了解了动态内存分配与释放的基本概念和操作,掌握了
new
和delete
的使用方法。 - 理解了智能指针的基本概念,掌握了
std::unique_ptr
、std::shared_ptr
和std::weak_ptr
的使用方法。 - 通过智能指针的使用,能够有效避免内存泄漏和双重释放问题,提高了程序的可靠性和稳定性。
提高建议:
- 多练习智能指针的使用:通过编写更多涉及智能指针的程序,熟悉各种智能指针的用法,提高对动态内存管理的能力。
- 深入理解智能指针的原理:通过阅读文档和相关书籍,深入理解智能指针的实现原理和使用场景,提高编写高效代码的能力。
- 避免常见内存管理错误:在实际项目中,尽量使用智能指针管理动态内存,避免手动管理内存带来的错误,提高代码的可读性和可维护性。
12.2 动态数组
动态数组是指在运行时动态分配内存来存储数组元素的技术。在C++中,动态数组通常使用new
和delete[]
操作符来管理其内存。尽管可以直接使用这些操作符进行内存管理,但标准库提供的容器类(如std::vector
)更为方便和安全。
12.2.1 使用new
和delete[]
管理动态数组
可以使用new
操作符为动态数组分配内存,并使用delete[]
操作符释放该内存。下面是一个基本示例,展示了如何分配和释放动态数组:
示例代码
#include <iostream>
int main() {
// 使用new分配动态数组
int* arr = new int[5];
// 初始化数组
for (int i = 0; i < 5; ++i) {
arr[i] = i * 2;
}
// 输出数组元素
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// 使用delete[]释放动态数组
delete[] arr;
return 0;
}
12.2.2 使用智能指针管理动态数组
智能指针可以用来管理动态数组的生命周期,以避免手动释放内存带来的问题。C++11引入的std::unique_ptr
可以用来管理动态数组。
示例代码
#include <iostream>
#include <memory>
int main() {
// 使用unique_ptr管理动态数组
std::unique_ptr<int[]> arr(new int[5]);
// 初始化数组
for (int i = 0; i < 5; ++i) {
arr[i] = i * 2;
}
// 输出数组元素
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// 无需手动释放内存,unique_ptr会自动管理
return 0;
}
12.2.3 使用std::vector
管理动态数组
虽然可以直接使用new
和delete[]
来管理动态数组,但使用标准库中的std::vector
通常是更好的选择。std::vector
是一个动态数组容器,提供了许多方便的成员函数来管理数组元素的添加、删除和访问。
示例代码
#include <iostream>
#include <vector>
int main() {
// 使用vector管理动态数组
std::vector<int> vec(5);
// 初始化数组
for (int i = 0; i < 5; ++i) {
vec[i] = i * 2;
}
// 输出数组元素
for (int i = 0; i < 5; ++i) {
std::cout << vec[i] << " ";
}
std::cout << std::endl;
// vector会自动管理内存,无需手动释放
return 0;
}
12.2.4 allocator
类
C++标准库提供了allocator
类,用于更灵活和低级别的内存管理。allocator
类允许我们分配未初始化的内存,并在其中构造对象。
示例代码
#include <iostream>
#include <memory>
int main() {
std::allocator<int> alloc; // 创建allocator对象
int* arr = alloc.allocate(5); // 分配未初始化的内存
for (int i = 0; i < 5; ++i) {
alloc.construct(&arr[i], i * 2); // 在分配的内存中构造对象
}
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
for (int i = 0; i < 5; ++i) {
alloc.destroy(&arr[i]); // 销毁对象
}
alloc.deallocate(arr, 5); // 释放内存
return 0;
}
重点与难点分析
重点:
- 动态数组的基本操作:掌握使用
new
和delete[]
分配和释放动态数组内存的方法。 - 智能指针的应用:理解如何使用
std::unique_ptr
管理动态数组的内存。 - 标准库容器的使用:熟悉
std::vector
的用法,理解其在动态数组管理中的优势。 allocator
类的使用:学习使用allocator
类进行更灵活的内存管理。
难点:
- 手动内存管理的注意事项:在使用
new
和delete[]
时,确保正确匹配,避免内存泄漏和未定义行为。 - 智能指针与原生指针的区别:理解智能指针如何自动管理内存,避免手动释放带来的问题。
- 选择合适的工具:在实际编程中,根据具体需求选择使用原生指针、智能指针或标准库容器。
- 低级内存管理:理解和使用
allocator
类进行手动内存管理。
练习题解析
- 练习12.4:编写一个程序,使用
new
和delete[]
管理动态分配的数组,并输出其元素。
-
- 示例代码:
#include <iostream>
int main() {
int* arr = new int[5];
for (int i = 0; i < 5; ++i) {
arr[i] = i * 2;
}
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
delete[] arr;
return 0;
}
- 练习12.5:编写一个程序,使用
std::unique_ptr
管理动态分配的数组,并输出其元素。
-
- 示例代码:
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int[]> arr(new int[5]);
for (int i = 0; i < 5; ++i) {
arr[i] = i * 2;
}
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
return 0;
}
- 练习12.6:编写一个程序,使用
std::vector
管理动态分配的数组,并输出其元素。
-
- 示例代码:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec(5);
for (int i = 0; i < 5; ++i) {
vec[i] = i * 2;
}
for (int i = 0; i < 5; ++i) {
std::cout << vec[i] << " ";
}
std::cout << std::endl;
return 0;
}
- 练习12.7:编写一个程序,使用
allocator
类管理动态分配的数组,并输出其元素。
-
- 示例代码:
#include <iostream>
#include <memory>
int main() {
std::allocator<int> alloc;
int* arr = alloc.allocate(5);
for (int i = 0; i < 5; ++i) {
alloc.construct(&arr[i], i * 2);
}
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
for (int i = 0; i < 5; ++i) {
alloc.destroy(&arr[i]);
}
alloc.deallocate(arr, 5);
return 0;
}
总结与提高
本节总结:
- 了解了动态数组的基本概念和操作,掌握了使用
new
和delete[]
管理动态数组的方法。 - 理解了智能指针在动态数组管理中的应用,掌握了使用
std::unique_ptr
管理动态数组的方法。 - 熟悉了
std::vector
的用法,理解了其在动态数组管理中的优势。 - 学习了
allocator
类的使用,掌握了更灵活的内存管理技术。
提高建议:
- 多练习动态数组的管理:通过编写更多涉及动态数组管理的程序,熟
悉各种管理方法的用法,提高对动态内存管理的能力。
2. 深入理解智能指针的原理:通过阅读文档和相关书籍,深入理解智能指针的实现原理和使用场景,提高编写高效代码的能力。
3. 优先使用标准库容器:在实际项目中,尽量使用std::vector
等标准库容器管理动态数组,以减少手动内存管理带来的错误,提高代码的可读性和可维护性。
4. 掌握低级内存管理技巧:通过练习使用allocator
类,掌握更加灵活和低级的内存管理技巧,以应对复杂的内存管理需求。
12.3 使用标准库:文本查询程序
这一小节将通过一个实际的文本查询程序示例,来演示如何使用C++标准库中的容器和算法处理动态内存。我们将实现一个简单的文本查询程序,能够读取一个文件,并允许用户查询文件中的某个单词在文中出现的次数及其所在行。
12.3.1 程序概述
文本查询程序将分为以下几个部分:
- 读取文件:读取文件内容并存储每行文本。
- 构建索引:创建一个索引,将每个单词映射到它出现的行号集合。
- 查询单词:允许用户输入一个单词,程序返回该单词出现的次数及其所在行。
示例代码
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <map>
#include <set>
class TextQuery {
public:
TextQuery(std::ifstream &infile);
std::set<int> query(const std::string &word) const;
void display_results(const std::set<int> &lines, const std::string &word) const;
private:
std::vector<std::string> file; // 存储文件每行文本
std::map<std::string, std::set<int>> word_map; // 单词到行号的映射
};
TextQuery::TextQuery(std::ifstream &infile) {
std::string line;
int line_number = 0;
while (std::getline(infile, line)) {
file.push_back(line);
std::istringstream iss(line);
std::string word;
while (iss >> word) {
word_map[word].insert(line_number);
}
++line_number;
}
}
std::set<int> TextQuery::query(const std::string &word) const {
auto it = word_map.find(word);
if (it != word_map.end()) {
return it->second;
} else {
return std::set<int>();
}
}
void TextQuery::display_results(const std::set<int> &lines, const std::string &word) const {
std::cout << "The word \"" << word << "\" occurs " << lines.size() << " times:" << std::endl;
for (int num : lines) {
std::cout << "\t(line " << num + 1 << ") " << file[num] << std::endl;
}
}
int main() {
std::ifstream infile("text.txt");
if (!infile) {
std::cerr << "Could not open the file!" << std::endl;
return 1;
}
TextQuery tq(infile);
while (true) {
std::cout << "Enter a word to search for, or q to quit: ";
std::string word;
if (!(std::cin >> word) || word == "q") break;
std::set<int> results = tq.query(word);
tq.display_results(results, word);
}
return 0;
}
12.3.2 详细分析
读取文件
程序首先打开一个文件,并逐行读取文件内容,将每行文本存储在一个std::vector<std::string>
中。这使得我们能够在查询时轻松访问和显示每行内容。
构建索引
在读取每行文本时,程序使用std::istringstream
将行文本分解为单词,并将每个单词插入到一个std::map<std::string, std::set<int>>
中,其中std::string
是单词,std::set<int>
是单词出现的行号集合。使用std::set
可以确保每个行号只出现一次。
查询单词
查询功能通过查找std::map
中的单词来实现。如果找到单词,则返回其对应的行号集合;否则,返回一个空的std::set<int>
。
显示结果
显示结果的函数接受一个行号集合和一个单词,然后输出该单词出现的次数及其所在行的内容。
重点与难点分析
重点:
- 使用标准库容器:熟悉
std::vector
、std::map
和std::set
的用法,并理解它们在不同场景下的作用。 - 文件输入输出:掌握如何使用
std::ifstream
读取文件内容,并使用std::istringstream
解析每行文本。 - 查询与显示:理解如何高效地进行单词查询,并显示查询结果。
难点:
- 索引构建:确保每个单词正确映射到其出现的所有行号,并处理重复单词和行号。
- 查询效率:利用
std::map
和std::set
的特性,确保查询操作的高效性。 - 动态内存管理:虽然标准库容器自动管理内存,但理解其背后的动态内存分配机制对于写出高效的代码仍然重要。
练习题解析
- 练习12.8:编写一个程序,读取一个文本文件,并输出文件中每个单词出现的次数。
-
- 示例代码:
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
int main() {
std::ifstream infile("text.txt");
if (!infile) {
std::cerr << "Could not open the file!" << std::endl;
return 1;
}
std::map<std::string, int> word_count;
std::string line;
while (std::getline(infile, line)) {
std::istringstream iss(line);
std::string word;
while (iss >> word) {
++word_count[word];
}
}
for (const auto &pair : word_count) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
- 练习12.9:修改文本查询程序,使其能够忽略大小写。
-
- 示例代码:
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <algorithm>
class TextQuery {
public:
TextQuery(std::ifstream &infile);
std::set<int> query(const std::string &word) const;
void display_results(const std::set<int> &lines, const std::string &word) const;
private:
std::vector<std::string> file;
std::map<std::string, std::set<int>> word_map;
static std::string to_lower(const std::string &str);
};
TextQuery::TextQuery(std::ifstream &infile) {
std::string line;
int line_number = 0;
while (std::getline(infile, line)) {
file.push_back(line);
std::istringstream iss(line);
std::string word;
while (iss >> word) {
word = to_lower(word);
word_map[word].insert(line_number);
}
++line_number;
}
}
std::set<int> TextQuery::query(const std::string &word) const {
auto it = word_map.find(to_lower(word));
if (it != word_map.end()) {
return it->second;
} else {
return std::set<int>();
}
}
void TextQuery::display_results(const std::set<int> &lines, const std::string &word) const {
std::cout << "The word \"" << word << "\" occurs " << lines.size() << " times:" << std::endl;
for (int num : lines) {
std::cout << "\t(line " << num + 1 << ") " << file[num] << std::endl;
}
}
std::string TextQuery::to_lower(const std::string &str) {
std::string lower_str = str;
std::transform(lower_str.begin(), lower_str.end(), lower_str.begin(), ::tolower);
return lower_str;
}
int main() {
std::ifstream infile("text.txt");
if (!infile) {
std::cerr << "Could not open the file!" << std::endl;
return 1;
}
TextQuery tq(infile);
while (true) {
std::cout << "Enter a word to search for, or q to quit: ";
std::string word;
if (!(std::cin >> word) || word == "q") break;
std::set<int> results = tq.query(word);
tq.display_results(results, word);
}
return 0;
}
- 练习12.10:编写一个程序,允许用户查询多个单词,并显示每个单词出现的次数及其所在行。
-
- 示例代码:
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <map>
#include <set>
class TextQuery {
public:
TextQuery(std::ifstream &infile);
std::map<std::string, std::set<int>> query(const std::vector<std::string> &words) const;
void display_results(const std::map<std::string, std::set<int>> &results) const;
private:
std::vector<std::string> file;
std::map<std::string, std::set<int>> word_map;
};
TextQuery::TextQuery(std::ifstream &infile) {
std::string line;
int line_number = 0;
while (std::getline(infile, line)) {
file.push_back(line);
std::istringstream iss(line);
std::string word;
while (iss >> word) {
word_map[word].insert(line_number);
}
++line_number;
}
}
std::map<std::string, std::set<int>> TextQuery::query(const std::vector<std::string> &words) const {
std::map<std::string, std::set<int>> results;
for (const auto &word : words) {
auto it = word_map.find(word);
if (it != word_map.end()) {
results[word] = it->second;
}
}
return results;
}
void TextQuery::display_results(const std::map<std::string, std::set<int>> &results) const {
for (const auto &pair : results) {
std::cout << "The word \"" << pair.first << "\" occurs " << pair.second.size() << " times:" << std::endl;
for (int num : pair.second) {
std::cout << "\t(line " << num + 1 << ") " << file[num] << std::endl;
}
}
}
int main() {
std::ifstream infile("text.txt");
if (!infile) {
std::cerr << "Could not open the file!" << std::endl;
return 1;
}
TextQuery tq(infile);
while (true) {
std::cout << "Enter words to search for, separated by spaces, or q to quit: ";
std::string line;
if (!std::getline(std::cin, line) || line == "q") break;
std::istringstream iss(line);
std::vector<std::string> words;
std::string word;
while (iss >> word) {
words.push_back(word);
}
auto results = tq.query(words);
tq.display_results(results);
}
return 0;
}
总结与提高
本节总结:
- 通过实现一个实际的文本查询程序,了解了如何使用C++标准库中的容器和算法处理动态内存。
- 掌握了文件输入输出、字符串处理、容器的使用和查询结果的显示。
- 理解了如何高效地构建索引,以便快速查询和显示结果。
提高建议:
- 多练习实际项目:通过更多实际项目练习,巩固对标准库容器和算法的使用,提高代码编写能力。
- 优化程序性能:在实际项目中,考虑程序的性能优化,如使用更高效的数据结构和算法。
- 深入理解标准库:通过阅读文档和相关书籍,深入理解C++标准库的各个组件及其使用场景,提高编写高效代码的能力。
本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“iShare爱分享”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。