Lazy loaded image
1. 从 C 到 C++:常见陷阱与安全改进
Words 1633Read Time≈ 5 min
2026-9-4
🎯
C++ 并不会自动消灭 C 的底层风险。真正的改进,是用带长度、边界和类型信息的抽象,让错误更早暴露。

1. 字符与字符串:结束符不是长度

三个容易混淆的值

写法
含义
常见用途
'0'
字符 0,编码值通常为 48
文本数字
'\0'
空字符,数值为 0
C 字符串结束标记
0
整数零
数值、空指针常量(旧式代码)
C 字符串依靠 '\0' 判断结尾,本身不记录长度:
如果缓冲区不足,或者遗漏结束符,strlen、strcpy 等函数就可能越界读取或写入。

C++:优先使用 std::string

std::string 单独保存长度,因此可以容纳二进制零字节;但把它交给只认 C 字符串的接口时,仍可能在 '\0' 处被截断。

工程案例:Redis SDS

Redis 没有直接把普通 C 字符串当作核心字符串结构,而是用 SDS 保存长度和容量:
这样做带来三个直接收益:读取长度是 O(1)、可安全保存二进制数据、扩容时能检查剩余空间。

2. 数组与指针:传参后,长度会消失

数组作为函数参数时通常会退化为指向首元素的指针:
因此,下面这种接口必须额外传入长度,调用者和函数还要共同维护约定:

C++:让容器携带边界

这里同时解决了四个问题:长度不丢失、只读参数不会被误改、空容器不会除零、求和使用更宽的整数类型降低溢出风险。

边界统一采用左闭右开

把条件写成 i < size,比 i <= size - 1 更直观,也能避开空数组时的无符号下溢。

3. 类型转换:编译器“帮忙”,不代表结果正确

有符号数与无符号数比较

问题不在比较符,而在两边类型不同。更稳妥的做法是先判断负数,再进行同类型比较:

C++ 四种显式转换

转换
用途
原则
static_cast
数值、相关类型之间的常规转换
最常用,但仍需验证范围
const_cast
增删 const 属性
极少使用;修改真正的常量是未定义行为
dynamic_cast
多态类型的运行时安全向下转换
失败时返回空指针或抛异常
reinterpret_cast
按底层位模式重新解释
高风险,只用于明确的底层场景
⚠️
显式转换的价值不是“让代码通过编译”,而是把危险点标出来,方便审查。转换越多,通常说明接口类型设计越可疑。

4. 位运算与整数:机器整数不是数学整数

移位的安全边界

有符号负数右移的行为容易依赖语言版本与实现;左移若超出可表示范围还可能触发未定义行为。做位掩码时,优先使用无符号类型,并确保移位量小于类型位数。
固定数量的标志位也可以用 std::bitset,让输出和按位操作更直观:

整数溢出

关键是让第一次乘法就在更宽的类型中发生。先用 int 算完再赋给 long long,溢出已经发生,补票没用。
如果数值可能超过 64 位整数,应使用 boost::multiprecision::cpp_int 等大整数类型;如果输入来自外部,还应在运算前做范围检查。

5. 一张表记住选择原则

场景
容易踩坑的写法
优先选择
普通文本
char* • 手工长度
std::string
动态数组
裸指针 + 单独维护长度
std::vector
固定长度数组
裸数组
std::array
只读连续数据视图
指针 + 长度
C++20 std::span
权限位/状态位
有符号整数移位
无符号定宽类型或 std::bitset
类型转换
C 风格强转
先修正类型设计,必要时使用命名转换
大整数
假设 int 足够
定宽整数、范围检查或多精度整数

结语

从 C 到 C++ 的核心变化,不是把 printf 换成 cout,而是把原本靠程序员记忆维护的信息交给类型和容器:
选择 std::string、std::vector、std::array 或 std::span,不是为了“更现代”,而是为了减少那些编译器不报错、运行时却会咬人的隐含约定。
上一篇
C++ String 常用操作
下一篇
嵌入式工程师习题集

Comments
Loading...