【C++面试】手写String类:同时实现拷贝构造与移动构造
发布时间:2026/10/8 13:32:01 锦皓数字建站

一、String类需要解决什么问题假设自己设计一个字符串类class String { private: char *data_; };如果创建String str(hello);希望内部结构类似String对象 ┌──────────────┐ │ data_ │──────┐ └──────────────┘ │ ↓ 堆内存 ┌─────────────┐ │ h e l l o \0│ └─────────────┘因为字符串长度运行时才能确定所以可以new char[len 1];动态申请空间。但是只写构造和析构还不够。例如String s1(hello); String s2 s1;这里会发生拷贝构造如果直接进行默认浅拷贝s1.data_ ─────┐ ↓ hello ↑ s2.data_ ─────┘两个对象指向同一块内存。当两个对象析构delete[] s1.data_; delete[] s2.data_;同一块内存就可能被释放两次。因此必须深拷贝让两个对象拥有独立资源。而如果源对象马上就不用了String s2 std::move(s1);就没必要再复制整个字符串可以直接把s1的资源交给s2这就是移动构造。所以这道题实际上是在考动态资源管理 ↓ 拷贝构造 ↓ 深拷贝 ↓ 移动构造 ↓ 资源所有权转移二、先实现普通构造和析构先写最基础版本#include cstring #include iostream class String { private: char *data_; size_t size_; public: // 普通构造 String(const char *str ) : data_(nullptr), size_(0) { if (str nullptr) { str ; } size_ std::strlen(str); data_ new char[size_ 1]; std::memcpy(data_, str, size_ 1); std::cout 普通构造 std::endl; } // 析构函数 ~String() { delete[] data_; std::cout 析构函数 std::endl; } };例如String s(hello);执行strlen(hello) ↓ size_ 5 ↓ new char[6] ↓ 复制 hello\0内存栈 s ┌──────────────┐ │ data_ │─────┐ │ size_ 5 │ │ └──────────────┘ │ ↓ 堆 ┌─────────────┐ │ h e l l o \0│ └─────────────┘当s离开作用域~String() ↓ delete[] data_自动释放资源。这其实也是RAII的典型应用。三、实现拷贝构造必须进行深拷贝现在String s1(hello); String s2 s1;调用的是String(const String other);实现String(const String other) : data_(nullptr), size_(other.size_) { data_ new char[size_ 1]; std::memcpy(data_, other.data_, size_ 1); std::cout 拷贝构造 std::endl; }注意这里不能data_ other.data_;因为这样只是复制指针地址而不是复制字符串。错误的浅拷贝s1 ┌────────────┐ │ data_ │─────┐ └────────────┘ │ ↓ hello ↑ ┌────────────┐ │ │ data_ │─────┘ └────────────┘ s2正确的深拷贝s1 ┌────────────┐ │ data_ │────────→ hello └────────────┘ s2 ┌────────────┐ │ data_ │────────→ hello └────────────┘虽然字符串内容相同但是内存地址不同因此s2[0] H;不会影响s1这就是深拷贝 Deep Copy所以面试问为什么 String 类必须自己实现拷贝构造可以回答因为 String 内部管理动态内存如果使用编译器默认拷贝构造只会复制指针地址两个对象会共享同一块内存析构时可能产生重复释放。因此需要实现深拷贝为新对象重新申请内存并复制字符串内容。四、实现移动构造直接接管资源现在String s1(hello); String s2 std::move(s1);这里希望调用String(String other);移动构造可以写成String(String other) noexcept : data_(other.data_), size_(other.size_) { other.data_ nullptr; other.size_ 0; std::cout 移动构造 std::endl; }移动之前s1 │ └────→ hello执行data_ other.data_;以后s1 ─────┐ ↓ hello ↑ s2 ─────┘如果停在这里两个对象还是指向同一资源所以必须other.data_ nullptr; other.size_ 0;最终s1 ↓ nullptr s2 ↓ hello这样 s1 析构时delete[] nullptr;是安全的。真正的hello已经交给s2管理所以拷贝构造 ↓ 重新申请内存 ↓ 复制数据而移动构造 ↓ 不复制字符串 ↓ 直接接管指针这就是移动语义性能更高的原因。移动构造建议写noexcept即String(String other) noexcept;因为像std::vectorString扩容时需要把旧元素搬到新的内存。如果移动构造明确不会抛异常标准容器就更容易安全地优先使用移动构造而不是拷贝构造。五、完整实现Rule of Five既然 String 自己管理动态内存那么除了析构函数 拷贝构造 移动构造还应该考虑拷贝赋值 移动赋值这就是Rule of Five 五法则一个适合面试手写的完整版本#include cstring #include iostream #include utility class String { private: char *data_; size_t size_; public: // 1. 普通构造 String(const char *str ) : data_(nullptr), size_(0) { if (str nullptr) { str ; } size_ std::strlen(str); data_ new char[size_ 1]; std::memcpy(data_, str, size_ 1); std::cout 普通构造 std::endl; } // 2. 析构函数 ~String() { delete[] data_; std::cout 析构函数 std::endl; } // 3. 拷贝构造 String(const String other) : data_(nullptr), size_(other.size_) { data_ new char[size_ 1]; std::memcpy(data_, other.data_, size_ 1); std::cout 拷贝构造 std::endl; } // 4. 移动构造 String(String other) noexcept : data_(other.data_), size_(other.size_) { other.data_ nullptr; other.size_ 0; std::cout 移动构造 std::endl; } // 5. 拷贝赋值 String operator(const String other) { if (this ! other) { // 先申请新空间 char *newData new char[other.size_ 1]; std::memcpy(newData, other.data_, other.size_ 1); // 再释放自己原来的资源 delete[] data_; data_ newData; size_ other.size_; } std::cout 拷贝赋值 std::endl; return *this; } // 6. 移动赋值 String operator(String other) noexcept { if (this ! other) { // 先释放自己原来的资源 delete[] data_; // 接管other的资源 data_ other.data_; size_ other.size_; // other不再拥有资源 other.data_ nullptr; other.size_ 0; } std::cout 移动赋值 std::endl; return *this; } // 获取字符串 const char *c_str() const { return data_ ? data_ : ; } // 获取长度 size_t size() const { return size_; } // 下标访问 char operator[](size_t index) { return data_[index]; } const char operator[](size_t index) const { return data_[index]; } };测试int main() { String s1(hello); // 拷贝构造 String s2 s1; // 移动构造 String s3 std::move(s1); String s4(world); // 拷贝赋值 s4 s2; String s5(C); // 移动赋值 s5 std::move(s3); std::cout s2 s2.c_str() std::endl; std::cout s5 s5.c_str() std::endl; return 0; }整个区别可以直接记成String s2 s1;调用拷贝构造过程s1 → hello 重新申请内存 ↓ 复制hello ↓ s2 → hello而String s3 std::move(s1);调用移动构造过程s1 → hello ↓ 把指针交出去 ↓ s1 → nullptr s3 → hello需要特别区分String s2 s1;这是拷贝构造因为s2原来不存在 现在正在创建而String s2(abc); s2 s1;这是拷贝赋值因为s2已经存在需要先处理自己原来的资源再复制s1。移动同样如此String s2 std::move(s1);是移动构造而String s2(abc); s2 std::move(s1);是移动赋值所以面试中可以这样总结自定义 String 类内部通过char*管理动态内存。普通构造根据字符串长度申请内存析构函数负责释放资源拷贝构造需要进行深拷贝重新申请一块独立内存并复制字符串移动构造则直接接管源对象的指针然后把源对象指针置空从而避免大字符串复制。因为类自己管理资源还需要同时考虑拷贝赋值和移动赋值也就是 Rule of Five。如果面试官继续问拷贝构造和移动构造最大的区别是什么可以回答拷贝构造不会破坏源对象它需要创建一份独立资源移动构造用于资源可以被转移的对象直接接管源对象资源并把源对象调整为合法但不再拥有原资源的状态因此对于动态资源较大的对象通常效率更高。如果继续问为什么移动以后要把other.data_置空可以回答因为资源已经转移给新对象。如果源对象仍然保留原指针那么两个对象析构时都会delete[]同一块内存造成 Double Free所以移动后需要让源对象不再拥有这块资源。如果继续问为什么移动构造写noexcept可以回答因为 STL 容器在扩容搬迁元素时需要考虑异常安全。如果移动构造声明为noexcept容器可以更放心地使用移动构造从而避免成本较高的深拷贝。整个 String 类的核心知识链就是char* ↓ 动态内存 ↓ 析构释放 ↓ 默认浅拷贝有问题 ↓ 拷贝构造实现深拷贝 ↓ 移动构造转移资源 ↓ 拷贝赋值 ↓ 移动赋值 ↓ Rule of Five如果这是现场笔试时间比较紧至少要把下面三个函数写正确~String(); String(const String other); String(String other) noexcept;也就是析构 深拷贝 移动资源这三个基本就是这道题最核心的考点。0voice · GitHub
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。