模块八:文件输入输出 (数据持久化)
在之前的模块中,我们学习了如何使用程序处理数据。然而,当程序结束运行时,这些数据通常会丢失。数据持久化 (Data Persistence) 指的是将程序中的数据存储到非易失性存储介质(如硬盘文件)中,以便在程序下次运行时能够重新加载和使用这些数据。本模块将介绍如何在 C++ 中使用文件输入输出流来实现数据持久化。
1. 文件流: #include <fstream>
要进行文件输入输出操作,我们需要包含头文件 <fstream>
。这个头文件提供了几个用于文件操作的类,最常用的包括:
std::ofstream
: 用于将数据写入文件(Output File Stream)。std::ifstream
: 用于从文件读取数据(Input File Stream)。std::fstream
: 用于同时进行文件的读写操作(File Stream)。
在使用这些类之前,请确保在你的代码中包含了 <fstream>
头文件。
C++
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
2. std::ofstream
: 写入文件
std::ofstream
类用于创建文件并向其写入数据。要使用它,你需要创建一个 std::ofstream
类的对象,并将要写入的文件名作为参数传递给构造函数。
创建并打开文件进行写入:
C++
#include <fstream>
#include <iostream>int main() {std::ofstream outputFile("my_data.txt"); // 创建一个名为 my_data.txt 的文件,并打开用于写入if (outputFile.is_open()) {outputFile << "这是要写入文件的第一行。" << std::endl;outputFile << "这是第二行数据。" << std::endl;int number = 123;outputFile << "数字: " << number << std::endl;outputFile.close(); // 写入完成后关闭文件std::cout << "数据已成功写入文件。" << std::endl;} else {std::cerr << "无法打开文件进行写入。" << std::endl;}return 0;
}
在这个例子中:
std::ofstream outputFile("my_data.txt");
创建了一个名为outputFile
的std::ofstream
对象,并尝试打开名为 "my_data.txt" 的文件。如果该文件不存在,则会创建一个新文件。如果文件已存在,其内容将被清空(默认行为)。outputFile.is_open()
方法用于检查文件是否成功打开。这是一个很重要的步骤,因为文件可能由于各种原因(例如,权限问题、磁盘空间不足等)而无法打开。- 我们使用插入运算符 (
<<
) 将数据写入文件流outputFile
,这与使用std::cout
向控制台输出数据非常相似。std::endl
用于在文件中插入一个换行符。 outputFile.close();
用于显式关闭文件。虽然当outputFile
对象销毁时文件通常会自动关闭,但显式关闭是一个好的习惯,可以确保所有数据都被写入磁盘。
3. std::ifstream
: 读取文件
std::ifstream
类用于从文件中读取数据。与写入类似,你需要创建一个 std::ifstream
类的对象,并将要读取的文件名传递给构造函数。
打开文件并读取内容:
C++
#include <fstream>
#include <iostream>
#include <string>int main() {std::ifstream inputFile("my_data.txt"); // 打开名为 my_data.txt 的文件用于读取if (inputFile.is_open()) {std::string line;while (std::getline(inputFile, line)) { // 逐行读取文件内容std::cout << "读取到一行: " << line << std::endl;}inputFile.close(); // 读取完成后关闭文件} else {std::cerr << "无法打开文件进行读取。" << std::endl;}return 0;
}
在这个例子中:
std::ifstream inputFile("my_data.txt");
创建了一个名为inputFile
的std::ifstream
对象,并尝试打开名为 "my_data.txt" 的文件进行读取。如果文件不存在,打开操作可能会失败。- 我们再次使用
inputFile.is_open()
来检查文件是否成功打开。 std::getline(inputFile, line)
函数用于从inputFile
中读取一行数据,直到遇到换行符或文件结束符,并将读取到的内容存储到line
字符串中。这个函数会返回一个表示读取操作是否成功的布尔值,因此我们可以将其用在while
循环的条件中,以便逐行读取整个文件。inputFile.close();
用于显式关闭文件。
你也可以使用提取运算符 (>>
) 从文件中读取数据,类似于使用 std::cin
从控制台读取数据。但是,使用 >>
时,数据会以空格、制表符或换行符作为分隔符进行读取。
使用 >>
读取文件内容:
C++
#include <fstream>
#include <iostream>
#include <string>int main() {std::ifstream inputFile("my_data.txt");if (inputFile.is_open()) {std::string word;int number;while (inputFile >> word >> number) { // 尝试读取一个字符串和一个整数std::cout << "读取到的单词: " << word << ", 数字: " << number << std::endl;}inputFile.close();} else {std::cerr << "无法打开文件进行读取。" << std::endl;}return 0;
}
注意: 如果文件 "my_data.txt" 的内容与代码中期望的格式不符(例如,连续的字符串或数字),则读取操作可能会失败或产生意想不到的结果。
4. 打开文件、检查文件是否成功打开
正如我们在上面的例子中看到的,打开文件是进行文件操作的第一步。除了在创建文件流对象时通过构造函数指定文件名外,你还可以先创建一个文件流对象,然后使用 open()
方法打开文件。
使用 open()
方法打开文件:
C++
#include <fstream>
#include <iostream>int main() {std::ofstream outputFile;outputFile.open("another_data.txt");if (outputFile.is_open()) {outputFile << "使用 open() 方法写入数据。" << std::endl;outputFile.close();} else {std::cerr << "无法打开文件 'another_data.txt'。" << std::endl;}return 0;
}
检查文件是否成功打开:
检查文件是否成功打开至关重要。你可以使用以下方法进行检查:
is_open()
方法: 返回一个布尔值,表示文件是否成功打开。- 将文件流对象作为布尔值进行检查: 文件流对象可以隐式转换为布尔值。如果文件成功打开,则转换为
true
,否则转换为false
。
C++
#include <fstream>
#include <iostream>int main() {std::ifstream inputFile("non_existent_file.txt");if (inputFile.is_open()) {std::cout << "文件已成功打开。" << std::endl;inputFile.close();} else {std::cerr << "文件打开失败。" << std::endl;}std::ofstream outputFile("my_data.txt");if (outputFile) { // 隐式转换为布尔值std::cout << "输出文件已成功打开。" << std::endl;outputFile.close();} else {std::cerr << "输出文件打开失败。" << std::endl;}return 0;
}
5. 读写文件内容 (类似 std::cin
/std::cout
)
我们已经看到,可以使用插入运算符 (<<
) 将数据写入 std::ofstream
对象,使用提取运算符 (>>
) 从 std::ifstream
对象读取数据,这与使用 std::cout
和 std::cin
非常相似。
写入不同类型的数据:
C++
std::ofstream outputFile("mixed_data.txt");
if (outputFile.is_open()) {outputFile << "姓名: " << "张三" << std::endl;outputFile << "年龄: " << 30 << std::endl;outputFile << "身高: " << 1.75 << std::endl;outputFile.close();
}
读取不同类型的数据:
C++
std::ifstream inputFile("mixed_data.txt");
if (inputFile.is_open()) {std::string nameLabel, name;std::string ageLabel;int age;std::string heightLabel;double height;inputFile >> nameLabel >> name;inputFile >> ageLabel >> age;inputFile >> heightLabel >> height;std::cout << nameLabel << name << std::endl;std::cout << ageLabel << age << std::endl;std::cout << heightLabel << height << std::endl;inputFile.close();
}
使用 getline()
读取包含空格的行:
如前所述,std::getline()
函数非常适合读取包含空格的整行文本。
C++
std::ofstream outputFile("paragraph.txt");
if (outputFile.is_open()) {outputFile << "这是一个包含多个单词的段落。" << std::endl;outputFile << "第二行也有一些内容。" << std::endl;outputFile.close();
}std::ifstream inputFile("paragraph.txt");
if (inputFile.is_open()) {std::string line;while (std::getline(inputFile, line)) {std::cout << "读取到的行: " << line << std::endl;}inputFile.close();
}
6. 关闭文件 (close()
方法)
虽然当文件流对象超出作用域时,文件通常会被自动关闭,但显式地调用 close()
方法是一个良好的编程习惯。它可以确保所有缓冲的数据都被写入磁盘,并且释放与文件关联的系统资源。
C++
#include <fstream>
#include <iostream>int main() {std::ofstream outputFile("temp_file.txt");if (outputFile.is_open()) {outputFile << "一些临时数据。" << std::endl;outputFile.close(); // 显式关闭文件std::cout << "文件已关闭。" << std::endl;} else {std::cerr << "无法打开文件。" << std::endl;}return 0;
}
7. 实战项目 6: 持久化的学生成绩管理
我们将选择第一个目标:让【学生成绩管理】程序能够将学生数据保存到文件中,并在下次运行时加载。
步骤:
1. 确定文件格式。
我们将使用一个简单的文本文件格式来存储学生数据。每一行代表一个学生的信息,姓名和分数之间用逗号或其他分隔符(例如空格)分隔。
示例文件内容 (student_data.txt
):
张三 95
李四 88
王五 92
2. 修改 Student
类(如果尚未完成)。
确保你的 Student
类包含 getName()
和 getScore()
方法。
3. 实现保存学生数据到文件的函数。
创建一个名为 saveStudentsToFile
的函数,该函数接收一个 std::vector<Student>
对象和一个文件名作为参数。函数将遍历 vector
中的每个 Student
对象,并将学生的姓名和分数写入到文件中。
C++
#include <fstream>
#include <iostream>
#include <vector>
#include <string>// 假设 Student 类的定义如下(在 student.h 或当前文件中):
class Student {
private:std::string name;int score;public:Student(const std::string& studentName, int studentScore) : name(studentName), score(studentScore) {}std::string getName() const { return name; }int getScore() const { return score; }
};void saveStudentsToFile(const std::vector<Student>& students, const std::string& filename) {std::ofstream outputFile(filename);if (outputFile.is_open()) {for (const auto& student : students) {outputFile << student.getName() << " " << student.getScore() << std::endl;}outputFile.close();std::cout << "学生数据已保存到文件 " << filename << std::endl;} else {std::cerr << "无法打开文件 " << filename << " 进行写入。" << std::endl;}
}
4. 实现从文件加载学生数据的函数。
创建一个名为 loadStudentsFromFile
的函数,该函数接收一个文件名作为参数,并返回一个 std::vector<Student>
对象。函数将尝试打开文件,逐行读取学生数据,并创建 Student
对象添加到 vector
中。
C++
#include <fstream>
#include <iostream>
#include <vector>
#include <string>
#include <sstream> // 用于字符串流// 假设 Student 类的定义如上std::vector<Student> loadStudentsFromFile(const std::string& filename) {std::vector<Student> students;std::ifstream inputFile(filename);if (inputFile.is_open()) {std::string line;while (std::getline(inputFile, line)) {std::stringstream ss(line);std::string name;int score;if (ss >> name >> score) {students.emplace_back(name, score);} else {std::cerr << "警告:无法解析文件中的行: " << line << std::endl;}}inputFile.close();std::cout << "学生数据已从文件 " << filename << " 加载。" << std::endl;} else {std::cerr << "无法打开文件 " << filename << " 进行读取。" << std::endl;}return students;
}
5. 修改 main
函数。
在 main
函数的开始部分,调用 loadStudentsFromFile
函数加载已保存的学生数据。在程序结束前(或者在用户选择保存时),调用 saveStudentsToFile
函数将当前的学生数据保存到文件中。
修改后的 main
函数示例(简化版):
C++
#include <iostream>
#include <vector>
#include <string>
#include <numeric>// 假设 Student 类的定义和 saveStudentsToFile, loadStudentsFromFile 函数如上int main() {std::string filename = "student_data.txt";std::vector<Student> students = loadStudentsFromFile(filename);int numStudents;std::cout << "请输入要添加的学生人数 (如果不需要添加,请输入 0): ";std::cin >> numStudents;std::cin.ignore();for (int i = 0; i < numStudents; ++i) {std::cout << "请输入第 " << i + 1 << " 个学生的姓名: ";std::string name;std::getline(std::cin, name);std::cout << "请输入第 " << i + 1 << " 个学生的分数: ";int score;std::cin >> score;std::cin.ignore();students.emplace_back(name, score);}if (!students.empty()) {int totalScore = 0;for (const auto& student : students) {totalScore += student.getScore();std::cout << student.getName() << " 的分数是: " << student.getScore() << std::endl;}double averageScore = static_cast<double>(totalScore) / students.size();std::cout << "平均分是: " << averageScore << std::endl;} else {std::cout << "没有学生数据。" << std::endl;}saveStudentsToFile(students, filename); // 在程序结束前保存数据return 0;
}
现在,每次运行程序时,它会尝试从 "student_data.txt" 文件加载学生数据。你可以在程序运行期间添加新的学生数据,并在程序结束时将所有数据保存回文件。下次运行时,之前的数据将被重新加载。
模块九:进一步学习 (指引方向) (更完善版本)
目录
- 标准模板库 (STL) 深入 1.1.
std::map
(进阶) 1.1.1. 迭代器的更多用法 1.1.2. 自定义比较函数 1.1.3.std::multimap
1.2.std::set
(进阶) 1.2.1. 迭代器的更多用法 1.2.2. 自定义比较函数 1.2.3.std::multiset
和std::unordered_set
1.3. 算法 (<algorithm>
) (进阶) 1.3.1. 更多常用算法 1.3.2. 迭代器和算法的配合 1.3.3. Lambda 表达式和函数对象 - 更深入的 OOP 2.1. 继承 (进阶) 2.1.1. 构造函数和析构函数的调用顺序 2.1.2. 虚继承和菱形继承问题 2.1.3. final 关键字 2.2. 多态 (进阶) 2.2.1. 静态多态(编译时多态) 2.2.2. 抽象类和纯虚函数 2.2.3. 接口 (Interface) 的概念
- 异常处理 3.1.
try
,catch
,throw
(进阶) 3.1.1. 标准异常类 3.1.2. 异常安全 3.1.3. 自定义异常类 3.1.4.noexcept
规范 - 模板 4.1. 函数模板 (进阶) 4.1.1. 多个模板参数 4.1.2. 模板特化 4.2. 类模板 (进阶) 4.2.1. 多个模板参数 4.2.2. 成员函数模板 4.2.3. 类模板的特化 4.2.4. 模板元编程 (简介)
- 智能指针 5.1.
std::unique_ptr
(进阶) 5.1.1. 自定义删除器 (Custom Deleters) 5.1.2.unique_ptr
和数组 5.2.std::shared_ptr
(进阶) 5.2.1. 循环引用和std::weak_ptr
5.2.2.std::shared_ptr
的线程安全性 - 调试技巧 6.1. 如何使用调试器 (更详细) 6.1.1. 常用调试器命令和操作 (以 GDB 和 Visual Studio 为例) 6.1.2. 高级调试技巧 6.1.3. 使用断言 (
assert
) - 构建系统 7.1. CMake (简介) (更详细) 7.1.1. 更复杂的
CMakeLists.txt
示例 7.1.2. 外部依赖管理 7.1.3. 构建类型 (Debug, Release) 7.1.4. 其他构建系统简介
1. 标准模板库 (STL) 深入
1.1. std::map
(进阶)
1.1.1. 迭代器的更多用法
除了基本的 begin()
和 end()
之外,std::map
还提供了其他迭代器,例如 rbegin()
和 rend()
用于反向迭代。
C++
#include <iostream>
#include <map>
#include <string>int main() {std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};std::cout << "Ages in reverse order:" << std::endl;for (auto it = ages.rbegin(); it != ages.rend(); ++it) {std::cout << it->first << ": " << it->second << std::endl;}return 0;
}
1.1.2. 自定义比较函数
std::map
默认使用 <
运算符对键进行排序。您可以提供自定义的比较函数或函数对象来改变排序方式。
C++
#include <iostream>
#include <map>
#include <string>struct CaseInsensitiveCompare {bool operator()(const std::string& a, const std::string& b) const {return std::lexicographical_compare(a.begin(), a.end(),b.begin(), b.end(),[](char c1, char c2) {return std::tolower(c1) < std::tolower(c2);});}
};int main() {std::map<std::string, int, CaseInsensitiveCompare> data;data["Apple"] = 1;data["banana"] = 2;data["Cherry"] = 3;std::cout << "Data (case-insensitive sorted):" << std::endl;for (const auto& pair : data) {std::cout << pair.first << ": " << pair.second << std::endl;}return 0;
}
1.1.3. std::multimap
std::multimap
允许存储具有相同键的多个键值对。
C++
#include <iostream>
#include <map>
#include <string>int main() {std::multimap<std::string, int> scores;scores.insert({"Alice", 90});scores.insert({"Bob", 85});scores.insert({"Alice", 95}); // 允许重复的键std::cout << "Scores:" << std::endl;for (const auto& pair : scores) {std::cout << pair.first << ": " << pair.second << std::endl;}auto range = scores.equal_range("Alice");std::cout << "\nAlice's scores:" << std::endl;for (auto it = range.first; it != range.second; ++it) {std::cout << it->second << std::endl;}return 0;
}
1.2. std::set
(进阶)
1.2.1. 迭代器的更多用法
类似于 std::map
,std::set
也支持 rbegin()
和 rend()
进行反向迭代。
1.2.2. 自定义比较函数
您可以为 std::set
提供自定义的比较函数或函数对象来定义元素的排序方式。
C++
#include <iostream>
#include <set>struct ReverseIntCompare {bool operator()(int a, int b) const {return a > b;}
};int main() {std::set<int, ReverseIntCompare> numbers = {5, 2, 8, 1, 9};std::cout << "Numbers in reverse order:" << std::endl;for (int num : numbers) {std::cout << num << " ";}std::cout << std::endl;return 0;
}
1.2.3. std::multiset
和 std::unordered_set
std::multiset
: 允许存储重复的元素,并保持排序。std::unordered_set
: 存储唯一的元素,但不保证排序。它通常使用哈希表实现,因此在平均情况下查找、插入和删除操作的时间复杂度为 O(1)。
C++
#include <iostream>
#include <set>
#include <unordered_set>int main() {std::multiset<int> multiNumbers = {5, 2, 8, 2, 9};std::cout << "MultiNumbers:" << std::endl;for (int num : multiNumbers) {std::cout << num << " ";}std::cout << std::endl;std::unordered_set<int> unorderedNumbers = {5, 2, 8, 1, 9, 2}; // 重复的 2 只会保留一个std::cout << "UnorderedNumbers (no guaranteed order):" << std::endl;for (int num : unorderedNumbers) {std::cout << num << " ";}std::cout << std::endl;return 0;
}
1.3. 算法 (<algorithm>
) (进阶)
1.3.1. 更多常用算法
std::count()
: 统计容器中特定值的出现次数。std::remove()
和std::remove_if()
: 从容器中移除特定的值或满足特定条件的元素(注意:这些算法不会改变容器的大小,通常需要与容器的erase()
方法一起使用)。std::replace()
和std::replace_if()
: 将容器中特定的值或满足特定条件的元素替换为新的值。std::accumulate()
: 计算容器中元素的总和(或其他累积操作)。
C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric> // for std::accumulateint main() {std::vector<int> data = {1, 2, 2, 3, 4, 2, 5};int countOfTwo = std::count(data.begin(), data.end(), 2);std::cout << "Count of 2: " << countOfTwo << std::endl;auto it = std::remove(data.begin(), data.end(), 2);data.erase(it, data.end());std::cout << "Data after removing 2s: ";for (int num : data) {std::cout << num << " ";}std::cout << std::endl;std::vector<int> numbers = {1, 2, 3, 4, 5};int sum = std::accumulate(numbers.begin(), numbers.end(), 0);std::cout << "Sum of numbers: " << sum << std::endl;return 0;
}
1.3.2. 迭代器和算法的配合
STL 算法通常通过迭代器来操作容器中的元素。理解不同类型的迭代器(例如,输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器)对于有效地使用算法至关重要。不同的算法对迭代器有不同的要求。
1.3.3. Lambda 表达式和函数对象
Lambda 表达式和函数对象(也称为仿函数)可以作为参数传递给算法,以自定义算法的行为。
C++
#include <iostream>
#include <vector>
#include <algorithm>int main() {std::vector<int> numbers = {1, 2, 3, 4, 5};// 使用 lambda 表达式对偶数进行平方std::vector<int> squaredEvens;std::for_each(numbers.begin(), numbers.end(),[&](int n) {if (n % 2 == 0) {squaredEvens.push_back(n * n);}});std::cout << "Squared even numbers: ";for (int num : squaredEvens) {std::cout << num << " ";}std::cout << std::endl;// 使用函数对象判断是否为奇数struct IsOdd {bool operator()(int n) const {return n % 2 != 0;}};auto it = std::find_if(numbers.begin(), numbers.end(), IsOdd());if (it != numbers.end()) {std::cout << "First odd number: " << *it << std::endl;}return 0;
}
2. 更深入的 OOP
2.1. 继承 (进阶)
2.1.1. 构造函数和析构函数的调用顺序
在派生类对象创建时,基类的构造函数先于派生类的构造函数被调用。当派生类对象销毁时,派生类的析构函数先于基类的析构函数被调用。这是为了确保基类的初始化在派生类之前完成,而清理工作则相反。
2.1.2. 虚继承和菱形继承问题
当一个类从多个基类继承,而这些基类又继承自同一个更上层的基类时,就会出现菱形继承问题。这会导致派生类中存在多个相同的上层基类子对象。虚继承(使用 virtual
关键字)可以解决这个问题,确保只有一个共享的上层基类子对象。
C++
#include <iostream>class Grandparent {
public:Grandparent() { std::cout << "Grandparent constructor" << std::endl; }virtual ~Grandparent() { std::cout << "Grandparent destructor" << std::endl; }void commonFunction() { std::cout << "Grandparent function" << std::endl; }
};class Parent1 : public virtual Grandparent {
public:Parent1() { std::cout << "Parent1 constructor" << std::endl; }~Parent1() { std::cout << "Parent1 destructor" << std::endl; }
};class Parent2 : public virtual Grandparent {
public:Parent2() { std::cout << "Parent2 constructor" << std::endl; }~Parent2() { std::cout << "Parent2 destructor" << std::endl; }
};class Child : public Parent1, public Parent2 {
public:Child() { std::cout << "Child constructor" << std::endl; }~Child() { std::cout << "Child destructor" << std::endl; }
};int main() {Child c;c.commonFunction();return 0;
}
2.1.3. final
关键字
final
关键字可以用于防止类被继承或防止虚函数被进一步重写。
C++
class Base {
public:virtual void method1() {}virtual void method2() final {} // method2 不能在派生类中被重写
};class Derived : public Base {
public:void method1() override {} // 可以重写// void method2() override {} // 错误:method2 是 final 的
};class FinalClass final { // FinalClass 不能被继承
public:void method() {}
};// class AnotherDerived : public FinalClass {}; // 错误:FinalClass 是 final 的
2.2. 多态 (进阶)
2.2.1. 静态多态(编译时多态)
静态多态主要通过函数重载和模板来实现。编译器在编译时根据函数参数的类型或模板参数来确定要调用的具体函数。
函数重载示例:
C++
#include <iostream>void print(int x) { std::cout << "Printing integer: " << x << std::endl; }
void print(double x) { std::cout << "Printing double: " << x << std::endl; }int main() {print(10); // 调用 print(int)print(3.14); // 调用 print(double)return 0;
}
模板示例 (之前已经展示过): 编译器会为每种实际使用的类型生成特定的函数或类。
2.2.2. 抽象类和纯虚函数
包含至少一个纯虚函数的类被称为抽象类。抽象类不能被实例化。派生类必须重写基类中的所有纯虚函数才能成为非抽象类并被实例化。纯虚函数通过在虚函数声明后添加 = 0
来声明。
C++
#include <iostream>class Shape {
public:virtual void draw() const = 0; // 纯虚函数virtual double area() const = 0; // 纯虚函数virtual ~Shape() {}
};class Circle : public Shape {
private:double radius_;
public:Circle(double r) : radius_(r) {}void draw() const override { std::cout << "Drawing a circle." << std::endl; }double area() const override { return 3.14159 * radius_ * radius_; }
};// Shape shape; // 错误:Shape 是抽象类,不能实例化int main() {Circle circle(5.0);circle.draw();std::cout << "Circle area: " << circle.area() << std::endl;return 0;
}
2.2.3. 接口 (Interface) 的概念
在 C++ 中,没有显式的 interface
关键字(像在 Java 或 C# 中那样)。然而,可以通过只包含纯虚函数的抽象类来模拟接口的行为。接口定义了一组派生类必须实现的方法,从而实现“契约式编程”。
3. 异常处理
3.1. try
, catch
, throw
(进阶)
3.1.1. 标准异常类
<stdexcept>
头文件定义了一系列标准的异常类,可以用于报告不同类型的错误,例如 std::runtime_error
, std::logic_error
, std::out_of_range
等。
C++
#include <iostream>
#include <stdexcept>
#include <vector>int main() {std::vector<int> data = {1, 2, 3};int index = 5;try {if (index >= data.size()) {throw std::out_of_range("Index out of bounds");}std::cout << "Element at index " << index << ": " << data.at(index) << std::endl;} catch (const std::out_of_range& error) {std::cerr << "Error: " << error.what() << std::endl;}return 0;
}
3.1.2. 异常安全
编写异常安全的代码意味着即使在异常被抛出的情况下,程序也能保持其状态的完整性,并且不会发生资源泄漏(例如,内存泄漏)。有不同的异常安全级别:
- 基本保证: 如果异常被抛出,程序不会崩溃,并且所有对象都处于有效的状态(但可能不是原始状态)。
- 强保证: 如果异常被抛出,程序的状态不会发生改变(就像操作从未发生过一样)。这通常通过“先复制再交换”等技术实现。
- 无抛出保证: 操作永远不会抛出异常(通常使用
noexcept
标记)。
3.1.3. 自定义异常类
您可以创建自己的异常类,通常通过继承自 std::exception
或其派生类来提供更具体的错误信息。
C++
#include <iostream>
#include <exception>
#include <string>class MyCustomError : public std::runtime_error {
public:MyCustomError(const std::string& message) : std::runtime_error(message) {}
};void processData(int value) {if (value < 0) {throw MyCustomError("Value cannot be negative: " + std::to_string(value));}std::cout << "Processing data: " << value << std::endl;
}int main() {try {processData(-5);} catch (const MyCustomError& error) {std::cerr << "Custom Error: " << error.what() << std::endl;} catch (const std::runtime_error& error) {std::cerr << "Runtime Error: " << error.what() << std::endl;} catch (...) {std::cerr << "An unknown error occurred." << std::endl;}return 0;
}
3.1.4. noexcept
规范
noexcept
是一个函数规范,用于表明函数是否会抛出异常。如果一个声明为 noexcept
的函数抛出了异常,程序可能会立即终止。使用 noexcept
可以帮助编译器进行优化,并且对于某些操作(例如,移动构造函数和析构函数)非常重要。
C++
#include <iostream>
#include <stdexcept>void mightThrow() {throw std::runtime_error("This function might throw.");
}void doesNotThrow() noexcept {std::cout << "This function does not throw." << std::endl;
}int main() {try {mightThrow();} catch (const std::runtime_error& e) {std::cerr << "Caught exception: " << e.what() << std::endl;}doesNotThrow();// 如果 doesNotThrow 内部抛出异常,程序可能会终止// try {// throw std::runtime_error("Exception from noexcept function");// } catch (...) {// std::cerr << "Caught exception (should not happen): " << std::endl;// }return 0;
}
4. 模板
4.1. 函数模板 (进阶)
4.1.1. 多个模板参数
函数模板可以接受多个类型参数。
C++
#include <iostream>template <typename T1, typename T2>
void printPair(const T1& a, const T2& b) {std::cout << "First: " << a << ", Second: " << b << std::endl;
}int main() {printPair(10, "Hello");printPair(3.14, true);return 0;
}
4.1.2. 模板特化
模板特化允许您为特定的类型提供模板的不同实现。
C++
#include <iostream>
#include <string>template <typename T>
void print(const T& value) {std::cout << "Generic print: " << value << std::endl;
}// 为 int 类型提供特化版本
template <>
void print<int>(const int& value) {std::cout << "Specialized print for int: " << value * 2 << std::endl;
}// 为 std::string 类型提供特化版本
template <>
void print<std::string>(const std::string& value) {std::cout << "Specialized print for string: " << value.length() << std::endl;
}int main() {print(5); // 输出: Specialized print for int: 10print(3.14); // 输出: Generic print: 3.14print(std::string("World")); // 输出: Specialized print for string: 5return 0;
}
4.2. 类模板 (进阶)
4.2.1. 多个模板参数
类模板也可以接受多个类型参数。
C++
#include <iostream>template <typename T1, typename T2>
class Pair {
public:Pair(const T1& first, const T2& second) : first_(first), second_(second) {}void print() const {std::cout << "First: " << first_ << ", Second: " << second_ << std::endl;}
private:T1 first_;T2 second_;
};int main() {Pair<int, std::string> myPair(100, "Example");myPair.print();return 0;
}
4.2.2. 成员函数模板
类模板的成员函数也可以是模板。
C++
#include <iostream>template <typename T>
class MyArray {
private:T* data_;int size_;
public:MyArray(int size) : size_(size), data_(new T[size]()) {}~MyArray() { delete[] data_; }template <typename U>void fill(const U& value) {for (int i = 0; i < size_; ++i) {data_[i] = static_cast<T>(value);}}void print() const {for (int i = 0; i < size_; ++i) {std::cout << data_[i] << " ";}std::cout << std::endl;}
};int main() {MyArray<double> doubleArray(5);doubleArray.fill(3); // 使用 int 类型填充 double 数组doubleArray.print();return 0;
}
4.2.3. 类模板的特化
类似于函数模板,类模板也可以进行特化,为特定的类型提供不同的实现。可以特化整个类,也可以只特化类的某些成员函数。
C++
#include <iostream>
#include <string>template <typename T>
class Printer {
public:void print(const T& value) {std::cout << "Generic Printer: " << value << std::endl;}
};// 特化 Printer<std::string>
template <>
class Printer<std::string> {
public:void print(const std::string& value) {std::cout << "String Printer: Length = " << value.length() << ", Value = " << value << std::endl;}
};int main() {Printer<int> intPrinter;intPrinter.print(123); // 输出: Generic Printer: 123Printer<std::string> stringPrinter;stringPrinter.print("Hello"); // 输出: String Printer: Length = 5, Value = Helloreturn 0;
}
4.2.4. 模板元编程 (简介)
模板元编程是一种在编译时使用模板生成代码的技术。它允许在编译阶段执行一些计算和逻辑,可以用于提高程序的性能和灵活性。模板元编程通常涉及复杂的模板技巧和类型操作。
5. 智能指针
5.1. std::unique_ptr
(进阶)
5.1.1. 自定义删除器 (Custom Deleters)
std::unique_ptr
允许您指定一个自定义的删除器,当 unique_ptr
被销毁时,将调用该删除器而不是默认的 delete
运算符。这对于管理不是通过 new
分配的资源(例如,文件句柄、自定义的内存分配器分配的内存)非常有用。
C++
#include <iostream>
#include <memory>
#include <fstream>// 自定义删除器,用于关闭文件
struct FileDeleter {void operator()(std::ofstream* file) const {if (file && file->is_open()) {file->close();std::cout << "File closed." << std::endl;}delete file;}
};int main() {std::unique_ptr<std::ofstream, FileDeleter> logFile(new std::ofstream("log.txt"));if (logFile) {*logFile << "This is a log message." << std::endl;}// 当 logFile 离开作用域时,FileDeleter 会被调用,关闭文件。return 0;
}
5.1.2. unique_ptr
和数组
std::unique_ptr
可以用于管理动态分配的数组,需要使用 unique_ptr<T[]>
的形式,并且会自动使用 delete[]
来释放内存。
C++
#include <iostream>
#include <memory>int main() {std::unique_ptr<int[]> intArray(new int[5]);for (int i = 0; i < 5; ++i) {intArray[i] = i * 2;}for (int i = 0; i < 5; ++i) {std::cout << intArray[i] << " ";}std::cout << std::endl;// 当 intArray 离开作用域时,delete[] 会被调用。return 0;
}
5.2. std::shared_ptr
(进阶)
5.2.1. 循环引用和 std::weak_ptr
当两个或多个通过 std::shared_ptr
相互引用的对象形成一个环时,它们的引用计数永远不会降至零,导致内存泄漏。std::weak_ptr
是一种不增加引用计数的智能指针,可以用来打破这种循环引用。weak_ptr
可以指向由 shared_ptr
管理的对象,但它不会阻止该对象的销毁。在使用 weak_ptr
之前,需要先将其转换为 shared_ptr
(可以使用 lock()
方法),如果原始对象已经被销毁,则转换会失败。
C++
#include <iostream>
#include <memory>class B; // 前向声明class A {
public:std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed" << std::endl; }
};class B {
public:std::weak_ptr<A> a_ptr; // 使用 weak_ptr 打破循环引用~B() { std::cout << "B destroyed" << std::endl; }
};int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a;// 如果 b_ptr 在 A 中也是 weak_ptr,或者 a_ptr 在 B 中是 shared_ptr,// 那么当 a 和 b 离开作用域时,A 和 B 的析构函数将不会被调用,导致内存泄漏。return 0;
}
5.2.2. std::shared_ptr
的线程安全性
std::shared_ptr
的控制块(包含引用计数和删除器)是线程安全的,允许多个线程安全地增加和减少引用计数。然而,通过同一个 shared_ptr
访问所管理的对象本身并不是线程安全的,需要额外的同步机制(例如,互斥锁)。
6. 调试技巧
6.1. 如何使用调试器 (更详细)
6.1.1. 常用调试器命令和操作 (以 GDB 和 Visual Studio 为例)
GDB (GNU Debugger):
break <file>:<line>
或b <file>:<line>
: 在指定文件和行号设置断点。run
或r
: 运行程序。next
或n
: 执行下一行代码(不进入函数调用)。step
或s
: 执行下一行代码(如果当前行是函数调用,则进入函数内部)。finish
: 执行完当前函数并返回。continue
或c
: 继续执行程序直到下一个断点或程序结束。print <variable>
或p <variable>
: 打印变量的值。watch <expression>
: 设置监视点,当表达式的值发生变化时暂停程序。backtrace
或bt
: 显示当前的函数调用堆栈。frame <number>
: 切换到指定堆栈帧。info locals
: 显示当前作用域的局部变量。quit
或q
: 退出 GDB。
Visual Studio Debugger:
- 设置断点: 在代码行号旁边的空白区域单击,或选中一行代码后按 F9。
- 启动调试: 按 F5 或选择“调试” -> “开始调试”。
- 单步执行:
- Step Over (F10): 执行当前行,然后跳到下一行。
- Step Into (F11): 如果当前行是函数调用,则进入该函数内部。
- Step Out (Shift + F11): 执行完当前函数并返回。
- Continue (F5): 继续执行程序。
- 查看变量: 在“局部变量”、“监视”窗口中查看变量的值。
- 调用堆栈: 在“调用堆栈”窗口中查看函数调用序列。
- 条件断点: 右键单击断点,选择“条件”,设置断点触发的条件。
- 数据断点: 可以在变量的值发生改变时触发断点(右键单击变量,选择“当值更改时中断”)。
6.1.2. 高级调试技巧
- 使用日志记录: 在关键代码路径中添加日志输出,以便在不使用调试器的情况下也能追踪程序行为。
- 二分查找法调试: 如果您不知道错误发生在哪里,可以尝试在代码中间设置断点,逐步缩小错误范围。
- 单元测试: 编写单元测试可以帮助您在早期发现和修复代码中的错误。
- 代码审查: 让其他开发人员检查您的代码,可以发现您可能忽略的错误。
- 使用静态分析工具: 静态分析工具可以在不运行代码的情况下检测潜在的错误和代码质量问题。
6.1.3. 使用断言 (assert
)
assert
是一个预处理宏,用于在运行时检查条件是否为真。如果条件为假,程序会终止并显示错误消息。断言通常用于在开发阶段检查代码中的假设是否成立。在发布版本中,断言通常会被禁用。
C++
#include <iostream>
#include <cassert>int divide(int a, int b) {assert(b != 0); // 断言:除数不能为零return a / b;
}int main() {int result = divide(10, 2);std::cout << "Result: " << result << std::endl;// divide(5, 0); // 这会触发断言,导致程序终止return 0;
}
7. 构建系统
7.1. CMake (简介) (更详细)
7.1.1. 更复杂的 CMakeLists.txt
示例
假设您的项目包含多个源文件和一个头文件目录。
CMake
cmake_minimum_required(VERSION 3.10)
project(MyAdvancedProject)# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)# 指定头文件搜索路径
include_directories(include)# 添加可执行文件,源文件包括 main.cpp 和 utils.cpp
add_executable(MyAdvancedProject main.cpp utils.cpp)# 如果需要链接库,可以使用 target_link_libraries
# target_link_libraries(MyAdvancedProject my_library)
7.1.2. 外部依赖管理
CMake 可以帮助管理外部库的依赖。常用的方法包括:
find_package()
: 用于查找系统中已安装的库。WorkspaceContent
模块: 允许在构建过程中下载和构建外部库。- 子模块 (Submodules): 将外部库作为 Git 子模块添加到项目中。
7.1.3. 构建类型 (Debug, Release)
CMake 支持不同的构建类型,通常包括 Debug 和 Release。Debug 构建包含调试信息,而 Release 构建会进行优化以提高性能。您可以在配置 CMake 时指定构建类型:
Bash
cmake -DCMAKE_BUILD_TYPE=Debug ..
# 或者
cmake -DCMAKE_BUILD_TYPE=Release ..
7.1.4. 其他构建系统简介
除了 CMake,还有其他一些流行的构建系统:
- Make: 一个经典的构建工具,使用 Makefile 来描述构建规则。
- Ninja: 一个小型且快速的构建系统,通常由其他构建系统(如 CMake)生成构建文件。
- Meson: 另一个跨平台的构建系统,旨在提供快速且用户友好的构建体验。
- Gradle 和 Maven: 主要用于 Java 项目,但也支持 C/C++ 项目。