使用流进行输入和输出
一、流的概述
C++ IO流是一套统一的数据传输抽象模型,把数据的输入/输出看作字符流的流动:
- 流的本质 :屏蔽控制台、文件、内存字符串等底层设备差异,使用统一
>>输入、<<输出运算符读写数据; - 分层结构:底层缓冲区 + 流对象,数据先存入缓冲区,缓冲区满/刷新时才真正传输;
- 四大流分类:标准控制台流、文件流、内存字符串流、错误流;
- 核心头文件:
<iostream>(控制台)、<fstream>(文件)、<sstream>(字符串流)。
二、重要的C++流类和流对象
1. 预定义全局控制台对象(<iostream>)
| 对象 | 作用 | 方向 |
|---|---|---|
std::cin |
标准输入(键盘) | 输入流 istream |
std::cout |
标准输出(控制台) | 输出流 ostream |
std::cerr |
标准错误输出,无缓冲 | 输出流 ostream |
std::clog |
标准日志错误,带缓冲 | 输出流 ostream |
2. 核心流类
- istream :输入流基类,提供
>>读取、get()、getline()等读取接口; - ostream :输出流基类,提供
<<写入、格式化控制接口; - ifstream:文件输入流,读取磁盘文件;
- ofstream:文件输出流,写入磁盘文件;
- fstream:文件读写双向流;
- istringstream / ostringstream / stringstream:内存字符串流,在string缓冲区完成转换。
三、使用std::cout格式化输出到控制台
3.1 修改数字显示格式
依赖<iomanip>格式化操纵符,控制进制、浮点数精度、正负号:
cpp
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
int num = 20;
// 进制:dec十进制(默认)、hex十六进制、oct八进制
cout << hex << num << endl;
cout << dec << num << endl;
double pi = 3.1415926;
// fixed固定小数位、setprecision设置精度
cout << fixed << setprecision(4) << pi; // 保留4位小数
// scientific科学计数法
cout << scientific << pi;
return 0;
}
常用操纵符:showpos显示正数加号、uppercase十六进制大写、noshowpos取消符号。
3.2 对齐文本与设置字段宽度
setw(n)设置输出占位宽度,left左对齐、right右对齐(默认)、setfill(c)填充空白字符:
cpp
cout << setw(8) << setfill('-') << left << 123;
// 输出:123----- 总宽度8,左对齐,空白用'-'填充
注意:setw仅对下一个输出元素生效,不会持久生效。
四、使用std::cin进行输入
4.1 读取到基础类型变量
>>运算符自动以空白(空格、回车、Tab)分隔数据,自动匹配int、double、string等基础类型:
cpp
int a; double b; string s;
cin >> a >> b >> s;
缺陷:遇到空格会截断字符串,无法读取带空格的一整行。
4.2 cin::get读取char*字符缓冲区
cin.get(char& ch):读取单个字符,包含空格、换行;cin.get(buf, size):读取最多size-1个字符存入char数组,遇换行停止,换行符留在输入缓冲区;
cpp
char buf[128];
cin.get(buf, 128); // 读取一行到字符数组
4.3 读取整行到std::string
getline(cin, str)读取一整行,包含空格,自动丢弃末尾换行符:
cpp
string line;
getline(cin, line);
坑点:混用cin >>和getline时,>>残留换行符会让getline读到空行,需用cin.ignore()清空缓冲区换行。
五、使用std::fstream处理文件
5.1 open()与close()打开、关闭文件
cpp
#include <fstream>
// 输出文件流
ofstream out;
out.open("test.txt"); // 打开,不存在则创建
out.close(); // 主动关闭,刷新缓冲区
// 输入文件流
ifstream in;
in.open("test.txt");
if (!in.is_open()) { /* 打开失败判断 */ }
in.close();
文件打开模式(可组合):
ios::out:写文件,覆盖原有内容;ios::app追加写入;ios::in:读文件;ios::binary:二进制读写;ios::trunc打开时清空文件。
5.2 创建文本文件,<<写入文本
cpp
ofstream out("doc.txt");
out << "姓名:张三" << endl;
out << "分数:" << 95.5;
out.close();
5.3 >>读取文本文件
cpp
ifstream in("doc.txt");
string name; double score;
in >> name >> score;
整行读取文件:getline(in, str)循环读取直到文件末尾eof()。
5.4 读写二进制文件
二进制读写不用<</>>(会转义换行),使用read()/write()直接操作字节缓冲区:
cpp
// 二进制写入
ofstream out("data.bin", ios::binary);
int val = 100;
out.write((char*)&val, sizeof(int));
// 二进制读取
ifstream in("data.bin", ios::binary);
int res;
in.read((char*)&res, sizeof(int));
六、使用std::stringstream完成字符串转换
<sstream>提供内存流,无需磁盘,在string缓冲区完成数值与字符串互转:
cpp
#include <sstream>
int main()
{
// 数字转字符串 ostringstream
ostringstream oss;
oss << 123 << "," << 3.14;
string str = oss.str();
// 字符串提取数字 istringstream
string s = "10 20.5";
istringstream iss(s);
int a; double b;
iss >> a >> b;
return 0;
}
典型用途:拼接动态文本、字符串拆分、类型互相转换,替代C语言atoi/sprintf。
流通用补充要点
- 缓冲区刷新 :
endl换行+刷新缓冲区;'\n'仅换行不刷新;cout.flush()强制刷新; - 流状态判断 :
cin.fail()读取失败、cin.eof()到达文件末尾、cin.clear()重置错误标志; - 流对象不可拷贝,仅能传递引用;文件/字符串流离开作用域会自动析构关闭资源。
异常处理
什么是异常
异常是程序运行时发生的不可预期错误 (内存访问越界、参数非法、文件打开失败、运算错误等),会打断正常执行流程。
传统C语言依靠返回值/全局错误码判断错误,容易被忽略;C++提供独立的try-throw-catch异常机制,分离业务逻辑与错误处理,异常发生时自动向上逐层匹配捕获代码。
导致异常的常见原因
- 资源错误:文件不存在、网络连接断开、堆内存分配失败
new bad_alloc; - 参数非法:数组/vector下标越界、函数传入空指针、数值超出合法范围;
- 运算故障:除零、数值溢出、类型转换失败;
- 标准库内置异常:
std::out_of_range、std::logic_error、std::runtime_error; - 自定义业务错误:校验不通过、权限不足、流程非法。
使用 try-catch 捕获异常
catch(...) 捕获所有异常
万能捕获块,可匹配任意类型抛出的异常,一般放在所有catch分支最后,作为兜底容错:
cpp
try {
// 可能报错的业务代码
} catch (int e) {
// 捕获int类型异常
} catch (...) {
// 捕获其余全部未知异常
cout << "发生未知异常" << endl;
}
缺点:无法获取异常携带的错误信息,仅适合兜底保护。
捕获特定类型的异常
按异常类型精准匹配,多个catch按从上到下顺序匹配,先写子类异常、后写基类异常:
cpp
try {
vector<int> vec{1,2,3};
vec.at(10); // 越界抛出 out_of_range
} catch (const std::out_of_range& err) {
cout << "容器越界:" << err.what() << endl;
} catch (const std::exception& err) {
cout << "标准异常:" << err.what() << endl;
}
使用 throw 抛出特定类型异常
throw主动抛出任意类型数据(基础类型、字符串、异常类对象),中断当前代码,向上寻找匹配catch:
cpp
void CheckAge(int age) {
if (age < 0 || age > 150) {
// 抛出字符串异常
throw "年龄数值非法";
}
if (age > 120) {
// 抛出标准异常对象
throw std::invalid_argument("年龄超出合理人类范围");
}
}
异常处理的工作原理
核心机制:栈展开(Stack Unwind)
throw触发异常,停止当前代码执行;- 逐层销毁当前作用域局部对象(智能指针、类变量自动析构,RAII保证资源释放,无内存泄漏);
- 向上逐层函数栈查找匹配类型的
catch捕获块; - 找到匹配catch后,执行异常处理逻辑;
- 若整个调用栈无任何匹配捕获,程序直接调用
std::terminate终止运行。
std::exception 标准异常基类
头文件<exception>,所有标准库异常都公有继承该类,核心虚函数:
cpp
virtual const char* what() const noexcept;
返回描述错误的字符串,所有派生异常都会重写该函数。
标准派生异常分类:
- 逻辑异常
logic_error:编译/代码逻辑问题,如invalid_argument、out_of_range; - 运行时异常
runtime_error:运行时外部故障,如bad_alloc内存分配失败。
从 std::exception 派生自定义异常类
业务场景自定义专属异常,统一继承std::exception,重写what():
cpp
#include <exception>
#include <string>
class UserErr : public std::exception {
private:
std::string msg;
public:
explicit UserErr(std::string m) : msg(m) {}
const char* what() const noexcept override {
return msg.c_str();
}
};
// 抛出自定义异常
throw UserErr("用户ID不存在");
继续前行(多核、多线程、工程规范)
当今的处理器有何不同
现代CPU核心特性:
- 多核架构:单CPU封装多个独立计算核心,可并行执行多段代码;
- 超线程(SMT):单个核心虚拟出2个逻辑线程,提升CPU流水线利用率;
- 缓存分层:L1/L2/L3高速缓存,缓解内存读写瓶颈;
- 单核主频提升停滞,性能提升依靠多核并行计算,串行程序无法利用多核算力。
如何更好地利用多个内核:C++多线程
线程是什么
线程是进程内独立的执行流,共享进程堆、全局变量,私有栈、寄存器;
一个进程可创建多个线程,多核CPU可同时调度多个线程并行运行;
C++11标准引入原生线程库,头文件<thread>,跨平台统一接口。
为何编写多线程程序
- 多核性能压榨:并行拆分计算任务,缩短总运行时间;
- 阻塞任务解耦:IO等待(文件/网络)放入子线程,主线程保持响应;
- 并发服务:服务端同时处理多个客户端请求。
线程如何交换数据
- 共享内存:全局变量、堆对象,多线程同时读写会产生数据竞争;
- 线程私有传递:创建线程时通过参数拷贝传递数据,无竞争;
- 同步容器/消息队列:线程间单向投递数据,规避裸共享变量。
使用互斥量、信号量同步线程
多线程共享资源必须同步,防止数据竞争:
- std::mutex 互斥锁 :同一时间仅一个线程持有锁,保护临界区读写;
lock_guard/unique_lock自动解锁; - std::condition_variable 条件变量:线程等待/唤醒,实现生产者-消费者模型;
- 信号量 semaphore(C++20):控制同时访问资源的最大线程数量。
多线程技术带来的问题
- 数据竞争:无同步读写共享变量,结果错乱、未定义行为;
- 死锁:多个线程互相持有对方需要的锁,全部永久阻塞;
- 伪共享:多线程修改同一缓存行不同变量,频繁缓存失效,性能暴跌;
- 线程创建销毁开销、线程调度切换损耗;
- 内存重排:CPU/编译器指令乱序,需
std::atomic原子变量保障内存可见性。
编写杰出的C++代码 核心规范
- 内存安全:优先
unique_ptr/shared_ptr智能指针,杜绝裸指针内存泄漏、野指针; - 资源管理:RAII范式,文件、套接字、锁依靠对象生命周期自动释放;
- 类型安全:多用
const、引用、enum类,减少强制类型转换; - 容器优先:STL容器替代手动数组,算法替代手写循环;
- 异常规范:区分可恢复异常与致命错误,不吞底层异常;
- 并发规范:共享资源强制同步,原子变量处理简单计数,减少锁竞争;
- 现代C++风格:lambda、移动语义、
auto、范围for,降低代码冗余; - 可读性优先:清晰命名、拆分函数、避免超长复杂逻辑。