MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++IO库与文件操作指南

2021-06-025.0k 阅读

C++ IO 库基础

输入输出流概述

在 C++ 中,输入输出(IO)操作是通过流(stream)来实现的。流是一种抽象,它代表了字节序列的源或目标。C++ 的 IO 库提供了一组类来处理不同类型的输入输出,包括标准输入输出(控制台)、文件输入输出以及字符串输入输出。

标准输入输出流主要用于与用户进行交互。std::cin 是标准输入流对象,用于从控制台读取数据;std::cout 是标准输出流对象,用于向控制台输出数据;std::cerr 用于输出错误信息,std::clog 用于输出日志信息,它们默认都关联到控制台。

例如,以下代码展示了基本的输入输出操作:

#include <iostream>

int main() {
    int num;
    std::cout << "请输入一个整数: ";
    std::cin >> num;
    std::cout << "你输入的整数是: " << num << std::endl;
    return 0;
}

在上述代码中,首先使用 std::cout 输出提示信息,然后通过 std::cin 从控制台读取一个整数存储到 num 变量中,最后再用 std::cout 输出读取到的整数。std::endl 不仅输出换行符,还会刷新输出缓冲区。

流的状态

每个流对象都有一个关联的状态,用于反映流的当前情况。流的状态通过以下几个标志位来表示:

  1. std::ios::goodbit:表示流处于正常状态,没有发生错误。
  2. std::ios::eofbit:当读取到文件末尾时,该标志位被设置。
  3. std::ios::failbit:当输入操作失败(例如读取类型不匹配)时,该标志位被设置。
  4. std::ios::badbit:当发生严重错误(如硬件故障)时,该标志位被设置。

可以通过流对象的 good()eof()fail()bad() 成员函数来检查相应的状态标志位。例如:

#include <iostream>

int main() {
    int num;
    std::cout << "请输入一个整数: ";
    std::cin >> num;
    if (std::cin.fail()) {
        std::cerr << "输入错误,不是有效的整数" << std::endl;
        std::cin.clear(); // 清除错误状态
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 忽略输入缓冲区中的剩余内容
    } else {
        std::cout << "你输入的整数是: " << num << std::endl;
    }
    return 0;
}

在这个例子中,当输入不是有效的整数时,std::cin.fail() 返回 true,程序会输出错误信息,然后通过 std::cin.clear() 清除错误状态,再使用 std::cin.ignore() 忽略输入缓冲区中的剩余内容,以便后续可以重新进行输入操作。

格式化输出

C++ 的 IO 库提供了丰富的格式化输出功能。可以使用操纵符(manipulator)来控制输出的格式。

  1. 控制整数输出格式
    • 十进制、八进制和十六进制输出:使用 std::dec(十进制)、std::oct(八进制)和 std::hex(十六进制)操纵符。例如:
#include <iostream>

int main() {
    int num = 255;
    std::cout << "十进制: " << std::dec << num << std::endl;
    std::cout << "八进制: " << std::oct << num << std::endl;
    std::cout << "十六进制: " << std::hex << num << std::endl;
    return 0;
}
- **设置填充字符和宽度**:`std::setw(n)` 用于设置输出宽度为 `n`,`std::setfill(c)` 用于设置填充字符为 `c`。例如:
#include <iostream>
#include <iomanip>

int main() {
    std::cout << std::setfill('0') << std::setw(5) << 123 << std::endl;
    return 0;
}
上述代码输出 `00123`,因为设置了填充字符为 `0`,宽度为 `5`。

2. 控制浮点数输出格式 - 设置精度std::setprecision(n) 用于设置浮点数的精度,即小数部分的位数。例如:

#include <iostream>
#include <iomanip>

int main() {
    double pi = 3.14159265358979323846;
    std::cout << std::setprecision(4) << pi << std::endl;
    return 0;
}
这里输出 `3.142`,精度设置为 4,四舍五入后得到该结果。
- **固定小数点表示法和科学计数法**:`std::fixed` 用于固定小数点表示法,`std::scientific` 用于科学计数法。例如:
#include <iostream>
#include <iomanip>

int main() {
    double num = 1234567.89;
    std::cout << std::fixed << std::setprecision(2) << num << std::endl;
    std::cout << std::scientific << std::setprecision(2) << num << std::endl;
    return 0;
}
输出结果分别为 `1234567.89` 和 `1.23e+06`。

文件操作

文件流类

C++ 提供了三个文件流类来处理文件的输入输出:

  1. std::ifstream:用于从文件中读取数据,继承自 std::istream
  2. std::ofstream:用于向文件中写入数据,继承自 std::ostream
  3. std::fstream:既可以读取文件,也可以写入文件,继承自 std::iostream

文件打开与关闭

  1. 打开文件
    • 使用构造函数:可以在创建文件流对象时直接指定文件名和打开模式。例如:
#include <fstream>

int main() {
    std::ofstream outFile("example.txt");
    if (!outFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    // 文件操作
    outFile.close();
    return 0;
}
- **使用 `open()` 成员函数**:也可以先创建文件流对象,然后再调用 `open()` 函数打开文件。例如:
#include <fstream>

int main() {
    std::ifstream inFile;
    inFile.open("example.txt");
    if (!inFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    // 文件操作
    inFile.close();
    return 0;
}
  1. 打开模式
    • std::ios::in:以读模式打开文件,用于输入操作。
    • std::ios::out:以写模式打开文件,用于输出操作。如果文件已存在,会清空文件内容;如果文件不存在,会创建新文件。
    • std::ios::app:以追加模式打开文件,在文件末尾写入数据。
    • std::ios::ate:打开文件并将文件指针移动到文件末尾。
    • std::ios::trunc:如果文件已存在,删除其内容并创建一个新的空文件。
    • std::ios::binary:以二进制模式打开文件,用于处理二进制数据。默认情况下,文件以文本模式打开。

例如,要以读写二进制模式打开文件,可以这样做:

#include <fstream>

int main() {
    std::fstream file("data.bin", std::ios::in | std::ios::out | std::ios::binary);
    if (!file) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    // 文件操作
    file.close();
    return 0;
}
  1. 关闭文件 在完成文件操作后,应及时关闭文件,以确保数据被正确写入磁盘并释放相关资源。可以通过调用文件流对象的 close() 成员函数来关闭文件。例如:
#include <fstream>

int main() {
    std::ofstream outFile("example.txt");
    outFile << "这是写入文件的内容";
    outFile.close();
    return 0;
}

也可以依靠文件流对象的析构函数自动关闭文件,当文件流对象超出作用域时,析构函数会被调用,从而关闭文件。但显式调用 close() 可以在需要时提前关闭文件。

文件读取操作

  1. 读取文本文件
    • 按字符读取:可以使用 get() 成员函数按字符读取文件内容。例如:
#include <iostream>
#include <fstream>

int main() {
    std::ifstream inFile("example.txt");
    if (!inFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    char ch;
    while (inFile.get(ch)) {
        std::cout << ch;
    }
    inFile.close();
    return 0;
}
- **按行读取**:`getline()` 函数用于按行读取文件内容。有两种重载形式,一种是针对 `std::istream` 的,另一种是针对 `std::string` 的。例如:
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream inFile("example.txt");
    if (!inFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << line << std::endl;
    }
    inFile.close();
    return 0;
}
- **格式化读取**:与 `std::cin` 类似,可以使用 `>>` 操作符进行格式化读取。例如:
#include <iostream>
#include <fstream>

int main() {
    std::ifstream inFile("example.txt");
    if (!inFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    int num;
    inFile >> num;
    std::cout << "读取到的整数: " << num << std::endl;
    inFile.close();
    return 0;
}
  1. 读取二进制文件 读取二进制文件时,通常使用 read() 成员函数。read() 函数接受两个参数,一个是指向存储数据的缓冲区的指针,另一个是要读取的字节数。例如:
#include <iostream>
#include <fstream>

struct Data {
    int id;
    double value;
};

int main() {
    std::ifstream inFile("data.bin", std::ios::binary);
    if (!inFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    Data data;
    inFile.read(reinterpret_cast<char*>(&data), sizeof(Data));
    std::cout << "ID: " << data.id << ", Value: " << data.value << std::endl;
    inFile.close();
    return 0;
}

在这个例子中,先定义了一个结构体 Data,然后从二进制文件中读取数据填充到该结构体对象中。注意要使用 reinterpret_cast<char*>(&data) 将结构体指针转换为 char* 类型,因为 read() 函数期望的是 char* 类型的缓冲区指针。

文件写入操作

  1. 写入文本文件
    • 使用 << 操作符:与 std::cout 类似,可以使用 << 操作符向文件中写入数据。例如:
#include <fstream>

int main() {
    std::ofstream outFile("example.txt");
    if (!outFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    outFile << "这是写入文件的文本内容" << std::endl;
    outFile.close();
    return 0;
}
- **使用 `put()` 成员函数**:`put()` 函数用于向文件中写入单个字符。例如:
#include <fstream>

int main() {
    std::ofstream outFile("example.txt");
    if (!outFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    outFile.put('A');
    outFile.close();
    return 0;
}
  1. 写入二进制文件 写入二进制文件时,通常使用 write() 成员函数。write() 函数接受两个参数,一个是指向要写入数据的缓冲区的指针,另一个是要写入的字节数。例如:
#include <fstream>

struct Data {
    int id;
    double value;
};

int main() {
    Data data = {1, 3.14};
    std::ofstream outFile("data.bin", std::ios::binary);
    if (!outFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    outFile.write(reinterpret_cast<const char*>(&data), sizeof(Data));
    outFile.close();
    return 0;
}

在这个例子中,将结构体 Data 的实例写入二进制文件。同样需要使用 reinterpret_cast<const char*>(&data) 将结构体指针转换为 const char* 类型,因为 write() 函数期望的是 const char* 类型的缓冲区指针。

文件定位

在文件操作中,有时需要定位到文件的特定位置进行读写操作。C++ 的文件流类提供了一些函数来实现文件定位。

  1. tellg()tellp()
    • tellg():用于获取输入流(std::ifstream)当前的读位置,返回一个 std::streamoff 类型的值,表示从文件开头到当前位置的字节数。例如:
#include <iostream>
#include <fstream>

int main() {
    std::ifstream inFile("example.txt");
    if (!inFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    char ch;
    inFile.get(ch);
    std::cout << "当前读位置: " << inFile.tellg() << " 字节" << std::endl;
    inFile.close();
    return 0;
}
- **`tellp()`**:用于获取输出流(`std::ofstream`)当前的写位置,返回值类型也是 `std::streamoff`。例如:
#include <iostream>
#include <fstream>

int main() {
    std::ofstream outFile("example.txt");
    if (!outFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    outFile << "Hello";
    std::cout << "当前写位置: " << outFile.tellp() << " 字节" << std::endl;
    outFile.close();
    return 0;
}
  1. seekg()seekp()
    • seekg():用于设置输入流的读位置。它有两种重载形式,一种接受一个 std::streamoff 类型的参数,表示从文件开头偏移的字节数;另一种接受两个参数,第一个是偏移量,第二个是 std::ios::seekdir 类型的枚举值,用于指定偏移的起始位置。std::ios::seekdir 枚举包括 std::ios::beg(文件开头)、std::ios::cur(当前位置)和 std::ios::end(文件末尾)。例如:
#include <iostream>
#include <fstream>

int main() {
    std::ifstream inFile("example.txt");
    if (!inFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    // 从文件开头偏移 5 个字节
    inFile.seekg(5, std::ios::beg);
    char ch;
    inFile.get(ch);
    std::cout << "从偏移 5 字节处读取的字符: " << ch << std::endl;
    inFile.close();
    return 0;
}
- **`seekp()`**:用于设置输出流的写位置,用法与 `seekg()` 类似。例如:
#include <iostream>
#include <fstream>

int main() {
    std::ofstream outFile("example.txt");
    if (!outFile) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    outFile << "Hello World";
    // 从文件开头偏移 6 个字节,然后写入字符 '!'
    outFile.seekp(6, std::ios::beg);
    outFile.put('!');
    outFile.close();
    return 0;
}

在上述代码中,先向文件写入 "Hello World",然后通过 seekp() 定位到第 6 个字节处,再写入字符 '!',最终文件内容变为 "Hello!orld"。

字符串流

字符串流类

C++ 还提供了字符串流类,用于在字符串和流之间进行数据的输入输出操作。主要有以下三个类:

  1. std::istringstream:用于从字符串中读取数据,继承自 std::istream
  2. std::ostringstream:用于向字符串中写入数据,继承自 std::ostream
  3. std::stringstream:既可以从字符串读取数据,也可以向字符串写入数据,继承自 std::iostream

字符串流的使用

  1. 从字符串读取数据
    • 使用 std::istringstream 可以方便地从字符串中按格式读取数据。例如:
#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::string str = "123 4.56 Hello";
    std::istringstream iss(str);
    int num;
    double dbl;
    std::string word;
    iss >> num >> dbl >> word;
    std::cout << "读取到的整数: " << num << std::endl;
    std::cout << "读取到的浮点数: " << dbl << std::endl;
    std::cout << "读取到的字符串: " << word << std::endl;
    return 0;
}
在这个例子中,`std::istringstream` 从字符串 `str` 中按空格分隔读取整数、浮点数和字符串。

2. 向字符串写入数据 - 使用 std::ostringstream 可以将数据按格式写入字符串。例如:

#include <iostream>
#include <sstream>
#include <string>

int main() {
    int num = 123;
    double dbl = 3.14;
    std::string word = "Hello";
    std::ostringstream oss;
    oss << num << " " << dbl << " " << word;
    std::string result = oss.str();
    std::cout << "生成的字符串: " << result << std::endl;
    return 0;
}
这里将整数、浮点数和字符串按格式写入 `std::ostringstream` 对象 `oss`,然后通过 `oss.str()` 获取生成的字符串。

3. 字符串流的格式化操作 字符串流同样支持与标准输入输出流类似的格式化操作。例如:

#include <iostream>
#include <sstream>
#include <iomanip>

int main() {
    double pi = 3.14159265358979323846;
    std::ostringstream oss;
    oss << std::fixed << std::setprecision(4) << pi;
    std::string result = oss.str();
    std::cout << "格式化后的字符串: " << result << std::endl;
    return 0;
}

输出结果为 3.1416,通过设置固定小数点表示法和精度,将 pi 格式化为指定精度的字符串。

字符串流在处理需要在字符串和数据类型之间进行转换的场景时非常有用,比如解析命令行参数、生成日志消息等。它提供了一种灵活且方便的方式来处理字符串和数据的交互。

通过深入了解 C++ 的 IO 库,包括标准输入输出流、文件流以及字符串流的使用,开发者能够更加高效地处理各种输入输出需求,无论是与用户交互、文件管理还是数据处理,都能通过这些工具实现稳健且功能丰富的程序。同时,合理运用流的状态检查、格式化操作以及文件定位等功能,可以进一步优化程序的性能和稳定性。在实际开发中,应根据具体的需求选择合适的 IO 操作方式,并注意资源的合理管理,如及时关闭文件,以确保程序的可靠性和高效性。