跳转到内容
新建笔记

C++ 标准库导航:流、资源管理与字符串

C++标准库包含函数、类、类模板和其他工具,不是只有从C继承的独立函数。“STL”通常指容器、迭代器、算法及相关泛型组件,是理解标准库的一条主线;输入输出、字符串、并发等也属于标准库。

本页采用C++17,提供完整导航,并重点整理原稿中的流、内存管理和字符串接口。容器、算法等已有独立笔记,不在这里重复一套教程。

需求主要头文件入口
容器与泛型算法<array>、<vector>、<map>等,<algorithm>、<iterator>、<functional>容器、迭代器、算法、适配器。
输入输出<iostream>、<fstream>、<sstream>、<iomanip>控制台、文件、内存字符串和格式控制。
数学和数值<cmath>、<complex>、<numeric>、<valarray>数学函数、复数、累计等数值算法和数值数组。
时间<chrono>、<ctime>时钟、时间点、时长和C兼容时间接口。
并发<thread>、<mutex>、<shared_mutex>、<condition_variable>、<future>、<atomic>线程、互斥量、原子内存序。
资源与内存<memory>、<new>、<scoped_allocator>、<cstdlib>智能指针、分配工具、分配器传递和C兼容分配。
文件系统、正则<filesystem>、<regex>路径/目录/文件属性与正则匹配。
错误处理<exception>、<stdexcept>、<system_error>异常、常见异常类型和错误码。
类型支持<typeinfo>、<type_traits>、<typeindex>、<cstddef>、<limits>类型信息、编译期特征、基础类型和数值限制。
通用值工具<utility>、<tuple>、<any>、<optional>、<variant>、<bitset>移动/交换、组合值、可选值、有限候选类型与位集合。
字符与字符串<string>、<string_view>、<cstring>、<cwchar>、<cwctype>、<clocale>、<locale>拥有/借用字符串、C字符串、宽字符与locale。
诊断与C兼容<cassert>、<cerrno>、<csignal>、<csetjmp>、<cstdarg>、<cstdio>、<cctype>等断言、错误状态、信号、可变参数等;各自有严格使用条件。

包含自己实际使用的接口对应头文件,不依赖别的头文件间接把它带进来。<cstdbool>在C++17已弃用,C++本来就有 bool/true/false,普通C++代码不需要它。C兼容头文件不代表相关接口适合任意C++对象,例如跨越非平凡对象寿命的跳转有额外限制。

标准C++字符流层次:ios_base、basic_ios与输入输出及文件字符串流

图保留原资料的类关系,实际常用名字通常是模板对 char的别名,例如 ifstream对应 basic_ifstream<char>。fstream同时具备输入和输出功能,但不是通过继承 ifstream和 ofstream实现的。

头文件常用对象/类型
<ios>ios_base、格式与状态基础设施。
<istream> / <ostream>istream / ostream输入输出接口。
<iostream>cin、cout、cerr、clog等标准流。
<fstream>ifstream只读文件流、ofstream写文件流、fstream双向文件流、filebuf文件流缓冲。
<sstream>istringstream、ostringstream、stringstream内存字符串流。
<iomanip>setw、setprecision、setfill等格式控制。

打开不存在或无权限的文件通常使流进入失败状态。仅判断 !file.bad()不足以确认打开成功,因为 failbit与 badbit含义不同。以下程序只读取命令行指定文件:

#include <fstream>
#include <iostream>
#include <string>
int main(int argc, char* argv[])
{
if (argc != 2) {
std::cerr << "Usage: reader <text-file>\n";
return 2;
}
std::ifstream input(argv[1]);
if (!input) {
std::cerr << "Cannot open input file\n";
return 3;
}
std::string line;
while (std::getline(input, line))
std::cout << line << '\n';
if (input.bad() || (!input.eof() && input.fail())) {
std::cerr << "Read failed before end of file\n";
return 4;
}
return std::cout ? 0 : 5;
}

循环在实际读取后判断流,不写 while (!eof())先猜下一次能否读到。文件流析构会关闭文件;需要提前释放文件或检查写入关闭失败时,可显式 close()并检查状态。

good()表示没有状态位;fail()关注格式/操作失败等;bad()关注严重I/O错误;eof()表示已检测到末尾。它们不是互斥枚举,可能同时置位。格式化提取 >>和按行 getline混用时,需处理之前留下的换行;ignore()默认只忽略一个字符,清除整行应按目的给出上限和分隔符。

标志含义
in / out请求读/写。
app每次写入定位到末尾。
ate打开后先定位末尾,随后仍可按支持的方式定位。
trunc打开成功时丢弃原内容,具有破坏性。
binary二进制模式,避免平台文本模式的特定转换;不自动序列化任意对象。

模式可用按位或组合,但不是所有组合都合法。默认 ofstream写打开通常会截断旧文件;要追加须明确使用app。双向文件流的读写定位及状态切换也要遵循接口规则。C++工作草案:文件流

#include <cassert>
#include <iomanip>
#include <sstream>
#include <string>
int main()
{
std::istringstream input("42 sample");
int number = 0;
std::string name;
input >> number >> name;
assert(input && number == 42 && name == "sample");
std::ostringstream output;
output << std::fixed << std::setprecision(2) << 3.1415;
assert(output.str() == "3.14");
}

在默认浮点格式下,precision主要控制有效数字;结合fixed时控制小数位。setw常只影响下一次相关格式化输出,不能把所有格式控制器都当作相同的持续状态。std::endl还会刷新流,普通换行通常只需 '\n'。

3. 内存管理:资源、初始化和配对

跳转到“3. 内存管理:资源、初始化和配对”

变量的作用域、存储期和实现中的栈/堆位置不是同一个问题。函数内可以有静态对象,编译器也可优化掉某些存储;动态分配区不是“程序里所有未用内存”。优先用容器与智能指针表达所有权,见指针与引用。

接口作用与边界
make_unique / unique_ptr独占对象所有权,自动析构并释放。
shared_ptr / weak_ptr共享所有权/不延长寿命的观察;不自动保护对象内部并发读写。
new T(...) / delete p分配并初始化一个对象/匹配销毁释放。
new T[n] / delete[] p数组形式必须配数组释放;基本类型是否初始化取决于写法。
malloc / calloc / freeC兼容原始存储分配;必须用free配对,不调用普通C++构造析构。
scoped_allocator_adaptor将分配器策略传递给嵌套的使用分配器对象;不是“离开词法作用域便自动释放”的通用开关。

malloc返回的字节未初始化;calloc把分配字节置零,不代表任意C++类型都完成了正确构造,也不保证所有类型的语义零值都等同全零位。二者可能返回空指针。释放后把某一变量设为空,不能修复仍指向原存储的其他别名。

连续二维数组与指针数组不同

跳转到“连续二维数组与指针数组不同”
#include <cassert>
#include <memory>
#include <vector>
int main()
{
auto single = std::make_unique<int>(10);
auto sequence = std::make_unique<int[]>(6); // 本例int元素值初始化为0。
sequence[5] = 9;
assert(*single == 10 && sequence[0] == 0 && sequence[5] == 9);
auto rows = new int[2][3]{}; // 指针类型int (*)[3],连续2×3。
rows[1][2] = 7;
assert(rows[1][2] == 7);
delete[] rows; // 一次匹配数组分配,不逐行delete。
std::vector<std::vector<int>> separate(2, std::vector<int>(3));
separate[1][2] = 7; // 各行拥有自己的存储,不保证行间连续。
assert(separate[1][2] == 7);
}

new T*[m]只创建m个指针,并不会同时创建m行元素。若再逐行分配,还要逐行释放、处理部分失败;上面的容器形式更容易保持异常安全。分配与释放不能混搭 new/free或 malloc/delete。C++工作草案:内存工具

4. string拥有字符,string_view借用字符

跳转到“4. string拥有字符,string_view借用字符”

std::string管理可变长字符序列;std::string_view是C++17的非拥有视图,不延长原字符串寿命,也不保证其末端有NUL。二者的size按 char数量计,不等于UTF-8文本的汉字数或用户感知字符数。

操作注意点
默认/C字符串/复制构造以C字符串构造通常读取到NUL;要保留内嵌NUL,指定长度。
size/length/emptysize与length同义,不包含末尾额外终止符。
append/push_back/insert追加/插入,可能重新分配。
erase/replace/clear删除/替换/清空,位置须符合要求。
[]/at/front/back有效字符索引小于size;at可检查越界,front/back要求非空。
== != <、+比较/字典序/连接,不是按自然语言排序。
find/rfind返回位置或 string::npos,使用前判断。
substr(pos,count)创建拥有内容的新字符串,pos越界会失败。
c_str/data借用内部缓冲地址,不能当作独立长期副本。

从C++11起,string::data()和 c_str()都提供末尾NUL保证;“data不以空字符终止”是过时表述。C++17增加非const字符串的可写data重载,可修改已有字符,但不能借它越过size随意写入。C++工作草案:string访问器

#include <cassert>
#include <cstring>
#include <string>
int main()
{
std::string text = "Hello, world!";
text += " How are you?";
assert(text.substr(0, 5) == "Hello");
const auto position = text.find("world");
assert(position != std::string::npos && position == 7);
text.replace(position, 5, "reader");
text.data()[0] = 'h'; // C++17,索引在已有字符范围内。
assert(text.front() == 'h' && text.back() == '?');
const std::string binary("A\0B", 3);
assert(binary.size() == 3);
assert(std::strlen(binary.c_str()) == 1); // C字符串接口在首个NUL停止。
const std::string fullCopy(binary.data(), binary.size());
assert(fullCopy == binary);
}

借出的指针或string_view在原对象销毁、重新分配或某些修改后可能失效。通过 c_str()传给只读C接口时,应保证调用期间原string仍有效;C接口若需要长期保存,应明确复制或所有权约定。

5. C字符串的缓冲区责任

跳转到“5. C字符串的缓冲区责任”

strcpy/strcat不接收目标容量,调用者必须证明缓冲足够容纳全部字符和终止NUL;数组写越界不会因为用了标准库函数便自动安全。char text[] = "Hello"只为这段初值留出空间,不能直接继续追加任意字符串。

#include <array>
#include <cassert>
#include <cstdio>
#include <cstring>
int main()
{
std::array<char, 50> buffer{};
const int required = std::snprintf(buffer.data(), buffer.size(),
"%s%s", "Hello, world!", " How are you?");
assert(required >= 0 && static_cast<std::size_t>(required) < buffer.size());
assert(std::strcmp(buffer.data(), "Hello, world! How are you?") == 0);
}

snprintf要检查负值错误及输出截断;有缓冲长度参数也不等于可以忽略结果。普通C++文本拼接优先使用string,再在系统边界进行明确转换。