目录
到目前为止,我们已经使用了 iostream 标准库,它提供了 cin 和 cout 方法分别用于从标准输入读取流和向标准输出写入流。
本教程介绍如何从文件读取流和向文件写入流。这就需要用到 C++ 中另一个标准库 fstream,它定义了三个新的数据类型:
数据类型 | 描述 |
---|---|
ofstream | 该数据类型表示输出文件流,用于创建文件并向文件写入信息。 |
ifstream | 该数据类型表示输入文件流,用于从文件读取信息。 |
fstream | 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。 |
要在 C++ 中进行文件处理,必须在 C++ 源代码文件中包含头文件
打开文件是进行读写操作之前的必要步骤。在 C++ 中,我们使用流对象的 open() 成员函数来打开文件。
下面是 open() 函数的标准语法:
void open(const char* filename, std::ios::openmode mode);
在这里,open() 成员函数的第一个参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式,即 std::ios::openmode。
模式标志 | 描述 |
---|---|
ios::app | 追加模式。所有写入都追加到文件末尾。 |
ios::ate | 文件打开后定位到文件末尾。 |
ios::in | 打开文件用于读取。 |
ios::out | 打开文件用于写入。 |
ios::trunc | 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。 |
您可以把以上两种或两种以上的模式结合使用。例如,如果您想要以写入模式打开文件,并希望截断文件,以防文件已存在,那么您可以使用下面的语法:
- ofstream outfile;
- outfile.open("file.dat", ios::out | ios::trunc );
类似地,您如果想要打开一个文件用于读写,可以使用下面的语法:
- ifstream afile;
- afile.open("file.dat", ios::out | ios::in );
当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。但程序员应该养成一个好习惯,在程序终止前关闭所有打开的文件。在 C++ 中,我们使用流对象的 close() 成员函数来关闭文件。
下面是 close() 函数的标准语法:
void close();
在C++中,我们可以使用流插入运算符(<<)将数据写入文件。与将数据输出到屏幕上的情况类似,只需要将输出目标从cout对象更改为ofstream或fstream对象。
下面是一个简单的示例代码,演示了如何使用ofstream对象向文件写入信息:
- #include
-
- int main() {
- std::ofstream outfile("example.txt"); // 打开文件进行写入操作
-
- if (outfile.is_open()) {
- outfile << "Hello, World!" << std::endl; // 使用流插入运算符写入信息到文件
- outfile << "This is a line of text." << std::endl;
- outfile.close(); // 关闭文件
- std::cout << "File written successfully." << std::endl;
- } else {
- std::cout << "Unable to open file." << std::endl;
- }
-
- return 0;
- }
在这个示例中,我们创建了一个名为outfile的ofstream对象,并将文件名作为构造函数的参数传递给它。通过使用流插入运算符(<<),我们可以将文本写入到文件中。在这个示例中,我们写入了两行文本并在每行末尾添加了换行符。最后,我们使用close()函数关闭文件。
需要注意的是,在进行文件写入操作时,需要确保文件存在并具有适当的写入权限。如果文件不存在,ofstream对象将会自动创建一个新文件。
在C++中,使用流提取运算符(>>)从文件中读取信息是不安全的,因为它无法正确处理包含空格的字符串。如果文件中的信息包含空格,则会导致信息读取错误。
相反,在C++中,我们通常使用getline()函数从文件中逐行读取信息。getline()函数可以正确处理包含空格的字符串。下面是一个读取文件的示例代码:
- #include
- #include
- #include
-
- int main()
- {
- std::ifstream infile("example.txt"); // 打开文件进行读取操作
-
- if (infile.is_open())
- {
- std::string line;
- while (std::getline(infile, line)) // 逐行读取文件内容
- {
- std::cout << line << std::endl; // 在控制台输出读取到的内容
- }
- infile.close(); // 关闭文件
- }
- else
- {
- std::cout << "Unable to open file." << std::endl;
- }
-
- return 0;
- }
在上述示例中,我们创建了一个 ifstream 对象 infile 并将文件名作为构造函数的参数传递给它。然后,我们使用 std::getline() 函数逐行读取文件内容,并将读取的每一行输出到控制台。
需要注意的是,在进行文件读取操作时,需要确保文件存在并具有适当的读取权限。
下面的 C++ 程序以读写模式打开一个文件。在向文件 afile.dat 写入用户输入的信息之后,程序从文件读取信息,并将其输出到屏幕上:
- #include
- #include
- #include
-
- int main()
- {
- std::ofstream outfile("afile.dat", std::ios::out | std::ios::binary); // 以写模式打开文件
-
- if (outfile.is_open())
- {
- std::cout << "输入一些文本: ";
- std::string input;
- std::getline(std::cin, input);
-
- outfile.write(input.c_str(), input.size()); // 将用户输入的信息写入文件
-
- outfile.close(); // 关闭文件
- std::cout << "文件写入成功." << std::endl;
- }
- else
- {
- std::cout << "无法打开文件." << std::endl;
- }
-
- std::ifstream infile("afile.dat", std::ios::in | std::ios::binary); // 以读模式打开文件
-
- if (infile.is_open())
- {
- std::string line;
- std::getline(infile, line); // 从文件读取信息
- std::cout << "文件内容: " << line << std::endl;
-
- infile.close(); // 关闭文件
- }
- else
- {
- std::cout << "Unable to open file." << std::endl;
- }
-
- return 0;
- }
在上述示例中,我们首先使用 std::ofstream 对象以写模式打开文件 afile.dat,然后通过 write() 函数将用户输入的信息写入文件。接着关闭文件,并输出写入成功的提示。
然后,我们使用 std::ifstream 对象以读模式打开同一个文件,通过 getline() 函数从文件中读取信息,并将读取到的信息输出到屏幕上。
需要注意的是,我们在打开文件时使用了 std::ios::binary 模式,这样可以确保以二进制方式进行读写操作,并适用于包含二进制数据的文件。
同时,为了正确处理用户输入的字符串,我们使用 std::getline() 函数而不是流提取运算符(>>)来读取用户输入的信息。这样可以确保读取整行内容,包括空格等特殊字符。
对于 istream 类型(如 ifstream),可以使用成员函数 seekg() 来重新定位文件位置指针。seekg() 函数的参数通常是一个长整型值,表示要定位的偏移量。第二个参数可以指定查找方向,可以是 ios::beg(从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。
下面是一个示例代码,演示了如何使用 seekg() 函数定位 "get" 文件位置指针:
- #include
- #include
-
- int main()
- {
- std::ifstream infile("output.txt", std::ios::in | std::ios::binary);
-
- if (infile.is_open())
- {
- // 获取当前文件位置指针
- std::streampos currentPosition = infile.tellg();
- std::cout << "当前位置: " << currentPosition << std::endl;
-
- // 重新定位文件位置指针到文件开头
- infile.seekg(0, std::ios::beg);
- std::cout << "寻找开始后的位置: " << infile.tellg() << std::endl;
-
- // 重新定位文件位置指针到文件末尾
- infile.seekg(0, std::ios::end);
- std::cout << "寻找到底后的位置: " << infile.tellg() << std::endl;
-
- // 重新定位文件位置指针到文件中间
- infile.seekg(currentPosition, std::ios::beg);
- std::cout << "中间位置: " << infile.tellg() << std::endl;
-
- infile.close();
- }
- else
- {
- std::cout << "无法打开文件。" << std::endl;
- }
-
- return 0;
- }
在上述示例中,我们首先使用 tellg() 函数获取了当前的文件位置指针,并将其保存在 currentPosition 变量中。然后,我们使用 seekg() 函数将文件位置指针分别定位到文件的开头、末尾和中间,并使用 tellg() 函数输出了定位后的位置。
需要注意的是,文件位置指针是一个整数值,它表示从文件的起始位置到指针所在位置的字节数。tellg() 函数返回的也是一个整数值,表示当前的文件位置指针。
类似地,对于 ostream 类型(如 ofstream),可以使用成员函数 seekp() 来重新定位文件位置指针。seekp() 的用法与 seekg() 类似,只是它用于定位 "put" 文件位置指针(即写入操作)。
在C++中,异常处理是一种机制,用于处理程序在运行过程中发生的异常情况。异常是指在程序执行期间发生的意外或错误情况,可能导致程序无法正常执行下去。
C++提供了一个异常处理的机制,使用关键字 try、catch 和 throw 来实现。以下是异常处理的基本语法:
- try {
- // 可能会引发异常的代码块
- }
- catch (ExceptionType1 exception1) {
- // 处理 ExceptionType1 异常的代码块
- }
- catch (ExceptionType2 exception2) {
- // 处理 ExceptionType2 异常的代码块
- }
- ...
- catch (ExceptionTypeN exceptionN) {
- // 处理 ExceptionTypeN 异常的代码块
- }
在 try 块中,放置可能会引发异常的代码。如果在 try 块中发生异常,则会立即跳转到最匹配的 catch 块,并执行相应的异常处理代码。catch 块按顺序进行匹配,直到找到与抛出的异常类型匹配的块。一旦找到匹配的 catch 块,后续的块将被忽略。
异常类型可以是标准的 C++ 异常类,也可以是用户自定义的异常类。catch 块中的参数是接收异常对象的变量,在 catch 块中可以使用该变量来处理异常。
在 catch 块中,可以执行与异常相关的处理逻辑,如输出错误消息、记录日志或进行恢复操作。如果没有找到匹配的 catch 块,异常将会传播到调用函数的上层,并继续寻找匹配的 catch 块。
c++异常机制为什么没有finally?
在C++中,确实没有像其他编程语言(如Java和C#)中的 finally 关键字那样提供一个专门的块来处理无论是否发生异常都必须执行的代码。有几个原因可以解释为什么C++没有引入 finally 关键字:
尽管C++没有提供 finally 关键字,但可以通过其他方式来实现类似的功能。例如,可以使用RAII(资源获取即初始化)技术来管理资源,确保在任何情况下都会正确释放资源。此外,可以在 try 块中添加适当的清理代码,以处理在发生异常时需要执行的特定操作。
以下是一个示例,演示了如何使用RAII和适当的清理代码来模拟 finally 的行为:
- #include
- #include
-
- class FileHandler
- {
- public:
- FileHandler(const std::string& filename)
- : file(filename)
- {
- // 打开文件
- if (!file.is_open())
- {
- throw std::runtime_error("无法打开文件.");
- }
- }
-
- ~FileHandler()
- {
- // 关闭文件
- file.close();
- }
-
- void writeData(const std::string& data)
- {
- // 写入数据到文件
- file << data;
- }
-
- private:
- std::ofstream file;
- };
-
- int main()
- {
- try
- {
- FileHandler file("output.txt");
- file.writeData("Hello, World!");
- // 在这里可以执行其他操作,不必担心资源泄漏
-
- // 如果在这之后发生异常,FileHandler 的析构函数会确保文件被关闭
- throw std::runtime_error("出了问题.");
- }
- catch (const std::exception& e)
- {
- std::cerr << "捕获到异常: " << e.what() << std::endl;
- }
-
- return 0;
- }
在上述示例中,我们定义了一个 FileHandler 类,用于打开文件并在析构函数中关闭文件。在 main() 函数中,我们使用 FileHandler 对象来打开文件,并在 writeData() 函数中写入数据。无论发生何种情况,当 FileHandler 对象的生命周期结束时,析构函数会被自动调用,确保文件被正确关闭。
需要注意的是,虽然C++没有 finally 关键字,但仍然可以通过适当的代码设计和资源管理来实现相似的功能,并确保代码在异常情况下能够正确处理资源。
除了 try-catch 块,在C++中,还可以使用 throw 语句抛出异常,抛出的异常可以是任何类型的对象,但通常建议使用标准库中定义的异常类(如 std::exception 或其子类)来表示异常。抛出异常的一般语法如下:
throw SomeExceptionType("Exception message");
其中,SomeExceptionType 是指异常对象的类型,可以是任何类型,但通常是从 std::exception 类派生的自定义异常类。"Exception message" 是一个字符串,用于描述异常的详细信息。
例如,下面的代码演示了如何抛出一个 std::runtime_error 异常:
- #include
- #include
-
- void myFunction(int a, int b)
- {
- if (b == 0)
- {
- throw std::runtime_error("除以零");
- }
-
- std::cout << "a / b = " << a / b << std::endl;
- }
-
- int main()
- {
- try
- {
- myFunction(10, 2);
- myFunction(20, 0); // 这里会抛出一个异常
- myFunction(30, 3);
- }
- catch (const std::exception& e)
- {
- std::cerr << "捕捉到异常: " << e.what() << std::endl;
- }
-
- return 0;
- }
在上述示例中,myFunction() 函数接受两个参数 a 和 b,并检查 b 是否为零。如果 b 为零,则抛出一个 std::runtime_error 异常。在 main() 函数中,我们调用 myFunction() 三次,当第二次调用时会抛出一个异常,此时程序将跳转到 catch 块中,输出异常信息并继续执行后续代码。
需要注意的是,在抛出异常之前应该确保资源得到了正确的释放,否则可能会导致资源泄漏或数据不一致。通常情况下,可以使用 RAII 技术来管理资源,并确保资源能在对象生命周期结束时自动释放,即使发生异常也能正确地处理资源。
在C++中,标准库
下面是对每个异常的详细说明:
std::exception:该异常是所有标准C++异常的父类。
std::bad_alloc:该异常可以通过new抛出,表示在动态内存分配过程中无法分配所需的内存。
std::bad_cast:该异常可以通过dynamic_cast抛出,表示类型转换失败。
std::bad_typeid:该异常可以通过typeid运算符抛出,表示无效的类型标识符。
std::bad_exception:这个异常在处理C++程序中无法预期的异常时非常有用。
std::logic_error:理论上可以通过读取代码来检测到的异常,表示程序逻辑错误。
std::domain_error:当使用了一个无效的数学域时,比如在执行数学函数时传入了无效的参数,会抛出该异常。
std::invalid_argument:当使用了无效的参数时,比如传入了一个无效的参数值,会抛出该异常。
std::length_error:当创建了太长的std::string对象时,会抛出该异常。
std::out_of_range:该异常可以通过某些方法抛出,比如在使用std::vector或std::bitset<>::operator时,当索引超出范围时会抛出该异常。
std::runtime_error:理论上不可以通过读取代码来检测到的异常,表示在运行时发生的错误。
std::overflow_error:当发生数学上溢时,比如在执行算术运算时结果超出了数据类型的表示范围,会抛出该异常。
std::range_error:当尝试存储超出范围的值时,比如在执行数学运算时结果超出了数据类型的表示范围,会抛出该异常。
std::underflow_error:当发生数学下溢时,比如在执行算术运算时结果小于了数据类型的最小值,会抛出该异常。
这些标准异常类提供了一种统一的机制来处理和报告可能发生的异常情况,我们可以根据具体的异常类型来采取相应的处理措施。
可以通过继承和重载std::exception类来定义新的异常。通过自定义异常类,可以根据特定的应用程序需求定义和抛出自己的异常类型。
下面是一个示例:
- #include
- #include
- #include
-
- class MyException : public std::exception
- {
- public:
- MyException(const std::string& message) : m_message(message) {}
-
- const char* what() const noexcept override
- {
- return m_message.c_str();
- }
-
- private:
- std::string m_message;
- };
在上面的示例中,我们定义了一个名为MyException的自定义异常类,它继承自std::exception类。我们在构造函数中接受一个字符串参数作为异常消息,并将其保存在私有成员变量m_message中。
我们还重载了what()方法,它是std::exception类的虚函数。what()方法返回一个指向异常消息的C风格字符串。在这个示例中,我们简单地返回了保存的异常消息。
现在,我们可以在程序中使用MyException异常类,并在需要的地方抛出它:
- void foo()
- {
- throw MyException("Something went wrong!");
- }
-
- int main()
- {
- try
- {
- foo();
- }
- catch (const MyException& e)
- {
- // 处理自定义异常
- std::cout << "Caught exception: " << e.what() << std::endl;
- }
- catch (const std::exception& e)
- {
- // 处理其他标准异常
- std::cout << "Caught exception: " << e.what() << std::endl;
- }
-
- return 0;
- }
-
在上面的示例中,我们在foo()函数中抛出了一个MyException异常,并在main()函数中使用try-catch块捕获并处理它。在catch块中,我们可以根据异常的类型来执行相应的操作。
这样,就可以定义和使用自己的异常类来处理特定的异常情况,并提供有用的错误信息。请记住,在自定义异常类时,继承自std::exception是一种良好的实践,可以使您的异常类与标准异常类保持一致,并符合C++异常处理的规范。