C++:模拟实现string类

作为C++初学者,模拟实现标准库中的 string类 是一个非常好的练习。他不技能帮助我们理解字符串的底层存储逻辑,还能深入掌握类的封装、构造函数、运算符重载等核心知识点。今天我想分享一下自己实现 string类 的过程和思考。

类的基本框架设计

首先,我们需要确定 string类 的核心成员变量,一个字符串类至少需要存储字符串数据、当前长度和容量:

cpp 复制代码
class string
{
private:
	char* _str = nullptr;	//存储字符串数据
	size_t _size = 0;		//当前字符串长度
	size_t _capacity = 0;	//容量(当前开辟的空间最多可存储的字符数,不含'\0')
	static const size_t npos;//表示无效位置的静态常量
};

其中 npos 是一个特殊值,通常定义为 -1 (由于是 size_t 类型,会自动转换为最大的无符号整数),用于表示"未找到"或"到字符串末尾"等场景。

构造函数与析构函数

默认构造函数

我们实现一个支持默认参数的构造函数,既可以创建空字符串,也可以用C风格字符串初始化:

cpp 复制代码
string(const char* str = "")
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];	//+1是为了存储'\0'
	strcpy(_str, str);
}

这里有个细节:字符串常量""本身包含一个'\0',所以即使传入空字符串,也能正确处理

拷贝构造函数

拷贝构造函数需要实现深拷贝,避免两个对象共用同一块内存:

cpp 复制代码
	//现代写法,利用临时对象的资源转移
	string(const string& s)
	{
		string tmp(s._str);	//先创建临时对象
		swap(tmp);	//与临时对象交换资源
	}

	//交换函数
	void swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}

这种写法既简洁又安全,不需要手动释放内存,还能避免自赋值问题

析构函数

析构函数负责释放动态分配的内存:

cpp 复制代码
~string()
{
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

赋值运算符重载

赋值运算符也需要处理深拷贝,同样可以用现代写法实现:

cpp 复制代码
string& operator=(string tmp)    //传值参数会触发拷贝构造
{
	swap(tmp);    //榆林市对象交换资源
	return *this;
}

这种写法非常巧妙:通过传值方式接收参数,自动完成依次拷贝,然后通过 swap函数 交换当前对象和临时对象的资源,临时对象销毁时会自动释放原有的资源。

基本功能实现

容量管理:reserve函数

reserve函数 用于预留空间,当需要插入大量数据时可以提前扩容,减少频繁的内存分配:

cpp 复制代码
void string::reserve(size_t n)
{
	if (n > _capacity)	//只有当n大于当前容量时才会扩容
	{
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}

插入单个字符:push_back

实现向字符串末尾插入单个字符的功能:

cpp 复制代码
void string::push_back(char ch)
{
	if (_size == _capacity)
	{
		//如果容量为0则先扩容到4,否则扩容到原来的2倍
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

	_str[_size] = ch;	//插入字符
	_size++;
	_str[_size] = '\0';	//字符串结束标志
}

追加字符串:append

实现向字符串末尾追加C风格字符串的功能:

cpp 复制代码
void string::append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)	//检查是否需要扩容
	{
		//扩容到足够容纳新字符串的大小
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}
	strcpy(_str + _size, str);	//拷贝字符串
	_size += len;
}

运算符重载:+=

为了使用更方便,我们可以重载+=运算符:

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

	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}

插入与删除操作

插入操作

插入操作需要先移动数据,再插入新内容。插入单个字符:

cpp 复制代码
void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);	//确保插入位置有效

	if (_size == _capacity)	//检查是否需要扩容
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

	//从后往前移动数据,避免覆盖
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;	//插入字符
	++_size;
}

插入字符串的逻辑类似,但需要考虑插入多个字符的情况:

cpp 复制代码
	void string::insert(size_t pos, const char* s)
	{
		assert(pos <= _size);
		size_t len = strlen(s);
		if (_size + len > _capacity)	//检查是否需要扩容
		{
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}

		//移动数据,留出插入空间
		size_t end = _size + len;
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];
			--end;
		}
		//拷贝插入的字符串
		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = s[i];
		}

		_size += len;
	}

删除操作

删除操作需要将删除位置后的字符前移:

cpp 复制代码
void string::erase(size_t pos, size_t len)
{
	if (len >= _size - pos)	//如果删除长度超过剩余长度,直接截断
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else  //否则将后面的字符前移
	{
		for (size_t i = pos + len; i <= _size; i++)
		{
			_str[i - len] = _str[i];
		}
		_size -= len;
	}
}

查找与子串操作

查找操作

实现查找字符和字符串的功能:

cpp 复制代码
// 查找字符
size_t string::find(char ch, size_t pos)
{
    for (size_t i = pos; i < _size; i++)
    {
        if (_str[i] == ch)
        {
            return i;
        }
    }
    return npos;  // 未找到返回npos
}

// 查找字符串
size_t string::find(const char* str, size_t pos)
{
    assert(pos < _size);
    const char* ptr = strstr(_str + pos, str);  // 利用库函数
    if (ptr == nullptr)
    {
        return npos;
    }
    else
    {
        return ptr - _str;  // 计算相对位置
    }
}

子串操作

提取子串功能

cpp 复制代码
string string::substr(size_t pos, size_t len)
{
    assert(pos < _size);
    // 如果长度超过剩余部分,就取到字符串末尾
    if (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;
}

关系运算符重载

为了比较字符串,我们需要重载关系运算符:

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 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 !(s1 == s2);
}

输入输出运算符重载

为了方便地使用 cout 和 cin 操作我们的 string 类,需要重载输入输出运算符:

cpp 复制代码
// 输出运算符
ostream& operator<<(ostream& out, const string& s)
{
    for (auto ch : s)  // 利用迭代器遍历
    {
        out << ch;
    }
    return out;
}

// 输入运算符
istream& operator>>(istream& in, string& s)
{
    s.clear();  // 先清空原有内容
    const int N = 256;
    char buff[N];  // 用缓冲区暂存输入
    int i = 0;

    char ch;
    in >> ch;
    // 读取直到遇到空格或换行
    while (ch != ' ' && ch != '\n')
    {
        buff[i++] = ch;
        if (i == N - 1)  // 缓冲区满了就先存入string
        {
            buff[i] = '\0';
            s += buff;
            i = 0;
        }
        ch = in.get();  // 读取下一个字符
    }

    // 处理剩余字符
    if (i > 0)
    {
        buff[i] = '\0';
        s += buff;
    }
    return in;
}

总结与反思

通过模拟实现 string 类,我对 C++ 的类设计有了更深入的理解:

  1. 内存管理:字符串操作的核心是内存管理,需要特别注意动态内存的分配与释放,避免内存泄漏和野指针。

  2. 深拷贝与浅拷贝:这是自定义类时非常重要的概念,对于包含动态内存的类,必须实现深拷贝。

  3. 异常安全:虽然目前的实现还比较简单,但已经能体会到异常安全的重要性,比如使用 swap 技术可以提高代码的安全性。

  4. 接口设计 :一个好的类需要设计直观易用的接口,比如重载+=[]等运算符,让类的使用更加自然。

当然,这个实现还有很多可以改进的地方,比如增加迭代器的完善支持、实现更高效的字符串拼接、处理更多边界情况等。但作为初学者的练习,这个版本已经覆盖了 string 类的核心功能,让我对 C++ 的面向对象编程有了更具体的认识。

相关推荐
superman超哥2 小时前
Rust Cargo Run 与 Cargo Test 命令:开发工作流的双引擎
开发语言·后端·rust·cargo run·cargo test·开发工作流·双引擎
XFF不秃头2 小时前
力扣刷题笔记-合并区间
c++·笔记·算法·leetcode
p&f°2 小时前
Java面试题(全)自用
java·开发语言
编程之路,妙趣横生2 小时前
STL(七) unordered_set 与 unordered_map 基本用法 + 模拟实现
c++
猴子年华、2 小时前
【每日一技】:GitHub 精确查询
开发语言·python·github
持续升级打怪中2 小时前
深入解析深浅拷贝:原理、实现与最佳实践
开发语言·前端·javascript
码农水水2 小时前
蚂蚁Java面试被问:接口幂等性的保证方案
java·开发语言·面试
毕设源码-钟学长2 小时前
【开题答辩全过程】以 高校课程档案管理系统的设计与实现为例,包含答辩的问题和答案
java·开发语言
寂柒2 小时前
c++--
c++