【C++学习笔记】【基础】4.string类(2)——模拟实现

🍕阿i索 个人主页
《C语言专栏》 《C++专栏》
《数据结构专栏》 《LaTeX专栏》
《软件配置问题》 《Linux 专栏》
待更新...

前言.

本篇简单整理string类的模拟实现。


一、string底层模拟实现细节注意

模块一:类与对象 & 六大默认成员函数

1. 类成员变量初始化顺序规则

对应代码与原注释
复制代码
private:
	// 成员变量声明顺序
	char* _str;
	size_t _size;
	size_t _capacity;

// 如果将初始化列表后两个成员变量用_size初始化(×,因为初始化列表初始化顺序是按照定义的顺序)
// 如果调整定义的顺序,后期增加变量可能改变顺序,使代码可维护性不高

初始化铁则 :C++ 类成员变量的初始化顺序只由类内的声明顺序决定 ,和构造函数初始化列表的书写顺序无关。

若错误编写如下初始化列表:

复制代码
// 错误写法:_size 先初始化,再用 _size 初始化 _str
string(const char* str)
    : _size(strlen(str)), _str(new char[_size + 1])
{}

按照声明顺序,_str 会优先初始化,但此时 _size 还未赋值,属于使用未初始化变量,触发未定义行为。

解决: 仅在初始化列表初始化 _size_str 内存开辟和_capacity 赋值放到函数体内部,规避顺序带来的隐患,提升代码可维护性。

2. 缺省参数的语法规则与合并构造函数

复制代码
// 头文件:函数声明,书写缺省参数
string(const char* str = "");
// 源文件:函数定义,禁止重复写缺省参数
string::string(const char* str)
	: _size(strlen(str))
{
	_str = new char[_size + 1];
	_capacity = _size;
	memcpy(_str, str, _size + 1);
}

缺省参数只能在函数声明处定义 ,函数实现(定义)中重复书写缺省参数会编译报错。

利用缺省参数 str = "":默认构造函数带参构造函数合并为一个,减少代码冗余:

  • string s1;:使用缺省值 "",创建空字符串对象;
  • string s2("hello");:传入自定义字符串,正常初始化。

空字符串 "" 调用 strlen 结果为 0,保证空对象内存开辟逻辑正常执行。

3. const 修饰成员函数(常量成员函数)

复制代码
// 常量成员函数:函数尾部加 const
size_t size() const
{
	return _size;
}
const char* c_str() const
{
	return _str;
}

// []运算符 const 重载
const char& operator[](size_t pos)const
{
	assert(pos < _size);
	return _str[pos];
}

成员函数括号后方的 const 表示:该函数承诺不会修改类的任何成员变量

调用权限规则:

  • 普通对象:可以调用普通成员函数、常量成员函数;
  • const 修饰的对象:仅能调用常量成员函数,调用普通成员函数会编译报错。

重载设计思想operator[]begin()end() 这类接口,同时提供普通版本 + const 版本,是 C++ 容器的标准写法:

  • 普通版本:供普通对象使用,支持读写;
  • const 版本:供 const 对象使用,保证只读,符合封装性要求。

4. 下标运算符 operator[] 返回引用的设计

复制代码
// 普通版本:返回 char& 引用,支持修改
char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}
// const版本:返回 const char& 常量引用,禁止修改
const char& operator[](size_t pos)const
{
	assert(pos < _size);
	return _str[pos];
}

返回引用的原因

  • 若返回普通值(值传递):只是返回字符副本,无法执行 s[0] = 'a' 这类修改操作;
  • 若返回引用:直接返回数组元素本身,既可以读取,也可以作为左值修改原数据。

常量引用的作用:

  • const char& 限制返回值不可修改,配合常量成员函数,保证 const string 对象完全只读。

5. 析构函数细节与动态内存释放

复制代码
~string()
{
	delete[] _str;    // 释放动态数组
	_str = nullptr;  // 指针置空
	_size = 0;
	_capacity = 0;
}

动态数组释放规则:

  • 使用 new char[] 开辟的动态数组 ,必须搭配 delete[] 释放;如果只用 delete,会导致数组后半部分内存泄漏、程序崩溃。

野指针规避 内存释放后,将 _str 赋值为 nullptr

  • 释放后的指针会变成野指针,置空后可避免后续误操作野指针,同时便于调试。

成员变量清零 主动将 _size_capacity 置 0 是良好编码习惯,防止对象析构后被意外调用,减少 bug。


模块二:静态成员变量

静态常量 static const size_t npos

复制代码
// 类内:静态常量 声明
const static size_t npos;
// 类外全局域:静态常量 定义+初始化
const size_t string::npos = -1;

//const static整型可以声明定义一体

静态成员通用特性

  • 静态成员变量不属于某个对象 ,属于整个类,所有对象共享同一份数据;

常规静态成员:

  • 类内仅做声明 ,必须在类外全局作用域 完成定义和初始化,不能在构造函数中初始化。

特殊语法:const static 整型变量 C++ 语法特例:

  • const static 修饰的整型 / 枚举类型变量,可以直接在类内完成初始化(声明定义合一)。

size_t 无符号类型特性:

  • size_t无符号整型 ,取值范围 [0, 数据类型最大值],永远无法表示负数。 当 -1 赋值给无符号变量时,会自动转换为该类型的最大值,以此标记「查找、截取操作失败」。

模块三:迭代器体系(原生指针模拟迭代器)

1. 迭代器本质与 typedef 类型别名

复制代码
// 类型别名:用原生指针模拟迭代器
typedef char* iterator;               // 普通迭代器
typedef const char* const_iterator;   // 常量迭代器

// 迭代器起止接口
iterator begin()
{
	return _str;
}
iterator end()
{
	return _str + _size;
}

const_iterator begin() const
{
	return _str;
}
const_iterator end() const
{
	return _str + _size;
}

迭代器本质 string 底层是连续堆字符数组 ,因此直接使用原生指针模拟迭代器,是最简单、最高效的迭代器实现方式。

typedef 作用:

  • 为指针类型起别名 iterator/const_iterator,对外屏蔽底层实现:使用者只需要关心迭代器接口,无需知道底层是指针还是类对象,符合封装思想。

容器迭代器通用规则(左闭右开区间) C++ 所有标准容器迭代器都遵循 [begin, end) 规则:

  • begin():指向第一个有效元素
  • end():指向最后一个有效元素的下一个位置,不代表有效数据。

2. 范围 for 循环底层原理

复制代码
// 范围for的底层是迭代器
for (auto ch : s4)
{
	cout << ch << " ";
}

语法糖本质: 范围 for 是编译器提供的语法糖,编译阶段会自动转换为迭代器遍历,等价代码:

复制代码
auto it = s4.begin();
while (it != s4.end())
{
    auto ch = *it;
    cout << ch << " ";
    ++it;
}

使用前提:

  • 自定义类想要支持范围 for 循环,必须实现 begin()end() 两个迭代器接口,否则编译报错。

const 对象适配:

  • 当遍历 const string 对象时,编译器会自动调用 const 版本的 begin()/end(),保证只读特性。

模块四:字符串基础接口与 C 库函数结合

1. clear() 接口设计思想

复制代码
void clear()
{
	_str[0] = '\0';
	_size = 0;
}

仅清空字符串有效内容不释放堆内存、不修改 _capacity 容量

目的: 复用已开辟的堆空间,避免频繁执行 new/delete 内存操作,提升对象重复使用时的性能。

原理: C 风格字符串以 \0 作为结束标志,将数组首元素置为 \0 后,所有 C 语言字符串函数都会判定该字符串为空。

2. c_str() 接口:C/C++ 语言兼容

复制代码
const char* c_str() const
{
	return _str;
}

作用: 返回底层 C 风格字符数组指针,用于兼容 C 语言接口、老旧代码、系统 API ,实现 C/C++ 代码互通。返回值为 const char*,禁止外部代码修改底层字符数组,防止破坏类内部数据。

注意:

  • 类内部遍历(迭代器 / 范围 for):按照 _size 遍历所有有效字符,可以识别内嵌 \0
  • c_str() 返回的指针:被 C 库函数使用时,遇到第一个 \0 就终止读取,数据会被截断

3. strstr 库函数与指针运算

复制代码
size_t string::find(const char* str, size_t pos )
{
	assert(pos < _size);
	const char* ptr=strstr(_str + pos, str);
	if (ptr)
	{
		return ptr - _str; // 指针相减计算下标
	}
	else
	{
		return npos;
	}
}

strstr 函数功能:

C 标准库函数:在目标字符串中查找子串。查找成功返回子串首地址 ,查找失败返回 NULL

同数组指针减法规则:

指向同一块连续内存 的两个 char* 指针相减,结果为两个指针之间的元素个数(数组下标差值),这是数组专属的指针运算特性。

偏移查找: 通过 _str + pos 让查找起点偏移到指定位置,简化手动循环匹配子串的代码。


模块五:运算符重载体系

1. 关系运算符:代码复用设计

复制代码
bool operator<=(const string& s) const
{
	return *this < s || *this == s;
}
bool operator>(const string& s) const
{
	return !(*this <= s);
}
bool operator>=(const string& s) const
{
	return !(*this > s);
}

复用思想:

  • 实现两个基础运算符<(小于)、==(等于),其余四个关系运算符全部基于基础运算符组合实现。

优势:

  • 减少重复代码,降低维护成本;
  • 全局比较逻辑统一,修改规则时仅需改动基础函数,不会出现逻辑不一致。

2. strcmp 字符串比较规则

复制代码
bool operator<(const string& s) const
{
	return strcmp(_str, s._str) < 0;
}
bool operator==(const string& s) const
{
	return strcmp(_str, s._str) == 0;
}

strcmp(a, b) 按照ASCII 码字典序比较两个 C 风格字符串,返回值规则:

  1. 字符串 a 字典序 < b:返回负数
  2. 字符串 a 字典序 == b:返回 0
  3. 字符串 a 字典序 > b:返回正数。 你的所有关系比较逻辑都基于该规则封装。

3. 复合赋值运算符 += 重载

复制代码
string& operator+=(const char* str)
{
	append(str);
	return *this;
}
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

函数复用: += 运算符直接调用已实现的 push_back(尾插字符)、append(追加字符串),不重复编写逻辑。

返回引用的意义: 返回 string& 引用,支持连续运算 语法,例如:s += 'a' += "bcd",符合 C++ 运算符重载规范。


模块六:命名空间、模板与函数重载

1. 自定义命名空间 asuo

复制代码
namespace asuo
{
	class string
	{
		// 类实现
	};
}

作用:命名隔离 自定义 string 放入 asuo 命名空间,避免和 C++ 标准库 std::string 发生命名冲突

使用方式:

外部使用时必须指定空间:asuo::string s;,和标准库 std::string 明确区分。

2. 模板函数 + 函数重载(swap 两套实现)

复制代码
// 1. 通用模板swap:利用函数模板实现泛型交换,适配所有数据类型
template<class T>
void swap(T& a, T& b)
{
    // 先拷贝构造临时对象,再两次赋值,共触发三次深拷贝
    // 针对string类型,会完整复制堆上字符数据,数据量大时性能很低
    T c(a); 
    a = b;
    b = c;
}

// 2. 针对自定义string类的重载swap版本
// inline:内联函数,编译器直接展开代码,减少函数调用栈开销
inline void swap(string& a, string& b)
{
    // 调用string成员swap,仅交换内部指针、大小、容量等成员变量
    // 不拷贝实际字符内容,彻底避免深拷贝,交换效率极高
    a.swap(b);
}

1. 通用模板 swap

依靠template<class T>实现泛型编程 ,一份代码可以支持任意类型交换,通用性强。 但交换string对象时,代码逻辑会执行拷贝构造 + 两次赋值 ,产生三次深拷贝,需要复制堆区所有字符、频繁操作内存,字符串越长、交换越频繁,性能损耗越严重。

2. string 专属重载 swap

  1. 重载优先级 :当参数为string类型时,普通重载函数优先级高于函数模板,编译器会优先执行该优化版本。
  2. 性能优势 :内部调用string成员swap只交换对象内部指针、长度、容量,不操作堆上字符数据,全程无深拷贝,开销极小。
  3. inline 作用 :该函数代码简短,inline建议编译器将代码原地展开,省去函数调用的栈开销,进一步提升效率。

3. 整体设计目的

之所以同时实现模板 swap 和 string 专属重载 swap,就是为了解决多次深拷贝带来的性能问题:既保留函数模板的通用能力,又通过重载为字符串做专项优化,兼顾通用性与运行效率。


模块七:输入输出流相关知识点

1. 流运算符 << / >> 设计为全局函数

复制代码
// 全局函数,非类成员
std::ostream& operator<<(std::ostream& out, const string& s);
std::istream& operator>>(std::istream& in,string& s);

不能作为成员函数的原因:

成员运算符重载要求:运算符左操作数必须是当前类对象 。 而 cout << s 的左操作数是 std::ostream(cout),不是 string,因此必须实现为全局函数

返回流引用:

返回 ostream& / istream& 流引用,支持连续流操作 ,例如:cout << s1 << s2 << endl;

2. operator>>getline 读取规则差异 & 经典坑点

复制代码
// >> 运算符:以空格、换行作为分隔符
// getline:以指定字符为结束符,默认读取整行(包含空格)
  1. operator>> 读取规则
    • 自动跳过开头的空白字符(空格、换行、制表符);
    • 读取过程中遇到空格 / 换行立即停止;
    • 适用场景:读取单个单词、无空格字符串。
  2. getline 读取规则
    • 不会跳过空白字符,完整读取所有内容(包含空格);
    • 直到遇到指定结束符(默认换行 \n)才停止读取;
    • 适用场景:读取整行文本。
  3. 经典坑点
  • 使用 cin >> s 读取数据后,输入缓冲区会残留换行符 ,后续直接调用 getline 会立刻读取到残留换行符,导致读取空行。这是 C++ 流使用中高频踩坑点。

模块八:容器设计思想与内存管理

1. reserveresize 职责划分

reserve(size_t n)

  • 仅预分配内存(扩容) ,只修改 _capacity不改变有效字符数 _size,不修改字符串内容;且不支持缩容。

resize(size_t n, char ch)

修改有效字符数 _size,分两种场景:

  • n < _size:缩容,截断多余字符,末尾补 \0
  • n > _size:先扩容,再用指定字符填充新增位置。
解读:
  1. 职责分离思想 这是 STL 容器的标准设计范式:
    • reserve:面向性能优化,提前规划内存,避免多次扩容带来的开销;
    • resize:面向内容修改,改变字符串对外展示的实际长度。
  2. 缩容规则 代码中 reserve 仅在 n > _capacity 时执行扩容,主动放弃缩容。

2. 动态数组内存布局规则

内存结构
复制代码
// _str 指向堆内存布局
[字符1][字符2]...[有效字符N][\0]
  1. 容量计算规则
  • _size:有效字符个数,不包含末尾 \0
  • _capacity:最大可存储的有效字符数,不包含末尾 \0
  • 实际堆内存总大小:_capacity + 1,多出的 1 字节专门存放字符串结束符 \0,保证兼容 C 语言字符串。

模块九:断言 assert 调试机制

复制代码
assert(pos < _size);
assert(pos <= _size);
  1. assert 本质 assert(条件表达式) 是 C 语言标准库提供的调试宏,用于校验参数、边界的合法性。
  2. 运行模式差异
    • Debug(调试)模式:表达式为 false 时,程序直接终止,并打印报错位置、错误信息,快速定位 bug;
    • Release(发布)模式:assert 宏会被编译器完全移除,不产生任何代码,不影响程序运行效率。
  3. 使用场景 多用于函数入口校验参数合法性(如下标、插入位置、查找位置),属于调试辅助手段,不用于正式的业务逻辑判断。

二、模拟实现代码

string.h文件
复制代码
#pragma once
#include<assert.h>   // 断言校验
#include<string.h>   // C语言字符串库函数
#include<algorithm>  // 算法库 max/swap
#include<iostream>   // 标准输入输出

// 自定义命名空间asuo,避免与C++标准库std::string命名冲突
namespace asuo
{
	// 模拟实现C++标准库string类
	class string
	{
	private:
		// 三大核心成员变量
		char* _str;         // 指向堆区字符数组的指针,存储字符串内容
		size_t _size;       // 有效字符个数(不包含末尾'\0')
		size_t _capacity;   // 堆空间容量(最大可存储字符数,不包含'\0')

	public:
		// 静态常量:表示查找失败的位置,无符号最大值
		const static size_t npos;

		// ===================== 构造 & 析构 & 拷贝 & 赋值(六大默认函数) =====================
		// 构造函数:带缺省参数,兼容默认构造 + 带参构造
		// 缺省参数"" 代表空字符串,自动补'\0'
		string(const char* str = "");

		// 析构函数:释放堆区内存,防止内存泄漏
		~string();

		// 拷贝构造函数:手动实现深拷贝,解决默认浅拷贝内存重复释放问题
		string(const string& s);

		// 赋值运算符重载:深拷贝赋值,支持对象之间赋值
		string& operator=(const string& s);

		// ===================== 容量管理接口 =====================
		// 调整有效字符个数:扩容/缩容,多出位置用指定字符填充
		void resize(size_t n, char ch = '\0');

		// 扩容函数:只开辟空间,不修改有效字符数,缩容不处理
		void reserve(size_t n);

		// 清空有效字符:仅将首位置'\0',不释放堆空间、不修改容量
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		// ===================== 字符串追加接口 =====================
		// 尾插单个字符
		void push_back(char ch);

		// 追加C风格字符串
		void append(const char* str);

		// 运算符重载:+= 追加C风格字符串
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

		// 运算符重载:+= 追加单个字符
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		// ===================== 基础信息获取接口 =====================
		// 获取当前有效字符个数
		size_t size() const
		{
			return _size;
		}

		// 获取C风格字符串指针(兼容C语言接口)
		const char* c_str() const
		{
			return _str;
		}

		// 成员swap:直接交换三个成员变量,避免深拷贝,提升效率
		void swap(string& s);

		// ===================== 插入 & 删除接口 =====================
		// 在pos位置插入单个字符
		void insert(size_t pos, char ch);

		// 在pos位置插入C风格字符串
		void insert(size_t pos, const char* str);

		// 从pos位置开始删除len个字符,默认删除到末尾
		void erase(size_t pos = 0, size_t len = npos);

		// ===================== 查找 & 截取接口 =====================
		// 从pos位置开始查找字符ch,返回下标;失败返回npos
		size_t find(char ch, size_t pos = 0);

		// 从pos位置开始查找子串str,返回起始下标;失败返回npos
		size_t find(const char* str, size_t pos = 0);

		// 从pos位置截取len长度子串,返回新string对象
		string substr(size_t pos = 0, size_t len = npos);

		// ===================== 关系运算符重载(大小比较) =====================
		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;

		// ===================== 迭代器实现(原生指针模拟) =====================
		typedef char* iterator;               // 普通迭代器:可读可写
		typedef const char* const_iterator;   // 常量迭代器:仅可读

		// 返回起始迭代器(指向第一个有效字符)
		iterator begin()
		{
			return _str;
		}

		// 返回末尾迭代器(指向最后一个有效字符的下一位,即'\0')
		iterator end()
		{
			return _str + _size;
		}

		// 常量对象专属起始迭代器
		const_iterator begin() const
		{
			return _str;
		}

		// 常量对象专属末尾迭代器
		const_iterator end() const
		{
			return _str + _size;
		}

		// ===================== []下标运算符重载 =====================
		// 普通对象调用:可读可写,越界断言拦截
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		// 常量对象调用:只读不可写,越界断言拦截
		const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}
	};

	// ===================== 全局运算符重载(流输入输出) =====================
	// 重载<< 输出string对象
	std::ostream& operator<<(std::ostream& out, const string& s);

	// 重载>> 读取字符串(自动忽略空格/换行,不读取空格)
	std::istream& operator>>(std::istream& in, string& s);

	// 自定义getline:读取整行,以指定字符为结束符(默认换行符)
	std::istream& getline(std::istream& in, string& s, char delim = '\n');

	// 通用模板swap:所有类型都可使用,默认会触发拷贝
	template<class T>
	void swap(T& a, T& b)
	{
		T c(a); a = b; b = c;
	}

	// 针对asuo::string的特化swap:调用成员swap,效率更高
	inline void swap(string& a, string& b)
	{
		a.swap(b);
	}
}
string.cpp文件
复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"string.h"

namespace asuo
{
	// 静态常量类外初始化:size_t npos = -1 转为无符号即最大值,表示查找失败
	const size_t string::npos = -1;

	// ===================== 成员函数实现 =====================
	// 构造函数:缺省参数仅在头文件声明处书写
	string::string(const char* str)
		: _size(strlen(str)) // 初始化列表仅调用一次strlen,优化效率
	{
		// 开辟堆空间:有效字符 + 末尾'\0'
		_str = new char[_size + 1];
		_capacity = _size;
		// 使用strcpy无法拷贝内嵌\0,改用memcpy按字节完整拷贝
		memcpy(_str, str, _size + 1);
	}

	// 析构函数:释放堆内存,置空指针、清零成员变量
	string::~string()
	{
		delete[] _str;    // 释放字符数组堆内存
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}

	// 拷贝构造函数:深拷贝(核心解决浅拷贝重复析构问题)。string s2(s1)
	string::string(const string& s)
	{
		// 为新对象单独开辟独立堆空间
		_str = new char[s._capacity + 1];
		// memcpy完整拷贝所有字节(包含内嵌\0)
		memcpy(_str, s._str, s._size + 1);
		_size = s._size;
		_capacity = s._capacity;
	}

	// 赋值运算符重载:深拷贝赋值,防止自赋值、浅拷贝。s1=s3
	string& string::operator=(const string& s)
	{
		// 自赋值判断:自己赋值给自己直接返回,无需操作
		if (this != &s)
		{
			// 1. 先开辟新空间(防止原空间释放后找不到源数据)
			char* tmp = new char[s._capacity + 1];
			memcpy(tmp, s._str, s._size + 1);
			// 2. 释放当前对象旧堆空间
			delete[] _str;
			// 3. 指针指向新空间,更新大小与容量
			_str = tmp;
			_size = s._size;
			_capacity = s._capacity;
		}
		return *this;
	}

	// 成员swap:直接交换三个成员变量,零拷贝、高效率
	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}

	// resize:调整有效字符长度
	void string::resize(size_t n, char ch)
	{
		if (n <= _size)
		{
			// 场景1:缩容,删除多余的,保留前n个,末尾补'\0'
			_size = n;
			_str[_size] = '\0';
		}
		else
		{
			// 场景2:扩容,先保证容量足够,再填充字符
			reserve(n);
			for (size_t i = _size; i < n; i++)
			{
				_str[i] = ch;
			}
			_size = n;
			_str[_size] = '\0';
		}
	}

	// reserve:扩容接口,仅扩大容量,不缩容、不修改有效字符
	void string::reserve(size_t n)
	{
		// 仅当新容量 > 当前容量时才扩容
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			memcpy(tmp, _str, _size + 1);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}

	// push_back:尾插单个字符
	void string::push_back(char ch)
	{
		// 容量已满,执行扩容:空对象扩为4,非空二倍扩容
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		// 尾插字符,更新有效长度,手动补末尾'\0'
		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';
	}

	// append:追加C风格字符串
	void string::append(const char* str)
	{
		size_t len = strlen(str);
		// 总长度超过容量,执行扩容(取二倍容量/总长度最大值)
		if (_size + len > _capacity)
		{
			reserve(std::max(_size + len, _capacity * 2));
		}
		// 追加字符串,memcpy保证内嵌\0正常拷贝
		memcpy(_str + _size, str, len);
		_str[_size + len] = '\0';
		_size += len;
	}

	// insert:在pos位置插入单个字符
	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size); // 位置合法性断言
		// 容量满则扩容
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		// 无符号数遍历优化:end从后向前挪,避免>=导致无符号越界死循环
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}
		_str[pos] = ch;
		_size++;
	}

	// insert:在pos位置插入C风格字符串
	void string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		size_t len = strlen(str);
		if (len == 0) return; // 空字符串直接返回

		// 容量不足则扩容
		if (_size + len > _capacity)
		{
			reserve(std::max(_size + len, _capacity * 2));
		}
		// 数据后移,规避无符号size_t越界问题
		size_t end = _size + len;
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];
			--end;
		}
		// 拷贝待插入字符串
		memcpy(_str + pos, str, len);
		_size += len;
		_str[_size] = '\0';
	}

	// erase:删除指定区间字符
	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size); // 起始位置合法校验
		// 场景1:删除到末尾 / 长度超出剩余字符,直接截断
		if (len == npos || len >= _size - pos)
		{
			_size = pos;
			_str[_size] = '\0';
		}
		// 场景2:删除部分字符,后续字符向前覆盖
		else
		{
			memcpy(_str + pos, _str + pos + len, _size - (pos + len) + 1);
			_size -= len;
		}
	}

	// find:查找单个字符
	size_t string::find(char ch, size_t pos)
	{
		assert(pos < _size);
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
				return i;
		}
		return npos; // 查找失败
	}

	// find:查找子串
	size_t string::find(const char* str, size_t pos)
	{
		assert(pos < _size);
		// 调用C库strstr查找子串
		const char* ptr = strstr(_str + pos, str);
		if (ptr)
		{
			return ptr - _str; // 指针差值得到下标
		}
		else
		{
			return npos;
		}
	}

	// substr:截取子串,返回新string对象
	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;
	}

	// 关系运算符实现
	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);
	}

	// ===================== 全局流函数实现 =====================
	// 重载<< 输出:借助范围for+迭代器遍历
	std::ostream& operator<<(std::ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}

	// 重载>> 输入:使用缓冲区buff[256],解决长字符串频繁扩容、无法读空格问题
	std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();
		char buff[256];
		int i = 0;
		char ch;
		ch = in.get();
		// 遇到空格/换行停止读取
		while (ch != '\n' && ch != ' ')
		{
			buff[i++] = ch;
			// 缓冲区存满,批量追加
			if (i == 255)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		// 处理缓冲区剩余字符
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}

	// 自定义getline:读取整行,以delim为结束符
	std::istream& getline(std::istream& in, string& s, char delim)
	{
		s.clear();
		char buff[256];
		int i = 0;
		char ch;
		ch = in.get();
		while (ch != delim)
		{
			buff[i++] = ch;
			if (i == 255)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
}
test.cpp测试文件
复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include"string.h"
using namespace std;

namespace asuo
{
	// 测试1:构造函数、[]重载、迭代器、范围for遍历
	void test_string1()
	{
		// 测试默认构造(空字符串)
		string s1;
		cout << "空字符串:" << s1.c_str() << endl;

		// 测试带参构造
		string s2("hello world");
		cout << "带参构造:" << s2.c_str() << endl;

		// 测试尾插字符
		s2.push_back('x');
		cout << "push_back后:" << s2.c_str() << endl;

		// 测试[]运算符(普通对象读写)
		s2[0] = 'x';
		for (size_t i = 0; i < s2.size(); i++)
		{
			s2[i]++;
		}
		cout << "[]修改后:" << s2.c_str() << endl;

		// 测试const对象 + const []重载(只读)
		const string s5("hello world");
		cout << "const对象[]遍历:";
		for (size_t i = 0; i < s5.size(); i++)
		{
			cout << s5[i] << '-';
		}
		cout << endl;

		// 隐式构造、直接构造
		string s3 = "hello world";
		string s4 = ("hello world");

		// 范围for遍历(底层依赖迭代器)
		cout << "范围for遍历:";
		for (auto ch : s4)
		{
			cout << ch << " ";
		}
		cout << endl;

		// 普通迭代器遍历 + 修改
		cout << "普通迭代器遍历修改:";
		string::iterator it4 = s4.begin();
		while (it4 != s4.end())
		{
			*it4 += 1;
			cout << *it4 << " ";
			++it4;
		}
		cout << endl;

		// const迭代器遍历(只读)
		cout << "const迭代器遍历:";
		for (auto ch : s5)
		{
			cout << ch << " ";
		}
		cout << endl;
		string::const_iterator it5 = s5.begin();
		while (it5 != s5.end())
		{
			cout << *it5 << " ";
			++it5;
		}
		cout << endl;
	}

	// 测试2:push_back、append、+= 追加接口
	void test_string2()
	{
		string s1;
		cout << "空串:" << s1.c_str() << endl;

		string s2("hello world");
		// 连续尾插字符
		s2.push_back('x');
		s2.push_back('x');
		s2.push_back('x');
		s2.push_back('x');
		cout << "连续push_back:" << s2.c_str() << endl;

		// 测试append追加字符串
		string s3("hello");
		s3.append("xxxxxxxxxxxxxx");
		cout << "append长串:" << s3.c_str() << endl;

		// 测试+= 字符 + 字符串
		string s4("hello");
		s4.append("xx");
		s4.append("xx");
		s4 += '*';
		s4 += "hello world";
		cout << "+= 混合追加:" << s4.c_str() << endl;
	}

	// 测试3:insert插入、erase删除接口
	void test_string3()
	{
		string s1("hello world");
		cout << "原串:" << s1.c_str() << endl;
		// 中间插入单个字符
		s1.insert(5, 'x');
		cout << "中间插字符:" << s1.c_str() << endl;
		// 头部插入单个字符
		s1.insert(0, 'x');
		cout << "头部插字符:" << s1.c_str() << endl;

		string s2("hello world");
		s2.insert(5, "xxx");
		cout << "中间插字符串:" << s2.c_str() << endl;
		s2.insert(0, "yyy");
		cout << "头部插字符串:" << s2.c_str() << endl;

		// 部分删除、截断删除
		s2.erase(3, 3);
		s2.erase(2);
		cout << "erase删除后:" << s2.c_str() << endl;
	}

	// 测试5:拷贝构造、赋值重载(深拷贝核心测试)
	void test_string5()
	{
		string s1("hello world");
		// 拷贝构造
		string s2(s1);
		cout << "原串s1:" << s1.c_str() << endl;
		cout << "拷贝s2:" << s2.c_str() << endl;

		// 修改s1,验证深拷贝(s2不受影响)
		s1[0] = 'x';
		cout << "修改后s1:" << s1.c_str() << endl;

		// 赋值重载
		string s3("hello worldxxxxxxxx");
		s1 = s3;
		cout << "赋值后s1:" << s1.c_str() << endl;
		cout << "源串s3:" << s3.c_str() << endl;
	}

	// 浅拷贝/深拷贝专项测试
	void test1()
	{
		string s1("hello world");
		string s2(s1);
		cout << "深拷贝测试 s1:" << s1.c_str() << endl;
		cout << "深拷贝测试 s2:" << s2.c_str() << endl;
	}

	// 测试:字符串内嵌\0(验证memcpy替代strcpy的效果)
	void test2()
	{
		string s1("hello world");
		s1 += 'x';
		s1 += '\0'; // 主动添加内嵌'\0'
		s1 += "uuu";

		// << 遍历有效字符(识别内嵌\0)
		cout << "重载<<输出(识别内嵌\\0):" << s1 << endl;
		// c_str C风格输出(遇到第一个\0终止)
		cout << "c_str输出(C风格截断):" << s1.c_str() << endl;

		// 拷贝构造验证内嵌\0正常拷贝
		string s2(s1);
		cout << "拷贝后输出:" << s1 << endl;
	}

	// 测试:resize 扩容/缩容功能
	void test3()
	{
		string s1;
		// 扩容+填充字符
		s1.resize(11, '*');
		cout << "resize扩容填充:" << s1 << endl;
		// 缩容截断
		s1.resize(10);
		cout << "resize缩容:" << s1 << endl;
		// 再次扩容
		s1.resize(20, '#');
		cout << "再次resize扩容:" << s1 << endl;
	}

	// 测试:find查找、substr截取(模拟URL解析场景)
	void test4()
	{
		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;
			}
		}
	}

	// 测试:>> 流输入、getline整行读取
	void test5()
	{
		string s1, s2("xxxxxx");
		// >> 读取(自动分割空格)
		cin >> s1 >> s2;
		cout << ">>读取s1:" << s1 << endl;
		cout << ">>读取s2:" << s2 << endl;

		// getline读取整行(包含空格)
		getline(cin, s1);
		cout << "getline读取整行:" << s1 << endl;
	}

	// 测试:swap交换接口
	void test6()
	{
		string s3("hello world"), s4("xxxxxxx");
		// 成员swap
		s3.swap(s4);
		// 全局特化swap
		swap(s3, s4);
		cout << "swap交换完成" << endl;
	}
}

int main()
{
	// 异常捕获,拦截内存/断言错误
	try
	{
		// 根据需要开启对应测试函数
		//asuo::test_string1();
		//asuo::test_string2();
		//asuo::test_string3();
		//asuo::test_string5();
		//asuo::test1();
		//asuo::test2();
		//asuo::test3();
		//asuo::test4();
		//asuo::test5();
		asuo::test6();
	}
	catch (const exception& e)
	{
		cout << "异常:" << e.what() << endl;
	}
	return 0;
}

三、模拟实现可能遇见的问题

问题 1:构造函数多次调用 strlen,效率低下

错误版本代码

复制代码
// 问题代码:原始带参构造
string(const char* str)
	:_str(new char[strlen(str)+1])  // 第1次 strlen
	, _size(strlen(str))           // 第2次 strlen
	, _capacity(strlen(str))       // 第3次 strlen
{
	strcpy(_str, str);
}

问题分析

  1. 同一字符串连续调用 3 次 strlen,重复遍历字符数组,效率低;
  2. 依赖成员变量定义顺序:如果后续调整 _size/_capacity 定义顺序,代码会出错,可维护性差。

解决思路

只在初始化列表执行一次 strlen_size 赋值,堆内存开辟、_capacity 放到函数体内部。

复制代码
// 修复后:合一构造函数(默认构造 + 带参构造)
string(const char* str = "")
	: _size(strlen(str))  // 仅调用 1 次 strlen
{
	_str = new char[_size + 1];
	_capacity = _size;
	strcpy(_str, str); // 后续又替换为 memcpy,见问题3
}

问题 2:默认浅拷贝 → 堆内存重复释放崩溃(拷贝构造)

问题根源:编译器默认拷贝构造

编译器默认生成的拷贝构造是值拷贝(浅拷贝),只复制指针地址,不新开堆内存:

复制代码
// 编译器默认生成的浅拷贝构造(伪代码,隐患代码)
string::string(const string& s)
{
	// 只拷贝指针地址,两个对象 _str 指向**同一块堆内存**
	_str = s._str;  
	_size = s._size;
	_capacity = s._capacity;
}

导致的问题

  1. 多个 string 对象共享同一块堆空间;
  2. 析构时同一堆内存被多次 delete[],程序直接崩溃。

解决思路:手动实现深拷贝

为新对象单独开辟独立堆内存,再拷贝数据,两个对象内存完全隔离。

复制代码
// 深拷贝构造函数(最终版)
string::string(const string& s)
{
	// 1. 新对象单独开辟堆空间,和原对象内存分离
	_str = new char[s._capacity + 1];
	// 2. 拷贝数据(后续替换 memcpy,见问题3)
	memcpy(_str, s._str, s._size+1);
	_size = s._size;
	_capacity = s._capacity;
}

问题 3:strcpy 无法拷贝字符串内嵌 \0

问题代码(所有使用 strcpy )

复制代码
// 问题代码:strcpy 版本,遇到 \0 立刻停止拷贝
strcpy(_str, str);
strcpy(tmp, s._str);
strcpy(_str + pos, _str + pos + len);

问题分析

  • strcpy 拷贝规则:读到 \0 就终止
  • 如果字符串中间主动存入 \0(如 s += '\0'),strcpy 只会拷贝 \0 之前的内容,数据丢失。

解决思路

使用 memcpy按指定字节数完整拷贝 ,不识别 \0,可以完整拷贝含内嵌空字符的字符串。

复制代码
// 旧:strcpy(_str, str);
// 新:指定拷贝 _size+1 个字节(包含末尾 \0)
memcpy(_str, str, _size + 1);

拷贝构造、赋值、erase、append 等所有字符串拷贝位置,全部替换为 memcpy


问题 4:赋值运算符重载 - 自赋值问题

问题代码(无自赋值判断)

复制代码
// 问题代码:没有判断 this == &s,自赋值会崩溃
string& string::operator=(const string& s)
{
	// 先释放旧空间
	delete[] _str;         
	// 此时 _str 已经野指针,再取 s._str 逻辑错乱
	char* tmp = new char[s._capacity + 1];
	memcpy(tmp, s._str, s._size + 1);
	_str = tmp;
	_size = s._size;
	_capacity = s._capacity;
	return *this;
}

问题分析

当执行 s1 = s1;(对象自赋值):

  1. delete[] _str 释放自身内存;
  2. 后续读取 s._str 时,内存已被释放,变成野指针,内存访问错误。

解决思路

增加判断:if (this != &s)自己赋值给自己直接返回,不执行内存操作。

复制代码
string & string::operator=(const string & s)
{
	// 核心修复:判断是否自赋值
	if(this!=&s)
	{
		char* tmp = new char[s._capacity + 1];
		memcpy(tmp, s._str, s._size + 1);
		delete[] _str;
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

问题 5:size_t 无符号类型倒序遍历,越界死循环(insert 函数)

问题代码

复制代码
// 问题代码:使用 size_t + >= 倒序遍历
void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	// size_t 是【无符号整型】
	size_t end = _size;
	// 致命问题:end 是无符号,end == 0 时 --end 不会变成 -1,而是变成极大正数
	while (end >= pos) 
	{
		_str[end + 1] = _str[end];
		--end; // 无符号下溢 → 死循环
	}
	_str[pos] = ch;
	_size++;
}

问题分析

  1. size_t = 无符号整数 ,取值范围 [0, 极大值]永远不会小于 0
  2. end = 0 执行 --end,会发生无符号下溢,变成一个超级大的正数;
  3. end >= pos 条件永久成立,程序进入死循环

两种解决方案

方案 1(临时转 int)

end 定义为 int,规避无符号下溢:

复制代码
int end = _size;
while (end >= (int)pos)
{
	_str[end + 1] = _str[end];
	--end;
}
方案 2(修改遍历边界,纯 size_t 安全写法)

不从 _size 开始倒推,修改起始位置和循环条件 end > pos,全程无下溢:

复制代码
void string::insert(size_t pos, char ch)
{ 
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	// 修复:起始位置改为 _size+1,循环条件 end > pos,无下溢风险
	size_t end = _size+1;
	while (end > pos)
	{
		_str[end] = _str[end-1];
		--end;
	}
	_str[pos] = ch;
	_size++;
}

插入字符串版本 insert(pos, const char*) 同理,也是用这套边界逻辑修复。


问题 6:空对象(capacity=0)二倍扩容失效

问题代码(单纯二倍扩容)

复制代码
// 问题代码:空对象 _capacity = 0,0 * 2 依旧 = 0,永远无法扩容
if (_size == _capacity)
{
	reserve(_capacity * 2); 
}

问题分析

string 是空对象(默认构造): _capacity = 00 * 2 = 0 → 调用 reserve(0) 不会扩容,push_back 永远无法添加字符。

解决思路

三目运算判断:

  • 如果 _capacity == 0(空对象),直接默认扩容到 4

  • 非空对象,正常二倍扩容。

    if (_size == _capacity)
    {
    // 修复:空对象扩为4,否则二倍扩容
    reserve(_capacity==0?4:_capacity * 2);
    }


问题 7:全局通用 swap 导致多次深拷贝,效率低

问题代码:直接使用库模板 swap

复制代码
// 通用模板 swap 源码
template<class T>
void swap(T& a, T& b)
{
	T c(a);   // 调用拷贝构造(深拷贝)
	a = b;    // 调用赋值重载(深拷贝)
	b = c;    // 调用赋值重载(深拷贝)
}

问题分析

asuo::string 使用通用 swap,会触发 3 次深拷贝,堆内存频繁申请 / 释放,效率极差。

解决思路

  1. 实现成员函数 swap:直接交换三个基础成员变量(指针、两个数值),无堆拷贝;
  2. 重载全局 swap,优先调用成员 swap
第一步:实现高效成员 swap
复制代码
// 成员swap:只交换三个成员变量,无深拷贝
void string::swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
第二步:重载全局 swap(特化)
复制代码
// 针对 string 特化,优先调用高效成员swap
inline void swap(string& a, string& b)
{
	a.swap(b);
}

问题 8:流运算符 >> 逐字符读取,频繁扩容 + 长串性能差

问题代码(原始逐字符 push_back)

复制代码
// 问题代码:每读一个字符就 push_back,频繁触发扩容
std::istream& operator>>(std::istream& in, string& s)
{
	s.clear();
	char ch;
	while (in.get(ch) && ch != ' ' && ch != '\n')
	{
		s.push_back(ch); // 逐字符追加,扩容次数极多
	}
	return in;
}

问题分析

  1. 长字符串会触发几十次扩容,性能低;
  2. 提前 reserve(1024) 又会造成短字符串内存浪费。

解决思路

引入固定缓冲区 char buff[256]

  1. 字符先存入缓冲区,攒满 255 个再批量 +=

  2. 循环结束后处理缓冲区剩余字符;

  3. 大幅减少扩容次数,兼顾长短串性能。

    std::istream& operator>>(std::istream& in, string& s)
    {
    s.clear();
    char buff[256]; // 缓冲区,批量缓存字符
    int i = 0;
    char ch;
    ch = in.get();
    while (ch != '\n'&&ch!=' ')
    {
    buff[i++] = ch;
    if (i == 255) // 缓冲区存满,批量追加
    {
    buff[i] = '\0';
    s += buff;
    i = 0;
    }
    ch = in.get();
    }
    if (i > 0) // 处理剩余字符
    {
    buff[i] = '\0';
    s += buff;
    }
    return in;
    }

getline 函数使用了完全相同的缓冲区方案,修复逻辑一致。

相关推荐
我不是懒洋洋1 小时前
从零实现一个消息队列:生产消费与持久化
c++
数据皮皮侠AI1 小时前
上市公司战略性新兴产业专利数据库(2003-2024)
大数据·人工智能·笔记·机器学习·回归
玖玥拾2 小时前
C/C++ 数据结构(五)链表的应用、对象池
c语言·数据结构·c++·链表·对象池·双向链表
袁小皮皮不皮2 小时前
6.HCIP OSPF域间防环机制与虚链路
服务器·网络·笔记·网络协议·学习·智能路由器
一口吃俩胖子2 小时前
【脉宽调制DCDC功率变换学习笔记026】补偿设计和闭环性能
笔记·学习
三品吉他手会点灯2 小时前
C语言学习笔记 - 48.流程控制2 - 什么是流程控制
c语言·开发语言·笔记·学习
John_ToDebug2 小时前
Windows客户端热修复技术:从原理到工程实践
c++·经验分享·hook
凡人叶枫2 小时前
Effective C++ 条款37:绝不重新定义继承而来的缺省参数值
linux·c++·windows
王老师青少年编程2 小时前
2022年CSP-X复赛真题及题解(T4:摧毁)
c++·真题·csp·信奥赛·复赛·csp-x·摧毁