在C++开发中,我们经常使用标准库中的std::string来处理字符串操作。但作为C++开发者,理解字符串底层实现原理至关重要。手动实现一个简易String类不仅能加深对内存管理、拷贝控制的理解,还能掌握RAII(资源获取即初始化)这一C++核心思想。
我刚开始学习C++时,对深浅拷贝、移动语义这些概念总是似懂非懂。直到自己动手实现了一个String类,才真正理解了这些抽象概念的实际意义。通过这个练习,你会对以下核心概念有更深刻的认识:
我们先从最基本的类定义开始。一个简易String类至少需要以下成员:
cpp复制class MyString {
public:
// 构造函数
MyString(const char* str = "");
// 拷贝构造函数
MyString(const MyString& other);
// 移动构造函数
MyString(MyString&& other) noexcept;
// 析构函数
~MyString();
// 拷贝赋值运算符
MyString& operator=(const MyString& other);
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept;
// 其他成员函数...
private:
char* m_data; // 存储字符串数据
size_t m_length; // 字符串长度(不含'\0')
};
注意:这里我们选择将长度单独存储而不是依赖strlen计算,这能提高频繁长度查询的效率。
字符串类的核心在于内存管理。我们需要考虑以下几点:
一个健壮的构造函数实现应该如下:
cpp复制MyString::MyString(const char* str) {
if (str == nullptr) {
m_data = new char[1];
*m_data = '\0';
m_length = 0;
} else {
m_length = strlen(str);
m_data = new char[m_length + 1];
strcpy(m_data, str);
}
}
拷贝构造函数需要实现深拷贝,避免多个对象共享同一块内存:
cpp复制MyString::MyString(const MyString& other) {
m_length = other.m_length;
m_data = new char[m_length + 1];
strcpy(m_data, other.m_data);
}
拷贝赋值运算符需要考虑自赋值情况,并遵循"拷贝并交换"惯用法:
cpp复制MyString& MyString::operator=(const MyString& other) {
if (this != &other) {
char* temp = new char[other.m_length + 1];
strcpy(temp, other.m_data);
delete[] m_data;
m_data = temp;
m_length = other.m_length;
}
return *this;
}
技巧:先分配新内存再释放旧内存,可以保证异常安全。如果new抛出异常,原对象仍保持有效状态。
C++11引入的移动语义可以避免不必要的拷贝:
cpp复制// 移动构造函数
MyString::MyString(MyString&& other) noexcept
: m_data(other.m_data), m_length(other.m_length) {
other.m_data = nullptr;
other.m_length = 0;
}
// 移动赋值运算符
MyString& MyString::operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
m_length = other.m_length;
other.m_data = nullptr;
other.m_length = 0;
}
return *this;
}
实现一些常用的字符串操作:
cpp复制// 获取C风格字符串
const char* MyString::c_str() const {
return m_data;
}
// 获取字符串长度
size_t MyString::length() const {
return m_length;
}
// 判断字符串是否为空
bool MyString::empty() const {
return m_length == 0;
}
重载一些常用运算符使类更易用:
cpp复制// 下标运算符
char& MyString::operator[](size_t pos) {
if (pos >= m_length) {
throw std::out_of_range("Index out of range");
}
return m_data[pos];
}
// const版本下标运算符
const char& MyString::operator[](size_t pos) const {
if (pos >= m_length) {
throw std::out_of_range("Index out of range");
}
return m_data[pos];
}
// 字符串连接
MyString operator+(const MyString& lhs, const MyString& rhs) {
MyString result;
result.m_length = lhs.m_length + rhs.m_length;
result.m_data = new char[result.m_length + 1];
strcpy(result.m_data, lhs.m_data);
strcat(result.m_data, rhs.m_data);
return result;
}
// 比较运算符
bool operator==(const MyString& lhs, const MyString& rhs) {
return strcmp(lhs.c_str(), rhs.c_str()) == 0;
}
添加substr方法获取子字符串:
cpp复制MyString MyString::substr(size_t pos, size_t len) const {
if (pos > m_length) {
throw std::out_of_range("Position out of range");
}
size_t actualLen = std::min(len, m_length - pos);
MyString sub;
sub.m_length = actualLen;
sub.m_data = new char[actualLen + 1];
strncpy(sub.m_data, m_data + pos, actualLen);
sub.m_data[actualLen] = '\0';
return sub;
}
实现find方法查找子串位置:
cpp复制size_t MyString::find(const MyString& substr, size_t pos) const {
if (substr.m_length == 0) return pos <= m_length ? pos : npos;
if (pos + substr.m_length > m_length) return npos;
const char* result = strstr(m_data + pos, substr.m_data);
return result ? result - m_data : npos;
}
重载<<和>>运算符支持流操作:
cpp复制std::ostream& operator<<(std::ostream& os, const MyString& str) {
os << str.c_str();
return os;
}
std::istream& operator>>(std::istream& is, MyString& str) {
char buffer[1024];
is >> buffer;
str = MyString(buffer);
return is;
}
实际标准库实现通常会使用小字符串优化,对小字符串直接存储在对象内部,避免堆分配:
cpp复制class MyString {
private:
static const size_t SSO_SIZE = 15; // 假设15字节
union {
struct {
char* m_data;
size_t m_length;
} m_large;
char m_small[SSO_SIZE + 1];
};
bool m_isSmall;
// 其他成员...
};
写时复制可以节省内存,但会增加复杂度:
cpp复制class MyString {
private:
struct StringData {
char* data;
size_t length;
size_t refCount;
StringData(const char* str, size_t len);
~StringData();
};
StringData* m_data;
// 其他成员...
};
注意:现代C++标准库通常不再使用COW,因为多线程环境下性能可能反而下降。
编写测试用例验证类的正确性:
cpp复制void test_constructor() {
MyString s1; // 默认构造
MyString s2("hello"); // C字符串构造
MyString s3(s2); // 拷贝构造
MyString s4(std::move(s3)); // 移动构造
assert(s2.length() == 5);
assert(strcmp(s2.c_str(), "hello") == 0);
assert(s3.empty()); // 移动后源对象应为空
assert(s4 == s2); // 移动后新对象应与原对象相同
}
void test_assignment() {
MyString s1("hello");
MyString s2;
s2 = s1; // 拷贝赋值
MyString s3;
s3 = std::move(s2); // 移动赋值
assert(s1 == s2); // 拷贝后应相等
assert(s2.empty()); // 移动后源对象应为空
assert(s3 == s1); // 移动后新对象应与原对象相同
}
比较自定义String与std::string的性能:
cpp复制void test_performance() {
const int COUNT = 1000000;
// 测试拷贝性能
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < COUNT; ++i) {
MyString s1("test string");
MyString s2 = s1;
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "MyString copy: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms\n";
// 同样测试std::string...
}
问题现象:程序运行时间越长,内存占用越高。
解决方案:
问题现象:多个对象修改同一块内存,或释放已释放的内存。
解决方案:
问题现象:多线程环境下出现数据竞争。
解决方案:
以下是简化版的完整实现代码:
cpp复制#include <cstring>
#include <iostream>
#include <stdexcept>
#include <utility>
class MyString {
public:
// 构造函数
MyString(const char* str = "") {
m_length = strlen(str);
m_data = new char[m_length + 1];
strcpy(m_data, str);
}
// 拷贝构造函数
MyString(const MyString& other) {
m_length = other.m_length;
m_data = new char[m_length + 1];
strcpy(m_data, other.m_data);
}
// 移动构造函数
MyString(MyString&& other) noexcept
: m_data(other.m_data), m_length(other.m_length) {
other.m_data = nullptr;
other.m_length = 0;
}
// 析构函数
~MyString() {
delete[] m_data;
}
// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) {
char* temp = new char[other.m_length + 1];
strcpy(temp, other.m_data);
delete[] m_data;
m_data = temp;
m_length = other.m_length;
}
return *this;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
m_length = other.m_length;
other.m_data = nullptr;
other.m_length = 0;
}
return *this;
}
// 获取C风格字符串
const char* c_str() const { return m_data; }
// 获取长度
size_t length() const { return m_length; }
// 判断是否为空
bool empty() const { return m_length == 0; }
// 下标运算符
char& operator[](size_t pos) {
if (pos >= m_length) throw std::out_of_range("Index out of range");
return m_data[pos];
}
// const下标运算符
const char& operator[](size_t pos) const {
if (pos >= m_length) throw std::out_of_range("Index out of range");
return m_data[pos];
}
private:
char* m_data;
size_t m_length;
};
// 字符串连接
MyString operator+(const MyString& lhs, const MyString& rhs) {
MyString result;
result.m_length = lhs.m_length + rhs.m_length;
result.m_data = new char[result.m_length + 1];
strcpy(result.m_data, lhs.m_data);
strcat(result.m_data, rhs.m_data);
return result;
}
// 比较运算符
bool operator==(const MyString& lhs, const MyString& rhs) {
return strcmp(lhs.c_str(), rhs.c_str()) == 0;
}
// 输出运算符
std::ostream& operator<<(std::ostream& os, const MyString& str) {
os << str.c_str();
return os;
}
手写String类是理解C++核心概念的绝佳练习。在实际项目中,我们通常会直接使用std::string,但了解其底层实现原理能帮助我们写出更高效、更安全的代码。