第28章 输入/输出和文件

C++20新特性:输入输出增强

C++20对输入输出系统进行了多项增强,包括同步流(syncstream)等:

同步流(syncstream)

C++20引入了同步流,用于线程安全的输出,避免多线程输出时的交织:

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
#include <iostream>
#include <syncstream>
#include <thread>
#include <vector>

void printNumbers(int id) {
for (int i = 0; i < 5; i++) {
// 使用同步流确保输出不交织
std::osyncstream(std::cout) << "Thread " << id << ": " << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main() {
std::vector<std::thread> threads;

// 创建5个线程
for (int i = 0; i < 5; i++) {
threads.emplace_back(printNumbers, i);
}

// 等待所有线程完成
for (auto& t : threads) {
t.join();
}

return 0;
}

format库与I/O

C++20的format库也可以与I/O操作结合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <format>
#include <iostream>
#include <fstream>

int main() {
// 格式化输出到控制台
std::cout << std::format("Hello, {}!\n", "world");

// 格式化输出到文件
std::ofstream file("output.txt");
if (file.is_open()) {
file << std::format("Name: {}, Age: {}\n", "Alice", 30);
file << std::format("Pi is approximately {:.2f}\n", 3.14159);
file.close();
}

return 0;
}

C++输入输出流

C++的输入输出系统是基于流(stream)的概念设计的。流是一种抽象,代表了数据的流动。

标准输入输出流

C++提供了三个标准流对象:

  • cin:标准输入流,通常关联到键盘
  • cout:标准输出流,通常关联到屏幕
  • cerr:标准错误流,通常关联到屏幕
  • clog:标准日志流,通常关联到屏幕
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
#include <iostream>

int main() {
// 标准输出
std::cout << "Hello, World!" << std::endl;
std::cout << "The answer is " << 42 << std::endl;

// 标准错误
std::cerr << "Error: Something went wrong!" << std::endl;

// 标准日志
std::clog << "Log: Operation completed" << std::endl;

// 标准输入
int x;
std::cout << "Enter a number: ";
std::cin >> x;
std::cout << "You entered: " << x << std::endl;

// 读取字符串
std::string name;
std::cout << "Enter your name: ";
std::cin >> name;
std::cout << "Hello, " << name << "!" << std::endl;

// 读取一行
std::string line;
std::cout << "Enter a line: ";
std::cin.ignore(); // 忽略之前的换行符
std::getline(std::cin, line);
std::cout << "You entered: " << line << 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
#include <iostream>
#include <iomanip>

int main() {
// 整数
int num = 42;
std::cout << "Decimal: " << num << std::endl;
std::cout << "Octal: " << std::oct << num << std::endl;
std::cout << "Hexadecimal: " << std::hex << num << std::endl;
std::cout << "Hexadecimal (uppercase): " << std::uppercase << num << std::endl;

// 浮点数
double pi = 3.141592653589793;
std::cout << "Default: " << pi << std::endl;
std::cout << "Fixed: " << std::fixed << pi << std::endl;
std::cout << "Scientific: " << std::scientific << pi << std::endl;
std::cout << "Precision 2: " << std::setprecision(2) << pi << std::endl;

// 宽度和填充
std::cout << "Width 10: " << std::setw(10) << num << std::endl;
std::cout << "Width 10 with fill: " << std::setw(10) << std::setfill('*') << num << std::endl;

// 布尔值
bool flag = true;
std::cout << "Boolean: " << flag << std::endl;
std::cout << "Boolean (alpha): " << std::boolalpha << flag << std::endl;

// 重置流状态
std::cout << std::resetiosflags(std::ios::scientific | std::ios::fixed);
std::cout << "Reset: " << pi << std::endl;

return 0;
}

输入输出错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

int main() {
int x;
std::cout << "Enter a number: ";

// 检查输入是否成功
if (std::cin >> x) {
std::cout << "You entered: " << x << std::endl;
} else {
std::cout << "Invalid input!" << std::endl;
// 清除错误状态
std::cin.clear();
// 忽略无效输入
std::cin.ignore(1000, '\n');
}

return 0;
}

文件输入输出

C++的文件输入输出是通过文件流类实现的,主要包括:

  • ifstream:输入文件流,用于读取文件
  • ofstream:输出文件流,用于写入文件
  • fstream:文件流,用于读写文件

文件的打开和关闭

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
#include <fstream>
#include <iostream>

int main() {
// 写入文件
std::ofstream outFile("example.txt");

if (outFile.is_open()) {
outFile << "Hello, File!" << std::endl;
outFile << "The answer is " << 42 << std::endl;
outFile.close();
std::cout << "File written successfully!" << std::endl;
} else {
std::cerr << "Error opening file for writing!" << std::endl;
}

// 读取文件
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::cerr << "Error opening file for reading!" << std::endl;
}

return 0;
}

文件打开模式

模式描述
ios::in输入模式,打开文件用于读取
ios::out输出模式,打开文件用于写入
ios::app追加模式,在文件末尾写入
ios::trunc截断模式,打开文件时清空内容
ios::binary二进制模式,以二进制形式读写
ios::ate定位模式,打开文件后定位到文件末尾
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
#include <fstream>
#include <iostream>

int main() {
// 以追加模式打开文件
std::ofstream outFile("example.txt", std::ios::app);

if (outFile.is_open()) {
outFile << "Appended line" << std::endl;
outFile.close();
std::cout << "Line appended successfully!" << std::endl;
} else {
std::cerr << "Error opening file!" << std::endl;
}

// 以二进制模式读写文件
std::fstream binFile("data.bin", std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);

if (binFile.is_open()) {
// 写入整数
int num = 42;
binFile.write(reinterpret_cast<char*>(&num), sizeof(num));

// 定位到文件开头
binFile.seekg(0, std::ios::beg);

// 读取整数
int readNum;
binFile.read(reinterpret_cast<char*>(&readNum), sizeof(readNum));
std::cout << "Read number: " << readNum << std::endl;

binFile.close();
} else {
std::cerr << "Error opening binary file!" << 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
#include <fstream>
#include <iostream>

int main() {
std::fstream file("example.txt", std::ios::in | std::ios::out);

if (file.is_open()) {
// 获取当前位置
std::streampos pos = file.tellg();
std::cout << "Current position: " << pos << std::endl;

// 读取一行
std::string line;
std::getline(file, line);
std::cout << "Read line: " << line << std::endl;

// 获取新位置
pos = file.tellg();
std::cout << "New position: " << pos << std::endl;

// 定位到文件开头
file.seekg(0, std::ios::beg);
std::cout << "Position after seekg(0): " << file.tellg() << std::endl;

// 定位到文件末尾
file.seekg(0, std::ios::end);
std::cout << "Position at end: " << file.tellg() << std::endl;

file.close();
} else {
std::cerr << "Error opening file!" << std::endl;
}

return 0;
}

字符串流

字符串流是一种在内存中进行输入输出操作的流,主要包括:

  • istringstream:输入字符串流,用于从字符串读取数据
  • ostringstream:输出字符串流,用于向字符串写入数据
  • stringstream:字符串流,用于读写字符串

字符串流的使用

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 <sstream>
#include <iostream>
#include <string>

int main() {
// 输出字符串流
std::ostringstream oss;
oss << "Hello, " << "String Stream!" << std::endl;
oss << "The answer is " << 42 << std::endl;

std::string str = oss.str();
std::cout << "Output string: " << str << std::endl;

// 输入字符串流
std::string input = "123 456 789";
std::istringstream iss(input);

int a, b, c;
iss >> a >> b >> c;
std::cout << "Read numbers: " << a << ", " << b << ", " << c << std::endl;

// 字符串流用于类型转换
std::string numStr = "12345";
std::istringstream iss2(numStr);
int num;
iss2 >> num;
std::cout << "String to int: " << num << std::endl;

// 数字转字符串
int x = 67890;
std::ostringstream oss2;
oss2 << x;
std::string str2 = oss2.str();
std::cout << "Int to string: " << str2 << std::endl;

// 字符串流用于格式化
double pi = 3.1415926535;
std::ostringstream oss3;
oss3 << std::fixed << std::setprecision(2) << pi;
std::string piStr = oss3.str();
std::cout << "Formatted pi: " << piStr << 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
#include <iostream>

class Point {
private:
int x;
int y;

public:
Point(int x = 0, int y = 0) : x(x), y(y) {}

// 友元函数,重载输出操作符
friend std::ostream& operator<<(std::ostream& os, const Point& p);

// 友元函数,重载输入操作符
friend std::istream& operator>>(std::istream& is, Point& p);
};

// 定义输出操作符重载
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}

// 定义输入操作符重载
std::istream& operator>>(std::istream& is, Point& p) {
char ch1, ch2, ch3;
is >> ch1 >> p.x >> ch2 >> p.y >> ch3;
if (ch1 != '(' || ch2 != ',' || ch3 != ')') {
is.setstate(std::ios::failbit);
}
return is;
}

int main() {
Point p1(10, 20);
std::cout << "Point p1: " << p1 << std::endl;

Point p2;
std::cout << "Enter a point (format: (x,y)): ";
std::cin >> p2;

if (std::cin) {
std::cout << "You entered: " << p2 << std::endl;
} else {
std::cout << "Invalid input!" << std::endl;
}

return 0;
}

文件系统操作(C++17+)

C++17引入了<filesystem>库,提供了跨平台的文件系统操作功能。

文件系统基本操作

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
#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
// 当前路径
fs::path currentPath = fs::current_path();
std::cout << "Current path: " << currentPath << std::endl;

// 创建目录
fs::path dirPath = currentPath / "test_dir";
if (!fs::exists(dirPath)) {
if (fs::create_directory(dirPath)) {
std::cout << "Directory created: " << dirPath << std::endl;
}
}

// 创建嵌套目录
fs::path nestedPath = dirPath / "subdir1" / "subdir2";
if (!fs::exists(nestedPath)) {
if (fs::create_directories(nestedPath)) {
std::cout << "Nested directories created: " << nestedPath << std::endl;
}
}

// 遍历目录
std::cout << "\nDirectory contents: " << std::endl;
for (const auto& entry : fs::directory_iterator(currentPath)) {
std::cout << entry.path() << " " << (fs::is_directory(entry.status()) ? "[dir]" : "[file]") << std::endl;
}

// 文件状态
fs::path filePath = currentPath / "example.txt";
if (fs::exists(filePath)) {
std::cout << "\nFile exists: " << filePath << std::endl;
std::cout << "File size: " << fs::file_size(filePath) << " bytes" << std::endl;
std::cout << "Is regular file: " << fs::is_regular_file(filePath) << std::endl;
std::cout << "Is directory: " << fs::is_directory(filePath) << std::endl;
}

// 重命名文件或目录
fs::path oldPath = dirPath / "old_name.txt";
fs::path newPath = dirPath / "new_name.txt";

// 创建测试文件
std::ofstream oldFile(oldPath);
oldFile << "Test" << std::endl;
oldFile.close();

if (fs::exists(oldPath)) {
fs::rename(oldPath, newPath);
std::cout << "\nFile renamed: " << oldPath << " -> " << newPath << std::endl;
}

// 删除文件
if (fs::exists(newPath)) {
fs::remove(newPath);
std::cout << "File deleted: " << newPath << std::endl;
}

// 删除目录
if (fs::exists(dirPath)) {
fs::remove_all(dirPath);
std::cout << "Directory deleted: " << dirPath << std::endl;
}

return 0;
}

最佳实践

1. 输入输出流

  • 使用命名空间:为了代码简洁,可以使用using namespace std;,但在头文件中应避免
  • 错误处理:总是检查输入操作是否成功
  • 流状态:在输入失败后,记得清除错误状态并忽略无效输入
  • 格式化:使用流操纵符进行格式化输出,而不是手动拼接字符串
  • 性能考虑:对于大量输出,考虑使用std::ios::sync_with_stdio(false);提高性能

2. 文件操作

  • 文件检查:在打开文件后,总是检查是否成功打开
  • 资源管理:使用RAII(资源获取即初始化)管理文件流,或确保在使用完毕后关闭文件
  • 异常处理:考虑使用异常处理文件操作错误
  • 二进制模式:在处理二进制文件时,使用std::ios::binary模式
  • 路径处理:使用C++17的<filesystem>库处理路径,提高跨平台兼容性

3. 字符串流

  • 类型转换:使用字符串流进行类型转换,比atoiitoa等函数更安全、更灵活
  • 格式化:使用字符串流进行复杂的字符串格式化
  • 内存操作:对于需要在内存中进行大量读写操作的场景,使用字符串流

4. 自定义操作符

  • 一致性:自定义输入输出操作符时,保持与标准类型的行为一致
  • 错误处理:在自定义输入操作符中,正确设置流的错误状态
  • 文档:为自定义类型的输入输出格式提供清晰的文档

常见错误和陷阱

1. 输入输出错误

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
// 错误:忽略输入失败
int x;
std::cin >> x;
// 没有检查输入是否成功
std::cout << "You entered: " << x << std::endl;

// 正确:检查输入是否成功
int x;
if (std::cin >> x) {
std::cout << "You entered: " << x << std::endl;
} else {
std::cout << "Invalid input!" << std::endl;
std::cin.clear();
std::cin.ignore(1000, '\n');
}

// 错误:混用cin和getline
std::string name;
int age;
std::cout << "Enter your name: ";
std::cin >> name;
std::cout << "Enter your age: ";
std::cin >> age;

std::string address;
std::cout << "Enter your address: ";
std::getline(std::cin, address); // 错误:会读取到空行

// 正确:在cin后使用ignore
std::string name;
int age;
std::cout << "Enter your name: ";
std::cin >> name;
std::cout << "Enter your age: ";
std::cin >> age;
std::cin.ignore(); // 忽略换行符

std::string address;
std::cout << "Enter your address: ";
std::getline(std::cin, address); // 正确

2. 文件操作错误

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
// 错误:没有检查文件是否打开
std::ifstream file("nonexistent.txt");
std::string line;
std::getline(file, line); // 会失败

// 正确:检查文件是否打开
std::ifstream file("nonexistent.txt");
if (file.is_open()) {
std::string line;
std::getline(file, line);
} else {
std::cerr << "Error opening file!" << std::endl;
}

// 错误:文件路径问题
std::ofstream file("C:\\Users\\name\\file.txt"); // Windows路径

// 正确:使用正斜杠或双反斜杠
std::ofstream file("C:/Users/name/file.txt"); // 推荐
std::ofstream file("C:\\Users\\name\\file.txt"); // 也可以

// 错误:忘记关闭文件
std::ofstream file("example.txt");
file << "Hello";
// 没有关闭文件

// 正确:使用RAII或手动关闭
std::ofstream file("example.txt");
file << "Hello";
file.close(); // 手动关闭

// 或者使用作用域
{
std::ofstream file("example.txt");
file << "Hello";
} // 文件会自动关闭

3. 字符串流错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 错误:重复使用字符串流而不重置
std::ostringstream oss;
oss << "First";
std::string s1 = oss.str();

// 错误:没有重置,会追加
oss << "Second";
std::string s2 = oss.str(); // s2是"FirstSecond"

// 正确:重置字符串流
std::ostringstream oss;
oss << "First";
std::string s1 = oss.str();

oss.str(""); // 清空内容
oss << "Second";
std::string s2 = oss.str(); // s2是"Second"

4. 文件系统操作错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 错误:使用C++17前的文件系统操作
// 不同平台实现不同

// 正确:使用C++17的filesystem库
#include <filesystem>
namespace fs = std::filesystem;

// 错误:不检查操作结果
fs::create_directory("test"); // 可能失败但没有检查

// 正确:检查操作结果
if (fs::create_directory("test")) {
std::cout << "Directory created" << std::endl;
} else {
std::cout << "Failed to create directory" << std::endl;
}

小结

本章介绍了C++中的输入输出和文件操作,包括:

  1. C++输入输出流:标准输入输出流、流操纵符、输入输出错误处理
  2. 文件输入输出:文件的打开和关闭、文件打开模式、文件位置指针
  3. 字符串流:输入字符串流、输出字符串流、字符串流的使用
  4. 自定义输入输出操作符:重载输入输出操作符
  5. 文件系统操作:C++17的filesystem库
  6. 最佳实践:输入输出流、文件操作、字符串流、自定义操作符的使用
  7. 常见错误和陷阱:输入输出错误、文件操作错误、字符串流错误、文件系统操作错误

输入输出是程序与外部世界交互的重要方式,文件操作则是程序持久化数据的关键手段。掌握这些内容对于编写实用的C++程序至关重要。在实际编程中,应根据具体情况选择合适的输入输出方式,遵循最佳实践,避免常见错误。

在后续章节中,我们将学习C++的标准模板库、多线程编程等内容,进一步提高C++编程能力。