【C++】第七节:string

时间:2024-10-29 20:04:50

前言

STL 的六大组件

1、标准库中的string类

1.1 string类的常用接口说明

  • string类对象的常见构造constructor

void TestString()
{
	string s1; // 构造空的string类对象s1
	string s2("hello C++"); // 用C格式字符串构造string类对象s2
	string s3(5, 'a'); // 用5个字符a构造string类对象s3
	string s4(s3); // 拷贝构造s4
}
  • string类对象的容量操作Capacity

注意:

  1. size() 与 length() 方法底层实现原理完全相同,引入 size() 是为了与其他容器的接口保持一致,一般情况下基本都是用 size()。
  2. clear() 只是将 string 中有效字符清空,不改变底层空间大小。
  3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到 n 个,不同的是当字符个数增多时:resize(n) 用0来填充多出的元素空间,resize(size_t n, char c)用字符 c来填充多出的元素空间。注意:resize 在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
  4. reserve(size_t res_arg=0):为 string 预留空间,不改变有效元素个数,当 reserve 的参数小于 string 的底层空间总大小时,reserver 不会改变容量大小。
  • string类对象的访问及遍历操作

void test_string1()
{
	string s1 = "healo world";

	s1[2] = 'l';

	// 使用迭代器遍历字符串
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

	// 使用反向迭代器遍历字符串
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit << " ";
		rit++;
	}
	cout << endl;

	// 使用范围for循环遍历字符串
	for (auto& ch : s1)
	{
		cout << ch;
	}
	cout << endl;

	// 等同于
	for (string::iterator it = s1.begin(); it != s1.end(); ++it)
	{
		cout << *it;
	}
	cout << endl;

	// 使用范围for修改字符串
	for (auto& ch : s1)
	{
		ch++;
	}
	cout << s1 << endl;
}

  • string类对象的修改操作

注意:

  1. 在 string 尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下 string类的 += 操作用的比较多,+= 操作不仅可以连接单个字符,还可以连接字符串。
  • string类非成员函数

1.2 练习

1.2.1 仅仅反转字母

917. 仅仅反转字母 - 力扣(LeetCode)

class Solution {
public:
    bool isLetter(char s)
    {
        if ((s >= 'a' && s <= 'z') || (s >= 'A' && s <= 'Z'))
            return true;
        else
            return false;
    }

    string reverseOnlyLetters(string s) 
    {
        int begin = 0;
        int end = s.size() - 1;
        while (begin < end)
        {
            // 这里用循环因为可能一次有多个非英文字母的字符
            // 如果字符串全部是非英文字母begin会一直++,所以要加上限制条件
            while (begin < end && !isLetter(s[begin]))
                ++begin;
            while (begin < end && !isLetter(s[end]))
                --end;

            swap(s[begin], s[end]);
            ++begin;
            --end;
        }
        return s;
    }
};

1.2.2 字符串中的第一个唯一字符

387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

提示:不需要过于麻烦,创建一个26大小的数组,把s中的字符存放进去再找只出现一次的字符。

class Solution 
{
public:
    int firstUniqChar(string s) 
    {
        int arr[26] = { 0 };

        for (auto& ch : s)
        {
            arr[ch - 'a']++;
        }
        // 等同于
        /*for (string::iterator it = s.begin(); it < s.end(); it++)
        {
            char& ch = *it;
            arr[ch - 'a']++;
        }*/
        
        for (int i = 0; i < s.size(); i++)
        {
            if (arr[s[i] - 'a'] == 1)
                return i;
        }
        return -1;
    }
};

1.2.3 字符串最后一个单词的长度* 

字符串最后一个单词的长度_牛客题霸_牛客网

int main() 
{
	string str;
	while (getline(cin, str))
	{
		size_t pos = str.rfind(' ');
		cout << str.size() - pos - 1 << endl;
	}
	return 0;
}

1.2.4 验证回文串 

125. 验证回文串 - 力扣(LeetCode)

class Solution {
public:
    bool isLetterOrNumber(char c)
    {
        if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
            return true;
        else
            return false;
    }

    bool isPalindrome(string s) 
    {
        // 大写字母转小写
        for (auto& ch : s)
        {
            if (ch >= 'A' && ch <= 'Z')
                ch += 32;
        }

        int begin = 0;
        int end = s.size() - 1;
        // 下面一段和反转字母的逻辑差不多
        while (begin < end)
        {
            while (begin < end && !isLetterOrNumber(s[begin]))
                ++begin;
                
            while (begin < end && !isLetterOrNumber(s[end]))
                --end;

            if (s[begin] != s[end])
            {
                return false;
            }
            else
            {
                ++begin;
                --end;
            }
        }
        return true;
    }
};

1.2.5 字符串相加 

415. 字符串相加 - 力扣(LeetCode)

class Solution {
public:
    string addStrings(string num1, string num2) 
    {
        string str;
        // 像正常的加法一样从最后一位开始计算
        int end1 = num1.size() - 1;
        int end2 = num2.size() - 1;
        // 进位
        int next = 0;
        // 只有两个字符串都走完了才退出
        while (end1 >= 0 || end2 >= 0)
        {
            // 将字符转成整数
            // num1或者num2的下标大于等于0就转化
            int x1 = end1 >= 0 ? num1[end1] - '0' : 0;
            int x2 = end2 >= 0 ? num2[end2] - '0' : 0;

            int ret = x1 + x2 + next;
            next = ret / 10;
            ret = ret % 10;

            str += ret + '0';
            --end1;
            --end2;
        }
        if (next == 1)
        {
            str += '1';
        }
        reverse(str.begin(), str.end());
        return str;
    }
};

1.2.6 反转字符串*

541. 反转字符串 II - 力扣(LeetCode)

class Solution {
public:
    string reverseStr(string s, int k) 
    {
        int n = s.length();
        for (int i = 0; i < n; i += 2 * k)
        {
            reverse(s.begin() + i, s.begin() + min(i + k, n));
        }
        return s;
    }
};

3、string类的模拟实现

3.1 经典的string类问题

class String
{
public:
	String(const char* str = "")
	{
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};

void TestString()
{
	String s1("hello bit!!!");
	String s2(s1);
}

说明:上述String类没有显式定义拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用 s1 构造 s2 时,编译器会调用默认的拷贝构造。最终导致的问题是 s1、s2 共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。

3.2 深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

3.2.1 传统写法的string类

class string
{
public:
	string(const string& s)
	{
		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}

	string& operator=(const string& s)
	{
		if (this != &s)
		{
			char* tmp = new char[s._capacity + 1];
			strcpy(tmp, s._str);

			delete[] _str;
			_str = tmp;
			_size = s._size;
			_capacity = s._capacity;
		}
		return *this;
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

3.3 模拟实现

3.3.1 string.h

#pragma once
namespace yf
{
	class string
	{
	public:
		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;
		}

		// 初始化列表总是在构造函数体之前执行
		// 成员变量在初始化列表中的初始化顺序是根据它们在类定义中的声明顺序执行的
		string(const char* str = "") // 这个""里默认就有一个\0
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str); // strcpy函数复制时包括\0

		}

		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		// 拷贝构造函数
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			// 这里调用构造函数,开辟一块新空间给tmp
			string tmp(s._str);
			//this->swap(tmp);
			swap(tmp);
		}

		/*string& operator=(const string& s)
		{
			if (this != &s)
			{
				string tmp(s);
				swap(tmp);
			}

			return *this;
		}*/

		// 直接简化成
		string& operator=(string tmp)
		{
			swap(tmp);

			return *this;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);

			return _str[pos];
		}

		const char& operator[](size_t pos) const
		{
			assert(pos < _size);

			return _str[pos];
		}

		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}

		const char* c_str() const
		{
			return _str;
		}

		// 预留空间
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}

		// 修改有效字符个数(_size),多出的空间用ch填充
		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_str[n] = '\0';
				_size = n;
			}
			else
			{
				reserve(n);
				while (_size < n)
				{
					_str[_size] = ch;
					++_size;
				}
				_str[_size] = '\0';
			}
		}

		size_t find(char ch, size_t pos = 0)
		{
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}
			return npos;
		}

		size_t find(const char* sub, size_t pos = 0)
		{
			// strstr查找子字符串,找到返回首次出现的指针,未找到返回null
			const char* p = strstr(_str + pos, sub);
			if (p)
			{
				return p - _str;
			}
			else
			{
				return npos;
			}
		}

		// 从pos位置开始截取len个字符将其返回
		string substr(size_t pos, size_t len = npos)
		{
			string s;
			size_t end = pos + len;
			if (len == npos || end > _size)
			{
				len = _size - pos; 
				end = _size;
			}
			reserve(len);
			for (size_t i = pos; i < end; i++)
			{
				s += _str[i];
			}

			return s;
		}

		// 尾插字符
		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			++ _size;
			_str[_size] = '\0';
		}

		// 追加一个字符串
		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, str);
			_size += len;
		}

		// 将ch插入到字符串的pos位置之前
		void insert(size_t pos, char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			size_t end = _size + 1; // 因为\0也要移过去
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				--end;
			}
			_str[pos] = ch;
			_size++;
		}

		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);

			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			int end = _size;
			// 如果pos为0,最后一次--end为-1,对于size_t类型是很大的数字,pos不可能比它大
			// 就造成了死循环
			while (end >= (int)pos)
			{
				_str[end + len] = _str[end];
				--end;
			}
			strncpy(_str + pos, str, len);
			_size += len;
		}

		// 删除从pos位置开始的字符串
		void erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				int begin = pos + len;
				while (begin <= _size)
				{
					_str[begin - len] = _str[begin];
					++begin;
				}
				_size -= len;
			}
		}

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

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

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

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

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

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

		string& operator+=(char ch)
		{
			push_back(ch);

			return *this;
		}

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

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

		// 清空有效字符
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	public:
		// 静态成员变量不能给缺省值,必须在类外初始化
		const static size_t npos;
	};

	const size_t string::npos = -1;

	// 输出运算符重载
	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto& ch : s)
		{
			cout << ch;
		}

		return out;
	}

	// 输入运算符重载(空间利用率较高)
	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buff[129];
		size_t i = 0; // 计数器,跟踪buff数组
		char ch;
		ch = in.get(); // 从输入流中读取字符,返回读取到的字符,如果到达文件末尾返回EOF
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 128)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}

	void test_string1()
	{
		string s1("hello world");
		string s2;

		// 方法1:
		for (size_t i = 0; i < s1.size(); i++)
		{
			cout << s1[i] << " ";
		}
		cout << endl;

		// 方法2:
		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			(*it)++; // 修改s1的值
			cout << *it << " ";
			it++;
		}
		cout << endl;

		// 方法3:
		for (auto& ch : s1)
		{
			ch++; // 修改s1的值,需要加上引用
			cout << ch << " ";
		}
		cout << endl;

		// 方法4:
		cout << s1.c_str() << endl;
	}

	void test_string2()
	{
		string s1("hello world");
		cout << s1.c_str() << endl;

		s1.push_back(' ');
		s1.append("hate world");
		cout << s1.c_str() << endl;

		s1 += '$';
		s1 += "&&&&&&";
		cout << s1.c_str() << endl;
	}

	void test_string3()
	{
		string s1("hello world");

		s1.insert(5, '*');
		cout << s1 << endl;

		s1.insert(0, '!');
		cout << s1 << endl;

		s1.erase(6, 3);
		cout << s1 << endl;
	}

	void test_string4()
	{
		string s1("hello world");
		string s2("hello world");

		cout << (s1 <= s2) << endl;

		s1[0] = 'z';
		cout << (s1 <= s2) << endl;

		cin >> s1;
		cout << s1 << endl;
	}

	void test_string5()
	{
		string s1("hello world");

		s1.resize(5);
		cout << s1 << endl;

		s1.resize(25, 'x');
		cout << s1 << endl;
	}

	void test_string6()
	{
		string s1("hello world");
		string s2 = s1;
		cout << s1 << endl;
		cout << s2 << endl;

		string s3 = ("xxxxxxxxxxxxxxx");
		s2 = s3;
		cout << s2 << endl;
		cout << s3 << endl;
	}
}

3.3.2 test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
#include <string>
using namespace std;
// 编译器默认只会向上找,所以要放在命名空间std下面
#include "string.h"

int main()
{
	yf::test_string6();
	return 0;
}