免费申请网站 免备案,域名注册后如何建网站,wordpress4.8.2下载,crm平台文章目录 C string 类的模拟实现#xff1a;从构造到高级操作前言第一章#xff1a;为什么要手写 C string 类#xff1f;1.1 理由与价值 第二章#xff1a;实现一个简单的 string 类2.1 基本构造与析构2.1.1 示例代码#xff1a;基础的 string 类实现2.1.2 解读代码 2.2 … 文章目录 C string 类的模拟实现从构造到高级操作前言第一章为什么要手写 C string 类1.1 理由与价值 第二章实现一个简单的 string 类2.1 基本构造与析构2.1.1 示例代码基础的 string 类实现2.1.2 解读代码 2.2 浅拷贝与其缺陷2.2.1 示例代码浅拷贝问题 2.3 深拷贝的解决方案2.3.1 示例代码实现深拷贝 第三章赋值运算符重载与深拷贝3.1 为什么需要重载赋值运算符3.2 实现赋值运算符重载3.2.1 示例代码赋值运算符重载3.2.2 解读代码 第四章迭代器与字符串操作4.1 迭代器的实现4.1.1 示例代码实现 string 类的迭代器 第五章字符串的常见操作5.1 查找操作5.1.1 示例代码实现字符和子字符串查找 5.1.2 静态 const 成员变量初始化规则详解5.1.2.1 静态成员变量属于类而不属于对象5.1.2.2 const 修饰符的作用5.1.2.3 整型和枚举类型的特殊处理5.1.2.4 复杂类型为什么不能在类内初始化5.1.2.5 为什么 static const size_t npos -1 可以在类内初始化5.1.2.6 总结为什么静态 const 的复杂类型不能在类内初始化 5.2 插入操作5.2.1 示例代码实现字符串插入 5.3 删除操作5.3.1 示例代码实现字符串删除 读者须知与结语 C string 类的模拟实现从构造到高级操作 欢迎讨论在实现 string 类的过程中如果有任何疑问欢迎在评论区交流我们一起探讨如何一步步实现它。 支持一下如果这篇文章对你有所帮助记得点赞、收藏并分享给更多对 C 感兴趣的小伙伴吧你们的支持是我创作的动力 前言
在 C 标准库中string 类是用于字符串操作的一个非常常见和重要的类它极大地简化了开发者处理字符串的过程。然而为了深入理解 C 的核心机制特别是内存管理、深拷贝与浅拷贝的差异、运算符重载等底层细节自己实现一个简易的 string 类是一个很好的练习。
通过本篇博客我们将一步步实现一个简单的 string 类并且深入探讨与之相关的现代 C 特性包括内存管理、深拷贝与浅拷贝、移动语义等。我们会从最基础的构造函数开始逐步扩展功能。 第一章为什么要手写 C string 类
1.1 理由与价值
在面试或者一些学习场景中手写 string 类不仅仅是对字符串操作的考察更多的是考察程序员对 C 内存管理的理解。例如深拷贝与浅拷贝的实现如何正确重载赋值运算符如何避免内存泄漏这些都是需要掌握的核心技能。
实现一个简易的 string 类可以帮助我们更好地理解
C 中动态内存管理如何正确地分配与释放内存。深拷贝与浅拷贝的区别当对象之间共享资源时如何避免潜在问题。运算符重载的实现尤其是赋值运算符和输出运算符的重载。现代 C 特性包括移动语义、右值引用等。
接下来我们会从一个简单的 string 类开始逐步扩展。 第二章实现一个简单的 string 类
2.1 基本构造与析构
我们先实现 string 类的基础部分包括构造函数、析构函数、字符串存储、内存管理等基础操作。在最初的实现中我们将模拟 C 标准库 string 类的基本行为让其能够存储字符串并在析构时正确释放内存。
2.1.1 示例代码基础的 string 类实现
#include iostream
#include cstring // 包含 strlen 和 strcpy 函数
#include cassert // 包含 assert 函数namespace W
{class string{public:// 默认构造函数string(const char* str ) {_size strlen(str); // 计算字符串长度_capacity _size;_str new char[_capacity 1]; // 动态分配内存strcpy(_str, str); // 复制字符串内容}// 析构函数~string() {if (_str) {delete[] _str; // 释放动态分配的内存_str nullptr;}}private:char* _str; // 存储字符串的字符数组size_t _capacity; // 分配的内存容量size_t _size; // 当前字符串的有效长度};
}int main() {W::string s(Hello, World!);return 0; // 程序结束时析构函数自动释放内存
}2.1.2 解读代码
在这个简单的 string 类中我们实现了两个重要的函数
构造函数为字符串动态分配内存并将传入的字符串内容复制到新分配的空间中。析构函数使用 delete[] 释放动态分配的内存以避免内存泄漏。
接下来我们将讨论拷贝构造函数以及浅拷贝带来的潜在问题。 2.2 浅拷贝与其缺陷
当前版本的 string 类只支持基本的构造和析构操作。如果我们通过另一个 string 对象来构造新的对象默认情况下会发生浅拷贝即对象共享同一块内存。这会带来潜在的内存管理问题特别是当对象被销毁时会导致多个对象同时试图释放同一块内存进而导致程序崩溃。
2.2.1 示例代码浅拷贝问题
void TestString() {W::string s1(Hello C);W::string s2(s1); // 浅拷贝s1 和 s2 共享同一块内存// 当程序结束时析构函数会尝试两次释放同一块内存导致程序崩溃
}问题分析浅拷贝的默认行为只复制指针的值即 s1 和 s2 都指向同一个内存区域。因此当程序执行析构函数时会尝试两次释放同一块内存导致程序崩溃。 2.3 深拷贝的解决方案 为了避免浅拷贝带来的问题我们需要在拷贝构造函数中实现深拷贝。深拷贝确保每个对象都有自己独立的内存空间不会与其他对象共享内存。 2.3.1 示例代码实现深拷贝
namespace W
{class string{public:// 构造函数string(const char* str ) {_size strlen(str);_capacity _size;_str new char[_capacity 1];strcpy(_str, str);}// 深拷贝构造函数string(const string s) {_size s._size;_capacity s._capacity;_str new char[_capacity 1]; // 分配新的内存strcpy(_str, s._str); // 复制字符串内容}// 析构函数~string() {delete[] _str;}private:char* _str;size_t _capacity;size_t _size;};
}void TestString() {W::string s1(Hello C);W::string s2(s1); // 深拷贝s1 和 s2 拥有独立的内存
}第三章赋值运算符重载与深拷贝
3.1 为什么需要重载赋值运算符 在C中当我们将一个对象赋值给另一个对象时默认情况下编译器会为我们生成一个浅拷贝的赋值运算符。这意味着赋值后的对象和原对象会共享同一个内存空间这会导致和浅拷贝相同的潜在问题特别是在一个对象被销毁时另一个对象继续使用该内存区域会引发错误。 为了解决这个问题我们需要手动重载赋值运算符确保每个对象都拥有自己独立的内存空间。
3.2 实现赋值运算符重载
在赋值运算符重载中我们需要考虑以下几点
自我赋值对象是否会被赋值给自己避免不必要的内存释放和分配。释放原有资源在赋值前我们需要释放被赋值对象原有的内存资源避免内存泄漏。深拷贝为目标对象分配新的内存并复制内容。
3.2.1 示例代码赋值运算符重载
namespace W
{class string{public:// 构造函数string(const char* str ) {_size strlen(str);_capacity _size;_str new char[_capacity 1];strcpy(_str, str);}// 深拷贝构造函数string(const string s) {_size s._size;_capacity s._capacity;_str new char[_capacity 1];strcpy(_str, s._str);}// 赋值运算符重载string operator(const string s) {if (this ! s) { // 避免自我赋值delete[] _str; // 释放原有内存_size s._size;_capacity s._capacity;_str new char[_capacity 1]; // 分配新内存strcpy(_str, s._str); // 复制内容}return *this;}// 析构函数~string() {delete[] _str;}private:char* _str;size_t _capacity;size_t _size;};
}void TestString() {W::string s1(Hello);W::string s2(World);s2 s1; // 调用赋值运算符重载
}3.2.2 解读代码 自我赋值检查自我赋值是指对象在赋值时被赋值给自己例如 s1 s1。在这种情况下如果我们没有进行检查就会先删除对象的内存然后再试图复制同一个对象的内容这样会导致程序崩溃。因此重载赋值运算符时自我赋值检查是非常必要的。 释放原有内存在分配新内存之前我们必须先释放旧的内存以防止内存泄漏。 深拷贝通过分配新的内存确保目标对象不会与源对象共享内存避免浅拷贝带来的问题。 第四章迭代器与字符串操作
4.1 迭代器的实现 迭代器是一种用于遍历容器如数组、string 等的工具它允许我们在不直接访问容器内部数据结构的情况下遍历容器。通过迭代器可以使用范围 for 循环等简便的方式遍历 string 对象中的字符。 在我们的 string 类中迭代器一般会被实现为指向字符数组的指针
4.1.1 示例代码实现 string 类的迭代器
namespace W
{class string{public:// 非const迭代器typedef char* iterator;// const迭代器typedef const char* const_iterator;// 构造函数与析构函数等...// 非const迭代器接口iterator begin() { return _str; }iterator end() { return _str _size; }// const迭代器接口针对const对象const_iterator begin() const { return _str; }const_iterator end() const { return _str _size; }private:char* _str;size_t _capacity;size_t _size;};}void TestIterator() {W::string s(Hello World!);// 非const对象使用迭代器for (W::string::iterator it s.begin(); it ! s.end(); it) {*it toupper(*it); // 转换为大写}std::cout s std::endl; // 输出HELLO WORLD!// const对象使用const迭代器const W::string cs(Const String!);for (W::string::const_iterator it cs.begin(); it ! cs.end(); it) {std::cout *it; // 只能读取不能修改}std::cout std::endl;for (auto ch : s) {ch tolower(ch); // 转换为小写}std::cout s std::endl; // 输出hello world!// 范围for循环遍历const对象for (const auto ch : cs) {std::cout ch; // 只能读取不能修改}std::cout std::endl;
} 第五章字符串的常见操作 在 C 标准库 string 类中提供了很多方便的字符串操作接口如查找字符或子字符串、插入字符、删除字符等。我们也需要在自定义的 string 类中实现这些操作。接下来我们将逐步实现这些功能并进行测试。 5.1 查找操作
C 中 string 类的 find() 函数用于查找字符串或字符在当前字符串中的位置。如果找到了字符或子字符串find() 会返回其位置如果找不到则返回 string::npos。
我们将在自定义的 string 类中实现类似的功能。
5.1.1 示例代码实现字符和子字符串查找
namespace W
{class string{public:// 构造函数与析构函数等...// 查找字符在字符串中的第一次出现位置size_t find(char c, size_t pos 0) const {assert(pos _size);for (size_t i pos; i _size; i) {if (_str[i] c) {return i;}}return npos; // 如果没有找到返回 npos}// 查找子字符串在字符串中的第一次出现位置size_t find(const char* str, size_t pos 0) const {assert(pos _size);const char* p strstr(_str pos, str);if (p) {return p - _str; // 计算子字符串的位置}return npos; // 如果没有找到返回 npos}public:static const size_t npos -1; // 定义 npos 为 -1表示未找到private:char* _str;size_t _capacity;size_t _size;};
}void TestFind() {W::string s(Hello, World!);// 查找字符size_t pos s.find(W);if (pos ! W::string::npos) {std::cout W found at position: pos std::endl;} else {std::cout W not found. std::endl;}// 查找子字符串size_t subPos s.find(World);if (subPos ! W::string::npos) {std::cout World found at position: subPos std::endl;} else {std::cout World not found. std::endl;}
}看到这里细心的小伙伴可能发现了我们在声明npos的时候直接给了初始值但是之前我们在【C篇】C类与对象深度解析四初始化列表、类型转换与static成员详解里明确说过静态成员变量只能在类外初始化以及const修饰的变量只能在初始化列表初始化但这里却可以 这是为什么呢不得不承认这是一看到就令人困惑的语法让我们来梳理一下
5.1.2 静态 const 成员变量初始化规则详解
5.1.2.1 静态成员变量属于类而不属于对象 静态成员变量是在类层次上定义的而不是在对象层次上。换句话说静态成员变量是所有对象共享的且只会有一份实例存在。因此静态成员变量的内存是在程序的全局区域分配的而不是在每个对象的内存中分配。 静态变量需要在全局范围内被初始化以确保在所有对象中共享的唯一实例具有一致的值。 5.1.2.2 const 修饰符的作用
const 表示变量的值在其生命周期内不能被修改。因此const 静态成员变量的值必须在类加载时确定并且在整个程序运行过程中保持不变。但是 const 静态成员的值不能在对象实例化时通过构造函数来提供必须直接在类级别初始化。 5.1.2.3 整型和枚举类型的特殊处理
C 允许整型如 int、char和枚举类型的 const 静态成员变量在类内部进行初始化。这是因为这些类型的值可以在编译时完全确定编译器不需要等待运行时计算或分配内存。
class MyClass {
public:static const int value 42; // 可以直接在类内初始化
};编译器可以将 value 当作编译时常量它可以直接内联到使用它的代码中不需要单独的存储空间。这种优化适用于常量表达式。 5.1.2.4 复杂类型为什么不能在类内初始化
对于复杂类型如 double、float 或自定义类等这些类型的初始化可能涉及到运行时的计算或需要分配更多的内存。C 的设计者为了避免复杂类型的静态成员在类内初始化时增加不必要的复杂性要求这些变量必须在类外进行初始化。
class MyClass {
public:static const double pi; // 在类内声明但不能直接初始化
};// 在类外初始化
const double MyClass::pi 3.14159;5.1.2.5 为什么 static const size_t npos -1 可以在类内初始化
size_t 是一种整型类型尽管其大小和符号位取决于平台但它仍然是整型常量的一种。因此npos 的初始化类似于前面提到的整型静态成员变量。由于 -1 可以表示为 size_t 的最大值这个值在编译时就可以确定因此它符合类内初始化的条件。
class String {
public:static const size_t npos -1; // 可以在类内初始化
};总结因为 npos 是整型常量并且编译器可以在编译时确定其值符合在类内部初始化的条件。 5.1.2.6 总结为什么静态 const 的复杂类型不能在类内初始化
整型和枚举类型的 const 静态成员变量可以在类内初始化因为它们是编译时常量编译器可以直接替换为常量值。复杂类型如 double 或对象类型需要在类外初始化因为这些类型的初始化可能依赖运行时的条件或动态内存分配。这是 C 设计者在保证效率和复杂性之间做出的权衡允许简单类型进行编译时优化但要求复杂类型在类外显式初始化以确保其初始化的灵活性和正确性。 没啥好说的人家设计的记住就行了 5.2 插入操作
C 中的 string 类允许我们在字符串的任意位置插入字符或子字符串。接下来我们将在自定义的 string 类中实现类似的插入功能。
5.2.1 示例代码实现字符串插入
其他没啥注意下面这个问题 无符号整型的易错问题 //注意下面这个写法当pos0时会出现死循环问题哦/*for (size_t i _size; i pos; --i) {_str[i len] _str[i];}*/namespace W
{class string{public:// 构造函数与析构函数等...// 在指定位置插入字符string insert(size_t pos, char c) {assert(pos _size); // 确保插入位置合法if (_size _capacity) {reserve(_capacity * 2); // 如果容量不够扩展容量}// 将 pos 位置后的字符后移一位for (size_t i _size; i pos; --i) {_str[i] _str[i - 1];}_str[pos] c;_size;_str[_size] \0;return *this;}// 在指定位置插入字符串string insert(size_t pos, const char* str) {assert(pos _size);size_t len strlen(str);if (_size len _capacity) {reserve(_size len); // 如果容量不够扩展容量}// 将 pos 位置后的字符后移 len 位//注意下面这个写法当pos0时会出现死循环问题哦/*for (size_t i _size; i pos; --i) {_str[i len] _str[i];}*///采用这种for (size_t i _size; i 1 pos; --i) {_str[i len] _str[i];}// 复制要插入的字符串memcpy(_str pos, str, len);_size len;_str[_size] \0;return *this;}private:char* _str;size_t _capacity;size_t _size;};
}void TestInsert() {W::string s(Hello World!);// 在第 5 位置插入逗号s.insert(5, ,);std::cout s std::endl;// 在第 6 位置插入字符串s.insert(6, Beautiful);std::cout s std::endl;
}5.3 删除操作
string 类允许我们删除指定位置的字符或子字符串。接下来我们实现字符串的删除功能。
5.3.1 示例代码实现字符串删除
namespace W
{class string{public:// 在指定位置删除若干字符string erase(size_t pos, size_t len npos) {assert(pos _size); // 确保删除的位置合法if (len 0 || pos _size) {// 如果 len 为 0 或 pos 已经到达字符串末尾无需执行任何操作return *this;}if (len npos || pos len _size) {len _size - pos; // 确保不越界删除}// 将 pos 后 len 位字符前移for (size_t i pos; i len _size; i) {_str[i] _str[i len];}_size - len;_str[_size] \0; // 更新字符串末尾return *this;}private:char* _str;size_t _capacity;size_t _size;public:static const size_t npos -1; // 定义 npos 为 -1 表示无效位置};
}void TestErase() {W::string s(Hello, Beautiful World!);// 删除第 5 位置后的 9 个字符s.erase(5, 9);std::cout s std::endl; // 输出Hello World!
} 读者须知与结语 在本文中我们手写了一个简易版的 string 类实现了诸如字符串插入、删除、查找等基本功能。然而这个实现仍然是非常简陋的使用了大量 C 风格的字符串函数如 strlen 和 strcpy。这些函数都假设字符串是以 \0 结尾的字符数组这意味着如果字符串中间出现 \0程序的行为将不可预期——它会错误地认为字符串已经结束。 此外这个简易 string 类在面对一些复杂的情况时也会显得捉襟见肘。例如我们并没有考虑多线程安全性、异常处理等高级特性而标准库的 std::string 早已针对这些问题进行了优化。标准库中的 string 类还支持更多的操作并且在效率和内存管理上做了大量优化因此我们的实现和真正的 std::string 相比可谓天差万别。 但这并不是我们这篇文章的初衷。我们的目的是通过手写一个 string 类让你深入理解底层的内存管理、拷贝控制、动态分配等核心概念。这些基础知识对于深入学习 C 编程和理解 STL 容器的实现原理至关重要。通过这个简化版的实现希望你能更加透彻地理解 std::string 背后的机制。 如果你有任何问题或者更好的想法欢迎在评论区分享你的观点。你们的反馈和支持是我创作的最大动力 欢迎讨论如果你在学习过程中遇到问题欢迎在评论区交流 支持一下如果这篇文章对你有所帮助请点赞、收藏并分享你们的支持是我创作的动力 以上就是关于【C篇】手撕 C string 类从零实现到深入剖析的模拟之路的内容啦各位大佬有什么问题欢迎在评论区指正或者私信我也是可以的啦您的支持是我创作的最大动力❤️