C++ std::string 面试 20 问:从 size 到 SSO,背完直接上场(最新整理)
作者:xiangyun61
摘要
面试官问“sizeof(std::string) 是多少”,你答 32 字节,他追问“为什么不是 24”,你卡住了。本文把 std::string 面试中最常被追问的 20 个问题整理成可直接背诵的速查表,每题给出一句话答案和面试官想听的展开点。覆盖 size/capacity 区别、reserve 与 resize、SSO 原理与容量、移动语义的真实边界、string_view 悬空陷阱、UTF-8 截断乱码,以及深浅拷贝的手撕要点。文末附完整面试思维导图和 5 道高频手撕题。
一、基础概念篇
Q1:std::string 和 C 风格 char* 的本质区别是什么?
一句话答案:char* 是指向字符数组的裸指针,不携带长度和所有权信息;std::string 是封装了字符数组的类,自动管理内存、维护 size 和 capacity,提供值语义。
面试官想听的展开:char* 需要手动 malloc/free,容易内存泄漏和缓冲区溢出;std::string 的 size() 是 O(1),而 strlen 是 O(n);std::string 支持嵌入 '\0',结束由 size 决定,不靠 '\0' 标记。
Q2:size() 和 length() 有什么区别?
一句话答案:完全等价,都返回当前有效字符数(字节数),length() 是为了兼容早期 STL 保留的别名。
面试官想听的展开:容器统一用 size(),length() 仅对 std::string 存在。
Q3:size() 和 capacity() 的区别是什么?
一句话答案:size() 是当前有效字符数,capacity() 是已分配内存能容纳的字符总数,size <= capacity 恒成立。
面试官想听的展开:reserve(n) 只改 capacity,resize(n) 改 size 并可能改 capacity;clear() 把 size 设为 0 但不释放 capacity。
Q4:std::string 的元素是连续存储的吗?
一句话答案:C++11 起强制保证连续存储,&s[0] == s.data(),可以安全传给 C API。
面试官想听的展开:C++98/03 不保证连续,C++11 的 [string.require] 明确规定 basic_string 的元素必须连续存放。
Q5:data() 和 c_str() 有什么区别?
一句话答案:C++11 起两者都保证 '\0' 结尾;c_str() 始终返回 const char*,data() 在 C++17 起有非 const 重载,返回 char*,可修改 [0, size()) 范围。
面试官想听的展开:写入 data()[size()](终止符位置)是未定义行为;保存这两个函数返回的指针后,任何可能重新分配的操作都会使其失效。
二、容量与内存管理篇
Q6:reserve() 和 resize() 的区别是什么?
一句话答案:reserve 只改 capacity,不碰 size,字符串内容不变;resize 改 size,可能同时增加 capacity,新增位置填充 '\0' 或指定字符。
面试官想听的展开:reserve 用于预分配避免反复扩容;resize 用于改变字符串有效长度。resize 缩小字符串时不会释放 capacity。
Q7:clear() 会释放内存吗?
一句话答案:不会。clear() 只把 size 设为 0,capacity 保持不变。
面试官想听的展开:这是有意设计,避免循环中反复分配。要确定性释放内存用 std::string().swap(s);shrink_to_fit() 是非绑定请求,实现可以忽略。
Q8:std::string 的扩容策略是什么?
一句话答案:标准只要求摊销常数时间复杂度,常见实现用指数增长:libstdc++ 是 2 倍,libc++ 和 MSVC 是 1.5 倍。
面试官想听的展开:指数增长保证 n 次追加的摊销复杂度为 O(n);固定增量会导致 O(n²)。
Q9:shrink_to_fit() 一定能释放多余内存吗?
一句话答案:不一定。标准规定它是 non-binding request,实现可以选择忽略。
面试官想听的展开:libstdc++ 和 libc++ 通常执行收缩,MSVC 在某些版本中可能忽略。需要确定性释放用 swap 技巧。
三、SSO 与底层布局篇
Q10:什么是 SSO?它的容量是多少?
一句话答案:SSO(短字符串优化)把短字符串直接存在对象内部的 union 缓冲区中,避免堆分配。libstdc++ 和 MSVC 的 SSO 容量是 15 字符,libc++ 是 22 字符(64 位平台)。
面试官想听的展开:libstdc++ 的 std::string 是 32 字节,libc++ 是 24 字节,libc++ 用指针最低位标记长短模式,节省了一个指针字段。
Q11:sizeof(std::string) 是多少?
一句话答案:GCC/libstdc++ 和 MSVC 是 32 字节,Clang/libc++ 是 24 字节(64 位平台)。
面试官想听的展开:32 字节布局包含一个指针、size、capacity 和一个 16 字节的 union 缓冲区;libc++ 的 24 字节布局通过指针最低位标记模式省去了独立标记字段。
Q12:移动一个 SSO 字符串是 O(1) 吗?
一句话答案:不是。SSO 字符串的数据在对象内部,没有堆指针可以窃取,移动构造执行的是内联缓冲区拷贝(memcpy 15 字节),不是 O(1) 指针交换。
面试官想听的展开:只有非 SSO(堆分配)字符串的移动才是 O(1) 指针窃取。这是很多面试者会答错的细节。
Q13:C++98 的 COW 为什么被废弃了?
一句话答案:C++11 规定 operator[] 不得使迭代器/引用失效,而 COW 的非常量 operator[] 可能触发复制并导致失效;此外 COW 的引用计数在多线程下是线程安全噩梦。COW 被事实上禁止。
面试官想听的展开:GCC 5.1(2015 年)默认切换到非 COW 的 SSO 实现,导致 ABI 断裂。
四、性能与 string_view 篇
Q14:operator+ 和 += 的性能差异在哪里?
一句话答案:operator+ 返回新字符串,每次调用都分配新内存并拷贝两个操作数;+= 原地追加,只在容量不足时扩容。
面试官想听的展开:循环或链式 operator+ 会创建大量临时对象,改用先 reserve 再 +=/append 可以快 3–5 倍。
Q15:string_view 比 const string& 快在哪?什么时候不该用?
一句话答案:当参数是 const char* 或字面量时,const string& 会隐式构造临时 std::string,而 string_view 零开销。但如果参数总是 std::string 左值,const string& 更轻量。
面试官想听的展开:string_view 的生命周期绝不能超过所引用的字符串。不能用于需要存储字符串的场景,否则会悬空。
Q16:移动一个 std::string 总是 O(1) 吗?append(std::move(s)) 呢?
一句话答案:非 SSO 字符串的移动是 O(1);SSO 字符串的移动是内联缓冲区拷贝。append(std::move(s)) 在目标容量不足时会退化为拷贝,不是移动。
面试官想听的展开:移动语义的前提是目标能直接接管源的内存;容量不够时只能拷贝。
五、编码与实战篇
Q17:std::string 的 length() 返回的是字符数还是字节数?
一句话答案:字节数。std::string 是 basic_string<char>,不识别 UTF-8。“你好”的 length() 是 6,不是 2。
面试官想听的展开:中文在 UTF-8 下占 3 字节,size() 数的是字节。需要字符数用 utf8cpp 的 utf8::distance()。
Q18:substr 截取中文为什么会产生乱码?
一句话答案:substr 按字节截取,UTF-8 是变长编码,截在中文的中间字节上会产生不完整的 UTF-8 序列,显示为乱码。
面试官想听的展开:安全做法是按 UTF-8 首字节模式识别字符边界,累计到目标字符数后返回字节偏移,再传给 substr。
Q19:Windows 控制台中文乱码的根本原因是什么?
一句话答案:Windows 控制台默认代码页是 936(GBK),而源文件通常是 UTF-8,控制台按 GBK 解码 UTF-8 字节导致乱码。
面试官想听的展开:解决方式是 SetConsoleOutputCP(65001) 或 chcp 65001 让控制台切到 UTF-8;MSVC 还需要 /utf-8 编译选项。
Q20:C++ 标准库能做 GBK 到 UTF-8 的转换吗?
一句话答案:不能。std::string 没有编码语义,std::wstring_convert 已弃用,codecvt 在 C++20 被移除。
面试官想听的展开:Windows 用 MultiByteToWideChar + WideCharToMultiByte 经 UTF-16 中转;Linux 用 iconv;跨平台推荐 Boost.Locale 或 ICU。
六、高频手撕题速览
面试中除了问概念,还经常要求手写代码。以下 5 道题出现频率最高:
题 1:手写一个简化版 String 类,实现构造、拷贝构造、赋值重载、析构。
核心考察点:深拷贝。默认拷贝是浅拷贝,两个对象指向同一块堆内存,析构时 double free。必须在拷贝构造和赋值重载中 new char[] 并 strcpy。
class String {
char* data_;
size_t size_;
public:
String(const char* str = "") : size_(strlen(str)) {
data_ = new char[size_ + 1];
strcpy(data_, str);
}
String(const String& other) : size_(other.size_) { // 深拷贝
data_ = new char[size_ + 1];
strcpy(data_, other.data_);
}
String& operator=(const String& other) {
if (this != &other) {
char* tmp = new char[other.size_ + 1];
strcpy(tmp, other.data_);
delete[] data_;
data_ = tmp;
size_ = other.size_;
}
return *this;
}
~String() { delete[] data_; }
};题 2:为什么拷贝构造和赋值重载都要写?只写一个行不行?
不行。拷贝构造用于创建新对象(String b = a;),赋值重载用于已存在对象之间的赋值(b = a;)。如果不写拷贝构造,编译器生成浅拷贝版本,两个对象共享堆内存,析构时 double free。赋值重载同理。
题 3:strcpy 和 memcpy 的区别是什么?
strcpy 以 '\0' 为结束标记,拷贝到 '\0' 为止;memcpy 按指定字节数拷贝,不关心内容。如果字符串包含嵌入的 '\0',必须用 memcpy。
题 4:reserve 和 resize 的手写实现要点是什么?
reserve 只改 capacity:分配新内存,拷贝原内容,释放旧内存,更新指针和 capacity,size 不变。resize 改 size:如果新 size > 旧 size,填充 '\0' 或指定字符;如果新 size < 旧 size,截断但不释放内存。
题 5:std::string 为什么不能 memset?
std::string 是封装类型,内部维护指针、size、capacity 等元数据。memset 直接操作原始内存,会破坏这些元数据,导致未定义行为。需要清零用 clear() 或赋值为空字符串。
七、面试思维导图
std::string 面试
├── 基础概念
│ ├── vs char*:封装/自动管理/值语义
│ ├── size vs length:等价
│ ├── size vs capacity:有效字符 vs 已分配容量
│ └── data vs c_str:C++11 起都保证 '\0'
├── 容量管理
│ ├── reserve vs resize:容量 vs 长度
│ ├── clear 不释放容量
│ ├── shrink_to_fit 非绑定
│ └── 扩容策略:2 倍 / 1.5 倍
├── 底层布局
│ ├── SSO:15 字符(libstdc++/MSVC)/ 22 字符(libc++)
│ ├── sizeof:32 字节(libstdc++/MSVC)/ 24 字节(libc++)
│ ├── COW 被 C++11 事实禁止
│ └── 连续内存保证(C++11 起)
├── 性能
│ ├── operator+ 临时对象
│ ├── reserve 预分配
│ ├── string_view 生命周期陷阱
│ └── append(std::move) 容量不足退化
├── 编码
│ ├── length() 返回字节数
│ ├── substr 按字节截取,中文会乱
│ └── GBK ↔ UTF-8 需系统 API/iconv
└── 手撕题
├── 深拷贝 String 类
├── strcpy vs memcpy
├── reserve/resize 实现
└── 为什么不能 memset八、总结
std::string 面试的核心考点可以归纳为三层递进:
第一层:会用——size/capacity 区别、reserve/resize 区别、find 返回 npos 必须判断。
第二层:懂底层——SSO 容量(15/22)、sizeof(32/24)、COW 为什么被禁、移动语义的真实边界(SSO 不是 O(1))。
第三层:能实战——string_view 生命周期陷阱、UTF-8 截断乱码、GBK/UTF-8 转换、深拷贝手撕实现。
面试中如果被问住,先给一句话答案,再展开细节。面试官问 sizeof(std::string),不要只答一个数字,接着补一句“libstdc++ 是 32 字节,libc++ 是 24 字节,差异在于 libc++ 用指针最低位标记 SSO 模式,省了一个字段”。这句话能直接把你和其他候选人区分开。
系列导航:
- 上一篇:《中文乱码终结者:C++ std::string 的 UTF-8 实战避坑指南》
- 下一篇:暂无(系列完结)
评论区互动: 这 20 问里,你被哪一问卡住过?是 SSO 容量、string_view 生命周期,还是手撕深拷贝?评论区说说,我看看要不要出一篇“手撕 String 类完整实现”的番外。
参考资料:
- cppreference:
std::basic_string - C++ Standard [string.require], [string.access], [string.capacity]
- libstdc++
basic_string.h(SSO 容量 15) - libc++
<string>头文件(SSO 容量 22)
补充:
🔍 基础认知
- 本质:
std::string是std::basic_string<char>的类型别名,是一个管理动态字符数组的类,自带内存分配、释放和扩容能力,替代 C 风格char*字符串。 - 与 C 风格字符串对比:
char*是裸指针,手动管理内存、易越界、长度需strlenO(n) 遍历;std::string是对象,自动管理内存、内部维护长度 O(1) 获取、支持运算符重载和丰富 API。
📏 核心属性与函数
- size() 与 length():完全等价,都返回当前有效字符数(不含
'\0'),时间复杂度 O(1)。length()更符合字符串语义直觉。 - capacity():返回当前已分配的内存能容纳的最大字符数,
size() <= capacity()恒成立。 - clear():只清空有效字符(size 变 0),capacity 不变,内存不释放。
- resize(n):改变 size。n 小于当前长度则截断;n 大于当前长度则填充字符(默认
'\0'),若超过 capacity 会触发扩容。 - reserve(n):只改变 capacity,不改变 size 和内容。用于预分配内存,避免频繁扩容。注意 reserve 参数小于当前 capacity 时不会缩容。
- shrink_to_fit():非绑定请求,调用后
capacity() >= size()仍可能成立,不能依赖它精确释放内存。想要强制缩容可用临时对象交换技巧:std::string(str).swap(str)。 - 扩容策略:标准未规定,常见实现为指数增长(1.5 倍或 2 倍),使
push_back均摊 O(1)。
到此这篇关于C++ std::string 面试 20 问:从 size 到 SSO,背完直接上场的文章就介绍到这了,更多相关C++ std::string面试必问内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
