Appearance
字节操作、对象生命周期与 C API 边界
C 和 C++ 都能操作原始内存,但 C++ 还区分存储(storage)与对象(object)。面试中谈 void*、malloc、memcpy、字符串 API 或结构体比较时,应先说清:自己处理的是字节,还是一个已经构造、拥有类型与生命周期的对象。
void*:通用地址,不是“无所不能的对象指针”
void* 可以保存任意对象指针的地址,因此常出现在 C 风格回调和内存分配 API 中;但它没有指向对象的静态类型,不能直接解引用,也不能进行有意义的 C++ 指针算术。
C++ 中 void* 到对象指针的转换需要显式写出;C 中允许隐式转换。应尽量把 C 风格 void* 边界封装在窄小的适配层中,业务代码使用有类型的指针、引用、容器和 RAII 类型。
new、operator new 与 malloc
malloc(size)只申请一块原始字节存储,成功返回void*,失败返回空指针;它不执行构造函数。- 表达式
new T(args...)通常先调用可替换的operator new取得存储,再在该存储上构造T;普通new失败通常抛std::bad_alloc。 free只释放由匹配的malloc家族得到的存储;delete先析构对象、再调用匹配的operator delete。两套 API 不能交叉配对。
C++ 的“自由存储区(free store)”与操作系统/分配器口语中的“堆(heap)”常重叠,但不是严格同义词;operator new 可以被替换或重载。优先把资源封装进RAII对象,而不是在业务代码中手写 new/delete。
分配器是否使用 brk、mmap、线程缓存或其他策略,以及阈值具体是多少,都是操作系统、运行库版本和配置相关的实现细节,不能背成“1 KB / 1 MB 一定走哪条路径”。
memcpy、memmove 与重叠区间
两者都按字节复制指定长度,不会检查 C 字符串结尾、不会调用构造/析构函数,也不会理解业务字段。
- 对重叠区间调用
memcpy是未定义行为,不能根据某个实现“似乎会倒序复制”而依赖它;重叠时必须用memmove。 - 将
memcpy用于 C++ 对象前,应确认类型是否适合按字节复制。对包含资源所有权、虚函数、指针或非平凡构造/析构语义的对象盲目复制字节,通常会破坏对象不变量甚至造成双重释放。 - 传输对象状态应优先定义序列化格式;复制普通对象优先使用拷贝/移动构造和赋值。
C 字符串与显式长度
C 字符串是以 \0 结尾的 char 序列:
cpp
char name[10] = "hello";
std::size_t used = std::strlen(name); // 5,不含结尾 '\0'
std::size_t bytes = sizeof(name); // 10,仅数组仍在当前作用域时strlen从起点扫描到第一个\0,所以输入必须指向有效且以\0终止的字符序列;它是运行期线性扫描。sizeof是编译期运算符,可求类型或对象大小;数组作为函数参数会退化为指针,函数内sizeof(arr)得到的是指针大小,而不是调用者数组长度。strcpy不知道目标缓冲区容量,长度不足会越界;strncpy也有“不保证补零 / 可能浪费填充”的陷阱。
新 C++ 代码优先使用 std::string 管理所有权、std::string_view 表示不拥有的带长度视图,或向 C API 同时传递 pointer + length。string_view 不会延长底层字符串生命周期。
对齐、padding 与结构体比较
为了满足成员的对齐要求,编译器可在成员之间和对象末尾插入 padding;大小与偏移依赖平台 ABI、编译器和编译选项。
因此一般不要用 memcmp(&a, &b, sizeof a) 判断两个结构体“值相等”。除了 padding 可能未初始化,即使每个业务成员相同字节也可能不同之外,还有几个更根本的问题:
- 指针成员:
memcmp比较的是指针值(地址),而不是指向的内容; - 浮点成员:
-0.0与+0.0用==相等但位模式不同,NaN的位模式也不固定,且NaN != NaN; - 非平凡可复制类型:
std::string、std::vector等成员的对象表示包含堆指针和元数据;内容相同但容量不同的两个对象位模式不同,对非平凡可复制类型读取对象表示本身也是未定义行为。
应逐成员比较,或为适合的类型使用默认比较运算符(C++20):
cpp
struct Point {
int x;
int y;
bool operator==(const Point&) const = default;
};
struct Line {
Point start;
Point end;
std::string label;
bool operator==(const Line&) const = default; // 逐成员使用各自的 ==,string 比较内容
};默认的 == 对每个成员调用该成员自己的 ==,因此 std::string 比较的是字符串内容;浮点成员仍按 == 语义(NaN != NaN)。C++11 中也可以用 std::tie(a.x, a.y) == std::tie(b.x, b.y)。若需要区分“字节完全一致”与“逻辑相等”,应明确这是两种不同的语义。
memcmp 只适合“按字节比较对象表示”的场景:确认类型是平凡可复制、结构无 padding(例如只含整型的紧凑结构)、成员不含指针和浮点,并且确实需要位级一致(如原始协议载荷、去重指纹)。工程中判断逻辑相等,一律优先成员比较或默认比较运算符。
union 的成员共享同一段存储;写入一个成员后,通常只有当前活动成员可按其类型安全读取。不要把它当成通用的跨类型位转换工具;需要位级重解释时优先考虑 std::bit_cast(满足其类型约束时)或明确的编码/解码逻辑。
面试速答
memcpy 和 memmove 的区别:二者都复制字节;memcpy 要求源和目标不重叠,重叠调用是未定义行为;memmove 保证重叠时也能得到正确结果。两者都不执行 C++ 构造析构,不能替代对象级复制。
为什么 memcmp 不适合比较一般结构体:结构体可能有未初始化 padding,字节不等不代表字段不等;同时某些字段也未必适合纯字节语义。应以类型语义逐字段比较或定义 operator==。