当前位置: 首页 > news >正文

【C++】深入浅出:string类模拟实现全解析

1 实现一个简单的string

首先,我们实现一个最简单的 string 类,它只包含一个指向动态字符数组的指针 _str

1.1 简单 string 类框架

namespace practice_string {class string {public:// 构造函数:使用C字符串初始化string(const char* str = ""): _str(new char[strlen(str) + 1]) // 多分配1个字节存放'\0'{strcpy(_str, str);}// 析构函数:释放动态分配的内存~string() {delete[] _str;_str = nullptr;}// 获取字符串长度(不包含'\0')size_t size() const {return strlen(_str);}// 重载[]运算符,支持像数组一样访问字符char& operator[](size_t i) {return _str[i];}// 获取C风格字符串(只读)const char* c_str() const {return _str;}private:char* _str; // 核心:指向动态字符数组的指针};
}

关键点:

  1. 构造函数:参数使用 const char* 而不是 char*char* 无法传入常量字符串,一旦传入常量字符串,就相当于 char* _str = const char* str。这是访问权限的放大,会导致报错。(访问权限的缩放规则对指针和引用生效)

  2. 内存管理:在堆上(new[])分配内存,并在析构时释放(delete[]),这是管理动态资源的类的典型特征。

  3. 默认参数:const char* str = "" 使得默认构造函数和带参构造函数合二为一。

1.2 拷贝构造

        假设我们自己不写拷贝函数,编译器会调用自动生成的拷贝函数(浅拷贝)。

        编译器默认生成的「拷贝构造」和「赋值重载」是浅拷贝(仅拷贝指针值,不拷贝指针指向的内容),会导致严重问题:两个对象的 _str 指向同一块内存,析构时会「重复释放同一块空间」,触发程序崩溃。程序如下:

void test_string() {string s1("hello");string s2(s1); // 编译器默认浅拷贝:s2._str = s1._str(同一块内存)
} 
// 函数结束时:s2先析构(释放内存),s1再析构(释放已释放的内存→崩溃)

详细解析:

        浅拷贝也称之为“值拷贝”,会把一块空间中的数据直接拷贝到另一块空间。也就是说,s1中 _str 指针的值被直接赋给了s2中的 _str 指针,这两个 string 对象的 _str 指针指向的是同一块空间,当函数退出时,s1 和 s2 结束生命周期,调用析构函数,就会将 _str 指向的空间释放 2 次,而一块空间是不能被重复释放的,所以导致了报错。

        而与“浅拷贝”对应的,“深拷贝”可以解决这个问题。

        深拷贝的核心逻辑:为新对象重新分配一块独立的内存,再拷贝原对象的内容,让两个对象的 _str 指向不同内存,彼此独立。

深拷贝版拷贝构造:

string(const string& s) : _str(new char[strlen(s._str) + 1]) { // 为s2新分配内存strcpy(_str, s._str); // 拷贝s1的内容到新内存
}

1.3 深拷贝版赋值重载

需注意两个细节:

① 防止「自赋值」(如 s1 = s1);

② 先释放原内存,再指向新内存(避免内存泄漏)。

string& operator=(const string& s) 
{// 1. 防止自赋值:若自己赋值给自己,直接返回(避免释放自身内存后拷贝)if (this != &s) { // 2. 先分配新内存,拷贝内容(若new失败,原内存不会被破坏)char* tmp = new char[strlen(s._str) + 1];strcpy(tmp, s._str);// 3. 释放原内存,指向新内存delete[] _str;_str = tmp;}return *this; // 支持链式赋值(如 s1 = s2 = s3)
}

2. 支持增删查改的 string 类

基础版 string 仅满足简单需求,实际使用需「动态扩容、尾插、插入、删除、查找」等功能。此时需新增两个成员变量:

  • _size:有效字符个数(不包含 '\0'),替代 strlen(避免每次调用都遍历字符串,提高效率);

  • _capacity:当前内存可容纳的最大有效字符数(不包含 '\0'),用于动态扩容管理。

2.1 框架与核心接口

namespace practice_string {class string {public:// 基础接口(复用+优化)size_t size() const { return _size; }size_t capacity() const { return _capacity; }char& operator[](size_t i) { assert(i < _size); return _str[i]; } // 越界检查const char* c_str() const { return _str; }// 核心功能:扩容、尾插、插入、删除、查找void reserve(size_t n);                // 扩容(仅扩大容量,不改变有效字符)void resize(size_t n, char ch = '\0'); // 调整size(可补字符)void push_back(char ch);               // 尾插单个字符void append(const char* str);          // 尾插字符串string& insert(size_t pos, const char ch); // 插入字符string& erase(size_t pos, size_t len); // 删除字符size_t find(const char ch, size_t pos = 0); // 查找字符private:char* _str;size_t _size;             // 有效字符数size_t _capacity;         // 容量(最大有效字符数)static const size_t npos; // 静态常量:表示“未找到”(值为-1,size_t最大值)};// 静态成员初始化(类外)const size_t string::npos = -1;
}

2.2 构造函数与析构函数

/* 默认构造函数 */
string(const char* str = "")
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];        // 多加一个空间给'\0'strcpy(_str, str);
}/* 拷贝构造函数 */
string(const string& s)
{_size = s._size;_capacity = _size;_str = new char[_capacity + 1];        // 多加一个空间给'\0'strcpy(_str, s._str);
}/* 析构函数 */
~string()
{delete[] _str;_str = nullptr;_size = 0;_capacity = 0;
}

2.3 扩容(reserve)

  • 作用:仅当 n > _capacity 时,扩大内存容量(避免频繁扩容,提高效率);

  • 细节:扩容后需拷贝原字符串内容,释放原内存。

void practice_string::reserve(size_t n) {if (n > _capacity) { // 仅当需要的容量大于当前容量时才扩容char* tmp = new char[n + 1]; // 多1字节存'\0'strcpy(tmp, _str); // 拷贝原内容delete[] _str;     // 释放原内存_str = tmp;        // 指向新内存_capacity = n;     // 更新容量}
}

2.4 赋值重载

/* 赋值重载函数 */
string& operator=(const string& s)
{if (this != &s){// 开辟一块新的空间,拷贝数据过去char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);// 释放原来的空间防止内存泄露,指向新空间delete[] _str;_str = tmp;}return *this;
}
string& operator=(const char* str)
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];        // 多加一个空间给'\0'strcpy(_str, str);return *this;
}

2.5 迭代器

/* 迭代器 */
typedef char* iterator;iterator begin()
{return _str;
}iterator end()
{return _str + _size;
}

2.6 resize

  • 作用:同时管理「容量」和「有效字符数」:

    • n < _size:截断字符串(仅修改 _size,不释放内存);

    • n > _size:先扩容(若需),再用 ch 补全新增的位置。

void practice_string::resize(size_t n, char ch) {if (n < _size) // 截断:直接在n位置放'\0',修改size{ _str[n] = '\0';_size = n;} else    // 扩容+补字符{ if (n > _capacity) reserve(n); // 容量不足则先扩容// 补字符(从原size到n)for (size_t i = _size; i < n; ++i) {_str[i] = ch;}_size = n;       // 更新size_str[_size] = '\0'; // 确保字符串结束符}
}

2.7 尾插字符/字符串

  • 尾插单个字符(push_back):先检查容量(满则扩容,默认扩为 2 倍或初始 2),再插入字符并更新 _size

/* 尾插单个字符 */
void push_back(char ch)
{// 空间不足,扩容 hello xxxxxxx &tmp xxxxxxxxxxxx if (_size == _capacity){size_t new_capacity = _capacity == 0 ? 2 : _capacity * 2;reserve(new_capacity);}// 放入字符_str[_size] = ch;++_size;_str[_size] = '\0';
}/* s1 += 'a' */
string& operator+=(const char ch)
{this->push_back(ch);return *this;
}
  • 尾插字符串(append):计算字符串长度,若 _size + 长度 > _capacity 则扩容,再拷贝字符串到末尾。

/* 尾插字符串 */
void append(const char* str)
{size_t len = strlen(str);// 如果空间不足,则增容if (_size + len > _capacity){size_t new_capacity = _size + len;reserve(new_capacity);}// 拷入新的字符串到原字符串后面strcpy(_str + _size, str);_size += len;
}/* s1 += "abc" */
string& operator+=(const char* str)
{this->append(str);return *this;
}

2.8 insert

  • 逻辑:先检查越界(pos 需小于 _size),再扩容(若需),然后「挪动数据」(从后往前挪,避免覆盖),最后插入字符 / 字符串。

  • pos下标位置插入字符:

/* 在pos下标位置插入字符 */
string& insert(size_t pos, const char ch)
{assert(pos < _size);// 空间不够就增容if (_size == _capacity){size_t new_capacity = _capacity == 0 ? 2 : _capacity * 2;reserve(new_capacity);}// 挪动数据,从'\0'开始挪int end = _size;while (end >= (int)pos) // pos是一个无符号数,end在与他比较时也会转化成无符号,// 如果pos给0,end减到-1也会比pos大(无符号),所以这里要强转{_str[end + 1] = _str[end];--end;}// 插入数据_str[end] = ch;++_size;return *this;
}
  • 在pos下标位置插入字符串

两种方式:

/* 在pos下标位置插入字符串 */
string& insert(size_t pos, const char* str)
{assert(pos < _size);size_t len = strlen(str);// 空间不足则扩容if (_size + len > _capacity){reserve(_size + len);        // 函数内会自动多开一个空间给'\0'}// 挪动数据int end = _size;while (end >= (int)pos) // pos是一个无符号数,end在与他比较时也会转化成无符号,// 如果pos给0,end减到-1也会比pos大(无符号),所以这里要强转{_str[end + len] = _str[end];--end;}// 放字符串数据(或者使用memcpy也行)for (size_t i = 0; i < len; ++i){_str[pos] = str[i];++pos;}_size += len;return *this;
}
  • memcpy搬数据的方法:

/* 在pos下标位置插入字符串 */
/* memcpy搬数据的方法 */
void insert(size_t pos, const char* str)
{assert(pos < _size);size_t len = strlen(str);// 空间不够就增容if (_capacity < _size + len){size_t new_capacity = _capacity + len;reserve(new_capacity);}// 得到要搬运有效数据的长度size_t memcpy_len = strlen(_str + pos);// 将数据先存在另一个空间,之后再搬回来char* tmp = new char[memcpy_len + 1];memcpy(tmp, _str + pos, memcpy_len + 1);        // 加上的1是'\0'的一个字节_size -= memcpy_len;// pos位置加上字符串strmemcpy(_str + _size, str, len + 1);        // 加上的1是'\0'的一个字节_size += len;// 把另一个空间中的数据搬回来memcpy(_str + _size, tmp, memcpy_len + 1);        // 加上的1是'\0'的一个字节delete[] tmp;tmp = nullptr;_size += memcpy_len;
}

2.9 erase

/* 从pos位置开始删除len个字符 */
string& erase(size_t pos, size_t len)
{assert(pos < _size);if (len >= _size - pos)        // pos后面的都被删了{_str[pos] = '\0';_size = pos;}else // 删除后pos后面还有数据存留{size_t i = pos + len;while (i <= _size)        // 移动数据{_str[i - len] = _str[i];++i;}_size -= len;}return *this;
}

2.10 find

/* 从pos下标位置开始查找字符的下标位置 */
size_t find(const char ch, size_t pos = 0)
{for (size_t i = pos; i < _size; ++i){if (_str[i] == ch){return i;        // 找到返回下标}}return npos;        // 没找到返回-1(无符号的-1,size_t的最大值)
}
/* 从pos下标位置开始查找字符串的下标位置 */
size_t find(const char* str, size_t pos = 0)
{char* p = strstr(_str, str);if (p == NULL){return npos;}else{return p - _str;        // p - _str刚好是中间相差的元素个数,即 p 指向元素的下标}
}

2.11 关系运算符重载

/* 字符串比较大小 */
bool operator<(const string& s)
{int ret = strcmp(_str, s._str);return ret < 0;
}bool operator==(const string& s)
{int ret = strcmp(_str, s._str);return ret == 0;
}bool operator<=(const string& s)
{return *this < s || *this == s;        // 尽量提高对代码的复用度,方便后续修改
}bool operator>(const string& s)
{return !(*this <= s);
}bool operator>=(const string& s)
{return !(*this < s);
}bool operator!=(const string& s)
{return !(*this == s);
}

2.12 输入输出重载

/* 输入重载 */
istream& operator>>(istream& in, string& s)
{while (1){char ch;//in >> ch;        不能使用>>,因为ch是一个char类型的变量,>>默认无法接收' '和'\n'//                        导致下面的if判断无法成功,陷入死循环ch = in.get();        // get函数可以直接获取字符,不会跳过if (ch == ' ' || ch == '\n')        // getline()函数和>>重载唯一的区别就是getline这里的判断没有ch == ' '{break;}else{s += ch;}}return in;
}/* 输出重载 */
ostream& operator<<(ostream& out, const string& s)
{for (size_t i = 0; i < s.size(); ++i){cout << s[i];}return out;
}

2.13 模拟实现的string功能测试代码

// 遍历与迭代器test
void test_string1()
{string s1;string s2("hello");cout << s1 << endl;cout << s2 << endl;cout << s1.c_str() << endl;cout << s2.c_str() << endl;// 三种遍历方式// 1.[]for (size_t i = 0; i < s2.size(); ++i){s2[i] += 1;cout << s2[i] << " ";}cout << endl;// 2.迭代器string::iterator it2 = s2.begin();while (it2 != s2.end()){*it2 -= 1;cout << *it2 << " ";++it2;}cout << endl;// 3.范围for// 范围for是由迭代器支持的,也就是说这段代码最终会被编译器替换成迭代器// 想要支持范围for,需要先支持 iterator begin() end()for (auto e : s2){cout << e << " ";}cout << endl;
}// pushback&insert_test
void test_string2()
{string s1("hello");s1.push_back(' ');s1.push_back('w');// char* arr = {0};// strcpy(arr, s1.c_str());cout << s1.c_str() << endl;// cout << arr << endl;s1.append("orld");cout << s1 << endl;string s2;s2 += "abcd";cout << s2 << endl;s2 += 'e';cout << s2 << endl;s2.insert(1, "XYZ");cout << s2 << endl;s2.insert(1, "XYZ");cout << s2 << endl;}// resize_test
void test_string3()
{string s1("hello");s1.resize(1);cout << s1 << endl;s1.resize(11,'x');cout << s1.size() << endl;cout << s1 << endl;//for (int i = 0; i < s1.size(); ++i)//{//        cout << (int)s1[i] << endl;//}
}// erase_test
void test_string4()        
{string s1("hello world");s1.erase(2, 3);cout << s1 << endl;
}// find_test
void test_string5()
{string s1("abcdefghijklmn");cout << s1.find('c') << endl;cout << s1.find("defg") << endl;cout << s1.find("asdgf") << endl;
}// cin >> s 和 cout << s
void test_string6()
{string s1;cin >> s1;cout << s1;
}

3. 深浅拷贝问题(传统vs现代)

C++ 的一个常见面试提示让你实现一个 string 类。

限于时间,不可能要求具备 std::string 的功能,但至少要求能正确管理资源,也就是完成 默认构造 + 析构 + 拷贝 + operator=()

  下面是默认构造+析构的简单代码:

class string
{
public:string(const char* str = ""):_str(new char[strlen(str) + 1]){strcpy(_str, str);}~string(){delete[] _str;_str = nullptr;}size_t size(){return strlen(_str);}char& operator[](size_t i){return _str[i];}private:char* _str;
};

3.1 传统写法(深拷贝)

手动分配新内存并复制数据。

// 拷贝构造函数(传统写法)
string(const string& s): _str(new char[strlen(s._str) + 1])
{strcpy(_str, s._str);
}// 赋值运算符重载(传统写法)
string& operator=(const string& s) {if (this != &s) { // 1. 防止自我赋值char* tmp = new char[strlen(s._str) + 1]; // 2. 分配新空间strcpy(tmp, s._str);                      // 3. 拷贝数据delete[] _str;                            // 4. 释放旧空间_str = tmp;                               // 5. 指向新空间}return *this; // 6. 返回自身引用以支持连续赋值
}

关键点:

  • 自我赋值判断:if (this != &s) 至关重要,否则 delete[] _str 会先释放自身资源,导致后续操作出错。

  • 异常安全:先分配新空间和拷贝数据,成功后再释放旧空间。如果 new 失败抛出异常,旧数据依然完好。

3.2 现代写法

利用“拷贝-交换” ,更简洁且异常安全。

// 深拷贝 - 现代写法(更简洁)
// 拷贝构造
string(const string& s):_str(nullptr)
{string tmp(s._str);swap(_str, tmp._str);        // tmp._str变成nullptr,析构时也不会出错
}// 赋值
string& operator=(const string& s)
{if (this != &s){string tmp(s);swap(_str, tmp._str);        // tmp是一个临时局部变量,出函数时就会析构,把_str的值给tmp._str,tmp析构会自动释放原_str的空间}return *this;
}// 赋值更简洁的写法
// 这里最巧的是用了传值操作,相当于一次拷贝构造,此时的s就是我想要的东西
// 直接把_str和s._str一换,大功告成
string& operator=(string s)
{swap(_str, s._str);return *this;
}

关键点:

  • 巧妙利用传值:operator=(string s) 的参数 s 是通过拷贝构造生成的实参副本。函数体内只需交换 thiss 的资源。

  • 自动管理:函数结束时,形参 s 析构,自动释放了 this 对象原来的资源。代码极其简洁且安全。

  • 自我赋值:现代写法天然处理了自我赋值。如果是 s1 = s1,传参时 ss1 的副本,交换后,副本 s 带着 s1 原来的资源被析构,s1 的资源没变。

3.3 支持增删查改的string的拷贝构造和赋值

同样的,复杂一点的string同样可以使用现代写法来写拷贝构造和赋值函数。

传统写法:

/* 拷贝构造函数 */
string(const string& s)
{_size = s._size;_capacity = _size;_str = new char[_capacity + 1];        // 多加一个空间给'\0'strcpy(_str, s._str);
}/* 赋值重载函数 */
string& operator=(const string& s)
{if (this != &s){// 开辟一块新的空间,拷贝数据过去char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);// 释放原来的空间防止内存泄露,指向新空间delete[] _str;_str = tmp;}return *this;
}
string& operator=(const char* str)
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];        // 多加一个空间给'\0'strcpy(_str, str);return *this;
}

现代写法:

void swap(string& s)
{// ::的意思是我调用的不是这里的这个swap,而是全局域的swap::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}/* 拷贝构造现代写法 */
string(const string& s):_str(nullptr), _size(0), _capacity(0)
{string tmp(s._str);this->swap(tmp);
}/* 赋值现代写法 */
string& operator=(string s)
{this->swap(s);return *this;
}

4. 扩展阅

C++面试中string类的一种正确写法 | 酷 壳 - CoolShell

STL 的string类怎么啦?_stl 字符串-CSDN博客


文章转载自:

http://MnMOYRTy.wsnfh.cn
http://pq2YXCun.wsnfh.cn
http://iVmfeWZj.wsnfh.cn
http://wX4mDwGN.wsnfh.cn
http://X2wdM3lq.wsnfh.cn
http://vxDNoeKa.wsnfh.cn
http://ndxZt51t.wsnfh.cn
http://bonW7ZHD.wsnfh.cn
http://qgUsVJz1.wsnfh.cn
http://mnR1C0CS.wsnfh.cn
http://kd4nlzLH.wsnfh.cn
http://9XcLAhNw.wsnfh.cn
http://VQdWxUkx.wsnfh.cn
http://oIbqxsy6.wsnfh.cn
http://zfROden2.wsnfh.cn
http://VtDZxD4m.wsnfh.cn
http://aHlxTCOK.wsnfh.cn
http://WAu8JTlv.wsnfh.cn
http://T0lvmRES.wsnfh.cn
http://dg85sFEG.wsnfh.cn
http://ngEyKSqF.wsnfh.cn
http://qHdISAHO.wsnfh.cn
http://YqkKaJfK.wsnfh.cn
http://tG8JzW5n.wsnfh.cn
http://ZR2tx4oh.wsnfh.cn
http://yID9b3XB.wsnfh.cn
http://4e1U57Tz.wsnfh.cn
http://OF1409Ef.wsnfh.cn
http://KXegARC0.wsnfh.cn
http://UPWB8NF4.wsnfh.cn
http://www.dtcms.com/a/365118.html

相关文章:

  • maven scope=provided || optional=true会打包到jar文件中吗?
  • 资产管理还靠Excel?深度体验系统如何让企业高效数字化升级!
  • 机器学习从入门到精通 - 机器学习调参终极手册:网格搜索、贝叶斯优化实战
  • CVE-2025-6507(CVSS 9.8):H2O-3严重漏洞威胁机器学习安全
  • net9 aspose.cell 自定义公式AbstractCalculationEngine,带超链接excel转html后背景色丢失
  • 原创未发表!POD-PINN本征正交分解结合物理信息神经网络多变量回归预测模型,Matlab实现
  • LightDock:高效蛋白质-DNA对接框架
  • 小白成长之路-develops -jenkins部署lnmp平台
  • GPT在嵌入式代码设计与硬件PCB设计中的具体应用
  • Git或TortoiseGit的小BUG(可解决):空库报错Could not get hash of ““
  • Android Handler 消息循环机制
  • Python基础(⑨Celery 分布式任务队列)
  • 【计算机科学与应用】基于FME的自动化数据库建设方法及应用实践
  • 产线自动化效率上不去?打破设备和平台的“数据孤岛”是关键!
  • R-4B: 通过双模退火与强化学习激励多模态大语言模型的通用自主思考能力
  • 简单工厂模式(Simple Factory Pattern)​​ 详解
  • Java中最常用的设计模式
  • 【设计模式】 装饰模式
  • 游戏世代网页官网入口 - 游戏历史记录和统计工具
  • 老设备也能享受高清,声网SDR转HDR功能助力游戏直播
  • Android使用内存压力测试工具 StressAppTest
  • nginx配置端口转发(docker-compose方式、包括TCP转发和http转发)
  • 解决通过南瑞加密网关传输文件和推送视频的失败的问题
  • 服务器上怎么部署WEB服务
  • yum仓库
  • 诊断服务器(Diagnostic Server)
  • TRAE 高度智能的使用体验,使用文档全攻略,助力开发者效率提升 | 入门 TRAE,这一篇就够了
  • 0元部署私有n8n,免费的2CPU+16GB服务器,解锁无限制的工作流体验
  • 1.Linux:命令提示符,history和常用快捷键
  • WPF外部打开html文件