1. 面试场景下的String类实现要点
在C++面试中,实现一个简化版的string类是最能考察候选人基本功的题目之一。面试官通过这个题目可以快速评估你对以下几个核心概念的掌握程度:
- 内存管理能力(特别是动态内存分配与释放)
- 拷贝控制(拷贝构造、赋值运算符)
- 操作符重载
- 异常安全性
- C++11/14/17新特性的理解
一个合格的面试级String类实现需要满足以下基本要求:
- 能正确处理空字符串和NULL指针输入
- 支持深拷贝语义
- 提供必要的接口(如c_str()、size())
- 正确处理自我赋值情况
- 保证异常安全(内存分配失败时不会泄漏资源)
提示:在实际面试中,建议先实现基础版本,确保正确性后再考虑优化。面试官通常会关注你的实现思路和问题解决过程,而非一味追求完美代码。
2. 基础版本String类实现解析
2.1 头文件选择与成员变量
基础版本只需要<cstring>头文件,它提供了strlen、strcpy等基本字符串操作函数:
cpp复制#include <cstring> // 用于strlen, strcpy等操作
#include <algorithm> // 用于std::swap
成员变量只需要一个char*指针,遵循以下约定:
_data永远指向动态分配的、以'\0'结尾的字符数组- 即使是空字符串也分配1字节空间存储'\0'
cpp复制class String {
private:
char* _data;
};
2.2 构造函数实现要点
默认构造函数需要分配1字节空间存储空字符串:
cpp复制String() : _data(new char[1]) {
*_data = '\0';
}
C风格字符串构造函数需要考虑NULL指针输入:
cpp复制String(const char* str) : _data(new char[strlen(str ? str : "") + 1]) {
strcpy(_data, str ? str : "");
}
这里使用了条件运算符处理NULL指针,确保即使传入NULL也不会导致程序崩溃。
2.3 拷贝控制实现
2.3.1 拷贝构造函数
必须实现深拷贝,否则多个String对象会共享同一块内存:
cpp复制String(const String& rhs) : _data(new char[rhs.size() + 1]) {
strcpy(_data, rhs._data);
}
2.3.2 析构函数
需要释放动态分配的内存,并将指针置为NULL避免悬垂指针:
cpp复制~String() {
delete[] _data;
_data = nullptr; // 防御性编程
}
2.3.3 赋值运算符
采用copy-and-swap惯用法实现,具有强异常安全性:
cpp复制String& operator=(String rhs) { // 注意这里是传值而非引用
swap(rhs); // 交换资源所有权
return *this;
}
void swap(String& rhs) noexcept {
std::swap(_data, rhs._data);
}
这种实现方式自动处理了自我赋值情况,并且是异常安全的,因为所有可能抛出异常的操作(如内存分配)都在生成参数副本时完成。
2.4 常用成员函数
提供基本的字符串操作接口:
cpp复制size_t size() const {
return strlen(_data);
}
const char* c_str() const {
return _data;
}
3. 进阶功能实现
3.1 操作符重载
3.1.1 下标操作符
需要提供const和非const两个版本:
cpp复制char& operator[](size_t index) {
return _data[index];
}
const char& operator[](size_t index) const {
return _data[index];
}
3.1.2 比较操作符
利用strcmp实现字符串比较:
cpp复制bool operator==(const String& rhs) const {
return strcmp(_data, rhs._data) == 0;
}
bool operator<(const String& rhs) const {
return strcmp(_data, rhs._data) < 0;
}
3.2 引入_size成员优化
基础版本每次调用size()都要计算字符串长度,可以通过增加_size成员变量来优化:
cpp复制class String {
public:
String(const char* str = "") {
_size = strlen(str);
_data = new char[_size + 1];
strcpy(_data, str);
}
// 其他成员函数需要相应修改...
private:
char* _data;
size_t _size;
};
这种空间换时间的优化在字符串较长且频繁调用size()时效果明显。
3.3 移动语义支持
C++11引入了移动语义,可以大幅提升性能:
cpp复制// 移动构造函数
String(String&& rhs) noexcept : _data(nullptr) {
swap(rhs);
}
// 移动赋值运算符
String& operator=(String&& rhs) noexcept {
swap(rhs);
return *this;
}
移动操作将资源所有权从一个对象转移到另一个对象,避免了不必要的内存分配和拷贝。
4. 高效版本String类实现
4.1 引用计数技术
引用计数是一种常见的内存优化技术,允许多个String对象共享同一块内存:
cpp复制class String {
public:
String(const char* str = "") {
_data = new char[strlen(str) + 1 + sizeof(int)];
*_data_ref_count() = 1; // 初始化引用计数
strcpy(_data + sizeof(int), str);
}
// 拷贝构造只需增加引用计数
String(const String& rhs) : _data(rhs._data) {
++(*_data_ref_count());
}
~String() {
if (--(*_data_ref_count()) == 0) {
delete[] _data;
}
}
private:
char* _data;
int* _data_ref_count() const {
return reinterpret_cast<int*>(_data);
}
char* _data_str() const {
return _data + sizeof(int);
}
};
4.2 写时复制(Copy-on-Write)
引用计数结合写时复制可以进一步提升性能:
cpp复制char& operator[](size_t index) {
if (*_data_ref_count() > 1) {
// 如果有其他引用,先复制一份
char* new_data = new char[size() + 1 + sizeof(int)];
*_data_ref_count() -= 1; // 减少原引用计数
*reinterpret_cast<int*>(new_data) = 1;
strcpy(new_data + sizeof(int), _data_str());
_data = new_data;
}
return _data_str()[index];
}
这种技术只有在真正修改字符串内容时才进行复制,避免了不必要的内存拷贝。
5. 面试常见问题与解答
5.1 为什么拷贝构造函数参数必须是const引用?
如果使用传值方式,会导致无限递归调用拷贝构造函数。使用引用避免了不必要的拷贝,const确保不会意外修改原对象。
5.2 如何处理自我赋值情况?
copy-and-swap惯用法自动处理了自我赋值情况。在赋值运算符中,参数是通过拷贝构造创建的副本,交换后临时对象会析构原内容。
5.3 为什么移动构造函数和移动赋值运算符要标记为noexcept?
标记为noexcept的移动操作可以被标准库容器更高效地使用。例如,std::vector在扩容时会优先使用移动操作(如果是noexcept),否则会使用拷贝操作。
5.4 如何测试String类的正确性?
可以编写以下测试用例:
- 构造空字符串和普通字符串
- 测试拷贝构造和赋值
- 测试自我赋值
- 测试作为函数参数和返回值
- 测试在标准库容器中的使用
6. 性能优化技巧
6.1 小字符串优化(SSO)
许多标准库实现会为短字符串使用栈空间而非堆内存:
cpp复制class String {
private:
static const size_t SSO_SIZE = 15;
union {
char* _data;
char _sso_buffer[SSO_SIZE + 1];
};
size_t _size;
bool is_sso() const { return _size <= SSO_SIZE; }
};
6.2 内存预分配
如果需要频繁修改字符串,可以预分配更多内存减少重新分配次数:
cpp复制void reserve(size_t new_capacity) {
if (new_capacity <= _capacity) return;
char* new_data = new char[new_capacity + 1];
strcpy(new_data, _data);
delete[] _data;
_data = new_data;
_capacity = new_capacity;
}
6.3 移动语义与STL容器
现代C++中,STL容器会利用移动语义提升性能。例如:
cpp复制std::vector<String> vec;
vec.push_back(String("hello")); // 会调用移动构造函数而非拷贝构造
7. 实际项目中的考量
在实际项目中,String类实现还需要考虑:
- 线程安全性(特别是引用计数实现)
- 异常安全性(内存分配失败处理)
- 与标准库的兼容性
- 编码支持(UTF-8、UTF-16等)
- 性能分析工具集成
我曾在项目中实现过一个支持多线程的引用计数String类,关键是在增减引用计数时使用原子操作:
cpp复制std::atomic<int>* _data_ref_count() const {
return reinterpret_cast<std::atomic<int>*>(_data);
}
这种实现虽然增加了些许开销,但保证了线程安全,在多线程环境下表现良好。