C++中string类的模拟实现(常用接口)

一、写作背景

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++中字符串的工作原理,并在今后的编程中更加得心应手地使用它们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值