C++“STL”——模拟实现String类

        “STL(标准模版库)”是C++必不可少的一个数据结构和软件算法的库,今天我们来模式实现“string”类。

一、string类

        1.定义

        为了方便,string类可以定义在自己的命名空间里,它的本质相当于一个顺序表,所以有些操作可以用顺序表的经验:

namespace Mynamespace
{

    class string
    {
        char* _str;

        size_t _capacity;

        size_t _size;
        
        static const size_t npos = -1;
    };
}

        2.构造函数

        string(const char* str = "")
        {
            _str = new char[strlen(str)+1];
            char* der = _str;
            const char* sour = str;
            while (*sour != '\0')
            {
                *(der++) = *(sour++);
            }
            *der = '\0';

            _size = strlen(str);
            _capacity = _size;
        }

        在构造函数中,我们使用了缺省参数,在开辟的空间上,默认size和capacity是不包括‘\0’的,但是要开辟给‘\0’的空间,所以需要多new一个字节的空间交给‘\0’,后续的操作就是将输入的字符串拷贝给new出的空间,为了防止打印的时候越界访问,所以需要手动加上‘\0’。为了提高运行效率,可以先开辟一部分空间,避免频繁扩容带来的效率低下的问题。

        3.析构函数

        ~string()
        {
            delete[] _str;
            _capacity = 0;
            _size = 0;
        }

        析构函数的主要作用就是在对象的生命周期结束后释放资源,但是需要注意的是符号的对应(new 和 delete)。

        4.拷贝构造

        在模拟的string类中,因为涉及到对内存的管理,因此需要实现拷贝构造。

        string(const string& s)
        {
            _str = new char[s._capacity];
            char* der = _str;
            const char* sour = s._str;
            while (*sour != '\0')
            {
                *(der++) = *(sour++);
            }
            *der = '\0';
            _size = s._size;
            _capacity = s._capacity;
        }

        拷贝构造是构造函数的一个重载,顾名思义,就是为了复制资源到要初始化的对象。

        在这里。拷贝构造主要是深拷贝,避免两个对象中的str指针指向同一块内存空间导致析构的时候重复释放内存资源。因此需要重新new一片资源。

        4.赋值运算符重载

        string& operator=(const string& s)
        {
            delete[] _str;

            _str = new char[strlen(s._str) + 1];
            char* der = _str;
            const char* sour = s._str;
            while (*sour != '\0')
            {
                *(der++) = *(sour++);
            }
            *der = '\0';

            _size = s._size;
            _capacity = s._capacity;

            return *this;
        }

        赋值运算符重载和拷贝构造相似,但是在用法上有差别。

        在这里,我选择重新开辟一块空间用来保存右值的内容。为了避免内存泄漏口,一定要释放原来的空间。并更新size和capacity。

        5.iterator迭代器(begin、end)

        typedef char* iterator;

        iterator begin()
        {
            return _str;
        }

        iterator end()
        {
            return _str + _size;
        }

        为了能够在范围for使用迭代器,这里将char*给typedef了,其中,“begin”和“end”函数返回的分别是指向首元素和“\0”的指针。

示例:

#include"string.h"

void test()
{
	Mynamespace::string mystring1 = "123456789";

	for (auto ch : mystring1)
	{
		cout << ch;
	}

}
	


int main()
{
	test();

	return 0;
}

运行结果:

        6.扩容

void Mynamespace::string::reserve(size_t n)
{
	if (n > _capacity)
	{
		char* flag = new char[n + 1];
		char* der = flag;
        const char* sour = _str;
        while (*sour != '\0')
        {
            *(der++) = *(sour++);
        }
        *der = '\0';
		delete[] _str;
		_str = flag;


		_capacity = n;
	}
}

        由于C++中没有像C语言那样的“realloc”函数,所以需要手动扩容,另开空间,复制,清理原空间。值得注意的是,扩容和以前操作一样,要多开一个空间给‘\0’。不同编译器的操作不一样,这里我们只允许增加,不允许减少。

        7.重构数据个数

void Mynamespace::string::resize(size_t n, char c)
{
	if (n > _size)
	{
		reserve(n > _capacity ? n : _capacity);

		for (size_t i = _size; i < n; i++)
		{
			_str[i] = c;
		}
		_str[n] = '\0';
		_size = n;
	}
	else
	{
		_size -= n;
		_str[_size] = '\0';
	}
}

        在这里,如果n小于当前数据长度,就把n之后的数据擦除,反之就用“c”补齐数据。

       8.判空

bool Mynamespace::string::empty()const
{
	return _size == 0;
}

        9.返回容量(capacity)

size_t Mynamespace::string::capacity()const
{
	return _capacity;
}

        10.返回字符个数

size_t Mynamespace::string::size()const
{
	return _size;
}

         11.尾插一个字符

void Mynamespace::string::push_back(char c)
{
	if (_capacity == _size)
	{
		size_t New = _capacity == 0 ? 4 : _capacity * 2;
		reserve(New);
	}
	_str[_size++] = c;
	_str[_size] = '\0';
}

        尾插逻辑同顺序表。在增加数据之前一定要判读空间是否够用。

        12.追加字符串

void Mynamespace::string::append(const char* str)
{
	if (_size + strlen(str) >= _capacity)
	{
		reserve(_size + strlen(str));
	}
	char* der = _str + _size;
	const char* sour = str;
	while (*sour != '\0')
	{
		*(der++) = *(sour++);
	}

	*der = '\0';

	_size = _size + strlen(str);
}

        这个函数的目的是在原来的字符后追加字符串,都是一系列的拷贝过程。

        13.清除所有数据

void Mynamespace::string::clear()
{
	_size = 0;
	_str[0] = '\0';
}

        这里将size清零,为了防止字符的输出,这里将字符首元素修改为“\0”。

        14.交换数据

void Mynamespace::string::swap(string& s)
{
	Mynamespace::string flags = s;
	s = *this;
	*this = flags;
}

        15.返回第一个字符指针

const char* Mynamespace::string::c_str()const
{
	return _str;
}

        这个操作可以变相的访问私有变量,在有些情况下可以代替友元。

        16.返回字符在string中第一次出现的位置

size_t Mynamespace::string::find(char c, size_t pos) const
{
	char* flag = _str;
	for (size_t i = pos; i < _size; i++)
	{
		if (flag[i] == c)
		{
			return i;
		}
	}
	return npos;
}

        用一个循环来遍历整个字符串,找到相应字符就返回该字符下标,否则返回“-1”的无符号形式。

        17.返回字符串第一次出现的位置

size_t Mynamespace::string::find(const char* s, size_t pos) const
{
	const char* ch = s;
	size_t beginnum = 0;
	for (size_t i = pos; i <= _size - strlen(s); i++)
	{
		beginnum = i;
		for (size_t j = 0; j < strlen(s); j++)
		{
			if (s[j] != _str[i + j])
			{
				break;
			}
			else
			{
				if (j == strlen(s) - 1)
				{
					return beginnum;
				}
				continue;
			}
				
		}

	}
	return npos;
}

        这里用了简单粗暴的办法:为了便于理解直接从第一个位置一次对比,找到之后返回它的坐标。两个for循环导致时间复杂度较高。我们还可以复用第16点的函数,可以简化逻辑。

        18.在指定位置插入字符

Mynamespace::string& Mynamespace::string::insert(size_t pos, char c)
{
	if (_capacity <= _size + 1)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	char* der = &_str[_size];
	const char* sour = &_str[_size - 1];
	for (size_t i = 0; i < _size - pos; i++)
	{
		*(der--) = *(sour--);
	}
	*der = c;
	_size++;
	_str[_size] = '\0';
	return *this;
}

        插入单个字符的逻辑顺序同顺序表。写法多样,但要注意不要越界访问修改,越界访问可能会造成析构的时候报错。

        19.在指定位置插入字符串

Mynamespace::string& Mynamespace::string::insert(size_t pos, const char* str)
{
	for (size_t i = 0; i < strlen(str); i++)
	{
		insert(pos++, str[i]);
	}
	return *this;
}

        这里复用了在指定位置插入字符的函数。逻辑上没有太大的难度,只需遍历插入的字符串即可。

        20.删除指定位置的元素

Mynamespace::string& Mynamespace::string::erase(size_t pos, size_t len)
{

	for (size_t i = pos; i < _size; i++)
	{
		_str[pos++] = _str[i + len];
	}
	_size -= len;
	return *this;
}

        逻辑同顺序表,只需将指定位置之后的数据往前覆盖即可,注意“\0”也需要前移。

        21.+=运算符重载(字符)

        +=一个字符本质上是在末尾加上一个字符,和尾插的逻辑一样,因此可以服用尾插函数

Mynamespace::string& Mynamespace::string::operator+=(char c)
{
	push_back(c);
	return *this;
}

        22.+=运算符重载(字符串)

+=一个字符串本质上是在末尾加上一个字符串,这里可以复用在指定位置插入字符串(或者追加字符串)函数。size指向的位置就是末尾的位置。

Mynamespace::string& Mynamespace::string::operator+=(const char* str)
{
	return insert(_size, str);
}

        23.[ ]运算符重载

char& Mynamespace::string::operator[](size_t index)
{
	return _str[index];
}


const char& Mynamespace::string::operator[](size_t index)const
{
	return _str[index];

        24.>,<,==,<=,>=,!= 运算符重载       

        bool operator<(const string& s)
        {
            return strcmp(_str, s._str) < 0;
        }

        bool operator<=(const string& s)
        {
            return *this < s || *this == s;
        }

        bool operator>(const string& s)
        {
            return !(*this <= s);
        }

        bool operator>=(const string& s)
        {
            return !(*this < s);
        }

        bool operator==(const string& s)
        {
            return strcmp(_str, s._str) == 0;
        }

        bool operator!=(const string& s)
        {
            return !(*this == s);
        }

        这里的函数主要是重载“<,==”的逻辑,使用“strcmp”函数来判断,其他运算符的重载可以根基相关符号的关系来判断。

        25.<<运算符重载

ostream& Mynamespace::operator<<(ostream& _cout, const Mynamespace::string& s)
{
	_cout << s._str;
	return _cout;
}

        直接在函数内部输出字符即可,为了实现连续的输出,所以要返回_cout,即左值:“cout”。

        26.>>运算符重载

istream& Mynamespace::operator>>(istream& _cin, Mynamespace::string& s)
{
	s.clear();
	s.reserve(256);
	char ch = _cin.get();

	while (ch != ' ' && ch != '\n')
	{
		s += ch;  
		ch = _cin.get();

	}
	return _cin;
}

        因为我在测试的时候,相同逻辑,使用“>>”取不到‘\0’,‘\n’,因此会死循环,而且“>>”会忽略空格,这导致如果想输入空格字符,用“>>”是输入不进去的,所以使用“cin”的成员函数“get”,它和C语言的getchar一样,可以一次性读取单个字符,其中包括“\0”和“\n”。

        为了实现连续的输入,因此需要返回左值:“>>”。

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值