一句话核心结论:strcpy 是不安全的,strncpy 也不完全安全,std::string + std::string_view 才是 C++ 的正解;4 种 C++ 类型转换各有分工,static_cast 不是 C 风格转换的”替代品”,而是更精准的工具。
前言 如果你问一个 C++ 程序员:”strcpy 危不危险?”,他会说”危险”。但如果你追问”strncpy 安全吗?”,能立刻答出”strncpy 也不安全 “的,可能就只剩一半了。
字符串与内存,是 C/C++ 面试中最古老、也最容易被忽视 的题目。从 strcpy 到 std::string_view,从 malloc 到 new,从 C 风格强制转换到 static_cast——这些 API 看起来人畜无害,实际上每一个都暗藏陷阱 。
读完这一篇,你能彻底搞懂:
strcpy / strncpy / strcat / memcpy 的真实安全性差异std::string 内部到底怎么存数据?SSO 是什么?std::string_view 为什么是 C++17 最重要的字符串类型?4 种 C++ 类型转换分别怎么用?什么时候必须用 dynamic_cast? 如何写一个不会出错的字符串工具类? 一、开篇三连问 1.1 strcpy 为什么危险? 1 2 3 4 5 void unsafe_copy (const char * src) { char buf[8 ]; strcpy (buf, src); }
strcpy 会无脑复制到 '\0' 结束符为止 ,完全不看目标缓冲区有多大。这就是 缓冲区溢出(Buffer Overflow) 的典型成因——也是 Heartbleed、Shellshock 等著名漏洞的元凶。
1.2 strncpy 就安全吗? 1 2 3 char buf[8 ];strncpy (buf, "hello world" , sizeof (buf));buf[sizeof (buf) - 1 ] = '\0' ;
strncpy 的”安全”是假象 :
如果 src 长度 ≥ n,它不会补 '\0' !这是个惊天大坑。 如果 src 长度 < n,它会把 '\0' 一直填充到 n 个字节(性能浪费 )。 所以 strncpy 既不是边界检查版本,也不是安全版本——它是一个古老、蹩脚的临时方案 。 1.3 static_cast 到底在 cast 什么? 1 2 3 4 double d = 3.14 ;int i = static_cast <int >(d); Base* b = new Derived (); Derived* d = static_cast <Derived*>(b);
static_cast 没有运行时检查 ——它相信程序员。如果你写了不安全的转换,编译器会放行 ,出问题你自己负责。
下面我们逐个击破。
二、字符串字面量、char*、char[]、std::string 的本质区别 在写 strcpy 之前,必须先搞懂字符串在 C/C++ 中到底有几种存在形式。
2.1 四种字符串载体对比表 特性 字符串字面量 char*char[]std::string存储位置 只读数据段(.rodata) 指针本身在栈/堆,指向任意位置 栈或全局内存 栈/堆(SSO 或堆) 可写性 ❌ 不可写(UB) ⚠️ 取决于指向 ✅ 可写 ✅ 可写 大小 编译期固定 4/8 字节(指针) 编译期或运行时 动态(SSO 通常 15 字节内) 生命周期 进程整个生命周期 由指向位置决定 作用域结束或全局 作用域结束或显式析构 类型 const char[N]char*char[N]std::basic_string<char>C++ 推荐 ❌ ❌ ⚠️ 偶尔 ✅ 强烈推荐 C 推荐 ✅ ✅ ✅ ❌(无 C++ 标准库)
2.2 字符串字面量不可写:经典 UB 案例 1 2 3 4 char * p = "hello" ; p[0 ] = 'H' ; std::cout << p << std::endl;
1 2 3 4 5 const char * p1 = "hello" ; char p2[] = "hello" ; p2[0 ] = 'H' ; std::cout << p2 << std::endl;
2.3 const char* 与 std::string 的关系 题目 118 问的就是这个。const char* 是 C 风格字符串(带 '\0' 结尾的字符数组),std::string 是 C++ 风格字符串(带长度信息的对象)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <string> #include <cstring> std::string s = "abc" ; const char * c_s = s.c_str (); const char * c_s = "abc" ;std::string s (c_s) ; std::string s = "abc" ; const int len = s.length ();char * c = new char [len + 1 ]; strcpy (c, s.c_str ()); delete [] c; char * c = "abc" ; std::string s (c) ;
注意:s.c_str() 返回的指针,生命周期与 s 绑定 。s 析构或被修改后,指针立即失效。千万不要把 c_str() 返回的指针存起来长期用 。
2.4 函数参数传递建议表 参数语义 推荐类型 原因 只读输入 std::string_view (C++17)零拷贝,不强制构造 std::string 只读输入(C 兼容) const char*与 C API 互通 需要修改但不修改长度 std::string&避免拷贝 需要修改且要传出所有权 std::string(按值传)走移动语义,零拷贝 C 风格缓冲区 char* buf, size_t len显式长度
1 2 3 4 5 void print_name (const std::string& name) ; void print_name (std::string_view name) ;
三、C 字符串函数族:危险等级一览 3.1 常见字符串函数安全性对比表 函数 功能 安全性 替代方案 strcpy(dest, src)复制字符串 ❌ 极危险,无长度检查 std::string、strlcpystrcat(dest, src)追加字符串 ❌ 极危险,无长度检查 std::string::appendstrncpy(dest, src, n)复制 n 字节 ⚠️ 不补 '\0',易踩坑 snprintf 或 std::stringstrncat(dest, src, n)追加 n 字节 ⚠️ 参数是追加长度,不是总长度 std::string::appendstrcmp(s1, s2)比较 ✅ 安全 std::string::comparestrlen(s)取长度 ⚠️ 不含 '\0',需 O(n) std::string::size()sprintf(buf, fmt, ...)格式化 ❌ 无长度检查 snprintfsnprintf(buf, n, fmt, ...)格式化 ✅ 有长度检查 std::format (C++20)memcpy(dst, src, n)复制内存 ⚠️ 不处理重叠 memmovememmove(dst, src, n)复制内存 ✅ 处理重叠 - memset(buf, val, n)填充字节 ⚠️ 不能用于非 trivially 复制类型 std::fill_n
3.2 strcpy 内部实现原理 1 2 3 4 5 6 7 8 9 10 char * strcpy (char * dest, const char * src) { char * d = dest; while ((*d++ = *src++) != '\0' ) { } return dest; }
为什么有返回值? 为了支持链式表达式:
1 2 char buf[100 ];strcpy (buf, strcpy (buf + 50 , "hello" ));
这是典型的”接口设计冗余” ——大多数场景用不上,但少数场景很方便。
3.3 strcat 内部实现 1 2 3 4 5 6 7 char * strcat (char * dest, const char * src) { char * d = dest + strlen (dest); while ((*d++ = *src++) != '\0' ) { } return dest; }
双重 O(n) 开销 ——既要扫一遍 dest 找末尾,又要扫一遍 src 复制。std::string::append 在已知长度时是 O(1)。
3.4 strncpy 的三大坑 1 2 3 4 5 6 7 8 9 10 char * strncpy (char * dest, const char * src, size_t n) { size_t i; for (i = 0 ; i < n && src[i] != '\0' ; i++) { dest[i] = src[i]; } for (; i < n; i++) { dest[i] = '\0' ; } return dest; }
坑点 1:不保证 '\0' 结尾
1 2 3 4 char buf[8 ];strncpy (buf, "abcdefghijklmn" , 8 );
坑点 2:src 长度 < n 时强制填充 '\0'
1 2 3 char buf[100 ];strncpy (buf, "hi" , 100 );
坑点 3:参数含义在不同函数间不一致
strncpy(dest, src, n) 中 n 是目标最大长度 strncat(dest, src, n) 中 n 是追加字符数 (不是总长度!)1 2 3 4 char buf[10 ] = "abc" ;strncat (buf, "defghij" , 5 );
3.5 strcpy 缓冲区溢出实战 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <cstring> #include <iostream> void vulnerable_function (const char * input) { char buf[16 ]; strcpy (buf, input); std::cout << "buf = " << buf << std::endl; } int main () { const char * attacker = "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD" "EEEEEEEEFFFFFFFFGGGGGGGGHHHHHHHH" ; vulnerable_function (attacker); return 0 ; }
防御方案对比表
方案 代码 优缺点 strncpy + 手动补 '\0'strncpy(buf, src, sizeof(buf)-1); buf[sizeof(buf)-1]='\0';⚠️ 易遗漏 snprintfsnprintf(buf, sizeof(buf), "%s", src);✅ 永远安全 std::stringstd::string s = src;✅ 自动扩容 C11 Annex K strcpy_s strcpy_s(buf, sizeof(buf), src);✅ 安全但非标准 (Windows 才有) BSD strlcpy strlcpy(buf, src, sizeof(buf));✅ 安全,Linux/macOS 有,Windows 无
3.6 推荐:现代 C++ 字符串处理范式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <string> #include <string_view> #include <format> std::string s = "hello" ; s += " world" ; s.append (10 , '!' ); void process (std::string_view sv) { if (sv.find ("error" ) != std::string_view::npos) { } } std::string msg = std::format("Hello, {}! You are {} years old." , name, age); char buf[100 ];std::format_to_n(buf, sizeof (buf), "x = {}, y = {}" , x, y);
3.7 字符串安全演进路线图 graph LR
A["🔴 C 时代<br>strcpy/strcat/strncpy<br>无长度检查/易溢出"]
B["🟡 C 过渡期<br>snprintf/strncat<br>手动长度计算"]
C["🟢 C++ 基础<br>std::string<br>自动管理内存"]
D["🔵 C++17<br>std::string_view<br>零拷贝引用"]
E["🟣 C++20<br>std::format<br>类型安全格式化"]
A ==>|"痛点驱动"| B
B ==>|"类型系统升级"| C
C ==>|"性能优化"| D
D ==>|"安全格式化"| E
style A fill:#FFB3C6,stroke:#F48FB1,stroke-width:2px,color:#333
style B fill:#FFDAB9,stroke:#FFAB76,stroke-width:2px,color:#333
style C fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
style D fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
style E fill:#C7CEEA,stroke:#9FA8DA,stroke-width:2px,color:#333四、内存操作三剑客:memset / memcpy / memmove 4.1 三大函数对比表 特性 memsetmemcpymemmove功能 按字节填充 内存拷贝 内存拷贝 支持重叠 N/A ❌ 不支持 ✅ 支持 用途 初始化、清零 块拷贝 块拷贝(可能重叠) 典型实现 循环写字节 rep movsb 或循环智能判断方向 复杂度 O(n) O(n) O(n) 性能 极快(SIMD) 极快(SIMD) 略慢(分支判断)
4.2 memset:按字节填充 1 2 3 4 5 6 7 void * memset (void * s, int c, size_t n) { unsigned char * p = (unsigned char *)s; for (size_t i = 0 ; i < n; i++) { p[i] = (unsigned char )c; } return s; }
坑点:只能用于 trivially copyable 类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <cstring> struct Point { double x, y, z; }; Point p; memset (&p, 0 , sizeof (p)); struct String { char * data; size_t len; }; String s; memset (&s, 0 , sizeof (s));
4.3 memcpy:高速内存拷贝 1 2 3 4 5 6 7 8 void * memcpy (void * dest, const void * src, size_t n) { unsigned char * d = (unsigned char *)dest; const unsigned char * s = (const unsigned char *)src; for (size_t i = 0 ; i < n; i++) { d[i] = s[i]; } return dest; }
现代实现 :编译器会自动用 rep movsb(x86)或 ldm/stm(ARM)等 SIMD 指令优化,速度可达 10GB/s+。
坑点:不能处理重叠内存
1 2 3 4 char buf[] = "1234567890" ;memcpy (buf + 2 , buf, 5 );
4.4 memmove:处理重叠的安全拷贝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void * memmove (void * dest, const void * src, size_t n) { unsigned char * d = (unsigned char *)dest; const unsigned char * s = (const unsigned char *)src; if (d < s) { for (size_t i = 0 ; i < n; i++) { d[i] = s[i]; } } else if (d > s) { for (size_t i = n; i > 0 ; i--) { d[i-1 ] = s[i-1 ]; } } return dest; }
4.5 内存重叠拷贝流程图 graph TB
A["📋 源缓冲区<br>[1][2][3][4][5]"]
B["🎯 目标缓冲区<br>与源重叠"]
A --> C{"判断方向"}
C -->|"目标在源前<br>d < s"| D["➡️ 从前向后复制<br>安全"]
C -->|"目标在源后<br>d > s"| E["⬅️ 从后向前复制<br>安全"]
C -->|"完全重叠<br>d == s"| F["⏭️ 跳过复制<br>空操作"]
D --> G["✅ 拷贝完成"]
E --> G
F --> G
style A fill:#C7CEEA,stroke:#9FA8DA,stroke-width:2px,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,stroke-width:2px,color:#333
style C fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
style D fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
style F fill:#FFDAB9,stroke:#FFAB76,stroke-width:2px,color:#333
style G fill:#FFB3C6,stroke:#F48FB1,stroke-width:2px,color:#3334.6 实战:memmove 处理 std::string 内部重叠 1 2 3 4 5 6 7 8 9 void string_erase (char * data, size_t pos, size_t len, size_t total_len) { memmove (data + pos, data + pos + len, total_len - pos - len); }
为什么 std::string::erase 用 memmove 而不是 memcpy? 因为源区间和目标区间在同一个缓冲区 里,必然重叠 。
4.7 memset 的典型应用与误区 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <cstring> char buffer[1024 ];memset (buffer, 0 , sizeof (buffer)); int arr[100 ];memset (arr, 0 , sizeof (arr)); int arr[100 ];memset (arr, 1 , sizeof (arr)); struct User { std::string name; int age; }; User u; memset (&u, 0 , sizeof (u)); struct Base { virtual void foo () ; };struct Derived : Base { int x; };Derived d; memset (&d, 0 , sizeof (d));
4.8 memset(this, 0, sizeof(*this)) 真的能用吗?——题目 158 能用,但不是万能。 看以下场景:
场景 能否 memset(this, 0, sizeof(*this)) 全是 POD 成员(int、char、struct) ✅ 可以 含 std::string 成员 ❌ 会破坏 SSO 或堆指针 含 std::vector 成员 ❌ 会破坏堆指针和析构逻辑 含虚函数 ❌ 会破坏 vptr(虚表指针) 父类有虚函数 ❌ 同上 想用 placement new 重新构造 ⚠️ 析构顺序会很奇怪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Widget { std::string name_; int value_; public : Widget () { memset (this , 0 , sizeof (*this )); } ~Widget () { } }; class Widget { std::string name_; int value_ = 0 ; public : Widget () = default ; Widget (const char * n, int v) : name_ (n), value_ (v) {} };
五、std::string 的内部实现:SSO、COW 与现代演进 5.1 历史:COW(Copy-On-Write)已被废弃 早期 GCC 的 libstdc++ 和 MSVC 都曾实现过 COW 字符串 :多个 std::string 对象共享同一个底层缓冲区,只在第一次写入时才拷贝 。
1 2 3 4 std::string a = "hello, world, this is a long string" ; std::string b = a; b[0 ] = 'H' ;
为什么 COW 被废弃了?
优点 致命缺点 ✅ 拷贝”零成本” ❌ 引用计数原子操作,比想象中贵 ✅ 适合大量共享场景 ❌ 多线程下原子操作严重拖慢性能 ❌ 与 C++11 移动语义冲突(移动应该零开销) ❌ 迭代器失效规则变得诡异 ❌ operator[] 引用要偷偷做 COW 检查,破坏 constexpr
C++11 标准明确要求 std::string 不再是 COW 实现 。GCC 5+ 已经切换到 SSO 模式。
5.2 现代主流:SSO(Small String Optimization) SSO 思想 :短字符串直接存在 std::string 对象内部 (栈上),不分配堆内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class string_sso {private : union { struct { char * ptr; size_t len; size_t cap; } long_str; struct { char buf[sizeof (long_str)]; uint8_t small_size; } short_str; }; };
不同标准库的 SSO 阈值
标准库 SSO 最大长度 说明 libstdc++ (GCC) 15 字节 主流选择 libc++ (Clang/LLVM) 22 字节 略大 MSVC 15 字节 与 GCC 相同 EASTL 可配置 游戏引擎常用
注意:是字节数 (不含 '\0'),不是字符数。UTF-8 字符串每个字符 1-4 字节不等。
5.3 SSO 内存布局图 graph TB
subgraph "长字符串(> 15 字节)"
L1["ptr<br>8B"]
L2["len<br>8B"]
L3["cap<br>8B"]
L4["📦 堆内存<br>实际数据"]
end
subgraph "短字符串(≤ 15 字节)"
S1["buf[0..14]<br>15B 数据 + 1B 长度"]
S2["unused<br>无"]
S3["❌ 不分配堆"]
end
A["std::string 对象<br>总大小 32 字节(含 padding)"] --> L_MODE["长字符串模式"]
A --> S_MODE["短字符串模式"]
L_MODE --> L1
L_MODE --> L2
L_MODE --> L3
L_MODE -.->|"指针"| L4
S_MODE --> S1
S_MODE --> S2
S_MODE --> S3
style A fill:#C7CEEA,stroke:#9FA8DA,stroke-width:2px,color:#333
style L1 fill:#E8D5F5,stroke:#CE93D8,stroke-width:2px,color:#333
style L2 fill:#E8D5F5,stroke:#CE93D8,stroke-width:2px,color:#333
style L3 fill:#E8D5F5,stroke:#CE93D8,stroke-width:2px,color:#333
style L4 fill:#FFDAB9,stroke:#FFAB76,stroke-width:2px,color:#333
style S1 fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
style S2 fill:#F5F5F5,stroke:#9E9E9E,stroke-width:2px,color:#333
style S3 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#3335.4 SSO 实战验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <string> #include <iostream> #include <iomanip> template <typename T>void print_bytes (const T& obj, const char * label) { const unsigned char * p = reinterpret_cast <const unsigned char *>(&obj); std::cout << label << " (" << sizeof (T) << " bytes): " ; for (size_t i = 0 ; i < sizeof (T); i++) { std::cout << std::hex << std::setw (2 ) << std::setfill ('0' ) << (int )p[i] << " " ; } std::cout << std::dec << std::endl; } int main () { std::string short_str = "hi" ; std::string long_str = "this is a long string, definitely not in SSO!" ; print_bytes (short_str, "short" ); print_bytes (long_str, "long" ); const char * short_data = short_str.data (); const char * long_data = long_str.data (); const std::string* obj_ptr = &short_str; std::cout << "short_str.data() in this object: " << (short_data >= (const char *)obj_ptr && short_data < (const char *)obj_ptr + sizeof (std::string) ? "YES (SSO)" : "NO (heap)" ) << std::endl; std::cout << "long_str.data() in this object: " << (long_data >= (const char *)obj_ptr && long_data < (const char *)obj_ptr + sizeof (std::string) ? "YES (SSO)" : "NO (heap)" ) << std::endl; return 0 ; }
典型输出(GCC 11, x86-64) :
1 2 3 4 short (32 bytes): 68 69 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 long (32 bytes): 78 55 6f 55 d9 55 00 00 2b 00 00 00 2b 00 00 00 ... short_str.data() in this object: YES (SSO) long_str.data() in this object: NO (heap)
可以看到 short 字符串的 68 69 00... 就是 "hi\0",直接存在对象内部。
5.5 SSO 的性能影响 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <chrono> #include <string> #include <vector> void bench () { constexpr int N = 1'000'000 ; std::vector<std::string> v; v.reserve (N); auto start = std::chrono::high_resolution_clock::now (); for (int i = 0 ; i < N; i++) { v.emplace_back ("hi" ); } auto end = std::chrono::high_resolution_clock::now (); auto us = std::chrono::duration_cast <std::chrono::microseconds>(end - start).count (); std::cout << N << " short strings: " << us << " us\n" ; }
没有 SSO 的话 ,每次 emplace_back 都要 malloc + memcpy,性能会差 10-50 倍。
5.6 SSO 适用场景判断表 字符串长度 是否触发 SSO 性能特征 0-15 字节 ✅ 是 零堆分配,极快 16-22 字节 ⚠️ 取决于实现 libc++ 可能 SSO > 22 字节 ❌ 否 一次堆分配 多次 append ⚠️ 可能溢出到堆 即使初始 SSO 也会转换
六、std::string_view:C++17 的零拷贝引用 6.1 什么是 string_view std::string_view 是对一段连续字符序列的非拥有引用 ——它不拷贝数据,不管理生命周期,只是一个”窗口”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <string_view> #include <string> #include <iostream> void print (std::string_view sv) { std::cout << "[" << sv << "], length = " << sv.length () << "\n" ; } int main () { std::string s = "hello world" ; const char * c = "literal" ; char arr[] = {'H' , 'i' , '\0' }; print (s); print (c); print ("raw literal" ); print (arr); print (std::string_view (arr, 2 )); print (std::string_view (s).substr (6 )); print (std::string_view (s).substr (0 , 5 )); }
6.2 string_view vs const std::string& vs const char* 特性 std::string_viewconst std::string&const char*接受字面量 ✅ 零开销 ❌ 需要构造 std::string ✅ 零开销 接受 char* ✅ 零开销 ❌ 需要构造 std::string ✅ 直接 接受 std::string ✅ 零开销 ✅ 零开销 ❌ 需要 c_str() 存储长度 ✅ O(1) ✅ O(1) ❌ 需要 strlen 要求 '\0' 结尾 ❌ 不要求 N/A ✅ 要求 C++ 标准 C++17 C++98 C89
结论 :std::string_view 是最通用、最高效的只读字符串参数类型 。
6.3 string_view 内部结构 1 2 3 4 5 6 class string_view {private : const char * data_; size_t size_; };
大小 :通常 16 字节(一个指针 + 一个 size_t),比 std::string 小一倍 。
6.4 致命陷阱:悬挂引用(Dangling) string_view 不拥有数据 ——如果底层数据被销毁,string_view 立即失效。
1 2 3 4 5 6 7 8 9 10 11 std::string_view get_name () { std::string name = "Alice" ; return name; } int main () { auto name = get_name (); std::cout << name << std::endl; std::cout << name.length () << std::endl; }
1 2 3 4 5 std::vector<char > buf = {'h' , 'i' }; std::string_view sv (buf.data(), buf.size()) ;buf.push_back ('!' ); std::cout << sv << std::endl;
1 2 3 4 5 6 7 8 9 void log (std::string_view sv) ;log (std::string ("temp" ) + "literal" ); std::string s = std::string ("temp" ) + "literal" ; log (s);
6.5 何时使用 string_view 场景 推荐类型 函数参数(只读、零拷贝) ✅ std::string_view 函数返回值(指向已存在数据) ⚠️ 谨慎,必须保证数据生命周期 成员变量(长期存储) ❌ 绝对不要 !用 std::string 局部变量(短生命周期) ✅ 可以,但别传出作用域 全局静态数据 ✅ 可以指向字面量 字符串字面量 ✅ constexpr std::string_view sv = "hello";
6.6 string_view 实战优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 bool contains_keyword (const std::string& s, const std::string& keyword) { return s.find (keyword) != std::string::npos; } contains_keyword ("user input" , "admin" ); bool contains_keyword (std::string_view s, std::string_view keyword) { return s.find (keyword) != std::string_view::npos; } contains_keyword ("user input" , "admin" ); constexpr std::string_view HELLO = "Hello, World!" ;static_assert (HELLO.size () == 13 );static_assert (HELLO.substr (0 , 5 ) == "Hello" );
七、malloc / free vs new / delete 详解 7.1 C 与 C++ 内存管理对比表 特性 malloc / freenew / delete来源 C 标准库 C++ 运算符 构造/析构 ❌ 不调用 ✅ 自动调用 类型安全 ❌ 返回 void*,需强转 ✅ 返回具体类型 失败处理 返回 NULL(或 nullptr) 抛 std::bad_alloc 数组支持 malloc(n * sizeof(T))new T[n](调用 n 次构造)可重载 ❌ 不可以 ✅ 可以(operator new) 重载时机 N/A 编译期 C++ 推荐 ❌(除非与 C 库互操作) ✅ 强烈推荐
7.2 底层关系 7.3 实战:new vs malloc 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <new> #include <cstdlib> #include <cstring> #include <iostream> class Widget { int * data_; size_t size_; public : Widget (size_t n) : size_ (n), data_ (new int [n]) { std::cout << "ctor Widget(" << n << ")\n" ; } ~Widget () { std::cout << "dtor Widget\n" ; delete [] data_; } }; int main () { Widget* w1 = new Widget (10 ); delete w1; Widget* w2 = (Widget*)malloc (sizeof (Widget)); free (w2); return 0 ; }
输出 :
1 2 3 ctor Widget(10) dtor Widget (无 Widget 输出,因为 malloc 不调构造)
7.4 placement new:在指定内存上构造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <new> #include <cstdlib> #include <cstring> int main () { void * buffer = std::malloc (sizeof (Widget)); if (!buffer) return 1 ; Widget* w = new (buffer) Widget (20 ); w->~Widget (); std::free (buffer); return 0 ; }
典型用途 :
内存池 :预分配大块内存,按需构造对象嵌入式系统 :裸机环境手动管理内存std::vector 的 reserve :预分配容量,构造时用 placement newstd::optional / std::variant :内部存储用 placement new7.5 数组形式:new[] / delete[] 1 2 3 4 5 6 7 int * arr = new int [100 ]; delete [] arr; int * p = new int [10 ];delete p;
为什么必须有 delete[]? 因为编译器会在数组前存储元素个数(cookie),delete[] 据此调用对应次数的析构函数。
1 2 3 4 5 6 void * allocate_with_cookie (size_t n, size_t elem_size) { size_t * cookie = (size_t *)malloc (sizeof (size_t ) + n * elem_size); *cookie = n; return cookie + 1 ; }
7.6 内存管理”十诫” 编号 戒律 违反后果 1 new 配 delete,new[] 配 delete[]内存泄漏或崩溃 2 malloc 配 free,别混用 new/freeUB 3 一个指针只能 delete 一次 double free 4 不要 delete 同一块栈内存 UB 5 delete 后立即置 nullptr防止悬挂 6 优先用智能指针(unique_ptr/shared_ptr) 自动管理 7 自定义类禁用拷贝时要禁用拷贝构造/赋值 防止浅拷贝灾难 8 不要在构造函数里 new 后抛出异常 内存泄漏(用 RAII) 9 基类析构函数必须 virtual 派生类析构不调用 10 大对象用 std::move 转移所有权 避免不必要拷贝
八、4 种 C++ 类型转换详解(题目 93、157) C 风格的强制转换 (T)expr 在 C++ 中仍然可用 ,但 C++ 提供了4 种更精准的转换运算符 。理解它们的分工,是写出类型安全、易于维护 代码的基础。
8.1 总览表 转换 用途 编译期/运行期 安全性 static_cast基础类型、类层次上行/下行、void* 反向 编译期 ⚠️ 中(程序员负责) dynamic_cast类层次下行转换(带类型检查) 运行期(RTTI) ✅ 高 const_cast添加/移除 const 或 volatile 编译期 ⚠️ 低(破坏常量性) reinterpret_cast指针与整数互转、不相关类型指针 编译期 ❌ 极低(接近裸操作)
8.2 static_cast:编译期静态转换 用途 1:基本类型之间
1 2 3 4 5 6 int i = 42 ;double d = static_cast <double >(i); char c = static_cast <char >(i); double pi = 3.14159 ;int truncated = static_cast <int >(pi);
用途 2:类层次上行转换(安全)
1 2 3 4 5 6 class Base { virtual void foo () {} };class Derived : public Base { void bar () {} };Derived d; Base* b = static_cast <Base*>(&d); Derived* d2 = static_cast <Derived*>(b);
用途 3:枚举与整数互转
1 2 3 enum class Color { Red, Green, Blue };int c = static_cast <int >(Color::Red); Color col = static_cast <Color>(2 );
用途 4:void* 反向转换
1 2 3 void * buf = malloc (sizeof (int ));int * p = static_cast <int *>(buf);
用途 5:任何类型 → void
1 2 int x = 42 ;static_cast <void >(x);
8.3 dynamic_cast:安全的下行转换 核心特性 :依赖 RTTI(Run-Time Type Information),运行期检查类型 ,失败返回 nullptr(指针)或抛 bad_cast(引用)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <typeinfo> class Shape { virtual void draw () {} }; class Circle : public Shape { public : void area () {} };class Square : public Shape { public : void area () {} };void process (Shape* s) { if (auto * c = dynamic_cast <Circle*>(s)) { c->area (); } else if (auto * sq = dynamic_cast <Square*>(s)) { sq->area (); } } int main () { Circle c; process (&c); Square sq; process (&sq); return 0 ; }
引用版本:失败抛异常
1 2 3 4 5 6 7 8 void process_ref (Shape& s) { try { Circle& c = dynamic_cast <Circle&>(s); c.area (); } catch (const std::bad_cast& e) { std::cerr << "cast failed: " << e.what () << "\n" ; } }
8.4 const_cast:添加/移除 const/volatile 1 2 3 4 5 6 7 8 9 10 11 const int x = 42 ;int * p = const_cast <int *>(&x); *p = 100 ; void process (char * str) ; void process (const char * str) ; const char * s = "literal" ;process (const_cast <char *>(s));
典型应用场景 :与 C 库互操作
1 2 3 4 5 const Data* data = get_data ();some_c_function (const_cast <Data*>(data));
8.5 reinterpret_cast:底层位级重解释 最危险的转换 ——直接重新解释内存,不做任何检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int * p = new int (42 );uintptr_t addr = reinterpret_cast <uintptr_t >(p); int * p2 = reinterpret_cast <int *>(addr); struct A { int x; };struct B { double y; };A a; B* b = reinterpret_cast <B*>(&a); b->y = 3.14 ; using FuncPtr = void (*)();void foo () {}FuncPtr fp = reinterpret_cast <FuncPtr>(&foo);
严格别名规则(Strict Aliasing) :C++ 标准规定,不同类型的指针不能指向同一块内存 。reinterpret_cast 是绕开这个规则的工具,但后果自负 。
8.6 4 种 cast 决策流程图 graph TD
START(["🎯 我要转换类型"])
START --> Q1{"转换是否相关<br>(数值、类层次、void*)?"}
Q1 -->|"是"| Q2{"类层次下行?"}
Q1 -->|"否"| Q3{"是否要改<br>const/volatile?"}
Q1 -->|"否"| CAST_REINT["🔴 reinterpret_cast<br>位级重解释,极危险"]
Q2 -->|"是,运行时检查"| CAST_DYN["🟢 dynamic_cast<br>运行期 RTTI,安全"]
Q2 -->|"否,编译期转换"| CAST_STAT["🟡 static_cast<br>编译期检查,程序员负责"]
Q3 -->|"是"| CAST_CONST["🟠 const_cast<br>修改 const/volatile"]
Q3 -->|"否"| CAST_REINT
CAST_STAT --> DONE(["✅ 完成"])
CAST_DYN --> DONE
CAST_CONST --> DONE
CAST_REINT --> DONE
style START fill:#C7CEEA,stroke:#9FA8DA,stroke-width:2px,color:#333
style Q1 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
style Q2 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
style Q3 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
style CAST_STAT fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
style CAST_DYN fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
style CAST_CONST fill:#FFDAB9,stroke:#FFAB76,stroke-width:2px,color:#333
style CAST_REINT fill:#FFB3C6,stroke:#F48FB1,stroke-width:2px,color:#333
style DONE fill:#E8D5F5,stroke:#CE93D8,stroke-width:2px,color:#3338.7 4 种 C++ 类型转换 vs C 风格转换对比表 维度 C++ 4 种 cast C 风格 (T)expr 可读性 ✅ 一眼看出转换意图 ❌ 无法区分 类型安全 ✅ 编译期拒绝非法转换 ❌ 几乎无所不能 可搜索性 ✅ grep "static_cast" 找出所有 ❌ 强转散布各处 误用难度 ⚠️ 中等(reinterpret_cast 也危险) ❌ 太容易误用 C 兼容 ❌ C 没有 ✅ 通用 编译速度 略慢(更多解析) 略快 性能 完全相同(都是编译期指令) 完全相同
题目 157 标准答案 :
更加安全:编译期能拒绝明显错误的转换(如 static_cast 不能去掉 const,必须用 const_cast) 更直接明显:一眼看出是什么类型转什么类型 可读性更好:能体现程序员的意图 可搜索性:方便在大型代码库中审计 8.8 实战:综合运用 4 种 cast 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <iostream> #include <vector> #include <memory> #include <typeinfo> extern "C" int process_raw_data (void * data, size_t size) ;class DataProcessor { std::vector<uint8_t > buffer_; public : DataProcessor (std::vector<uint8_t > buf) : buffer_ (std::move (buf)) {} int send_to_c_library () { uint8_t * ptr = static_cast <uint8_t *>(buffer_.data ()); void * c_ptr = reinterpret_cast <void *>(ptr); return process_raw_data (c_ptr, buffer_.size ()); } }; class Base { virtual void serialize () {} };class Derived : public Base { int value_ = 42 ; public : void serialize () override { } int get_value () const { return value_; } }; void polymorphic_processor (Base* base) { if (auto * d = dynamic_cast <Derived*>(base)) { std::cout << "Derived value: " << d->get_value () << "\n" ; } } int main () { std::vector<uint8_t > data = {1 , 2 , 3 , 4 , 5 }; DataProcessor proc (std::move(data)) ; int result = proc.send_to_c_library (); std::cout << "C result: " << result << "\n" ; Derived d; polymorphic_processor (&d); return 0 ; }
8.9 4 种 cast 的常见反模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const int x = 10 ;int * p = static_cast <int *>(&x); Base* b = new Derived (); Derived* d = reinterpret_cast <Derived*>(b); const int true_const = 42 ;int * p = const_cast <int *>(&true_const);*p = 100 ; int * p = static_cast <int *>(static_cast <void *>(&some_double));
8.10 何时不要 用 4 种 cast 场景 推荐做法 原因 数值类型转换 不用 cast 隐式转换即可,或用 std::byte 显式 算术类型 → 布尔 static_cast<bool> 或 !!隐式转换可能丢信息 整数 → 枚举 static_cast<EnumType>OK 派生类指针 → 基类指针 隐式转换 (不用 cast)上行永远安全 修改设计避免转换 重新设计类型系统 釜底抽薪
9.1 三种格式化方式对比表 方式 标准 安全性 类型安全 可读性 性能 sprintfC89 ❌ ❌ ⚠️ 快 snprintfC99 ✅ ❌ ⚠️ 快 std::stringstreamC++98 ✅ ✅ ❌ 慢 fmt::format第三方 ✅ ✅ ✅ 极快 std::formatC++20 ✅ ✅ ✅ 极快
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <format> #include <string> #include <iostream> int main () { std::string name = "Alice" ; int age = 30 ; std::string s1 = std::format("Hello, {}!" , name); std::string s2 = std::format("{1} is {0} years old" , age, name); std::string s3 = std::format("pi = {:.4f}" , 3.141592653 ); std::string s4 = std::format("hex: {:#x}, oct: {:#o}, bin: {:#b}" , 255 , 255 , 255 ); std::string s5 = std::format("[{:>10}]" , "hi" ); std::string s6 = std::format("[{:<10}]" , "hi" ); std::string s7 = std::format("[{:^10}]" , "hi" ); std::string s8 = std::format("[{:*^10}]" , "hi" ); std::string s9 = std::format("{:*=10}" , 42 ); std::cout << s1 << "\n" << s2 << "\n" << s3 << "\n" << s4 << "\n" << s5 << "\n" << s6 << "\n" << s7 << "\n" << s8 << "\n" << s9 << "\n" ; return 0 ; }
1 2 3 4 5 6 7 #include <format> void safe_format (char * buf, size_t buf_size) { auto result = std::format_to_n(buf, buf_size - 1 , "x={} y={} z={}" , 1 , 2 , 3 ); buf[result.size] = '\0' ; }
9.4 自定义类型的格式化支持 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <format> struct Point { int x, y; }; template <>struct std ::formatter<Point> { constexpr auto parse (std::format_parse_context& ctx) { return ctx.begin (); } auto format (const Point& p, std::format_context& ctx) const { return std::format_to(ctx.out (), "({}, {})" , p.x, p.y); } }; int main () { Point p{3 , 4 }; std::string s = std::format("point = {}" , p); }
十、实战:写一个安全的字符串工具类 10.1 安全字符串复制函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <cstring> #include <string> #include <string_view> #include <stdexcept> std::string safe_copy_v1 (const char * src) { if (!src) throw std::invalid_argument ("null src" ); size_t len = std::strlen (src); char * buf = new char [len + 1 ]; std::snprintf (buf, len + 1 , "%s" , src); std::string result (buf) ; delete [] buf; return result; } std::string safe_copy_v2 (const char * src) { if (!src) throw std::invalid_argument ("null src" ); return std::string (src); } std::string safe_copy_v3 (std::string_view src) { return std::string (src); }
10.2 安全的字符串拼接 1 2 3 4 5 6 7 8 9 10 char buf[100 ];strcat (buf, "hello" );strcat (buf, " world" );std::string s = "hello" ; s += " world" ; s.append (10 , '!' );
10.3 自定义”安全字符串”包装类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class SafeString { std::string data_; public : SafeString () = default ; explicit SafeString (const char * s) : data_(s ? s : "" ) { } SafeString& append (std::string_view sv) { data_.append (sv); return *this ; } const char * c_str () const { return data_.c_str (); } size_t size () const { return data_.size (); } std::string_view substr (size_t pos, size_t count = std::string_view::npos) const { return std::string_view (*this ).substr (pos, count); } operator std::string_view () const { return data_; } };
10.4 内存拷贝辅助函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <cstring> #include <type_traits> #include <stdexcept> template <typename T>typename std::enable_if<std::is_trivially_copyable<T>::value, void >::type safe_copy (T* dest, const T* src, size_t n) { if (!dest || !src) throw std::invalid_argument ("null pointer" ); std::memmove (dest, src, n * sizeof (T)); } template <typename T>typename std::enable_if<std::is_trivially_copyable<T>::value, void >::type safe_zero (T* arr, size_t n) { if (!arr) throw std::invalid_argument ("null pointer" ); std::memset (arr, 0 , n * sizeof (T)); }
十一、面试题集中答疑 11.1 题目 12 完整答案 int 转字符串、字符串转 int?strcat、strcpy、strncpy、memset、memcpy 的区别?
int ↔ string 转换 :
1 2 3 4 5 6 7 8 9 10 std::string s = std::to_string (42 ); int i = std::stoi ("42" ); long l = std::stol ("1234567890" );double d = std::stod ("3.14" );char buf[20 ];sprintf (buf, "%d" , 42 ); int j = atoi ("42" );
字符串函数对比表
函数 功能 关键区别 安全性 strcpy复制整个字符串 无长度检查,依赖 '\0' ❌ 极危险 strncpy复制 n 字节 不保证 '\0' 结尾 ⚠️ 易踩坑 strcat追加整个字符串 无长度检查 ❌ 极危险 strncat追加最多 n 字节 n 是追加字符数(非总长)⚠️ 易混淆 memset按字节填充 与具体类型无关 ⚠️ 仅适合 POD memcpy内存块复制 高速,不处理重叠 ⚠️ 重叠时 UB memmove内存块复制 处理重叠 ✅ 安全
替代方案 :
1 2 3 4 5 6 7 std::string s1 = "hello" ; std::string s2 = s1; s1 += " world" ; std::memmove (dest, src, n);
11.2 题目 118 完整答案 const char 与 string 之间的关系,传递参数问题? *
参见 §2.3,核心是:
std::string 拥有数据,const char* 只是观察者c_str() 返回的指针生命周期与 std::string 绑定函数参数推荐 std::string_view(C++17)或 const std::string&(C++11/14) 11.3 题目 156 完整答案 strcpy 函数和 strncpy 函数的区别?哪个函数更安全?
函数原型 :
1 2 char * strcpy (char * dest, const char * src) ;char * strncpy (char * dest, const char * src, size_t n) ;
核心区别 :
维度 strcpystrncpy复制长度 整个字符串(到 '\0') 最多 n 字节 目标要求 任意大小(危险 ) 必须有 n 字节空间 结尾保证 总是补 '\0' 不保证补 '\0' (src ≥ n 时)源 src > n 溢出 dest 截断,不补 '\0' 源 src < n 正常 末尾用 '\0' 填充到 n 字节 src 与 dest 重叠 UB UB
谁更安全?
两个都不完全安全 :
strcpy 完全无长度检查——strcpy 升级版的 strcpy_s(C11 Annex K)才安全strncpy 不保证 '\0' 结尾,是典型的”看起来安全实际不安全”真正安全的做法 :
1 2 3 4 5 6 7 8 size_t strlcpy (char * dest, const char * src, size_t dest_size) ;errno_t strcpy_s (char * dest, rsize_t dest_size, const char * src) ;std ::string dest = src;
11.4 题目 157 完整答案 为什么要用 static_cast 转换而不用 c 语言中的转换?
更安全 :编译期拒绝明显错误的转换
static_cast 不能去掉 const(必须用 const_cast)static_cast 不能做不相关类型指针转换(必须用 reinterpret_cast)更清晰 :一眼看出转换意图
1 2 3 int i = static_cast <int >(d); int * p = const_cast <int *>(&x); long addr = reinterpret_cast <long >(p);
可读性更好 :能体现程序员的意图和风险等级
可搜索性 :方便代码审计(grep "static_cast" vs 散落的 (T))
与 C++ 类型系统协同 :能配合模板、智能指针等现代特性
十二、字符串与内存知识点速查表 12.1 字符串操作速查 操作 C 风格 C++ 推荐 安全性 复制 strcpy(dst, src)std::string s = src;C++ ✅ 追加 strcat(dst, src)s += src; 或 s.append(src);C++ ✅ 取长度 strlen(s)s.size() 或 s.length()C++ ✅ 比较 strcmp(s1, s2)s1 == s2 或 s1.compare(s2)C++ ✅ 切片 substr + copystring_viewC++17 ✅ 格式化 sprintf(buf, ...)std::format(...)C++20 ✅ 搜索 strstr(haystack, needle)haystack.find(needle)C++ ✅ 分割 strtok(破坏原串)范围 for + find C++ ✅
12.2 内存操作速查 操作 函数 注意事项 填充字节 memset(buf, val, n)仅 POD 类型 块拷贝(不重叠) memcpy(dst, src, n)性能最高 块拷贝(可重叠) memmove(dst, src, n)略慢但安全 申请内存 malloc(n) / new TC++ 用 new 释放内存 free(p) / delete pC++ 用 delete 数组分配 new T[n] / delete[] p配对使用 预分配 std::vector::reserve避免多次扩容 智能指针 std::unique_ptr<T>自动释放
12.3 类型转换速查 场景 推荐 cast 备选 int ↔ doublestatic_cast- Derived* → Base*(上行)隐式转换 - Base* → Derived*(下行,安全优先)dynamic_caststatic_cast(如果确认类型)去除 const const_cast- 指针 ↔ 整数 reinterpret_cast- void* → 具体类型static_cast- 枚举 ↔ 整数 static_cast-
十三、字符串与内存常见面试追问 13.1 Q: strlen 时间复杂度?std::string::size() 呢? 函数 复杂度 原因 strlen(s)O(n) 必须遍历到 '\0' std::string::size()O(1) std::string 内部存储长度
13.2 Q: std::string 的 c_str() 和 data() 有什么区别? 函数 返回类型 C++11 起的保证 c_str()const char*以 '\0' 结尾 data()const char*C++11 前不一定以 '\0' 结尾,C++11 后保证
现代 C++ 中两者几乎等价 。data() 是更通用的”获取底层数据”接口,c_str() 强调”C 字符串兼容”。
13.3 Q: std::string::operator[] 和 at() 区别? 1 2 3 std::string s = "hello" ; char c1 = s[10 ]; char c2 = s.at (10 );
operator[] 追求性能,不做边界检查 ;at() 安全,有性能开销 。
13.4 Q: std::string 在不同 STL 实现下大小一样吗? 不一样,参见 §5.2。
13.5 Q: memset(arr, 1, sizeof(arr)) 把 int arr[10] 初始化成什么了? 每个字节都是 0x01 ,所以整型值是 0x01010101 = 16843009,不是 1 。
13.6 Q: reinterpret_cast 和 C 风格 (void*)p 有什么区别? 编译期检查:reinterpret_cast 不允许把指针转成更小的整数(某些编译器) 可读性:搜索 reinterpret_cast 一目了然 行为完全相同 13.7 Q: 为什么 dynamic_cast 要求基类有虚函数? 因为 dynamic_cast 用 RTTI(运行期类型信息)检查类型,RTTI 信息存储在**虚函数表(vtable)**中。没有虚函数就没有 vtable,就没有 RTTI。
1 2 3 4 5 class Base {}; class Derived : public Base {};Base* b = new Derived (); Derived* d = dynamic_cast <Derived*>(b);
13.8 Q: std::move 之后还能用对象吗? std::move 本身不做任何事 ——它只是把对象转为右值引用,实际移动由对应的移动构造/赋值函数完成 。
1 2 3 4 5 6 7 std::string s = "hello" ; std::string s2 = std::move (s); s = "world" ; std::cout << s << std::endl;
十四、避坑清单:字符串与内存十大陷阱 编号 陷阱 后果 防御 1 strcpy 缓冲区溢出栈破坏、任意代码执行 用 std::string 或 snprintf 2 修改字符串字面量 段错误或静默数据破坏 用 char[] 而非 char* 3 strncpy 忘记补 '\0'字符串越界读取 用 snprintf 或 std::string 4 memset(this, 0, sizeof(*this)) 破坏 vptr虚函数调用崩溃 用成员初始化 5 memcpy 拷贝重叠内存数据损坏 用 memmove 6 delete[] 配 new,delete 配 new[]内存泄漏或崩溃 严格配对 7 string_view 指向已销毁的 std::string悬挂引用 不要把 string_view 长期存储 8 const_cast 修改真正常量UB 仅用于与 C 库互操作 9 reinterpret_cast 破坏严格别名规则UB,编译器优化错乱 仅在序列化等明确场景用 10 dynamic_cast 性能开销多态类型转换慢 确认类型时用 static_cast
十五、思考延伸 15.1 给初学者的建议 告别 C 风格字符串函数 :新代码一律 std::string + std::string_view + std::format理解 SSO :知道短字符串为什么快,长字符串为什么会触发堆分配记住 4 种 cast 的分工 :看到代码一眼能判断用了哪种、是否安全谨慎使用 string_view :它不拥有数据 ,生命周期是它的致命弱点内存管理三原则 :配对使用、避免悬挂、智能指针优先15.2 给资深开发者的建议 手写 string_view 检查器 :在 CI 中用 clang-tidy 检测 string_view 跨线程/跨异步任务的悬挂定制 std::string 的 SSO 阈值 :游戏/嵌入式可以用 EASTL 等替代实现研究 std::format 的实现 :fmt 库的 type-erased formatter 是泛型编程的典范对比 std::string 的 SSO vs std::string_view 的权衡 :根据业务选择写自己的内存池 + placement new :理解 std::vector、std::string 内部的内存管理15.3 开放问题(可深入研究) Q1 : std::string 在 shrink_to_fit() 后一定会释放多余内存吗?(答案:不一定 )Q2 : std::format 的编译期格式字符串检查(std::format_string)是如何实现的?Q3 : 为什么 std::string_view 的 remove_prefix 是 O(1) 而 std::string::erase 是 O(n)?Q4 : 编译器如何优化 memcpy(dst, src, 4) 到单条 mov 指令?Q5 : 4 种 cast 中,有没有一种是”运行时安全且零开销”的? 十六、系列导航 「C++ 面试题集锦」系列共 16 篇,从基础到高级系统梳理 C++ 核心知识点。
序号 篇名 链接 核心内容 1 第 1 篇:基础语法与面向对象 [待发布] 指针、引用、const、static、class vs struct 2 第 2 篇:内存管理与 RAII [待发布] 栈/堆、new/delete、智能指针、内存泄漏 3 第 3 篇:模板与泛型编程 [待发布] 函数模板、类模板、SFINAE、概念(C++20) 4 第 4 篇:STL 容器与迭代器 [待发布] vector、map、unordered_map、迭代器失效 5 第 5 篇:继承、多态与虚函数 [待发布] vtable、虚析构、抽象类、接口设计 6 第 6 篇:字符串、内存与类型转换 本文 strcpy/SSO/4 种 cast 7 第 7 篇:多线程与并发编程 [待发布] std::thread、mutex、atomic、内存序 8 第 8 篇:异常处理与错误码 [待发布] try/catch、noexcept、RAII 异常安全 9 第 9 篇:移动语义与完美转发 [待发布] 右值引用、std::move、引用折叠 10 第 10 篇:Lambda 与函数对象 [待发布] 闭包、捕获、std::function、std::bind 11 第 11 篇:智能指针深入 [待发布] unique_ptr、shared_ptr、weak_ptr、enable_shared_from_this 12 第 12 篇:C++17/20 新特性 [待发布] std::optional、std::variant、std::format、concept 13 第 13 篇:编译期编程 [待发布] constexpr、模板元编程、if constexpr 14 第 14 篇:性能优化与 profiling [待发布] cache line、false sharing、SIMD、内联 15 第 15 篇:常见面试手撕题 [待发布] LRU、智能指针实现、string 类 16 第 16 篇:综合复习与面经 [待发布] 高频考点、面试技巧、避坑总结
十七、写在最后 字符串、内存、类型转换——这是 C/C++ 的”三大基本功”。
字符串 :从 strcpy 的危险,到 std::string 的安全,再到 std::string_view 的高效,本质是C 风格裸指针 → C++ 资源管理 → C++17 零拷贝引用 的演进。
内存 :memset / memcpy / memmove 看似简单,但重叠、未初始化、类型不当 任何一个坑都会让程序崩溃。理解它们,才能理解 std::vector、std::string 的内部实现。
类型转换 :C 风格的 (T)expr 是”瑞士军刀”,C++ 的 4 种 cast 是”专业工具箱”。用对场景、用对类型 ,代码的可读性、安全性、可维护性都会显著提升。
行动建议 :
立刻 审查你项目中的 strcpy / strcat / sprintf,替换为 std::string / snprintf / std::format立刻 给所有函数参数(只读字符串)改为 std::string_view,对比性能理解 4 种 cast 的分工,在 Code Review 中明确指出误用学习 clang-tidy 的 bugprone-* 系列,自动化检测字符串漏洞思考延伸 :
你所在的团队,还在用 C 风格字符串 API 吗?迁移到 C++ 标准的 ROI 如何评估? 如果让你设计一个 std::string 的替代品,你会怎么取舍(SSO 阈值、COW、移动语义)? 4 种 cast 中,你觉得最被滥用的是哪一个?为什么? 本文是「C++ 面试题集锦」系列第 6 篇,共 16 篇。 最后更新:2026-06-16 | 维护者:Xu Qi