☰
【C++面试】手写String类:同时实现拷贝构造与移动构造
2026/10/8 13:31:47 网站建设 项目流程

一、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::vector<String>

扩容时需要把旧元素搬到新的内存。

如果移动构造明确:

不会抛异常

标准容器就更容易安全地优先使用移动构造,而不是拷贝构造。

五、完整实现: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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询