资讯详情

深入解析C++中的字符数组和处理字符串的方法

📅 2026/10/22 2:50:19 | 华诺云谱 👁 阅读
深入解析C++中的字符数组和处理字符串的方法
前言C 里「字符串」至少有四种形态字符数组、指向字符的指针、std::string、C17 的std::string_view。它们看起来都能表示一串字符但所有权、生命周期、是否以空字符结尾、能否修改规则各不相同。大量崩溃和乱码就来自把这四者当成同一回事。最常见的误解是「字符数组就是字符串」。字符数组只是一个数组它可以不包含结尾的\0也可以中途就有\0只有以\0结尾、且后续函数按此约定处理的字节序列才构成 C 风格字符串。另一个误解是「sizeof能求字符串长度」——对数组它给出的是整个数组的字节数对指针它给出的是指针本身的大小。还有人以为strncpy一定安全实际上它在源串过长时不会补\0。本文从字符数组的内存布局讲起梳理 C 风格 API 的边界条件再说明std::string与std::string_view的正确用法和失效规则最后给出解析与格式化的现代写法。示例以 C17 为基准涉及 C20 的地方会注明。文中所有结论按标准语义描述本机没有 C 工具链未做编译与运行验证。一、字符数组与字符串字面量字符串字面量hello的类型是const char[6]包含 5 个可见字符和 1 个结尾的空字符。它在程序里通常存放在只读段因此通过指针修改它是未定义行为。// 适用于 C17char a[] hello; // char[6]可修改内容被拷贝到栈上const char* p hello; // 指向只读字面量不能通过 p 修改char b[8] hello; // char[8]后 3 个字节被零初始化// char c[5] hello; // 错误没有空间存放结尾的 \0数组和指针在传参时会退化decay把数组传给函数函数收到的是指向首元素的指针数组长度信息丢失。这就是为什么void f(char s[])与void f(char* s)是同一个签名也是为什么函数内部无法用sizeof得到原始数组大小。求长度必须用std::strlencstring它从首字节开始数到第一个\0。如果数组里没有空字符strlen会越过数组边界继续读属于 UB。相比之下sizeof(a)在数组未退化的场合给出编译期常量两者含义完全不同// 适用于 C17#include cstring#include iostreamint main() {char text[32] hello;std::cout sizeof(text) \n; // 32std::cout std::strlen(text) \n; // 5const char* ptr text;std::cout sizeof(ptr) \n; // 指针大小通常是 4 或 8return 0;}还有一个容易忽略的点sizeof(hello)是 6std::strlen(hello)是 5。前者是数组类型的大小后者是字符数。二、C 风格 API 的边界条件C 标准库的字符串函数都假设输入以\0结尾且不接收目标缓冲区大小。strcpy、strcat、sprintf因此无法自行判断越界必须由调用者保证空间足够。这是缓冲区溢出最常见的来源现代代码应尽量避免。函数关键行为常见误用strcpy(dst, src)拷贝到\0为止不检查dst容量strncpy(dst, src, n)最多拷n字节源串不短于n时不补\0以为结果一定以空字符结尾strncat(dst, src, n)最多追加n字节并总是补\0把n当成目标剩余容量strlen(s)数到第一个\0对可能无终止符的数组调用strcmp(a, b)返回负、零、正用返回值是否等于 1 判断相等sprintf不接收容量格式化结果超出缓冲区strncpy的语义尤其反直觉当源串长度大于等于n时目标缓冲区不会以空字符结尾当源串较短时它会用零把剩余空间填满。所以「用strncpy更安全」是一个错觉除非你手动补上\0。需要格式化到固定缓冲区时用std::snprintfcstdio。它接收容量返回值是「假如空间足够本应写入的字符数」不含结尾空字符据此可以判断是否被截断// 适用于 C17#include cstdio#include stringstd::string format_id(int number, const char* name) {char buffer[64];int written std::snprintf(buffer, sizeof buffer, %d-%s, number, name);if (written 0) {return {}; // 编码错误}if (static_caststd::size_t(written) sizeof buffer) {return {}; // 已截断拒绝这个结果}return std::string(buffer, static_caststd::size_t(written));}注意这里用std::string(buffer, written)而不是直接std::string(buffer)。前者按已知长度构造即使缓冲区里提前出现\0也不会截短后者依赖空字符。两者在正常路径下结果相同但显式长度更不容易出错。三、std::string所有权清晰的字符串std::string自己管理内存拷贝是深拷贝赋值、拼接、查找都有明确语义。它保证c_str()返回以空字符结尾的缓冲区但这个指针在下一次修改字符串或字符串析构后失效。不要把c_str()的结果存起来长期使用。// 适用于 C17#include string#include string_view#include vectorstd::vectorstd::string_view split(std::string_view text, char delimiter) {std::vectorstd::string_view parts;std::size_t start 0;for (;;) {std::size_t pos text.find(delimiter, start);if (pos std::string_view::npos) {parts.push_back(text.substr(start));break;}parts.push_back(text.substr(start, pos - start));start pos 1;}return parts;}这个split返回的是std::string_view它们只是指向原文本的「窗口」不拥有字符。因此调用者必须保证原字符串在视图使用期间一直存活且没有被修改。如果函数接收的是临时std::string返回视图就会悬垂// 适用于 C17 —— 错误示例std::vectorstd::string_view bad() {std::string temp a,b,c;return split(temp, ,); // temp 在函数返回时销毁视图全部悬垂}需要把结果带出函数时应返回std::vectorstd::string或者让调用者传入源字符串并保证其生命周期。std::string的长度语义也要注意size()返回的是字节数不是字符数。UTF-8 编码下一个汉字通常占 3 个字节substr按字节切分可能把多字节字符切成两半产生非法序列。处理多字节文本时要么按码点遍历并识别 UTF-8 的续字节要么使用专门的 Unicode 库。标准库中没有直接按码点切分std::string的设施。std::string的另一个细节是短字符串优化small string optimization, SSO短字符串可能直接存放在对象内部不进行堆分配。但标准不保证这一点容量、增长策略、迭代器失效规则只规定「修改后可能失效」不要依赖具体实现的缓冲区地址或扩容倍数。四、解析与转换的现代写法把文本转成数字std::stoi、std::stod会抛std::invalid_argument或std::out_of_range适合异常可用的场景不想用异常时C17 的std::from_chars更好它不分配内存、不依赖 locale、通过返回码报告错误并且能告诉你解析停在了哪里。// 适用于 C17#include charconv#include string_view#include system_errorbool parse_int(std::string_view text, int out) {const char* begin text.data();const char* end begin text.size();auto result std::from_chars(begin, end, out);if (result.ec ! std::errc{}) {return false; // 范围错误或格式错误}if (result.ptr ! end) {return false; // 尾部还有多余字符}return true;}std::from_chars不会跳过前导空白也不接受0x前缀除非指定base为 16这与std::stoi的行为不同。整数版本在 C17 即可用浮点版本在标准上同样属于 C17但部分标准库实现支持较晚使用前应查阅自己工具链的支持情况。反向转换用std::to_chars同样不分配、不抛异常简单的场景用std::to_string就够但它固定使用%d风格的格式无法控制精度和进制且会产生一次std::string分配。常见坑点坑点 1用sizeof求字符串长度。❌std::size_t n sizeof(ptr);对指针得到的是指针大小。✅ 数组用sizeof(arr) - 1仅限确知含字面量时通用场景用std::strlen或直接用std::string。坑点 2修改字符串字面量。❌char* p abc; p[0] x;字面量在只读段写入是 UB。✅ 用char p[] abc;得到可修改的副本或const char*只读访问。坑点 3strncpy之后不补空字符。❌char buf[4]; std::strncpy(buf, hello, 4);之后调用strlen(buf)。✅ 保证最后一个字节为\0或改用snprintf/std::string。坑点 4把c_str()的指针长期保存。❌const char* p s.c_str(); s x; use(p);追加后指针可能失效。✅ 在需要时重新调用c_str()或先把内容拷贝到自己的缓冲区。坑点 5返回指向局部字符串的string_view。❌ 函数内构造std::string并返回其视图。✅ 返回std::string按值或要求调用者提供生命周期足够长的源。坑点 6用substr按字符切 UTF-8。❌s.substr(0, 1)取「第一个汉字」实际只取到 1 个字节。✅ 按 UTF-8 编码规则识别多字节序列或使用专门的 Unicode 处理库。坑点 7以为std::string一定以\0结尾可传给 C API。❌ 把s.data()或std::string_view::data()直接当 C 字符串传给strlen、printf或套接字接口。✅c_str()保证结尾空字符data()在 C11 起也保证但std::string_view不保证传给 C API 前要自行构造临时字符串。坑点 8忽略snprintf的返回值。❌ 只看缓冲区内容不知道结果是否被截断。✅ 用返回值判断 sizeof buffer即截断 0即编码错误。总结类型是否拥有内存是否保证\0结尾典型用途char[N]是栈或静态区取决于初始化固定大小缓冲区const char*否约定如此只读字面量、C API 参数std::string是堆或 SSO是常规字符串处理std::string_view否否只读、不拥有的切片字符数组是内存布局字符串是约定std::string是所有权std::string_view是视图。写代码时先问自己「这段数据归谁所有、活多久」再决定用哪一种。固定缓冲区只在确实需要避免分配、且能严格校验长度时使用其余场合优先std::string只读传参用std::string_view并在接口文档里写清生命周期要求。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑