一、写作背景
C++ 中的 string
类是我们在日常开发中最常用的类型之一。它提供了丰富的字符串操作技巧,为了深化常用接口的使用方式同时深入理解 string
的运作机制,我进行了一次string类常用接口模拟实现。
二、概要设计
为了避免自定义的string类名与标准库同名冲突,我将所有代码放在名为 Zeker
的命名空间中。 下面我将文件的常用接口声明展示出来:
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cassert>
using namespace std;
namespace Zeker
{
const size_t npos = -1; // 通用的、与实例无关的常量,表示查找失败的结果。
class string
{
friend ostream& operator<<(ostream& out, const string& s); //重载"<<"不能定义在public中作为成员函数
friend istream& operator>>(istream& in, string& s); //成员函数的左操作数必须是this,而"<<"左操作数必须是ostream类对象
public:
string(const char* str = "");
string(const string& str);
~string();
size_t size()const; //类外获取对象的字符串
size_t capacity()const;//类外获取对象的空间大小
const char* c_str()const;//类外获取对象的字符串个数
void dilatation(size_t n);
void append(const char* str); // 追加字符串
void push_back(char ch);
void resize(size_t n, char ch = '\0');
void erase(size_t pos, size_t len = npos);
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
string& insert(size_t pos, char ch);
string& insert(size_t pos, const char* str);
char& operator[](size_t i);
const char& operator[](size_t i)const;
string& operator+= (const string & str);
string& operator+= (char ch);
string& operator+= (const char* str);
string& operator= (const string& str);
bool operator==(const char* str);
bool operator==(const string& str);
bool operator!=(const char* str);
bool operator!=(const string& str);
bool operator<=(const string& str);
bool operator>=(const string& str);
bool operator<(const string& str);
bool operator>(const string& str);
private:
char* _str; // 字符串的实际存储空间
size_t _size; // 字符串的实际长度(不包括结尾的'\0')
size_t _capacity; // 已分配的内存大小(可容纳的字符数)
};
}
在概览需要实现的常用接口之后,我们来探究一下该如何实现这些常用接口。
三、功能模块详解
3.1构造、拷贝、析构
// 构造函数:从C风格字符串创建对象
Zeker::string::string(const char* str)
:_str(nullptr) // 使用列表初始化指针为空,防止野指针
{
_size = strlen(str); // 计算字符串长度(不包含'\0')
_capacity = _size; // 初始容量设为实际大小,确保空间利用率
_str = new char[_capacity + 1]; // 分配内存,+1是为了存放结束符'\0'
strcpy(_str, str); // 复制字符串内容,包括'\0'
}
// 拷贝构造函数:从另一个string对象创建新对象
Zeker::string::string(const string& str)
{
_size = strlen(str.c_str()); // 获取源对象的字符串长度
_capacity = _size; // 设置初始容量等于长度
_str = new char[_capacity + 1]; // 分配独立内存空间,+1用于'\0'
strcpy(_str, str.c_str()); // 深拷贝字符串内容
}
// 析构函数:释放资源并重置状态
Zeker::string::~string()
{
delete[] _str; // 释放动态分配的内存
_str = nullptr; // 避免悬挂指针
_size = _capacity = 0; // 重置大小和容量
}
3.2内存扩展
/ 内部扩容函数:增加存储容量
void Zeker::string::dilatation(size_t n)
{
char* newstr = new char[n + 1]; // 分配新内存,大小为n+1(为'\0'预留空间)
strcpy(newstr, _str); // 复制现有内容到新内存
delete[] _str; // 释放旧内存
_str = newstr; // 更新指针指向新内存
_capacity = n; // 更新容量
}
由于多个成员函数的实现都是需要内存的扩展,代码重复度较高,因此将内存的拓展分装成dilatation函数,
用于当现有容量不够时进行扩容。它创建一个新的、更大的内存空间,将原有数据复制过去,然后释放旧内存。这个方法被许多其他需要增加容量的方法所调用。
3.3字符串修改
3.3.1追加操作
// 追加C风格字符串
void Zeker::string::append(const char* str)
{
size_t len = strlen(str); // 计算要追加的字符串长度
if (len + _size > _capacity) // 检查是否需要扩容
{
size_t newcapacity = len + _size; // 新容量为当前长度加追加的长度
dilatation(newcapacity); // 扩展容量
}
strcpy(_str + _size, str); // 将新字符串复制到当前字符串末尾
_size += len; // 更新字符串长度
}
// 追加单个字符
void Zeker::string::push_back(char ch)
{
if (_size == _capacity) // 检查是否需要扩容
{
// 计算新容量:如果当前容量为0,设为2;否则翻倍
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
dilatation(newcapacity); // 扩展容量
}
_str[_size] = ch; // 在末尾添加字符
_size++; // 增加长度
_str[_size] = '\0'; // 确保字符串以'\0'结尾
}
append
用于追加一个字符串,而push_back
则在尾部添加单个字符。两者都会在需要时自动扩容。注意push_back
中使用了成倍扩容策略(当容量为0时设为2,否则翻倍),减少频繁扩容操作。
3.3.2插入操作
// 在指定位置插入C风格字符串
Zeker::string& Zeker::string::insert(size_t pos, const char* str)
{
size_t len = strlen(str); // 计算要插入的字符串长度
if (len + _size > _capacity) // 检查是否需要扩容
{
dilatation(len + _size); // 扩展容量
}
int end = _size;
while (end >= (int)pos) // 将插入点后的字符向后移动len个位置
{ // 注意:类型转换防止pos为0时出现无符号数溢出
_str[end + len] = _str[end];
end--;
}
strncpy(_str + pos, str, len); // 在插入点复制新字符串
_size += len; // 更新长度
_str[_size] = '\0'; // 确保字符串以'\0'结尾
return *this; // 返回对象引用,支持链式调用
}
insert
方法允许在指定位置插入内容。实现时需要将插入点后的所有字符向后移动,腾出空间,然后插入新内容。
3.4运算符重载
3.4.1访问运算符
// 非常量索引运算符:允许访问和修改指定位置的字符
char& Zeker::string::operator[](size_t i)
{
assert(i < _size); // 检查索引是否越界
return _str[i]; // 返回字符的引用,可以修改
}
3.4.2比较运算符
// 相等比较运算符(C字符串版)
bool Zeker::string::operator==(const char* str)
{
return strcmp(_str, str) == 0; // 使用strcmp比较,返回0表示相等
}
// 相等比较运算符(string对象版)
bool Zeker::string::operator==(const string& str)
{
return strcmp(_str, str.c_str()) == 0; // 比较内部字符串是否相等
}
// 不等比较运算符(C字符串版)
bool Zeker::string::operator!=(const char* str)
{
return !(*this == str); // 利用已实现的==运算符,取反
}
// 不等比较运算符(string对象版)
bool Zeker::string::operator!=(const string& str)
{
return !(*this == str); // 利用已实现的==运算符,取反
}
// 小于等于比较运算符
bool Zeker::string::operator<=(const string& str)
{
return *this < str || *this == str; // 小于或等于
}
// 大于等于比较运算符
bool Zeker::string::operator>=(const string& str)
{
return *this > str || *this == str; // 大于或等于
}
// 小于比较运算符
bool Zeker::string::operator<(const string& str)
{
return strcmp(_str, str.c_str()) < 0; // 使用strcmp比较,小于0表示小于
}
// 大于比较运算符
bool Zeker::string::operator>(const string& str)
{
return strcmp(_str, str.c_str()) > 0; // 使用strcmp比较,大于0表示大于
}
3.4.3输入输出流运算符
// 输出流运算符重载
ostream& Zeker::operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); i++) // 逐字符输出
{
out << s[i];
}
return out; // 返回流对象引用,支持链式调用
}
// 输入流运算符重载
istream& Zeker::operator>>(istream& in, string& s)
{
while (1)
{
char ch;
ch = in.get(); // 逐个读取字符
if (ch == ' ' || ch == '\n') // 遇到空格或换行符停止
{
break;
}
else
{
s += ch; // 追加到字符串
}
}
return in; // 返回流对象引用,支持链式调用
}
3.4.4赋值运算符
// 赋值运算符:将另一个string对象的内容赋给当前对象
Zeker::string& Zeker::string::operator=(const string& str)
{
if (this != &str) // 检查自赋值
{
char* tmp = new char[str._capacity + 1]; // 先分配新内存
strcpy(tmp, str._str); // 复制内容
delete[] _str; // 释放旧内存
_str = tmp; // 更新指针
_size = str._size; // 更新大小
_capacity = str._capacity; // 更新容量
}
return *this; // 返回对象引用,支持链式赋值
}
3.5查找操作
// 查找字符在字符串中首次出现的位置
size_t Zeker::string::find(char ch, size_t pos)
{
for (size_t i = pos; i < _size; i++) // 从指定位置开始向后遍历
{
if (_str[i] == ch) // 如果找到匹配字符
return i; // 返回位置索引
}
return npos; // 未找到返回npos(通常定义为-1,转换为size_t后是最大值)
}
// 查找子串在字符串中首次出现的位置
size_t Zeker::string::find(const char* str, size_t pos)
{
char* p = strstr(_str + pos, str); // 利用C库函数strstr查找子串
if (p == nullptr) // 未找到
return npos;
return p - _str; // 返回相对于字符串开始的位置
}
3.6调整大小
// 调整字符串大小,可选用指定字符填充
void Zeker::string::resize(size_t n, char ch)
{
if (n < _size) // 如果新大小小于当前大小,截断字符串
{
_str[n] = '\0'; // 在新的结束位置放置'\0'
_size = n; // 更新长度
}
else // 如果新大小大于当前大小,扩展并填充
{
if (n > _capacity) // 检查是否需要扩容
{
dilatation(n); // 扩展容量
}
for (size_t i = _size; i < n; i++) // 用指定字符填充新增部分
{
_str[i] = ch;
}
}
_size = n; // 更新长度
_str[_size] = '\0'; // 确保字符串以'\0'结尾
}
3.7删除操作
// 删除指定位置开始的一段字符
void Zeker::string::erase(size_t pos, size_t len)
{
if (len > _size - pos) // 如果要删除的长度超过可删除的范围
{
_str[pos] = '\0'; // 直接截断字符串
_size = pos; // 更新长度
}
else // 否则删除中间的一段
{
size_t i = pos + len; // 从删除段之后的位置开始
while (i <= _size) // 将后面的字符前移
{
_str[i - len] = _str[i]; // 数据直接前移len个单位
++i;
}
_size -= len; // 更新长度
}
}
3.8插入操作
// 在指定位置插入C风格字符串
Zeker::string& Zeker::string::insert(size_t pos, const char* str)
{
size_t len = strlen(str); // 计算要插入的字符串长度
if (len + _size > _capacity) // 检查是否需要扩容
{
dilatation(len + _size); // 扩展容量
}
int end = _size;
while (end >= (int)pos) // 将插入点后的字符向后移动len个位置
{ // 注意:类型转换防止pos为0时出现无符号数溢出
_str[end + len] = _str[end];
end--;
}
strncpy(_str + pos, str, len); // 在插入点复制新字符串
_size += len; // 更新长度
_str[_size] = '\0'; // 确保字符串以'\0'结尾
return *this; // 返回对象引用,支持链式调用
}
四.总结
通过实现Zeker::string
类,可以帮助我们深入了解了字符串处理和内存管理的基本原理。这个实现虽然简化了一些细节,但涵盖了string类的核心功能和设计思想。
在实际的生产环境中,我们通常会使用标准库提供的std::string
,因为它经过了充分的测试和优化。但是,理解其内部实现对于编写高效、健壮的C++代码至关重要。
希望这篇文章能帮助你更好地理解C++中字符串的工作原理,并在今后的编程中更加得心应手地使用它们。