《告别 “会用不会讲”:C++ string 底层原理拆解 + 手撕实现,面试 / 开发都适用》

《告别 “会用不会讲”:C++ string 底层原理拆解 + 手撕实现,面试 / 开发都适用》

🔥草莓熊Lotso:个人主页

❄️个人专栏:《C++知识分享》《Linux 入门到实践:零基础也能懂》

生活是默默的坚持,毅力是永久的享受。


 🎬博主简介:


目录

前言:

 一. 别再停留在 “会用”!深挖string底层让你真正懂字符串

1.1 只会调用string接口的痛点

1.2 探究string的价值

二.  0基础手撕:从0搭建string核心底层逻辑(附实现代码)

2.1 底层构造逻辑:string类的成变量与构造逻辑

2.1.1  构造与析构:对象的 “创建” 与 “销毁”

2.1.2 c_str与辅助接口:兼容C风格

2.1.3 容量管理:resize和reserve的协同使用

2.1.4 代码测试:验证当前模块功能

2.2 迭代器与下标:string遍历的"两大高效工具"

2.2.1 迭代器的基本框架与实现

2.2.2 operator[] 的底层逻辑与实现

2.2.3 代码测试:验证当前模块功能

2.3 字符串修改:push_back,append,insert与+=的实现

2.3.1 尾插单个字符:push_back的实现

2.3.2 追加字符串:append的实现

2.3.3 任意位置插入:insert的实现(插入字符/字符串)

2.3.4 运算符重载:+=实现字符 / 字符串追加

2.3.5 代码测试:验证当前模块功能

2.4 字符串删减与截取:erase,clear与substr的实现

2.4.1 任意位置删除:erase的实现(删字符/删区间)

2.4.2 清空字符串:clear的实现

2.4.3 截取子串:substr的实现

2.4.4 代码测试:验证当前模块功能

2.5 字符串查找:find的实现(找字符/子串)

2.5.1 查找单个字符:find(char)的实现

2.5.2 查找子串:find(const char*)的实现

2.5.3 代码测试:验证当前模块功能

2.6 字符串的比较和输入输出:operator<<,operator>>,getline与比较运算符的实现

2.6.1 字符串比较:operator==,operator< 等一系列比运算符的实现

2.6.2 输入输出:operator<<和operator>>与getline的实现

2.6.3 代码测试:验证当前模块功能

结尾:


前言:

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

 一. 别再停留在 “会用”!深挖string底层让你真正懂字符串

1.1 只会调用string接口的痛点

  • 在我们日常写代码时,多数人对 string 的认知停留在 "调用接口",但是面试官在面试时有时会让你手动实现一个string类,很多人就傻掉了。还有就是在把 string 对象当参数传进函数,返回后程序竟然直接崩溃,没有意识到其实是因为 浅拷贝 出现的内存冲突问题。

1.2 探究string的价值

  • 搞懂 string 的底层,远远不止应对面试那么简单,更是在日常开发中提高效率的关键,了解了 reserve 预分配容量的原理,就能降低扩容的次数;理解深拷贝的逻辑,就能避免传参,赋值时的内存错误。以及为何 string 可以使用的有三种 swap 函数。最为重要的是,string的底层逻辑是C++容器设计的”缩影“---吃透它,再学 vector, list 等容器会轻松很多。

声明:本篇博客会将最终实现代码的gitee链接放在底下;在讲解期间会附上当前部分代码并注明是那个文件,大家可以自己跟着实现一下,但是不一定会是完整的可运行代码,中间涉及命名空间是为了区分和库里的string,大家跟着一步步往对应文件里加就可以了。


二.  0基础手撕:从0搭建string核心底层逻辑(附实现代码)

2.1 底层构造逻辑:string类的成变量与构造逻辑

string 的底层本质上是靠三个成员变量支撑,先搭建好基本类框架,再逐步实现功能,新手也能一步步自己手撕出一个基本的string类。

  • string.h:
#pragma once #include<iostream> #include<assert.h> #include<algorithm> #include<string.h> using namespace std; namespace Lotso { class string { public: string(const char*); 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;//这个是不行的 }; };

2.1.1  构造与析构:对象的 “创建” 与 “销毁”

  • 构造函数负责为 string 对象分配初始内存,初始化状态;析构函数则在对象生命周期结束时,回收动态分配的内存,避免泄漏。同时,拷贝构造和赋值重载要实现深拷贝,确保多个对象间内存的独立,这个在之前类和对象中讲过,这里也会把链接放上,大家如果不理解这里深浅拷贝区别的一定要去看看,当然在后续的创作中博主也会再详细解析一下这个问题的。
参考往期博客:《吃透 C++ 类和对象(中):拷贝构造函数与赋值运算符重载深度解析》-ZEEKLOG博客

代码演示:(注意看注释)

  • string.h:
public: string(const char*); string(const string& s); string& operator=(const string& s); ~string();
  • string.cpp:
 //构造 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; }

--这里需要注意,拷贝构造和赋值运算符重载都采用了深拷贝的方式,即新对象会分配独立的内存空间来存储字符串内容,而不是简单的直接复制指针,这样可以避免多个对象共享同一块内存导致的"重复释放"等问题。

2.1.2 c_str与辅助接口:兼容C风格

c_str 函数的作用是返回string对象内部存储的C风格字符串(以'\0'结束),方便与C语言的字符串处理进行交互。

  • string.h:
public: // 返回C风格字符串,方便兼容C语言接口 const char* c_str() const { return _str; } // 获取有效字符数 size_t size() const { return _size; } // 获取容量 size_t capacity() const { return _capacity; }

2.1.3 容量管理:resize和reserve的协同使用

reserve 用于提前预留内存空间,避免频繁扩容;resize 则用于调整字符串的有效长度,在需要时还会调用 reserve 进行扩容,还可以指定填充字符。

  • string.h:
public: void resize(size_t n, char c = '\0'); void reserve(size_t n);
  • string.cpp:
 // 预留内存空间,只改变容量,不改变有效字符数 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';

2.1.4 代码测试:验证当前模块功能

  • test.cpp:
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,该模块功能可以正常使用。

2.2 迭代器与下标:string遍历的"两大高效工具"

2.2.1 迭代器的基本框架与实现

迭代器是遍历容器元素的抽象机制,对于string,可以通过封装指针来实现简单迭代器,结合下标访问可以覆盖不同遍历场景。

  • string.h:
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'所在的位置)的指针,这样就可以利用指针的算术运算和解引用操作来实现迭代器的功能。

2.2.2 operator[] 的底层逻辑与实现

下标访问是string最常用的操作之一,通过重载operator[ ],可以像访问数组一样操作string的字符,底层本质是对 _str 指针的索引访问,同时也需要确保访问不会越界(这个可以加断言)

  • string.h:
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[ ]支持两种核心场景:读取字符和修改字符,配合循环可实现字符串遍历,比迭代器更直观

关键说明:

  • 两个版本的重载:非const版本返回char&,支持修改字符(如s[1]='i');const版本返回const char&,仅允许读取(用于const对象)。
  • 越界检查:通过assert(pos<=_size)在调试阶段拦截越界访问, Release 模式下断言会失效,若需严格检查可改为抛异常。
  • 与迭代器的对比:operator[ ]更适合已知索引的场景(如随机访问第i个字符),迭代器更适合范围遍历,两者底层都是通过指针访问内存,效率一致。

2.2.3 代码测试:验证当前模块功能

  •  test.cpp:
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,该模块功能可以正常使用。

2.3 字符串修改:push_back,append,insert与+=的实现

  • 字符串的修改操作是string的核心功能,push_back用于尾插单个字符,append用于追加字符串,insert支持指定位置插入,但是三者的底层实现都需要处理内存扩容和数据迁移,确保高效的操作

2.3.1 尾插单个字符:push_back的实现

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'; }

关键逻辑

  • 扩容策略采用 “2 倍增长”(空串特殊处理为 1),平衡内存利用率和扩容次数;
  • 每次插入后强制补'\0',确保C_str()返回的字符串始终有效

2.3.2 追加字符串:append的实现

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; }

关键逻辑:

  • 直接复用reserve和strcpy,减少代码冗余;
  • 批量追加比循环调用push_back更高效(避免多次扩容)。
  • 扩容方案合理,避免频繁扩容。

2.3.3 任意位置插入:insert的实现(插入字符/字符串)

insert支持在指定位置插入单个字符或者字符串,核心是"先挪到原有字符,再插入新内容",需要特别处理扩容和内存重叠问题。

  • string.h:
public: void insert(size_t pos, char ch); void insert(size_t pos, const char* str);
  • string.cpp:
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; }

--这里都有两种挪动数据的方式,大家可以按照自己的习惯来选择

2.3.4 运算符重载:+=实现字符 / 字符串追加

+=是push_back和append的"语法糖",支持追加单个字符或者字符串,底层直接复用已有函数逻辑,简化代码的书写。

  • string.h:
public: string& operator+=(char ch) { push_back(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; }

优势

  • +=本质是对push_back和append的封装,避免重复编写扩容和字符拷贝逻辑;
  • 返回*this(对象引用)是实现链式操作的核心,确保每次调用后仍能继续操作当前对象;
  • 与append相比,+=更适合简单场景,代码可读性更高,两者底层效率一致。

2.3.5 代码测试:验证当前模块功能

  • test.cpp:
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,该模块功能可以正常使用。

2.4 字符串删减与截取:erase,clear与substr的实现

  • 字符串的删减(erase,clear)截取(substr)是高频操作。底层实现需处理字符挪动,内存状态的重置或者是子串拷贝,确保字符串的完整和正确性

2.4.1 任意位置删除:erase的实现(删字符/删区间)

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; } }

2.4.2 清空字符串:clear的实现

clear用于快速清空所有有效字符,底层无需释放内存(保留容量,便于后续复用),仅需重置_size和结束符

  • string.h:
public: void clear() { _str[0] = '\0'; _size = 0; }

优势:

  • 相比erase更加高效,仅修改状态即可。
  • 保留原有容量,后续插入字符可以避免重新扩容。

2.4.3 截取子串:substr的实现

substr用于指定位置截取连续n个字符,返回一个新的string对象,底层拷贝目标子串到新内存

  • string.h:
public: string substr(size_t pos=0, size_t len=npos);
  • string.cpp:
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; }

2.4.4 代码测试:验证当前模块功能

  • test.cpp:
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,该模块功能可以正常使用。

2.5 字符串查找:find的实现(找字符/子串)

  • find是字符串查找的核心接口,支持从指定位置开始查找单个字符或子串,返回首次出现的位置(未找到就返回npos),底层通过遍历比对实现,逻辑清晰使用场景广。

2.5.1 查找单个字符:find(char)的实现

查找单个字符时,从指定起始位置遍历字符串,逐个比对字符,找到则返回位置,遍历结束仍未找到则返回npos。

  • string.h:
public: // 返回ch在string中第一次出现的位置 size_t find(char ch, size_t pos = 0) const;
  • string.cpp:
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; }

关键逻辑:

  • 起始位置pos默认从0开始(整个字符串查找),也可以指定位置开始。
  • 遍历范围限制在[pos,_size),避免越界。

2.5.2 查找子串:find(const char*)的实现

查找子串时,需从起始位置开始,逐个匹配子串的每个字符,全部匹配则返回起始位置,否则继续向后移动比对,直到主串剩余长度不足子串长度为止。

  • string.h:
public: // 返回子串str在string中第一次出现的位置 size_t find(const char* str, size_t pos = 0) const;
  • string.cpp:
size_t string::find(const char* str, size_t pos) const { assert(pos < _size); //大家也可以看看一个算法,我这里挂上链接 //https://www.bilibili.com/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; } }

2.5.3 代码测试:验证当前模块功能

  • test.cpp:
namespace Lotso { void test_string7() { string url = "https://legacy.cplusplus.com/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,该模块功能可以正常使用。

2.6 字符串的比较和输入输出:operator<<,operator>>,getline与比较运算符的实现

字符串的比较(大小关系、相等性)和输入输出是基础且高频的操作。通过重载比较运算符,实现字符串大小判断,结合流插入 / 提取运算符,可让自定义的string类完全适配 C++ 的操作习惯。

2.6.1 字符串比较:operator==,operator< 等一系列比运算符的实现

  • string.h:
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;
  • string.cpp:
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); }

关键逻辑:

  • 字典序规则:如同查字典,先比第一个字符,不同则直接判断;若相同则比第二个,以此类推;若前 n 个字符全相同,长度短的字符串更小(如 "app" < "apple");
  • 复用设计:仅需实现==和<,其他运算符通过逻辑反转或交换参数推导,避免重复编写比对逻辑,减少出错概率。

2.6.2 输入输出:operator<<和operator>>与getline的实现

我们前面都是使用c_str进行打印观察的,这里还是实现一下流插入和流提取。

  • string.h:
//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');
  • string.cpp:
 //流插入(输出):将字符串内容写入输出流 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; }

输入输出细节:

  • operator>> 会自动跳过前导空白,且遇到空白字符停止,适合读取单词;
  • getline不跳过前导空白,会读取包括空格在内的所有字符,直到遇到delim(默认换行符),适合读取整行文本;

2.6.3 代码测试:验证当前模块功能

  • test.cpp:
#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; }

--这里我测试是没问题的,大家可以自己试试,涉及输入我就不展示我的了

说明:

  • 比较运算符完全遵循字典序,结果符合预期;
  • operator>>正确读取两个单词(以空格为分隔),getline正确读取包含空格的整行内容;
  • cin.ignore()用于清楚未读取的换行符,避免getline直接读取空行。

结尾:

往期回顾:

《从崩溃到精通:C++ 内存管理避坑指南,详解自定义类型 new/delete 调用构造 / 析构的关键逻辑》

别再用函数重载堆代码了!C++ 模板初阶教程:原理 + 实例 + 避坑,新手也能秒懂

C++ 开发者必看!STL 库 + 字符编码一篇通,告别乱码与重复造轮子

C++ string 类使用超全攻略:从入门到高效避坑,日常开发直接使用

结语:当你手撕string类后会发现,它本质是 “动态数组 + 内存管理”。扩容倍数、深拷贝等设计,都是效率与安全的权衡。这些逻辑不仅能避坑,更是学容器的通用思路。评论区可交流,后续也会分享更多容器实现。技术学习,“懂原理” 才是底气。

✨把这些内容吃透超牛的!放松下吧✨
ʕ˘ᴥ˘ʔ
づきらど


Read more

堪称全网最详细的前端面试八股文,面试必备(附答案)

面试官翻开你的简历时,已经在心里问出了这三个问题,而大多数人倒在了第二个。 作为面试过近200名前端工程师的技术负责人,我见过太多候选人带着漂亮的简历走进会议室——Vue/React全家桶倒背如流、项目经历写得满满当当、算法题刷了成百上千道。 可当我开始问「为什么选择这个架构方案」、「如果让你重新设计这个组件会怎么做」、「这个技术决策背后的业务逻辑是什么」 时,超过60% 的候选人都会出现短暂的沉默。 前端面试早已不是「背API就能过」的时代了。今天的面试官想看到的,是框架背后的设计思维、是业务场景下的技术决策逻辑、是代码之外的工程化素养。 这篇文章将彻底拆解前端面试中的核心八股文,但不止于标准答案——我会带你还原每一个技术问题背后的真实考察意图,并附上能让面试官眼前一亮的深度解析。 全文目录: 1.JavaScript面试题(323题) 2.CSS面试题(61题) 3.HTML面试题(57题) 4.React面试题(83题) 5.Vue面试题(80题) 5.算法面试题(19题) 7.计算机网络(71题) 8.

By Ne0inhk

前端如何渲染 Markdown 格式:从基础到实战全指南

在前端开发中,我们常需要将 Markdown 文本(如接口文档、博客内容、用户评论)渲染成美观的 HTML 页面。不同于纯文本展示,Markdown 渲染需要借助专门的库解析语法规则,再结合样式实现可视化。本文将聚焦 “如何在前端页面中渲染 Markdown 内容”,从主流库选型到实战案例,带你快速掌握核心方法。 一、前端渲染 Markdown 的核心逻辑 Markdown 本质是 “轻量级标记语言”,无法直接被浏览器识别。前端渲染的核心流程是: 1. 解析:通过库将 Markdown 文本(如 # 标题)转换为 HTML 字符串(如 <h1>标题</h1>); 2. 渲染:将解析后的

By Ne0inhk

军工科研平台如何用WebUploader+PHP实现实验数据的分片加密续传?

前端老哥的“懒人”大文件上传方案(Vue3+原生JS) 兄弟们!我是辽宁一名“头发没秃但代码量秃”的前端程序员,最近接了个外包活——给客户做文件管理系统,核心需求就仨字儿:“稳、省、兼容”!客户拍着桌子说:“20G大文件、文件夹上传下载、加密、断点续传,预算100块,你看着办!” 我揉着太阳穴想:“行吧,谁让我爱交朋友呢?今天把这系统的‘压箱底’代码扒给你,再送你份‘保姆级’文档,保证你直接交给客户,收钱不慌!” 一、需求拆解(客户的“魔鬼”要求,我用“懒人”方案搞定) 先给大伙儿捋捋客户的“奇葩”需求(其实是行业真实痛点): * 大文件上传:20G!比我家冰箱还沉(我家冰箱100斤)。 * 文件夹上传下载:

By Ne0inhk
独立开发者的Web游戏探索之路

独立开发者的Web游戏探索之路

下面这两个网站都是我在业余时间独立开发和持续迭代的 Web 游戏项目,更多是出于个人兴趣 + 技术实践,同时也希望验证轻量级网页游戏在用户参与度和 SEO 方面的潜力。 GuessAnswer.com 👉 https://guessanswer.com GuessAnswer 是一个基于问答 / 猜测机制的轻度游戏平台,核心玩法围绕「快速思考 + 即时反馈」。用户通过选择或输入答案参与互动,系统会实时返回结果,并记录相关数据。 从技术角度看,这类玩法非常依赖: * 前端交互体验 * 实时状态更新 * 用户反馈速度(延迟会明显影响体验) PlayBricksBreaker.com 👉 https://playbricksbreaker.com PlayBricksBreaker 是一个经典的打砖块(Bricks Breaker)网页游戏,重点在于还原熟悉的游戏手感,同时保证在浏览器环境下的流畅度和稳定性。 该项目涉及的核心技术点包括: * 碰撞检测逻辑 * 游戏状态管理 * 动画渲染与帧率控制 技术架构与实现细节 前端技术选型 两个项目都以 W

By Ne0inhk