String类常用接口的实现

目录

引言

一、为什么自己实现String?

二、整体框架与成员变量

三、构造与析构(RAII核心)

[3.1 默认构造函数](#3.1 默认构造函数)

[3.2 从C字符串构造](#3.2 从C字符串构造)

[3.3 拷贝构造函数](#3.3 拷贝构造函数)

[3.4 析构函数](#3.4 析构函数)

[3.5 赋值运算符重载](#3.5 赋值运算符重载)

四、容量相关接口

[4.1 size(), length(), capacity()](#4.1 size(), length(), capacity())

[4.2 reserve() ------ 手动扩容](#4.2 reserve() —— 手动扩容)

五、元素访问接口

[5.1 operator[] 和 at](#5.1 operator[] 和 at)

[5.2 front() 和 back()](#5.2 front() 和 back())

[5.3 c_str()](#5.3 c_str())

六、迭代器接口

七、修改操作(增、删、改)

[7.1 push_back 和 pop_back](#7.1 push_back 和 pop_back)

[7.2 append 系列](#7.2 append 系列)

[7.3 operator+= 与 operator+](#7.3 operator+= 与 operator+)

[7.4 insert ------ 在指定位置前插](#7.4 insert —— 在指定位置前插)

[7.5 erase ------ 删除一段字符](#7.5 erase —— 删除一段字符)

[7.6 clear](#7.6 clear)

八、查找与子串

[8.1 find ------ 查找子串或字符](#8.1 find —— 查找子串或字符)

[8.2 substr ------ 取子串](#8.2 substr —— 取子串)

九、比较运算符(全局函数)

十、输入输出流接口

[10.1 operator>> ------ 读取单词](#10.1 operator>> —— 读取单词)

[10.2 operator<< ------ 输出](#10.2 operator<< —— 输出)

[10.3 getline ------ 读取一行](#10.3 getline —— 读取一行)

十一、总结与改进建议


引言

本文从零实现一个简化版std::string,深入理解构造、拷贝、扩容、迭代器、增删改查等核心接口的设计细节与注意事项。


一、为什么自己实现String?

C++标准库中的std::string功能强大,但黑盒般的封装让初学者难以理解其内部机制。手动实现一个string类,可以帮你掌握:

  • RAII(资源获取即初始化):构造时分配内存,析构时自动释放。

  • 深拷贝与浅拷贝:避免指针悬挂和重复释放。

  • 动态扩容策略:如何高效管理容量。

  • 运算符重载:让自定义类型像内置类型一样使用。

  • 迭代器设计:泛型编程的基础。

本文基于C++11,实现一个名为mine::string的类,涵盖常用接口,并逐段讲解代码背后的设计思想。

二、整体框架与成员变量

cpp 复制代码
namespace mine {
    class string {
    private:
        char* _str;          // 指向动态分配的字符数组,以'\0'结尾
        size_t _size;       // 有效字符个数(不包括'\0')
        size_t _capacity;   // 当前可容纳的有效字符数(实际分配_capacity+1字节)
        static const size_t npos = -1;  // 用于find返回"未找到"或substr表示"直到结尾"
    public:
        // 接口声明...
    };
}

设计约定

  • _str指向堆空间,始终以'\0'终止,保证C字符串函数可用。

  • _size记录字符串长度,_capacity记录当前已分配的空间(不含终止符)。

    例如:字符串"abc"_size=3_capacity至少为3,实际分配_capacity+1字节。

  • npossize_t的最大值(-1转换为无符号),用作"未找到"或"直到末尾"的标志。

三、构造与析构(RAII核心)

3.1 默认构造函数

cpp 复制代码
string() {
    _str = new char[1]{'\0'};
    _capacity = 0;
    _size = 0;
}

讲解

  • 空字符串也需要一个有效的_str,指向一个只含'\0'的独立堆内存。

  • 容量设为0,表示没有额外的有效字符空间。_str[0]就是终止符。

  • 必须使用new分配,与析构中的delete[]配对。

3.2 从C字符串构造

cpp 复制代码
string(const char* str) {
    assert(str);                    // 防止传入空指针
    size_t len = strlen(str);         
    _size = _capacity = len;
    _str = new char[_capacity + 1]; // +1 给'\0'
    strcpy(_str, str);
}

讲解

  • 参数合法性检查:若传入nullptr,程序直接终止(可通过断言或异常处理)。

  • 长度计算使用strlen,效率O(N)。

  • _capacity = len,注意_capacity不包含终止符。分配内存时需额外多1字节存放'\0'

  • 使用strcpy拷贝,保证末尾自动添加'\0'

3.3 拷贝构造函数

cpp 复制代码
string(const string& s) {
    _str = new char[s._capacity + 1];
    strcpy(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
}

讲解

  • 必须深拷贝:不能直接_str = s._str,否则两个对象指向同一块内存,析构时会重复释放,同时两个对象进行操作时会导致紊乱

  • 新分配空间大小依据源对象的_capacity,保证容量一致。

  • 拷贝后两个对象完全独立,互不影响。

3.4 析构函数

cpp 复制代码
string(const string& s) {
    _str = new char[s._capacity + 1];
    strcpy(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
}

讲解

  • 释放堆内存,并将指针置空,避免悬垂指针。

  • 即使_str已经是nullptrdelete[]也是安全的(但这里构造确保非空)。

3.5 赋值运算符重载

cpp 复制代码
// 从C字符串赋值
string& operator=(const char* str) {
    assert(str);
    int len = strlen(str);
    char* tmp = new char[len + 1];
    strcpy(tmp, str);          // 先拷贝到新内存
    delete[] _str;             // 释放旧资源
    _str = tmp;
    _size = len;
    _capacity = len;
    return *this;
}
// 从string赋值
string& operator=(const string& s) {
    if (&s != this) {          // 自我赋值检查
        *this = s._str;        // 复用上面的C字符串赋值
    }
    return *this;
}

讲解

  • 异常安全 :先分配新内存并拷贝数据,成功后再释放旧内存。如果new失败,旧对象保持不变。

  • 自我赋值检测避免无谓操作和潜在错误。

  • 复用代码:operator=(const char*)实现具体逻辑,operator=(const string&)调用它,避免重复。

四、容量相关接口

4.1 size(), length(), capacity()

cpp 复制代码
size_t size() const { return _size; }
size_t length() const { return _size; }
size_t capacity() const { return _capacity; }

讲解

  • 返回类型应为size_t(无符号整数),与标准库一致。

  • 这些函数不修改对象,因此声明为const成员函数。

4.2 reserve() ------ 手动扩容

cpp 复制代码
void reserve(size_t n) {
    if (n > _capacity) {
        char* tmp = new char[n + 1];
        if (_str) strcpy(tmp, _str);
        else tmp[0] = '\0';
        delete[] _str;
        _str = tmp;
        _capacity = n;
    }
}

讲解

  • 仅当n > _capacity时才重新分配,否则不做任何事(不缩容)。

  • 分配新内存后,拷贝原有数据(如果存在),然后释放旧内存。

  • 注意边界:如果当前_strnullptr(一般不会发生),则新内存只放一个'\0'

  • 保留原有_size不变,只调整容量。

五、元素访问接口

5.1 operator[] 和 at

cpp 复制代码
char& operator[](size_t pos) {
    assert(pos < _size);
    return _str[pos];
}
const char& operator[](size_t pos) const {
    assert(pos < _size);
    return _str[pos];
}

讲解

  • 提供const和非const两个版本:const对象只能调用const版本,返回常量引用,禁止修改。

  • 边界检查使用assert(调试模式有效),at()可以改为抛出std::out_of_range异常。

  • 下标从0开始,_str[_size]'\0',不允许访问(除非作为终止符读取)。

5.2 front() 和 back()

cpp 复制代码
char& front() {
    return _str[0];
}
char& back() {
    assert(_size > 0);
    return _str[_size - 1];
}

讲解

  • front()即使空字符串也能返回_str[0](值为'\0'),但语义上应禁止。可以加上assert(_size > 0)

  • back()必须确保字符串非空,否则越界。

5.3 c_str()

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

讲解

  • 返回内部指针,方便与C字符串函数交互。

  • 返回类型为const char*,防止通过指针修改内部数据。

六、迭代器接口

cpp 复制代码
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; }

讲解

  • 迭代器就是原生指针,因为底层是连续数组。

  • begin()指向第一个字符,end()指向最后一个字符的下一个位置(即_str[_size],终止符位置)。

  • 范围for循环会自动调用begin()end()

七、修改操作(增、删、改)

7.1 push_back 和 pop_back

cpp 复制代码
void push_back(char c) {
    if (_size == _capacity) {
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
    }
    _str[_size++] = c;
    _str[_size] = '\0';
}
void pop_back() {
    assert(_size > 0);
    _str[--_size] = '\0';
}

讲解

  • push_back:容量不足时扩容(初始容量为4,之后每次翻倍)。插入字符后记得添加终止符。

  • pop_back:将_size减1,原位置用'\0'覆盖。必须确保非空。

7.2 append 系列

cpp 复制代码
string& append(const char* s) {
    *this += s;
    return *this;
}
string& append(const string& s) {
    *this += s;
    return *this;
}
string& append(const string& str, size_t subpos, size_t sublen) {
    assert(subpos <= str._size);
    if (sublen == 0) return *this;
    if (subpos + sublen > str._size) sublen = str._size - subpos;
    while (_capacity < _size + sublen) {
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
    }
    for (size_t i = 0; i < sublen; ++i) {
        _str[_size + i] = str._str[subpos + i];
    }
    _size += sublen;
    _str[_size] = '\0';
    return *this;
}

讲解

  • 前两个重载直接复用operator+=,简洁。

  • 第三个重载用于追加子串:需处理越界(自动截断),然后逐字符拷贝。可优化为memcpy

7.3 operator+= 与 operator+

cpp 复制代码
string string::operator+(const char* str)
{
	string tmp=_str;
	tmp += str;
	return tmp;
}
string string::operator+(const string& s)
{
	string tmp=_str;
	tmp += s;
	return tmp;
}
string& string::operator +=(const char ch)
{
	if (_capacity == _size)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	_str[_size++] = ch;
	_str[_size] = '\0';
	return *this;
}
string& string::operator += (const char* str)
{
	if (str == nullptr)
	{
		return *this;
	}
	int len = strlen(str);
	while(_capacity < _size + len)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	strcpy(_str + _size, str);
	_size += len;
	return *this;
}
string& string::operator += (const string& s)
{
	while(_capacity < _size + s._size)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	strcpy(_str + _size, s._str);
	_size += s._size;
	return *this;
}

讲解

  • operator+=修改自身,返回引用,支持链式操作。

  • operator+返回新对象,不改变原对象。实现上先拷贝构造一个临时对象,再追加。

  • 注意operator+应声明为const成员函数(或非成员函数),以便对右值操作。

7.4 insert ------ 在指定位置前插

cpp 复制代码
string& insert(size_t pos, const char* s) {
    assert(pos <= _size);
    size_t len = strlen(s);
    if (_capacity < _size + len) reserve(/*扩容*/);
    // 向后移动数据,腾出空间
    for (size_t i = _size; i >= pos; --i) {
        _str[i + len] = _str[i];
    }
    memcpy(_str + pos, s, len);
    _size += len;
    return *this;
}

讲解

  • 移动数据时必须从后往前拷贝,避免覆盖。如果使用memmove则更安全。

  • 循环中i的类型应为size_t,注意下溢问题。通常改用memmove一步完成。

  • 插入点pos可以等于_size(尾部插入),此时等同于append

7.5 erase ------ 删除一段字符

cpp 复制代码
string& erase(size_t pos = 0, size_t len = npos) {
    assert(pos < _size);
    if (len == npos || pos + len >= _size) {
        _str[pos] = '\0';
        _size = pos;
    } else {
        for (size_t i = pos + len; i <= _size; ++i) {
            _str[i - len] = _str[i];
        }
        _size -= len;
    }
    return *this;
}

讲解

  • 如果删除长度len超出尾部,则直接截断至pos位置。

  • 否则,将后面的字符向前移动,覆盖被删除区间。

  • 移动时包括终止符,保证字符串仍然正确结束。

7.6 clear

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

讲解 :只需将第一个字符置为终止符,并重置_size,容量不变。

八、查找与子串

8.1 find ------ 查找子串或字符

cpp 复制代码
size_t find(const char* s, size_t pos = 0) const {
    assert(pos <= _size);
    const char* p = strstr(_str + pos, s);
    return p ? p - _str : npos;
}
size_t find(char ch, size_t pos = 0) const {
    for (size_t i = pos; i < _size; ++i) {
        if (_str[i] == ch) return i;
    }
    return npos;
}

讲解

  • 利用C标准库函数strstr快速查找子串,效率O(N·M)(BF算法)。

  • 返回下标,若未找到返回npos

  • 字符查找手动循环即可。

8.2 substr ------ 取子串

cpp 复制代码
string substr(size_t pos = 0, size_t len = npos) const {
    assert(pos <= _size);
    if (len == npos || pos + len > _size) len = _size - pos;
    string sub;
    sub.reserve(len);
    for (size_t i = 0; i < len; ++i) sub += _str[pos + i];
    return sub;
}

讲解

  • 先调整len,避免越界。

  • 构造新字符串,提前reserve避免多次扩容。

  • 逐个字符追加(可改用memcpy优化)。

九、比较运算符(全局函数)

cpp 复制代码
bool operator<(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator<=(const string& s1, const string& s2)
{
	return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
	return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
	return !(s1 < s2);
}
bool operator==(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
	return !(s1 == s2);
}

讲解

  • 定义为非成员函数,支持左右操作数隐式转换(例如"abc" < s)。

  • 使用strcmp比较字典序,符合常规字符串比较规则。

十、输入输出流接口

10.1 operator>> ------ 读取单词

cpp 复制代码
istream& operator>>(istream& in, string& str) {
    str.clear();
    char ch;
    // 跳过前导空白字符
    while ((ch = in.get()) == ' ' || ch == '\n');
    // 读取非空白字符
    while (ch != ' ' && ch != '\n') {
        str += ch;
        ch = in.get();
    }
    return in;
}

讲解

  • 先清空目标字符串。

  • 跳过前导空白(空格、换行等),然后读取字符直到遇到空白

10.2 operator<< ------ 输出

cpp 复制代码
ostream& operator<<(ostream& out, const string& str) {
    for (char ch : str) out << ch;
    return out;
}

讲解:直接遍历字符串,逐个输出字符,不输出终止符。

10.3 getline ------ 读取一行

cpp 复制代码
istream& getline(istream& is, string& str) {
    str.clear();
    char ch;
    while ((ch = is.get()) != '\n') {
        str += ch;
    }
    return is;
}

讲解:读取直到换行符,不存储换行符。

十一、总结与改进建议

通过实现这个string类,我们掌握了:

  • 动态内存管理的三大件(构造、拷贝构造、赋值、析构)

  • 深拷贝的必要性及实现

  • 扩容策略与性能权衡

  • 运算符重载的常见模式

  • 迭代器的本质与用法

相关推荐
花间相见2 小时前
【大模型微调与部署03】—— ms-swift-3.12 命令行参数(训练、推理、对齐、量化、部署全参数)
开发语言·ios·swift
智者知已应修善业2 小时前
【数字稳压控制DAC/TLC5615驱动】2023-5-27
c++·经验分享·笔记·算法·51单片机
默 语2 小时前
Java的“后路“:不是退场,而是换了一种活法
java·开发语言·python
t***5442 小时前
Orwell Dev-C++和Embarcadero Dev-C++哪个更稳定
开发语言·c++
黑牛儿2 小时前
同样是 PHP-FPM 调优,别人能支撑 1000 + 并发,你却还在报 502?
开发语言·php
wjs20242 小时前
R 数据类型
开发语言
慕容卡卡2 小时前
你所不知道的RAG那些事
java·开发语言·人工智能·spring boot·spring cloud
Lyyaoo.2 小时前
【JAVA基础面经】List(Vector+ArrayList+LinkedList)
java·开发语言·list
立莹Sir2 小时前
JVM深度解析与实战指南:从源码到生产环境优化
开发语言·jvm·python