目录
一. string 类介绍
二. string 的简单实现
1. 类内成员变量
2. Member functions
string
~string
operator=
string(const string& str)
3. Capacity
size
capacity
empty
clear
reserve
resize
4.Modifiers
push_back
append
operator+=
insert
erase
swap
5. Iterator
begin
end
7. Element Access
operator[ ]
8. String operations
c_str
find
substr
9. Non-member function overloads
operator<<
operator>>
10. 字符串比较
一. string 类介绍
C++ 的 string 类是标准库中的一个重要类,它提供了一种方便和灵活的字符串处理方式。使用 string 类,可以方便地创建、操作和管理字符串,而无需手动管理底层的内存。
以下是 string 类的一些主要特点和功能:
1. 字符串的存储和管理:string 类提供了存储任意长度字符串的能力,它会自动管理内存,自动调整容量,并提供了对字符串的访问和修改方法。
2. 字符串的赋值和拷贝:可以通过 string 类的构造函数或赋值运算符将字符串赋值给 string 对象,也可以使用 string 类的成员函数对字符串进行拷贝、插入和连接等操作。
3. 字符串的比较和操作:string 类提供了一系列成员函数和运算符,用于比较和操作字符串。可以比较字符串的大小、判断两个字符串是否相等,以及执行字符串的截取、查找、替换等操作。
4. 字符串的迭代访问:可以像访问数组一样通过下标或迭代器来访问 string 类中的字符。
5. 字符串的输入输出:可以使用标准输入输出流来读取和输出 string 对象。
总之,string 类简化了 C++ 中字符串的处理,提供了更高级、更方便的字符串操作方式,避免了手动管理内存和处理字符串的繁琐性。它是一个常用且强大的工具,特别适用于处理动态长度的字符串。
二. string 的简单实现
1. 类内成员变量
- 我们的string 类需要存储字符串,所以我们当然需要一个指针指向一个空间,里面保存的是字符串
- 我们还需要对保存的字符串需要统计个数等,所以我们还需要一个 size 来统计当前对象的大小
- 我们既然是一个字符串管理的类,所以我们当然也需要一个可以当前可以存贮的最大容量 capacity
private:
		size_t _size;
		size_t _capacity;
		char* _str;
		static size_t npos; //后面会用到,暂时不用管2. Member functions
string
在我们的库里面 sring 的构造函数有很多种,我们只挑一两种常用的来实现
我们要怎么样实现?
- 我们的 string 的构造的话,我们会使用一个字符串去构造,所以我们需要一个是字符串的构造函数
- 我们还可能会用一个无参的构造函数,所以我们需要一个无参的构造函数
由于我们现在要实现上面的两个构造函数,其中一个就是字符串构造,还有一个是无参的,而我们的C++中有缺省值,所以我们可以写成一个函数,让无参的构造变成一个缺省,如果我们什么都不传的话,那么就是默认缺省,而我们的缺省值给什么合适呢?当然是给一个空字符串,详细见下面实现
        string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			//strcpy(_str, str);
			memcpy(_str, str, _size + 1);
		}~string
析构函数就是我们一个对象在生命周期结束的时候自动调用的函数,而我们的 string 里面的空间是 new 出来的,所以我们是有资源需要清理的,所以我们也需要写析构函数,而对于 string 来说析构函数就是比较简单的,我们只需要对里面的资源进行释放就可以了
		~string()
		{
			_size = 0;
			_capacity = 0;
			delete[] _str;
		}operator=
赋值重载就是我们用一个已有的对象对另一个已有的对象进行赋值,在赋值过程中,我们需要对被赋值的对象进行重新开辟空间,新开的空间和赋值的对象一样大,然后我们将赋值对象的字符串拷贝到新开的空间上,然后我们对之前的空间进行释放,然后我们把行开的空间给给被赋值的对象,然后修改其的 size 和 capacity,当然既然是赋值,我们需要对其进行返回,所以我们还需要将 *this 进行返回
		string& operator=(const string& str)
		{
			if (this != &str)
			{
				char* tmp = new char[str._capacity + 1];
				memcpy(tmp, str._str, str._size + 1);
				delete[] _str;
				_str = tmp;
				_size = str._size;
				_capacity = str._capacity;
			}
			return *this;
		}string(const string& str)
拷贝构造函数,我们是用一个已有的对象对一个还未定义的对象进行初始化,我们需要对未定义的对象进行开空间,然后将已有对象的字符串拷贝过去,然后初始话未定义对象的 size capacity
		string(const string& str)
		{
			_str = new char[str._capacity + 1];
			memcpy(_str, str._str, str._size + 1);
			_size = str._size;
			_capacity = str._capacity;
		}其实这里的赋值重载和拷贝构造还可以写的更简便一些,但是这里就不讲了
3. Capacity
capacity 就是我们常用的关于 string 里面的 size 或者 capacity 或者判断是否为空 empty ...
size
size 的实现很简单,我们只需要返回该对象中的 size 大小就好了
		size_t size() const
		{
			return _size;
		}capacity
capacity 也只需要返回其对应的 capacity即可
		size_t capacity() const
		{
			return _capacity;
		}empty
empty 是判断我们的字符串是否为空的,如果为空,我们返回 true 否则返回 false
		bool empty() const
		{
			return _size == 0;
		}clear
clear 就是我们清理掉我们当前对象里面的所有字符,由于我们的字符串的结尾是 '\0',所以我们可以直接将 '\0'放在 0 位置,然后我们对 size 也进行修改
		void clear()
		{
			_size = 0;
			_str[_size] = '\0';
		}reserve
reserve 就是我们申请空间,但是我们只申请,所以我们只会改变 capacity,当然我们的申请空间就是重新开一块空间,然后将之前字符串的内容拷贝到新的空间,然后释放旧的空间,申请结束后记得修改 capacity
		void reserve(size_t n = 0)
		{
			if (n <= _capacity) return;
			char* tmp = new char[n + 1];
			memcpy(tmp, _str, _size + 1);
			delete[] _str;
			_capacity = n;
			_str = tmp;
		}resize
resize 其中的一个作用是申请空间,但是我们还回修改其的 size 我们库里面的 resize 有两个重载函数,其中一个是只有一个变量 n,另外一个还有一个变量 c

其中第一个就是只设置 size 如果 n > 当前的 capacity 的话,那么就会扩容,n < size 就会在 size 位置截断,然后修改 size 为 n, 如果是第二个函数的话,其中一个不同点就是 n > size 将原本的size 前的字符保留,如果超出本来的大小那么就用 c 来补充
详细见代码
		void resize(size_t n, const char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				reserve(n);
				for (int i = _size; i < n; ++i)
				{
					_str[i] = ch;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}4.Modifiers
modify 里面的函数就是对 string 里面的一种修改
push_back
push_back 就是尾插,在插入之前,我们需要判断我们的容量是否充足,如果不充足的话,我们需要进行扩容,扩容后我们就直接插入
		void push_back(const char c)
		{
			if (_size == _capacity)
			{
				//扩容
				reserve(_capacity * 2);
			}
			_str[_size++] = c;
			_str[_size] = '\0';
		}append
append 就是在字符串后面追加一个字符串,当然如果容量不够的话,我们也需要进行扩容
		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				//扩容
				reserve(_size + len);
			}
			memcpy(_str + _size, str, len + 1);
			_size += len;
		}operator+=
在 string 里面 += 是一个很好用的函数,我们既可以加等一个字符,也可以加等一个字符串,而加等的实现我们可以复用我们的 push_back 和 append
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		string& operator+=(const char c)
		{
			push_back(c);
			return *this;
		}insert
insert 就是随机位置插入,而我们在插入之前我们需要进行判断插入位置是否合法,然后我们就需要进行判断是否需要扩容,以及扩容多少,如果我们的插入位置在中间或者前面的话我们还需要进行对里面的元素进行挪动,等将位置挪动结束后我们需要将要插入的字符串插入进去
		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			
			size_t len = strlen(str);
			if (len + _size > _capacity)
			{
				//扩容
				reserve(len + _size);
			}
			size_t end = _size;
			//挪动数据
			while (end >= pos && end != npos) // 由于我们的 end 和 pos 都是 size_t 类型的所
            以最小值就是 0, 那么这是如果我们 的 pos 位置就是 0 的话我们的 end 需要小于 0 才能
            结束,但是 size_t  类型不会小于0 , 所以这时就会陷入死循环,当我们的 end 现在是 0
            时,那么下一次就是 size_t 类型的最大值,所以我们当 end 为 npos 时就停止
			{
				_str[end + len] = _str[end--];
			}
			//插入数据
			for (size_t i = pos; i < pos + len; ++i)
			{
				_str[i] = str[i - pos];
			}
			_size += len;
		}我们看到我们的 insert 函数里面还有一个参数的缺省值为 npos 我们的 npos 是一个size_t 类型的最大值,但是该参数有什么用?
erase
erase 就是删除指定位置的值,删除的是中间的元素或者是前面的元素的话,我们也是需要对删除元素后面的元素进行挪动的
		void erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
			size_t n = len;
			if (len == npos || pos + len > _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				size_t end = pos;
				while (end + len <= _size)
				{
					_str[end] = _str[end + len];
					++end;
				}
				_size -= len;
			}
		}我们看到我们的 erase 函数里面还有一个参数的缺省值为 npos 我们的 npos 是一个size_t 类型的最大值,但是该参数有什么用?
就是如果我们不传这个参数的话,我们的默认就是从 pos 位置开始全部删除 。
swap
string 类型的 swap 函数,就是简单的交换 string 对象里面的成员变量
		void swap(lxy::string str)
		{
			std::swap(_str, str._str);
			std::swap(_size, str._size);
			std::swap(_capacity, str._capacity);
		}5. Iterator
iterator是迭代器,我们使用起来很方便,而 string 的迭代器实现比较简单,就是原生指针,而 iterator 就是一个 typedef 得到的,我们的变量调用的时候,我们的变量即可能是 const 也可能是普通的,所以我们还需要实现一个 const 的函数,const的变量由于不能被修改,所以我们的*this指针也是 const 的,所以我们在调用的时候默认的 this 指针由于权限问题const 的对象只能调用 const 的函数,所以我们还需要一个 const 的迭代器
		typedef char* iterator;
		typedef const char* const_iterator;begin
begin 就是返回字符串的起始位置,所以我们只要返回 str 指针就好
		iterator begin()
		{
			return _str;
		}
		const_iterator begin() const
		{
			return _str;
		}end
end 就是返回字符串最后一个位置的下一个位置,也是'\0'位置
		iterator end()
		{
			return _str + _size;
		}
		const_iterator end() const
		{
			return _str + _size;
		}7. Element Access
这是对里面元素的访问,这里只将一个 operator[ ]
operator[ ]
operator[ ]是对方括号的重载,可以让我们的 string 类可以像我们的数组一样用下标访问,而我的字符串本来就可以用下标访问,所以我们直接用下标访问字符串就可以了
		char& operator[](size_t pos)
		{
			assert(_size > pos);
			return _str[pos];
		}
		const char& operator[](size_t pos) const
		{
			assert(_size > pos);
			return _str[pos];
		}8. String operations
这个是我们对 string 操作
c_str
c_str 就是返回我们 C形式的字符串
		char* c_str() const
		{
			return _str;
		}find
find 就是返回第一次遇到要查找的字符或者字符串,如果没有,返回 npos
		size_t find(const char ch, size_t pos = 0)
		{
			assert(pos < _size);
			for (size_t i = 0; i < _size; ++i)
			{
				if (_str[i] == ch) return i;
			}
			//没有找到,返会 npos
			return npos;
		}
		size_t find(const char* str, size_t pos = 0)
		{
			assert(pos < _size);
			char* ptr = strstr(_str + pos, str);
			if (ptr)
			{
				return ptr - _str;
			}
			else return npos;
		}substr
substr 是对一个 string 进行截取,然后返回一个新的字符串,该函数也是从 pos 位置开始截取,然后我对截取的位置进行定义一个新的 string 对象,然后保存这些字符,最后返回该对象
		string substr(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
			int n = len;
			string tmp;
			if (len == npos || pos + len > _size)
			{
				return _str + pos;
			}
			else
			{
				for (size_t i = pos; i < pos + len; ++i)
				{
					tmp += _str[i];
				}
			}
			return tmp;
		}9. Non-member function overloads
非成员函数
我们的 string 如果我们像打印的话,我们只能调用 c_str 打印,但是为了方便我们还想要直接可以使用流插入和重载
operator<<
流插入,我们只需要打印出其str里面的值就好了,但是我们需要打印到 size 位置,而为了我们可以连续的打印,我们还需要返回我们的 cout
	ostream& operator<<(ostream& out, const lxy::string& str)
	{
		for (char ch : str)
			out << ch;
		return out;
	}operator>>
流提取,我们可以之间向 string 类对象输入,但是我们的 cin 如果遇到 空格 或者 换行的话就会停止,所以我们需要遇到 空格换行就停止,但是我们的 cin 又读不懂空格和换行,因为我们的cin默认是以空格和换行座位字符的分隔标志,所以我们需要使用 instream 里面的 get函数,该函数可以读到 空格和换行,而我们在一个输入的字符串中可能会遇到前导空格和换行,这些我们都是需要去掉的,我们插入完之后我们还是可能会连续输入,所以我们还是需要返回 in
	istream& operator>>(istream& in, lxy::string& str)
	{
		char ch = '\0';
		ch = in.get();
		//去除前面的空格或者换行
		while (ch == ' ' || ch == '\n') ch = in.get();
		
		char buffer[128] = { 0 };
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buffer[i++] = ch;
			if (i == 127)
			{
				buffer[i] = '\0';
				i = 0;
				str += buffer;
			}
			ch = in.get();
		}
		if (i)
		{
			buffer[i] = '\0';
			str += buffer;
		}
		return in;
	}10. 字符串比较
字符串比较是比较简单的,我们只要写出两个就可以进行复用,我们的字符串比较还可以复用 memcmp函数,但是还有一些细节,这里就不多说了
		bool operator<(const lxy::string& str)
		{
			int ret = memcmp(_str, str._str, _size < str._size ? _size : str._size);
			return ret == 0 ? _size < str._size : ret < 0;
		}
		bool operator==(const lxy::string& str)
		{
			return _size == str._size && memcmp(_str, str._str, _size);
		}
		bool operator<=(const lxy::string& str)
		{
			return *this < str || *this == str;
		}
		bool operator>(const lxy::string& str)
		{
			return !(*this <= str);
		}
		bool operator>=(const lxy::string& str)
		{
			return !(*this < str);
		}
		bool operator!=(const lxy::string& str)
		{
			return !(*this == str);
		}所有代码 https://gitee.com/naxxkuku/bit_c 在这个gitee仓库里面,需要的话可以自取


















