1 流和缓冲区
C++中,流( stream )和缓冲区( buffer )是两个紧密相关的概念,它们在处理输入和输出时起着重要的作用。
流( Stream )
流是一种抽象的概念,用于表示数据的流动。在 C++ 中,流是一个类对象,它封装了与输入/输出设备(如键盘、显示器、文件等)的交互。C++标准库提供了多种流类,如 std::cin (标准输入流,通常用于从键盘读取数据)、 std::cout (标准输出流,通常用于向显示器输出数据)以及 std::fstream (文件流,用于文件的读写)。
缓冲区( Buffer )
缓冲区是一个用于临时存储数据的内存区域。当使用流进行输入或输出时,数据通常不会直接发送到设备或从设备读取,而是首先存储在缓冲区中。缓冲区的使用可以提高输入/输出的效率,因为它允许程序以块的形式处理数据,而不是一个字符一个字符地处理。
对于输出流,程序首先将数据写入缓冲区,然后当缓冲区满或程序显式地刷新缓冲区时,数据才会被发送到输出设备。对于输入流,程序从缓冲区读取数据,当缓冲区为空时,流会从输入设备读取更多的数据填充缓冲区。
缓冲区的类型
C++中的流缓冲区主要有三种类型:
(1)全缓冲:当缓冲区满时,缓冲区的数据才会被发送。这种方式适用于大量数据的输入/输出。
(2)行缓冲:当遇到换行符时,缓冲区的数据才会被发送。这种方式通常用于终端设备,如键盘和显示器。
(3)不带缓冲:数据立即被发送,不经过缓冲区。这种方式通常用于错误报告和紧急情况。
iostream 文件
C++ 中的 iostream 文件中包含了一些用来实现管理流和缓冲区的类。iostream 中的 io 是 “input/output” 的缩写,而 stream 表示流,即数据流动的通道。
在 iostream 库中,主要包含了以下几个组件:
(1)缓冲区( streambuf 类): streambuf 类为流提供了一个缓冲区,这个缓冲区用于暂存输入/输出的数据,从而允许更高效的I/O操作。streambuf 是一个抽象基类,不能直接实例化。它的实现(如 filebuf 、 stringbuf 等)会作为其他流类(如 ifstream 、 ofstream 、 istringstream 、 ostringstream 等)的成员被使用。
(2)输入流( istream 类):用于从输入设备(如键盘)读取数据。
(3)输出流( ostream 类):用于向输出设备(如显示器)写入数据。
(4)输入输出流 ( iostream 类):从 istream 和 ostream 继承了输入和输出的方法(多重继承),既可以读取数据也可以写入数据。
(5)流对象:如 std::cin(标准输入流,通常用于从键盘读取数据)、std::cout(标准输出流,通常用于向显示器输出数据)、std::cerr(用于输出错误消息)和 std::clog(用于输出日志消息)。
(6)操纵器( manipulators):这些是用来控制流的状态或格式的函数,例如 std::endl、std::flush、std::setprecision 等。
(7)流提取运算符( >> ):用于从输入流中读取数据。
(8)流插入运算符( << ):用于向输出流中写入数据。
重定向
可以使用 C 标准库函数 freopen 函数来重定向标准输入流 std::cin 或标准输出流 std::cout 到文件或其他流。当重定向一个流时,实际上是改变了流与底层设备(如文件或控制台)的关联。例如,可以将 std::cin 重定向到一个文件,这样当从 std::cin 读取时,实际上是从文件中读取数据。如下为样例代码:
#include <iostream>
#include <fstream>
using namespace std;
int main() {
// 重定向 std::cin 到一个输入文件:在这个文件中输入 1 ,并保存
freopen("input.txt", "r", stdin);
// 重定向 std::cout 到一个输出文件
freopen("output.txt", "w", stdout);
int val;
cout << "get a number from input.txt : "; // cout 写入到 output.txt
cin >> val; // 实际上从 input.txt 读取
cout << val << endl; // cout 写入到 output.txt
// 关闭重定向
//freopen(NULL, "r", stdin); // 重置 std::cin 到标准输入
//freopen(NULL, "w", stdout); // 重置 std::cout 到标准输出
return 0;
}
执行完这段代码后,output.txt 被存入字符串:“get a number from input.txt : 1” 。
在上面代码中,freopen 函数将 std::cin 重定向到名为 input.txt 的文件用于读取,将 std::cout 重定向到名为 output.txt 的文件用于写入。因此,当从 std::cin 读取时,实际上是在读取 input.txt 文件的内容,而当向 std::cout 写入时,数据会被写入到 output.txt 文件中。
注意,重定向流可能会影响程序的其他部分,因为它改变了流的默认行为。因此,在重定向流之后,务必小心处理输入和输出,以避免意外的行为或错误。
另外,如果想要在执行完一些重定向操作之后恢复流的原始状态,可以使用 freopen 函数将流重新关联到其原始设备(比如将第一个参数设置为 NULL 即可将输入与输出定向为标准输入与输出)。
2 输出
输出流( output stream )是 I/O 流库中的一个关键概念,它允许程序将数据发送到特定的目的地,如控制台、文件或其他类型的设备。C++ 标准库中的 ostream 类是输出流的基础。
2.1 std::cout 的基本使用
std::cout 是 C++ 中最常用的输出流对象,它通常与控制台(或终端)相关联,用于向用户显示信息。可以使用插入运算符 << 向 std::cout 发送数据,该运算符将数据发送到输出流中。
。如下为样例代码:
#include <iostream>
#include <string>
using namespace std;
int main() {
int val1 = 1;
double val2 = 1.21;
char val3 = 'a';
string val4 = "hello";
std::cout << "int val1 = " << val1 << std::endl;
std::cout << "double val2 = " << val2 << std::endl;
std::cout << "char val3 = " << val3 << std::endl;
std::cout << "string val4 = " << val4 << std::endl;
return 0;
}
上面代码的输出为:
int val1 = 1
double val2 = 1.21
char val3 = a
string val4 = hello
除了 std::cout ,C++ 标准库还提供了其他一些输出流对象,例如:
std::cerr:用于输出错误消息。与 std::cout 不同的是,std::cerr 通常不会被缓冲,因此它用于需要立即显示的错误或诊断信息。
std::clog:用于输出日志消息。与 std::cerr 类似,但它通常会被缓冲。
std::wcout:与 std::cout 类似,但用于输出宽字符(wchar_t 类型)。
std::wcerr 和 std::wclog:与 std::cerr 和 std::clog 类似,但用于输出宽字符。
此外,还可以创建自定义的输出流,通过继承 std::ostream 类并重载插入运算符来实现。这允许控制数据如何被发送到特定的目的地,例如写入到特定的文件或进行特定的格式化处理。
在处理输出流时,还可以使用操纵器( manipulators )来更改流的状态或格式。例如,std::endl 操纵器不仅插入一个新行,还刷新输出缓冲区,确保所有数据都被发送到其目标。 std::flush 操纵器也可以用来刷新输出缓冲区。
2.2 std::cout 的格式化
std::cout 可以使用各种格式化标志和操纵器(manipulators)来控制输出的格式。以下是一些常见的格式化选项:
(1)整数输出
(1.1)十进制
int val = 12;
std::cout << val << std::endl;
// 输出 12
(1.2)十六进制
int val = 12;
std::cout << std::hex << val << std::endl;
// 输出 c
(1.3)八进制
int val = 12;
std::cout << std::oct << val << std::endl;
// 输出 14
(1.4)二进制
std::cout 默认不直接支持二进制数据的输出。如果想要输出一个整数的二进制表示,则要手动将整数转换为二进制字符串,然后再使用 std::cout 输出这个字符串。
#include <iostream>
#include <bitset>
void printBinary(int val) {
// std::bitset 可以将整数转换为二进制字符串
std::bitset<32> binaryVal(val); // 4 个字节,共 32 位
// 输出二进制字符串
std::cout << binaryVal << std::endl;
}
int main() {
int val = 12;
printBinary(val); // 输出 num 的二进制表示
return 0;
}
上面代码输出为:
00000000000000000000000000001100
(2)浮点数输出
(2.1)固定点表示法
默认情况下,std::cout 使用科学记数法来输出非常大或非常小的浮点数。使用 std::fixed 使浮点数总是以固定的小数位数显示。
double pi = 3.141592653589793;
// 不使用 std::fixed,默认输出可能使用科学记数法
std::cout << "default output: " << pi << std::endl;
// 使用 std::fixed 以小数位数显示
std::cout << std::fixed << "fixed output: " << pi << std::endl;
(2.2)固定点表示法
默认情况下,std::cout 使用科学记数法来输出非常大或非常小的浮点数。使用 std::fixed 使浮点数总是以固定的小数位数显示。
double pi = 3.141592653589793;
std::cout << std::scientific << pi << std::endl;
// 输出 3.141593e+00
(2.2)设置精度
设置精度使用接口 std::setprecision ,注意该接口需要引用头文件: #include
double pi = 3.141592653589793;
std::cout << std::setprecision(3) << pi << std::endl;
// 输出 3.14
(3)字符串输出
std::cout << "Hello, World!" << std::endl;
// 输出 Hello, World!
std::string str = "Hello, World!";
std::cout << str << std::endl;
// 输出 Hello, World!
(4)布尔值输出
布尔值默认以整数形式显示( true为 1 , false 为 0 )。可以通过 std::boolalpha 来改变这一点,使 true 和 false 以文字形式显示。
bool flag = true;
std::cout << flag << std::endl;
// 输出 1
std::cout << std::boolalpha << flag << std::endl;
// 输出 true
(5)设置填充和宽度
使用 std::setw 操纵器可以设置下一个输出字段的宽度。如果输出数据小于这个宽度,std::cout 会在数据前面或后面填充空格,直到达到指定的宽度。此外还可以使用 std::setfill 操纵器来设置填充字符。
int val = 123;
std::cout << std::setw(5) << std::setfill('0') << val << std::endl;
// 输出 00123
(6)设置左对齐和右对齐
使用std::left或std::right来控制输出的对齐方式。
#include <iostream>
#include <iomanip>
int main() {
std::cout << std::left << std::setw(10) << "Hello" << std::endl; // 左对齐
std::cout << std::right << std::setw(10) << "Hello" << std::endl; // 右对齐
return 0;
}
上面代码输出为:
Hello
Hello
2.3 多线程中应用 std::cout
C++ 中,std::cout 并不是线程安全的。这意味着如果从多个线程同时写入 std::cout,可能会遇到数据竞争(data race)的问题,这会导致未定义的行为(程序可能会崩溃或者无响应)。
可以使用互斥锁(如 std::mutex )来保护对 std::cout 的访问。每个线程在写入 std::cout 之前必须获取锁,并在写入完成后释放锁。这样可以确保每次只有一个线程可以访问 std::cout。如下为样例代码:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex g_coutMutex; // 全局互斥锁
void safePrint(const std::string& message) {
std::lock_guard<std::mutex> lock(g_coutMutex); // std::lock_guard是一个方便的RAII包装器,它会在构造时锁定互斥锁,并在析构时解锁。这使得代码更加简洁,并减少了出错的可能性。
std::cout << message << std::endl;
}
int main() {
std::thread t1(safePrint, "Hello from thread 1");
std::thread t2(safePrint, "Hello from thread 2");
t1.join();
t2.join();
return 0;
}
2.4 printf
printf 是 C 语言标准库中的一个函数,用于格式化输出到标准输出流(通常是终端或控制台窗口)。它接受一个格式字符串和与之对应的值作为参数,然后根据格式字符串中的说明符将值打印出来。 printf 函数是线程安全的,所以在 C++ 的多线程开发中,比 std::cout 用的更方便一些。
** printf 常用的格式说明符及其含义:**
%d 或 %i:带符号十进制整数。
%u:无符号十进制整数。
%f:浮点数(默认保留小数点后六位)。
%c:字符。
%s:字符串。
%p:指针地址。
%x 或 %X:无符号十六进制整数(小写或大写)。
%o:无符号八进制整数。
%%:输出一个 % 字符。
除了转换说明符,格式字符串还可以包含以下可选的标志、宽度、精度和长度修饰符:
标志:
-:左对齐输出。
+:在正数前面显示符号。
(空格):在正数前面显示空格。
#:对于 f、e、E、g、G,输出小数点;对于 o,输出前导零;对于 x 或 X,输出 0x 或 0X 前缀。
0:用零填充空白处。
精度:
对于整数(d、i、o、u、x、X),指定最小数字个数。
对于浮点数(e、E、f、g、G),指定小数点后的数字个数。
对于字符串(s),指定最大字符数。
长度修饰符:
h:指定短整型(short)或单字符(char)。
l:指定长整型(long)。
ll:指定长长整型(long long)。
L:指定宽字符或宽字符串。
j:指定 intmax_t 类型。
z:指定 size_t 类型。
t:指定 ptrdiff_t 类型。
3 输入
C++ 的输入流(Input Stream)通常指的是从某个数据源(如键盘、文件等)读取数据的流。C++标准库中的 头文件提供了基本的输入流功能,主要通过 std::istream 类及其派生类 std::cin 来实现。
std::cin是预定义的对象,代表从标准输入(通常是键盘)接收数据的输入流。可以使用 >> 运算符从std::cin读取数据,这些数据会被存储在相应的变量中。
如下为样例代码(使用 std::cin 从键盘读取数据):
#include <iostream>
int main() {
int val;
std::cout << "input an integer : ";
std::cin >> val; // 从标准输入读取整数并存储在变量 val 中
std::cout << "the input integer is: " << val << std::endl;
return 0;
}
4 文件操作
C++ 的文件操作主要涉及文件的打开、关闭、读取和写入。C++标准库中的 头文件提供了用于文件操作的类,其中 std::ifstream 用于读取文件, std::ofstream 用于写入文件,而 std::fstream 则既可以读取也可以写入文件。
打开文件
在读取或写入文件之前,需要使用相应的文件流对象打开一个文件。可以通过提供文件名来构造一个文件流对象,该对象会在构造时尝试打开文件。如下为样例代码:
#include <fstream>
#include <iostream>
int main() {
// 创建一个用于写入的文件流对象
std::ofstream outfile("test.txt");
// 检查文件是否成功打开
if (!outfile) {
std::cerr << "failed to open the file" << std::endl;
return 1;
}
// 写入一些数据到文件
outfile << "hello" << std::endl;
// 关闭文件
outfile.close();
return 0;
}
读取文件
使用 std::ifstream 可以读取文件的内容。可以使用流提取运算符 >> 或 getline 函数来读取数据。如下为样例代码:
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 创建一个用于读取的文件流对象
std::ifstream infile("test.txt");
// 检查文件是否成功打开
if (!infile)
{
std::cerr << "failed to open the file" << std::endl;
return 1;
}
// 读取文件内容
std::string line;
while (std::getline(infile, line))
{
std::cout << line << std::endl;
}
// 关闭文件
infile.close();
return 0;
}
快速读取大文件
如果需要快速读取一个大文件,则要避免逐行读取或者逐字符读取带来的额外开销。一种快速读取文件的方法是使用文件流的 read 成员函数,它可以一次读取多个字符,这样可以减少系统调用的次数,从而提高读取效率。如下为样例代码:
#include <fstream>
#include <iostream>
#include <vector>
int main() {
// 打开文件
std::ifstream file("large_file.bin", std::ios::binary);
// 检查文件是否成功打开
if (!file)
{
std::cerr << "failed to open the file" << std::endl;
return 1;
}
// 获取文件大小
file.seekg(0, std::ios::end);
std::streamsize fileSize = file.tellg();
file.seekg(0, std::ios::beg);
// 分配足够的内存来存储文件内容
std::vector<char> buffer(fileSize);
// 读取文件内容到buffer
if (!file.read(buffer.data(), fileSize))
{
std::cerr << "读取文件失败" << std::endl;
return 1;
}
// 在这里处理文件内容,例如可以将其输出到标准输出
std::cout.write(buffer.data(), fileSize);
// 关闭文件
file.close();
return 0;
}
读写文件
下面是一个同时包含读取和写入操作的示例:
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 写入文件
std::ofstream outfile("test.txt");
if (!outfile)
{
std::cerr << "failed to open the file" << std::endl;
return 1;
}
outfile << "first line" << std::endl; //写入第一行
outfile << "second line" << std::endl; //写入第二行
outfile.close();
// 读取文件
std::ifstream infile("test.txt");
if (!infile) {
std::cerr << "failed to open the file" << std::endl;
return 1;
}
std::string line;
while (std::getline(infile, line))
{
std::cout << line << std::endl;
}
infile.close();
return 0;
}
文件流的状态
可以使用文件流对象的 is_open() 成员函数来检查文件是否已成功打开。此外,还可以使用 fail() , eof() , 和 bad() 等成员函数来检查流的状态。
文件流的其他操作
clear(): 重置流的状态标志。
seekg() 和 seekp(): 移动文件的读写指针。
tellg() 和 tellp(): 返回文件的读写指针当前位置。
flush(): 清空输出缓冲区,确保所有数据都被写入文件。