深入理解字符串:从String类手动实现、代码详解到性能优化(万字长文&基础进阶&面试加分)
深入理解字符串:从String类手动实现、代码详解到性能优化(万字长文&基础进阶&面试加分)
首先对于面试来说,这应该是我们耳熟能详能够手写的基础八股,其次偶尔重复造轮子是为了磨练自己的技术水平!
在编程领域,字符串(String)是我们最常见的数据类型之一。尽管大多数编程语言都提供了内置的字符串类型,但是深入理解并手动实现一个简单的字符串类,可以帮助我们更深入地理解字符串的工作原理,以及内存管理、拷贝和移动语义等重要概念。
1. 手动实现基本的 String 类
首先,我们来看一个简单的 C++ 字符串类的实现:
代码语言:cpp代码运行次数:0运行复制#include <iostream>
#include <cstring>
class MyString {
private:
char* data; // 字符串数据
size_t length; // 字符串长度
public:
// 构造函数
MyString(ct char* str = "") {
length = strlen(str);
data = new char[length + 1]; // +1 为了存储 '\0'
strcpy(data, str);
}
// 拷贝构造函数
MyString(ct MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
// 移动构造函数
MyString(MyString&& other) noexcept {
length = other.length;
data = other.data;
other.data = nullptr; // 避免析构时重复释放
other.length = 0;
}
// 析构函数
~MyString() {
delete[] data; // 释放动态分配的内存
}
// 重载赋值运算符
MyString& operator=(ct MyString& other) {
if (this != &other) {
delete[] data; // 释放旧内存
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}
// 重载移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data; // 释放旧内存
data = other.data;
length = other.length;
other.data = nullptr; // 避免析构时重复释放
other.length = 0;
}
return *this;
}
// 获取字符串长度
size_t size() ct {
return length;
}
// 获取字符串内容
ct char* c_str() ct {
return data;
}
};
int main() {
MyString str1("Hello, World!");
MyString str2 = str1; // 拷贝构造
MyString str = std::move(str1); // 移动构造
std::cout << "str2: " << _str() << ", length: " << str2.size() << std::endl;
std::cout << "str: " << _str() << ", length: " << str.size() << std::endl;
return 0;
}
2. 深入理解每个函数的作用
2.1 构造函数
在我们的 MyString
类中,构造函数的作用是初始化一个新的 MyString
对象。我们提供了两种构造函数:默认构造函数和参数化构造函数。
- 默认构造函数:这个构造函数允许我们创建一个空的
MyString
对象。这是通过默认参数""
实现的,这样当我们不提供任何参数时,就会创建一个空字符串。 - 参数化构造函数:这个构造函数接受一个 C 风格字符串作为参数,并为其分配内存。我们首先通过
strlen
函数获取字符串的长度,然后使用new
运算符分配足够的内存来存储字符串和结束字符\0
。最后,我们使用strcpy
函数将输入字符串复制到新分配的内存中。
2.2 拷贝构造函数
拷贝构造函数的作用是创建一个新的 MyString
对象,该对象是现有对象的副本。在我们的实现中,拷贝构造函数接受一个 MyString
对象的引用作为参数,然后创建一个新的 MyString
对象,该对象具有与输入对象相同的长度和内容。这是通过分配新的内存并复制输入对象的数据来实现的。
2. 移动构造函数
移动构造函数的作用是创建一个新的 MyString
对象,该对象接管现有对象的资源。在我们的实现中,移动构造函数接受一个 MyString
对象的右值引用作为参数,然后创建一个新的 MyString
对象,该对象接管输入对象的数据和长度。这是通过直接将输入对象的数据和长度赋值给新对象,然后将输入对象的数据指针设置为 nullptr
和长度设置为 0
来实现的。
2.4 析构函数
析构函数的作用是清理 MyString
对象。在我们的实现中,析构函数释放了 MyString
对象的数据所占用的内存。这是通过使用 delete[]
运算符来释放 data
指针指向的内存来实现的。
2.5 赋值运算符重载
赋值运算符的作用是将一个 MyString
对象的内容赋值给另一个 MyString
对象。在我们的实现中,赋值运算符首先检查自我赋值的情况,然后释放接收对象的旧数据,分配新的内存,并复制输入对象的数据。
2.6 移动赋值运算符重载
移动赋值运算符的作用是将一个 MyString
对象的资源转移给另一个 MyString
对象。在我们的实现中,移动赋值运算符首先检查自我赋值的情况,然后释放接收对象的旧数据,接管输入对象的数据和长度,并将输入对象的数据指针设置为 nullptr
和长度设置为 0
。
. 实现自定义字符串类时的关键点
在实现自定义字符串类时,有几个关键的注意事项:
.1 内存管理
- 动态内存分配:在我们的
MyString
类中,我们使用new
运算符动态分配内存来存储字符串数据。因此,我们必须确保在析构函数中使用delete[]
运算符释放这些内存,以防止内存泄漏。 - 深拷贝与浅拷贝:在拷贝构造函数和赋值运算符中,我们需要确保实现深拷贝,即创建数据的新副本,而不是简单地复制数据指针。这样可以避免多个
MyString
对象指向同一内存区域,从而防止数据损坏和内存泄漏。
.2 移动语义
- 在 C++11 及以后版本中,我们可以使用移动构造函数和移动赋值运算符来提高性能。这是通过转移资源,而不是复制资源来实现的,从而避免了不必要的内存分配和数据复制。
. 字符串结束符
- 在我们的
MyString
类中,我们需要确保每个字符串以\0
结尾,以便与 C 风格字符串兼容。这是通过在分配内存时多分配一个字符,并在复制字符串时包括结束字符来实现的。
.4 异常安全
- 在内存分配和数据复制过程中,我们需要考虑异常安全性。这是通过在分配新内存或复制数据之前释放旧内存来实现的,从而确保在发生异常时不会导致内存泄漏。
4. 理解深拷贝和数据独立性
假设我们创建了一个 MyString
对象 str1
,并将其拷贝到 str2
。在这个过程中,str2
将获得 str1
的数据副本,而不是指向同一内存区域。这意味着对 str1
的修改不会影响 str2
,从而确保了数据的独立性。
MyString str1("Hello");
MyString str2 = str1; // str2 拷贝了 str1 的内容
5. 添加更多功能的MyString
下面是一个扩展了更多功能的 MyString
类的实现,包括字符串连接、子字符串查和字符替换等功能:
#include <iostream>
#include <cstring>
class MyString {
private:
char* data; // 字符串数据
size_t length; // 字符串长度
public:
// 构造函数
MyString(ct char* str = "") {
length = strlen(str);
data = new char[length + 1]; // +1 为了存储 '\0'
strcpy(data, str);
}
// 拷贝构造函数
MyString(ct MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
// 移动构造函数
MyString(MyString&& other) noexcept {
length = other.length;
data = other.data;
other.data = nullptr; // 避免析构时重复释放
other.length = 0;
}
// 析构函数
~MyString() {
delete[] data; // 释放动态分配的内存
}
// 重载赋值运算符
MyString& operator=(ct MyString& other) {
if (this != &other) {
delete[] data; // 释放旧内存
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}
// 重载移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data; // 释放旧内存
data = other.data;
length = other.length;
other.data = nullptr; // 避免析构时重复释放
other.length = 0;
}
return *this;
}
// 获取字符串长度
size_t size() ct {
return length;
}
// 获取字符串内容
ct char* c_str() ct {
return data;
}
// 字符串连接
MyString operator+(ct MyString& other) {
size_t new_length = length + other.length;
char* new_data = new char[new_length + 1];
strcpy(new_data, data);
strcat(new_data, other.data);
return MyString(new_data);
}
// 子字符串查
size_t find(ct MyString& sub) ct {
char* pos = strstr(data, sub.data);
if (pos) {
return pos - data;
} else {
return npos;
}
}
// 字符替换
void replace(char old_char, char new_char) {
for (size_t i = 0; i < length; ++i) {
if (data[i] == old_char) {
data[i] = new_char;
}
}
}
static ct size_t npos = -1;
};
int main() {
MyString str1("Hello, World!");
MyString str2 = str1; // 拷贝构造
MyString str = std::move(str1); // 移动构造
std::cout << "str2: " << _str() << ", length: " << str2.size() << std::endl;
std::cout << "str: " << _str() << ", length: " << str.size() << std::endl;
MyString str4 = str2 + str; // 字符串连接
std::cout << "str4: " << _str() << ", length: " << str4.size() << std::endl;
size_t pos = str4.find("World"); // 子字符串查
std::cout << "pos: " << pos << std::endl;
str4.replace('o', '0'); // 字符替换
std::cout << "str4: " << _str() << ", length: " << str4.size() << std::endl;
return 0;
}
这个版本的 MyString
类增加了以下功能:
- 字符串连接:通过重载
+
运算符实现。新的字符串长度是两个输入字符串的长度之和,内容是两个输入字符串的内容连接。 - 子字符串查:通过
find
函数实现。如果到子字符串,返回其在字符串中的位置;否则,返回npos
。 - 字符替换:通过
replace
函数实现。将字符串中的所有old_char
替换为new_char
。
6. 进一步性能优化
在实际使用中,性能是非常重要的考虑因素。可以讨论如何优化 MyString 类的性能,例如通过使用小字符串优化(Small String Optimization)来避免小字符串的内存分配。
小字符串优化是一种常见的优化手段,它可以避免小字符串的内存分配。具体来说,如果字符串的长度小于某个阈值(例如 15),则可以直接在对象内部存储字符串,而不是在堆上分配内存。这样可以避免小字符串的内存分配和释放开销,提高性能。
代码语言:cpp代码运行次数:0运行复制#include <iostream>
#include <cstring>
class MyString {
private:
union {
char local_buffer[16]; // 本地缓冲区,用于存储小字符串
char* heap_buffer; // 堆缓冲区,用于存储大字符串
};
size_t length; // 字符串长度
public:
// 构造函数
MyString(ct char* str = "") {
length = strlen(str);
if (length < 16) {
strcpy(local_buffer, str);
local_buffer[length] = '\0';
} else {
heap_buffer = new char[length + 1];
strcpy(heap_buffer, str);
}
}
// 拷贝构造函数
MyString(ct MyString& other) {
length = other.length;
if (length < 16) {
strcpy(local_buffer, other.local_buffer);
} else {
heap_buffer = new char[length + 1];
strcpy(heap_buffer, other.heap_buffer);
}
}
// 移动构造函数
MyString(MyString&& other) noexcept {
length = other.length;
if (length < 16) {
strcpy(local_buffer, other.local_buffer);
} else {
heap_buffer = other.heap_buffer;
other.heap_buffer = nullptr;
}
}
// 析构函数
~MyString() {
if (length >= 16) {
delete[] heap_buffer;
}
}
// 重载赋值运算符
MyString& operator=(ct MyString& other) {
if (this != &other) {
if (length >= 16) {
delete[] heap_buffer;
}
length = other.length;
if (length < 16) {
strcpy(local_buffer, other.local_buffer);
} else {
heap_buffer = new char[length + 1];
strcpy(heap_buffer, other.heap_buffer);
}
}
return *this;
}
// 重载移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
if (length >= 16) {
delete[] heap_buffer;
}
length = other.length;
if (length < 16) {
strcpy(local_buffer, other.local_buffer);
} else {
heap_buffer = other.heap_buffer;
other.heap_buffer = nullptr;
}
}
return *this;
}
// 获取字符串长度
size_t size() ct {
return length;
}
// 获取字符串内容
ct char* c_str() ct {
if (length < 16) {
return local_buffer;
} else {
return heap_buffer;
}
}
// 字符串连接
MyString operator+(ct MyString& other) {
size_t new_length = length + other.length;
char* new_data = new char[new_length + 1];
strcpy(new_data, c_str());
strcat(new_data, _str());
return MyString(new_data);
}
// 子字符串查
size_t find(ct MyString& sub) ct {
char* pos = strstr(c_str(), _str());
if (pos) {
return pos - c_str();
} else {
return npos;
}
}
// 字符替换
void replace(char old_char, char new_char) {
for (size_t i = 0; i < length; ++i) {
if (c_str()[i] == old_char) {
if (length < 16) {
local_buffer[i] = new_char;
} else {
heap_buffer[i] = new_char;
}
}
}
}
static ct size_t npos = -1;
};
int main() {
MyString str1("Hello, World!");
MyString str2 = str1; // 拷贝构造
MyString str = std::move(str1); // 移动构造
std::cout << "str2: " << _str() << ", length: " << str2.size() << std::endl;
std::cout << "str: " << _str() << ", length: " << str.size() << std::endl;
MyString str4 = str2 + str; // 字符串连接
std::cout << "str4: " << _str() << ", length: " << str4.size() << std::endl;
size_t pos = str4.find("World"); // 子字符串查
std::cout << "pos: " << pos << std::endl;
str4.replace('o', '0'); // 字符替换
std::cout << "str4: " << _str() << ", length: " << str4.size() << std::endl;
return 0;
}
7. 总结:理解字符串的底层实现
这篇文章已经非常详细地解释了如何手动实现一个字符串类,包括构造函数、拷贝构造函数、移动构造函数、析构函数以及赋值运算符的重载等关键部分。同时,也解释了内存管理、深拷贝与浅拷贝、移动语义、字符串结束符、异常安全、更多操作以及代码性能优化等关键概念。
如果要进一步完善,还可以考虑以下两个方面:
- 错误处理:当前的
MyString
类没有处理可能的错误,例如内存分配失败。可以添加错误处理代码,以增强其健壮性。 - 测试和验证:为了确保
MyString
类的正确性,可以编写一些测试用例来验证其功能。这也可以帮助读者更好地理解如何使用这个类。
手动实现一个字符串类不仅能帮助我们理解字符串的底层实现,还能让我们掌握内存管理、拷贝和移动语义等重要概念。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2024-12-25,如有侵权请联系 cloudcommunity@tencent 删除string对象内存字符串腾讯技术创作特训营S11#重启人生#感谢您对电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格的认可,转载请说明来源于"电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格
下一篇:C++日志管理从基础到完善
推荐阅读
留言与评论(共有 11 条评论) |
本站网友 嘀咕的意思 | 14分钟前 发表 |
还能让我们掌握内存管理 | |
本站网友 万科光明城市 | 23分钟前 发表 |
other.local_buffer); } else { heap_buffer = new char[length + 1]; strcpy(heap_buffer | |
本站网友 质量受权人 | 3分钟前 发表 |
2.4 析构函数析构函数的作用是清理 MyString 对象 | |
本站网友 国足出线 | 23分钟前 发表 |
2.2 拷贝构造函数拷贝构造函数的作用是创建一个新的 MyString 对象 | |
本站网友 ipo上市 | 26分钟前 发表 |
测试和验证:为了确保 MyString 类的正确性 | |
本站网友 白云二手房 | 1分钟前 发表 |
endl; MyString str4 = str2 + str; // 字符串连接 std | |
本站网友 成都精神病医院 | 17分钟前 发表 |
本站网友 山海关吧 | 22分钟前 发表 |
" << _str() << " | |
本站网友 浙江临安 | 16分钟前 发表 |
" << _str() << " | |
本站网友 北京歌华开元大酒店 | 27分钟前 发表 |
以及内存管理 |