🤖💻👨💻👩💻🌟🚀 🤖🌟 欢迎降临张有志的未来科技实验室 🤖🌟 专栏: C++ 👨💻👩💻 先赞后看,已成习惯👨💻👩💻 👨💻👩💻 创作不易,多多支持👨💻👩💻 🚀 启动创新引擎,揭秘C语言的密码🚀 ![]()
文章目录
- **1. 引言**
- 1.1 为什么要模拟实现 `string` 类?
- 1.2 文章目标和特色
- **2. 基础设计:定义一个简单的 MyString 类**
- 2.1 类的基本结构
- **MyString 类的结构图**
- 2.2 基本功能实现
- 2.3 测试代码验证
- **3. 动态内存管理与深拷贝**
- 动态内存管理的函数调用流程
- **3.1 实现拷贝构造函数**
- 为什么需要深拷贝?
- 实现代码:
- 测试代码:
- **3.2 实现拷贝赋值运算符**
- 实现代码:
- 测试代码:
- **3.3 实现移动构造函数与移动赋值运算符**
- **3.3.1 移动构造函数**
- 测试代码:
- **3.3.2 移动赋值运算符**
- 测试代码:
- **3.4 深拷贝与移动语义的比较**
- **3.5 测试所有动态内存管理功能**
- **4. 字符串常用功能的模拟实现**
- 4.1 获取字符串长度:`size()` 方法
- 4.2 访问字符串内容:`operator[]` 和 `at()`
- 4.3 拼接字符串:`operator+` 和 `append()`
- 4.4 字符串比较:`operator==` 和 `operator<`
- 4.5 子字符串操作:`substr()`
- **5. 运算符重载:让 MyString 类更像 std::string**
- 5.1 实现 `operator=` 重载
- 5.2 实现 `operator+` 重载(支持拼接)
- 5.3 实现流插入和提取运算符
- **6. 内存优化与异常安全**
- **6.1 避免内存泄漏**
- **改进代码:使用 `std::unique_ptr`**
- **6.2 异常安全的实现**
- **问题案例:异常情况下资源泄漏**
- **解决方案:RAII 和强异常安全性**
- **6.3 异常安全性级别**
- **6.4 测试异常安全性**
- **💡tips**
- **7. 扩展功能设计**
- 7.1 模拟实现 `find()`、`replace()` 等高级操作
- **8. Last**
- **内存管理和函数调用流程时序图**
1. 引言
1.1 为什么要模拟实现 string 类?
C++ 中的 std::string 是一个功能强大的库类,在实际应用中充当常用,但它的实现原理对于初学者而言却是一种黄金印记。为深入了解这个类的设计思想,也为了提升对动态内存管理和面向对象编程方法的理解,我们可以通过自己手动实现一个简化的 string 类。
1.2 文章目标和特色
- 文章目标: 通过分步实现一个功能完整的
string类,并增强对 C++ 基础编程技能的理解。 - 文章特色:
- 深入解析动态内存管理和深抽象设计
- 进一步认识运算符重载和层次化实现思路
- 提供明确的代码示例和详细说明
2. 基础设计:定义一个简单的 MyString 类
2.1 类的基本结构
首先,我们将构造一个基础的 MyString 类,使用动态内存分配来存储字符串内容:
-
数据成员:
char* data用于存储字符串内容size_t length用于存储字符串长度
-
结构:
- 默认构造函数
- 带参构造函数(从 C 风格字符串初始化)
- 构造时分配内存,构造时释放
MyString 类的结构图
2.2 基本功能实现
#include <cstring>
#include <iostream>
class MyString {
private:
char* data;
size_t length;
public:
// 默认构造函数
MyString() : data(nullptr), length(0) {}
// 带参构造函数
MyString(const char* str) {
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
}
// 析构函数
~MyString() {
delete[] data;
}
// 获取字符串长度
size_t size() const {
return length;
}
// 显示字符串内容
void display() const {
if (data) {
std::cout << data << std::endl;
} else {
std::cout << "(empty)" << std::endl;
}
}
};
2.3 测试代码验证
int main() {
MyString str1;
MyString str2("Hello, MyString!");
std::cout << "str1: ";
str1.display();
std::cout << "Length: " << str1.size() << std::endl;
std::cout << "str2: ";
str2.display();
std::cout << "Length: " << str2.size() << std::endl;
return 0;
}
输出:
str1: (empty)
Length: 0
str2: Hello, MyString!
Length: 16
3. 动态内存管理与深拷贝
动态内存管理是实现一个类的核心内容,特别是像字符串这样的类,因为它们需要根据运行时的数据长度动态分配内存。
动态内存管理的函数调用流程
我们需要实现以下功能:
- 拷贝构造函数(深拷贝)
- 拷贝赋值运算符
- 移动构造函数和移动赋值运算符(C++11 引入)
3.1 实现拷贝构造函数
拷贝构造函数用于根据已有对象创建一个新的对象。在动态内存管理中,拷贝构造函数需要深拷贝,即为新对象分配独立的内存,并将数据内容复制到新内存中,而不是直接共享原对象的内存。
为什么需要深拷贝?
如果使用浅拷贝(直接复制指针),会导致多个对象指向同一块内存。当一个对象析构时释放了内存,其他对象会访问已释放的内存,从而引发未定义行为。
实现代码:
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1]; // 分配独立内存
std::strcpy(data, other.data); // 拷贝字符串内容
}
测试代码:
MyString str1("Hello");
MyString str2(str1); // 使用拷贝构造函数
std::cout << str1 << " " << str2 << std::endl; // 输出 "Hello Hello"
深拷贝的结果:str1 和 str2 是独立的对象,修改其中一个不会影响另一个。
3.2 实现拷贝赋值运算符
拷贝赋值运算符负责将一个已存在的对象赋值给另一个已存在的对象。实现时需要考虑以下关键点:
1.自我赋值检查:避免对象自我赋值时错误地释放内存。
2. 释放旧资源:防止内存泄漏。
3. 分配新资源并复制数据:确保深拷贝。
实现代码:
MyString& operator=(const MyString& other) {
if (this == &other) return *this; // 检查自我赋值
delete[] data; // 释放旧资源
length = other.length;
data = new char[length + 1]; // 分配新资源
std::strcpy(data, other.data); // 拷贝内容
return *this;
}
测试代码:
MyString str1("Hello");
MyString str2("World");
str2 = str1; // 使用拷贝赋值运算符
std::cout << str1 << " " << str2 << std::endl; // 输出 "Hello Hello"
注意:通过深拷贝,str1 和 str2 不再共享内存。
3.3 实现移动构造函数与移动赋值运算符
移动语义是 C++11 引入的重要特性,用于优化性能。当对象是临时的或即将销毁时,可以将其资源直接“移动”到新对象,而不是复制内容。
3.3.1 移动构造函数
移动构造函数直接转移原对象的资源,而无需分配和拷贝新内存。
实现代码:
MyString(MyString&& other) noexcept
: data(other.data), length(other.length) {
other.data = nullptr; // 将原对象的指针置空
other.length = 0;
}
测试代码:
MyString createString() {
return MyString("Hello");
}
MyString str = createString(); // 使用移动构造函数
std::cout << str << std::endl; // 输出 "Hello"
在这段代码中,临时对象的资源被直接转移到 str,避免了不必要的内存分配和拷贝。
3.3.2 移动赋值运算符
移动赋值运算符将资源从一个对象移动到另一个对象,同时释放目标对象原有的资源。
实现代码:
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data; // 释放旧资源
data = other.data; // 转移资源
length = other.length;
other.data = nullptr; // 将原对象置于安全状态
other.length = 0;
}
return *this;
}
测试代码:
MyString str1("Hello");
MyString str2("World");
str2 = std::move(str1); // 使用移动赋值运算符
std::cout << str2 << std::endl; // 输出 "Hello"
注意:std::move 表示可以将资源从 str1 转移到 str2,而 str1 被置为空状态。
3.4 深拷贝与移动语义的比较
| 特性 | 深拷贝 | 移动语义 |
|---|---|---|
| 适用场景 | 通常对象 | 临时对象或资源转移 |
| 性能 | 较慢(需要分配和复制内存) | 高效(直接转移资源,无需分配) |
| 资源管理的安全性 | 资源独立 | 资源转移后需小心原对象的状态 |
3.5 测试所有动态内存管理功能
以下是一个综合测试代码,用于验证动态内存管理的正确性:
void testDynamicMemoryManagement() {
MyString str1("Hello");
MyString str2 = str1; // 拷贝构造
MyString str3;
str3 = str1; // 拷贝赋值
MyString str4 = MyString("World"); // 移动构造
MyString str5;
str5 = std::move(str4); // 移动赋值
std::cout << str1 << " " << str2 << " " << str3 << " " << str5 << std::endl;
}
testDynamicMemoryManagement();
预期输出:Hello Hello Hello World
通过以上测试,可以验证 MyString 的深拷贝、移动语义和动态内存管理是否正确。
4. 字符串常用功能的模拟实现
4.1 获取字符串长度:size() 方法
实现一个获取字符串长度的方法,直接返回存储的 length 值。
size_t size() const {
return length;
}
4.2 访问字符串内容:operator[] 和 at()
支持按索引访问字符串内容,operator[] 提供简单访问,at() 提供边界检查:
char& operator[](size_t index) {
return data[index];
}
char at(size_t index) const {
if (index >= length) {
throw std::out_of_range("Index out of range");
}
return data[index];
}
4.3 拼接字符串:operator+ 和 append()
支持字符串拼接操作:
MyString operator+(const MyString& other) const {
MyString newStr;
newStr.length = length + other.length;
newStr.data = new char[newStr.length + 1];
std::strcpy(newStr.data, data);
std::strcat(newStr.data, other.data);
return newStr;
}
void append(const MyString& other) {
char* newData = new char[length + other.length + 1];
std::strcpy(newData, data);
std::strcat(newData, other.data);
delete[] data;
data = newData;
length += other.length;
}
4.4 字符串比较:operator== 和 operator<
实现字符串的比较运算:
bool operator==(const MyString& other) const {
return std::strcmp(data, other.data) == 0;
}
bool operator<(const MyString& other) const {
return std::strcmp(data, other.data) < 0;
}
4.5 子字符串操作:substr()
支持提取子字符串:
MyString substr(size_t start, size_t len) const {
if (start >= length) {
throw std::out_of_range("Start index out of range");
}
len = std::min(len, length - start);
char* subStr = new char[len + 1];
std::strncpy(subStr, data + start, len);
subStr[len] = '\0';
return MyString(subStr);
}
5. 运算符重载:让 MyString 类更像 std::string
5.1 实现 operator= 重载
为类添加拷贝赋值运算符:
MyString& operator=(const MyString& other) {
if (this == &other) return *this;
delete[] data;
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
return *this;
}
5.2 实现 operator+ 重载(支持拼接)
MyString operator+(const MyString& other) const {
MyString newStr;
newStr.length = length + other.length;
newStr.data = new char[newStr.length + 1];
std::strcpy(newStr.data, data);
std::strcat(newStr.data, other.data);
return newStr;
}
5.3 实现流插入和提取运算符
friend std::ostream& operator<<(std::ostream& os, const MyString& str) {
os << str.data;
return os;
}
friend std::istream& operator>>(std::istream& is, MyString& str) {
char buffer[1024];
is >> buffer;
str = MyString(buffer);
return is;
}
以下是第六点:内存优化与异常安全的重新编写和详细说明。
6. 内存优化与异常安全
在实现 MyString 类时,内存优化和异常安全是不可忽视的重要问题。动态分配的内存如果没有得到正确管理,可能会引发内存泄漏或者未定义行为。此外,程序在遇到异常时,应确保资源得到妥善释放,保证程序的鲁棒性。
6.1 避免内存泄漏
问题:
在 C++ 中,动态分配的内存必须手动释放。如果内存分配失败或程序提前退出,可能会造成内存泄漏。
解决方案:
- 使用 RAII(Resource Acquisition Is Initialization)原则,将资源的分配和释放绑定到对象的生命周期。
- 如果需要进一步简化内存管理,可以使用智能指针(如
std::unique_ptr或std::shared_ptr)。
改进代码:使用 std::unique_ptr
通过 std::unique_ptr 自动管理动态分配的内存,避免显式释放。
#include <memory>
class MyString {
private:
std::unique_ptr<char[]> data; // 使用智能指针管理内存
size_t length;
public:
MyString() : data(nullptr), length(0) {}
MyString(const char* str) {
length = std::strlen(str);
data = std::make_unique<char[]>(length + 1); // 分配内存
std::strcpy(data.get(), str); // 拷贝内容
}
MyString(const MyString& other) {
length = other.length;
data = std::make_unique<char[]>(length + 1); // 分配新内存
std::strcpy(data.get(), other.data.get());
}
MyString& operator=(const MyString& other) {
if (this == &other) return *this;
length = other.length;
data = std::make_unique<char[]>(length + 1); // 分配新内存
std::strcpy(data.get(), other.data.get());
return *this;
}
void display() const {
if (data) {
std::cout << data.get() << std::endl;
} else {
std::cout << "(empty)" << std::endl;
}
}
};
优势:
std::unique_ptr会在对象析构时自动释放内存。- 减少显式调用
delete[]的需要,代码更加简洁、安全。
6.2 异常安全的实现
在动态内存管理中,异常的出现可能导致程序状态不一致或资源泄漏。例如,在赋值运算符的实现中,如果在内存分配后发生异常,可能会导致旧资源未释放或新资源分配失败。
问题案例:异常情况下资源泄漏
以下代码可能引发资源泄漏:
MyString& operator=(const MyString& other) {
if (this == &other) return *this;
delete[] data; // 释放旧资源
data = new char[other.length + 1]; // 如果此处抛出异常,旧资源已经被释放
std::strcpy(data, other.data);
return *this;
}
解决方案:RAII 和强异常安全性
使用临时对象(临时变量)的策略,实现强异常安全性。只有在所有操作完成后才替换原有对象的资源。
实现代码:
MyString& operator=(const MyString& other) {
if (this == &other) return *this;
// 创建临时对象,分配新资源
MyString temp(other);
// 交换当前对象和临时对象的资源
std::swap(data, temp.data);
std::swap(length, temp.length);
return *this;
}
工作原理:
- 使用拷贝构造函数创建临时对象
temp。 temp完成所有资源的分配和初始化。- 使用
std::swap将当前对象和临时对象的资源交换。 - 临时对象在离开作用域时析构,释放旧资源。
这种方式确保了在出现异常时,当前对象始终处于一致状态。
6.3 异常安全性级别
C++ 中,异常安全性通常分为以下三个级别:
-
基本保证:
- 在出现异常时,程序的状态不会崩溃或导致未定义行为。
- 当前对象可能处于不一致状态,但内存不会泄漏。
-
强保证:
- 在操作过程中发生异常时,当前对象的状态不变。
- 通过临时对象的策略实现。
-
无异常保证:
- 保证操作不会抛出任何异常。
- 通常适用于简单函数或通过
noexcept明确声明。
6.4 测试异常安全性
以下是一个测试异常安全性的代码示例:
#include <iostream>
#include <stdexcept>
void testExceptionSafety() {
try {
MyString str1("Hello, world!");
MyString str2("Another string");
std::cout << "Before assignment:" << std::endl;
str1.display();
str2.display();
str2 = str1; // 测试赋值运算符的异常安全
std::cout << "After assignment:" << std::endl;
str1.display();
str2.display();
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
}
int main() {
testExceptionSafety();
return 0;
}
💡tips
- 使用 RAII 和智能指针(如
std::unique_ptr)提高内存管理的安全性。 - 通过临时对象的策略,确保强异常安全性,保证操作的原子性。
- 对所有涉及动态内存管理的函数进行测试,确保异常发生时不会导致内存泄漏或状态不一致。
7. 扩展功能设计
7.1 模拟实现 find()、replace() 等高级操作
size_t find(const char* substr) const {
char* pos = std::strstr(data, substr);
if (pos) {
return pos - data;
}
return std::string::npos;
}
void replace(const char* oldStr, const char* newStr) {
if (!data || !oldStr || !newStr) return; // 空检查
int startIndex = findSubstring(oldStr);
if (startIndex == -1) return; // 如果未找到子串,直接返回
size_t oldStrLen = std::strlen(oldStr);
size_t newStrLen = std::strlen(newStr);
// 计算新字符串的长度
size_t newLength = length - oldStrLen + newStrLen;
// 分配新内存
char* newData = new char[newLength + 1];
// 复制旧数据到新内存
std::strncpy(newData, data, startIndex); // 复制子串前的部分
std::strcpy(newData + startIndex, newStr); // 替换部分
std::strcpy(newData + startIndex + newStrLen, // 复制子串后的部分
data + startIndex + oldStrLen);
// 更新成员变量
delete[] data;
data = newData;
length = newLength;
}
// 判断是否以 prefix 开头
bool startsWith(const char* prefix) const {
if (!data || !prefix) return false; // 空检查
size_t prefixLen = std::strlen(prefix);
if (prefixLen > length) return false; // 前缀长度大于字符串长度
return std::strncmp(data, prefix, prefixLen) == 0; // 比较前缀部分
}
// 判断是否以 suffix 结尾
bool endsWith(const char* suffix) const {
if (!data || !suffix) return false; // 空检查
size_t suffixLen = std::strlen(suffix);
if (suffixLen > length) return false; // 后缀长度大于字符串长度
return std::strncmp(data + length - suffixLen, suffix, suffixLen) == 0; // 比较后缀部分
}
839





