Skip to content

字节操作、对象生命周期与 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==。

使用 Markdown 与 VitePress 构建