C++string类

String类

1. string类的价值

在C语言中,C标准库提供了一系列str开头的库函数,这些库函数可以管理以\0结尾的一些字符的集合,但是这些库函数与字符串是分离的,不符合OOP思想,而且底层空间需要用户自行管理,粗心大意可能会越界访问。而string类则不会出现这样的问题。

在许多OJ题中,有关字符串的题目基本以string类的形式出现;在常规工作中,为了方便快捷,基本都会使用string类,很少使用C语言库中的字符串操作函数。

2. 标准库中的string类

2.1 string类的介绍
  1. 字符串是表示字符序列的类

  2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。

  3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。

  4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。

  5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

    总结:

  6. string是表示字符串的字符串类

  7. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
    3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;

  8. 不能操作多字节或者变长字符的序列

  9. 在使用string类时,必须包含#include头文件以及using namespace std;

2.2 string类的常用接口

由于string类的接口很多,但常用的只有二十多个,因此下面仅介绍部分接口,更多接口请查看文档!

  1. 常见构造函数(constructor)

    constructor功能
    string ()默认构造函数
    string (const char\* s)用c-string构造
    string (const string& s)拷贝构造
    string(size_t n, char c)用n个字符c构造
    string s1;	// 默认构造
    string s2("hello");	// 字符串构造
    string s3(s2);	// 拷贝构造
    
  2. 常见容量操作接口

    函数功能
    size_t size() const;返回字符串有效字符长度
    size_t length() const;返回字符串有效字符长度
    size_t capacity() const;返回总空间大小
    bool empty() const检测是否为空字符串
    void clear()清空有效字符
    void reserve (size_t n = 0);调整空间大小
    void resize (size_t n);
    void resize (size_t n, char c);
    调整有效字符个数

    说明:

    1. sizelength在底层的实现完全相同。历史原因,string比STL先问世,先有length接口,后来STL的容器中都有size接口,string为与STL保持一致,也增加了size接口。
    2. clear仅清空有效数据(改变size),不改变空间大小(capacity)。
    3. reserve可以扩容,但是不能缩容。假设传入的参数是n,当n>capacity时,会扩容,将capacity变为n;其余情况空间不做改变。
    4. resize调整个数n比原来有效字符长度size小,会删除多余的字符(size减小),但空间capacity不变;当调整个数n比原来有效字符长度size大时,会自动填充多余的空间,若capacity小于n,则自动扩容至n。
      (把pos位置直接变成’\0’,并将size变成pos)
  3. 访问和遍历操作

    函数功能
    char& operator[] (size_t pos);
    const char& operator[] (size_t pos) const;
    返回pos位置的字符
    iterator begin();
    const_iterator begin() const;
    获取第一个字符的迭代器
    iterator end();
    const_iterator end() const;
    获取最后一个字符的下一个位置的迭代器
    rbeginrendrbegin获取第一个字符的前一个位置的迭代器
    rend获取最后一个字符的迭代器
    范围forC++11支持的遍历方式,其底层本质是迭代器
    string s1("hello world!");
    cout << s1[0] << endl;	// operator[] - 输出h
    
    string::iterator it = s1.begin();	// begin()和end() - 输出hello world!
    while(it != s1.end())
    {
        cout << *it;
        it++;
    }
    
    string::reverse_iterator rit = s1.rbegin();	// rbegin()和rend() - 输出!dlrow olleh
    while (rit != s1.rend())
    {
        cout << *rit;
        rit++;
    }
    
    for (char ch : s1)	//	范围for - 输出hello world!
    {
    	cout << ch;
    }
    
  4. string类对象的修改操作

    函数功能
    void push_back (char c);在字符串后面尾插一个字符
    append系列在字符串后面追加一串字符
    operator+=在字符串后面追加字符或字符串
    const charT* c_str() const;返回C格式的字符串
    find从pos位置往后找字符串,找到则返回该位置,否则返回npos
    rfind从pos位置往前找字符串,找到则返回该位置,否则返回npos
    string substr (size_t pos = 0, size_t len = npos) const;从pos位置开始,截取len长度的字符串并返回
    string s1;
    s1.push_back('a');
    s1.append(1,'b');
    s1.append("cde");
    s1 += 'f';
    s1 += "ghi";
    cout << s1.c_str() << endl;	// 输出为abcdefghi
    cout << s1[s1.find('e', 0)] << endl; // 输出e
    string newstr = s1.substr(0, 3);
    cout << newstr << endl;	// 输出abc
    

    说明:

    对于尾插,一般使用operator+=比较多,因为它不仅可以尾插一个字符,也可以尾插一串字符串,甚至还能尾插一个string类。

  5. string类的非成员函数

    函数功能说明
    operator+ 在字符串后面加一个字符或字符串,返回一个拷贝。
    尽量少用,因为传值返回,导致深拷贝效率低
    operator>>输入运算符重载
    operator<<输出运算符重载
    getline获取一行字符串(不会因空格或’\0’而终止)
    relational operators比较大小(各种比较运算符的重载)
    swap交换两个string类

3. 模拟实现string类

#include<iostream>
#include<assert.h>

using namespace std;

/*
* 构造/赋值/析构
* begin/end
* size/capacity/clear/empty/resize/reserve/shrink_to_fit
* operator[]/at/back/front
* operator+=/append/push_back/assgin/insert/erase/pop_back
* c_str/find/rfind/substr
* operator+/relation operators/swap/operator>>/operator<</npos
* 
*/

namespace Myspace
{
    class string
    {
        public:
        typedef char* iterator;
        typedef const char* const_iterator;

        public:
        //
        // 构造/赋值/析构
        string(const char* s = "")
            : _size(strlen(s))
                , _capacity(_size)
                , _str(new char[_capacity + 1])
            {
                memcpy(_str, s, _capacity + 1);
            }

        /* 传统写法*/
        string(const string& str)
            : _size(str._size)
                , _capacity(str._capacity)
                , _str(new char[_capacity + 1])
            {
                memcpy(_str, str._str, str._capacity + 1);
            }

        /* 现代写法,有一种情况会出现问题,就是string是"hello\0world",
			此时只会拷贝hello,后面的丢失了。所以还是用传统写法好一点。
		string(const string& str) 
			// 这里要初始化列表处理一下,有的编译器会处理,但有的编译器不会处理
			: _size(0)
			, _capacity(0)
			, _str(nullptr)
		{
			string tmp(str._str);
			swap(tmp);
		}
		*/

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

        string& operator=(char c)
        {
            _str[0] = c;
            _size = _capacity = 1;
            _str[1] = '\0';
            return *this;
        }

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

        //
        // begin/end
        iterator begin()
        {
            return _str;
        }

        const_iterator begin() const
        {
            return _str;
        }

        iterator end()
        {
            return _str + _size + 1;
        }

        const_iterator end() const
        {
            return _str + _size + 1;
        }

        //
        // size/capacity/clear/empty/resize/reserve/shrink_to_fit
        size_t size()
        {
            return _size;
        }

        size_t capacity()
        {
            return _capacity;
        }

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

        bool empty()
        {
            return _size == 0;
        }

        void resize(size_t n, char c = '\0')
        {
            // 1. n < _size
            if (n < _size)
            {
                _size = n;
                _str[_size] = '\0';
            }
            // 2. n > _capacity
            else if (n > _capacity)
            {
                reserve(n);
                memset(_str + _size, c, n - _size);
                _size = n;
            }
            // 3. _size <= n <= _capacity
            else
            {
                memset(_str + _size, c, n - _size);
                _size = n;
            }
        }

        void reserve(size_t n = 0)
        {
            // 1. n > _capacity
            if (n > _capacity)
            {
                char* newspace = new char[n + 1];
                memcpy(newspace, _str, _capacity + 1);
                delete[] _str;
                _str = newspace;
                _capacity = n;
            }
            // 2. 其余情况不做处理
        }

        void shrink_to_fit()
        {
            if (_capacity > _size)
            {
                char* newspace = new char[_size + 1];
                memcpy(newspace, _str, _size + 1);
                delete[] _str;
                _str = newspace;
                _capacity = _size;
            }
        }

        //
        // operator[]/at/back/front
        char& operator[] (size_t pos)
        {
            assert(pos < _size);
            return _str[pos];
        }

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

        char& at(size_t pos)
        {
            assert(pos < _size);
            return _str[pos];
        }

        const char& at(size_t pos) const
        {
            assert(pos < _size);
            return _str[pos];
        }

        char& back()
        {
            return _str[_size - 1];
        }

        const char& back() const
        {
            return _str[_size - 1];
        }

        char& front()
        {
            return _str[0];
        }

        const char& front() const
        {
            return _str[0];
        }

        //
        // operator+=/append/push_back/pop_back/insert/erase
        string& operator+= (const string& str)
        {
            append(str);
            return *this;
        }

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

        string& append(const string& str)
        {
            int len = str._size;
            if (_size + len > _capacity)
            {
                // 扩容
                reserve(_size + len);
            }
            memcpy(_str + _size, str._str, len + 1);
            _size += len;
            return *this;
        }

        void push_back(char c)
        {
            if (_size + 1 > _capacity)
            {
                reserve(_size + 1);
            }
            _str[_size++] = c;
            _str[_size] = '\0';
        }

        void pop_back()
        {
            assert(_size);
            _str[_size - 1] = '\0';
            _size--;
        }

        string& insert(size_t pos, const string& str)
        {
            assert(pos <= _size);
            int len = str._size;
            if (len + _size > _capacity)
            {
                reserve(len + _size);
            }

            size_t i = _size - 1;
            while (i >= pos && i != npos)
            {
                _str[i + len] = _str[i];
                --i;
            }
            _size += len;
            memcpy(_str + pos, str._str, len);
            _str[_size] = '\0';
            return *this;
        }

        string& insert(size_t pos, size_t n, char c)
        {
            assert(pos <= _size);

            if (n + _size > _capacity)
            {
                reserve(n + _size);
            }

            size_t i = _size - 1;
            while (i >= pos && i != npos)
            {
                _str[i + n] = _str[i];
                --i;
            }
            _size += n;
            memset(_str + pos, c, n);
            _str[_size] = '\0';
            return *this;
        }

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

        //
        // c_str/find/rfind/substr
        const char* c_str() const
        {
            return _str;
        }

        size_t find(const string& str, size_t pos = 0) const
        {
            assert(pos < _size);
            char* ptr = strstr(_str + pos, str._str);
            if (!ptr)
                return npos;
            else
                return ptr - _str;
        }

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

        size_t rfind(const string& str, size_t pos = npos) const
        {
            if (pos >= _size || pos == npos)
            {
                pos = _size - 1;
            }
            int fast = pos;
            int slow = pos;
            int cur = str._size - 1;
            while (slow >= str._size - 1)
            {
                while (cur >= 0 && _str[fast] == str[cur])
                {
                    fast--;
                    cur--;
                }
                if (cur >= 0)
                {
                    slow--;
                    fast = slow;
                    cur = str._size - 1;
                }
                else
                {
                    return slow - str._size + 1;
                }
            }
            return npos;
        }

        size_t rfind(char c, size_t pos = npos) const
        {
            if (pos >= _size || pos == npos)
            {
                pos = _size - 1;
            }
            int i = pos;
            while (i-- >= 0)
            {
                if (_str[i] == c)
                    return i;
            }
            return npos;
        }

        string substr(size_t pos = 0, size_t len = npos) const
        {
            assert(pos < _size);
            int end = pos + len - 1;
            if (pos + len > _size || len == npos)
            {
                end = _size - 1; // 不包括'\0'
            }
            string tmp;
            for (int i = pos; i <= end; i++)
            {
                tmp += _str[i];
            }
            return tmp;
        }

        //
        // swap/relation operators/operator>>/operator<</getline/npos
        void swap(string& str)
        {
            std::swap(str._size, _size);
            std::swap(str._capacity, _capacity);
            std::swap(str._str, _str);
        }

        bool operator>(const 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 string& str)
        {
            return _size == str._size && memcmp(_str, str._str, _size) == 0;
        }

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

        bool operator>=(const string& str)
        {
            return *this > str || *this == str;
        }

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

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

        friend istream& operator>>(istream& in, string& str);
        friend ostream& operator<<(ostream& out, const string& str);

        static size_t npos;

        private:
        size_t _size;
        size_t _capacity;
        char* _str;
    };
    size_t string::npos = -1;

    istream& operator>>(istream& in, string& str)
    {
        str.clear();
        char tmp[128] = { 0 };
        char ch = in.get();
        while (ch == ' ' || ch == '\n')
        {
            ch = in.get();
        }

        int i = 0;
        while (ch != ' ' && ch != '\n')
        {
            tmp[i++] = ch;
            ch = in.get();
            if (i == 127)
            {
                str += tmp;
                i = 0;
            }
        }
        if (i)
        {
            tmp[i] = '\0';
            str += tmp;
        }
        return in;
    }

    ostream& operator<<(ostream& out, const string& str)
    {
        for (auto ch : str)
        {
            out << ch;
        }
        return out;
    }
}

4. 深浅拷贝

所谓浅拷贝,就是仅仅拷贝数值,不会考虑是否要开辟空间。
而深拷贝,则会考虑开辟空间,并且把数值和空间的指针一起拷贝。

请看下面两张图:

在这里插入图片描述
在这里插入图片描述

上面的图就很好的阐释了什么是深拷贝、什么是浅拷贝。
浅拷贝:仅拷贝地址,将某块已存在的空间的地址赋值给指针,而不是创建新的空间后再将地址赋值给指针。
深拷贝:创建和原空间一模一样的新空间,空间内容通过拷贝保持一致,然后将新空间的地址赋值给指针。

在string类中,因为存在动态开辟的空间,我们模拟实现string类的时候一定要使用深拷贝,以免浅拷贝出现差错!

5. string类部分接口示例

一、遍历string
  1. cout
    string类后面的’\0’,可以被访问到,只是有些编译器不会显示它。且size()函数不会把它算进去。

  2. operator[ ] 可以访问、修改

int main()
{
	string s1;
	string s2("Hello world");
	s2 += '!';

	cout << s2 << endl;

	// 遍历
	// 1. 下标+[]
	for (size_t i = 0; i <= s2.size(); ++i)
	{
		cout << s2[i] << ' ';
	}
	cout << endl; 
	// 1)其实是访问到'\0'了,只是有些编译器不会显示它

	cout << s2.size() << endl; 
	// 2)可以看出,size()不算'\0'


	// 3)两个[]的区别
	char s3[] = "hello world";
	s3[1];	// -> *(s3 + 1)
	s2[1];	// -> s2.operator[](1)

	return 0;
}
  1. 迭代器

也是一种遍历访问的方法。

  • 像指针一样的类型,有可能是指针,但不一定是指针(封装的指针)。
    begin()开头的位置,end()最后一个数据的下一个位置,也就是\0。
int main()
{
	string s1 = "hello world";
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;
	return 0;
}
  1. 范围for
int main()
{
	string s1("hello world");

	for (auto ch : s1) // for (char ch : s1)
	{
		cout << ch << ' ';
	}
	cout << endl;

	return 0;
}
  • 范围for的底层就是迭代器,所以不支持迭代器的类就不支持范围for,比如说stack(栈先进后出,不支持遍历,不支持迭代器,不支持范围for)

  • 任何容器都支持迭代器,且用法都是类似的。
    对于一些结构,比如链表、树等等,无法用下标+[ ]的方式访问,但是可以用迭代器访问。

int main()
{
	vector <int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	vector<int>::iterator vit = v.begin();
	while (vit != v.end())
	{
		cout << *vit << ' ';
		vit++;
	}
	cout << endl;

	list<int> lt;
	lt.push_back(10);
	lt.push_back(20);
	lt.push_back(30);
	lt.push_back(40);
	list<int>::iterator lit = lt.begin();
	while (lit != lt.end())
	{
		cout << *lit << ' ';
		lit++;
	}
	cout << endl;
	return 0;
}
  • 总结:iterator迭代器,提供了一种统一的方式访问和修改容器的数据。

  • 迭代器可以跟算法进行配合:
    reverse/sort/…
    算法可以通过迭代器去处理容器中的数据。

  • 范围for只能正向遍历,不能反向遍历。

  • 反向迭代器
    reverse_iterator

int main()
{
	string s1("hello world");
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit << ' ';
		++rit;
	}
	cout << endl;

	return 0;
}

rbegin()在最后一个数据位置,rend()在第一个数据的前一个位置。

  • const迭代器
    const_iterator

    const对象只能用const迭代器,防止权限放大

  • const反向迭代器
    const_reverse_iterator

二、容量相关
  • STL属于标准库,string属于标准库,string产生的比较早,它并不在STL的容器中。

  • size和length,一样的。length出来的比较早,当STL出来之后,又加了一个size,后面STL中都是size。

  • max_size,这个接口在不同的编译器中的结果可能是不同的,所以在实际使用中不用它。

  • capacity,VS2022下的PJ版和Linux下的SGI版的结果不一定相同。因为STL仅仅是一个规范,具体底层是如何实现的,每个版本的是不一样的,它们仅仅是实现的功能接口一样,底层是不同的。
    可以测试,在VS下,大概是1.5倍扩容,Linux下大概是2倍扩容。

    int main()
    {
     string s1;
     size_t sz = s1.capacity();
    
     for (int i = 0; i < 200; ++i)
     {
         if (sz != s1.capacity())
         {
             sz = s1.capacity();
             cout << "change capacity: " << sz << endl;
         }
         s1.push_back('c');
     }
     return 0;
    }
    

    同样的一段代码,在Linux下(SGI版本的STL)和微软的VS2022下(P.J.版本的STL),结果是不同的。
    Linux:
    在这里插入图片描述

    VS2022:
    在这里插入图片描述

  • clear,清掉数据,只会让size为0,capacity不会变。因为如果capacity也变了0,那么如果接下来又要用了,又得开辟空间,这样浪费效率。

  • empty,判断空

  • resize,扩容 + 初始化。

    1. 如果 n 小于当前字符串长度,则当前值将缩短为其第一个 n 个字符,删除第 n 个字符之外的字符。capacity不变。
    2. 如果 n 大于当前字符串长度,则通过在末尾插入任意数量的字符来扩展当前内容,以达到 n 的大小。如果指定了 c,则新元素将初始化为 c 的副本,否则,它们是值初始化的字符(空字符)。
  • reserve,申请开辟空间,适用于知道需要多少空间,然后提前开好空间,就不必去扩容了。
    当我输入100时,实际上不一定是开辟100个字节的空间,它会根据编译器自动给我们开辟一个差不多的空间,一定比100大。

    1. 如果 n 大于当前字符串容量,则该函数会导致容器将其容量增加到 n 个字符(或更大)。
    2. 在所有其他情况下,它被视为缩小字符串容量的非约束性请求:容器实现可以自由地进行优化,并使字符串的容量大于 n。
  • strink_to_fit,主动缩容,将容量缩容至合适大小

  • 总结:string基本上不缩容,只有在clear清掉内容之后,再reserve,才会缩容。

三、读取元素
  • operator[ ], 越界了断言

  • at, 越界了抛异常

四、修改
  • append
  • push_back
  • operator+=
  • assign,从第一个位置开始覆盖,重新写入,但capacity不会缩
  • insert
  • erase
  • replace
五、string操作
  • c_str

  • find

    int main()
    {
    	// string url = "https://legacy.cplusplus.com/reference/string/string";
    	string url = "https://www.huya.com/991111";
    
    	size_t pos1 = url.find("://");
    	string protocol;
    	if (pos1 != string::npos)
    	{
    		protocol = url.substr(0, pos1);
    	}
    	cout << protocol << endl;
    
    	size_t pos2 = url.find('/', pos1 + 3);
    	string domain;
    	if (pos2 != string::npos)
    	{
    		domain = url.substr(pos1 + 3, pos2 - pos1 - 3);
    	}
    	cout << domain << endl;
    
    	string uri;
    	uri = url.substr(pos2 + 1);
    	cout << uri << endl;
    	return 0;
    }
    
  • rfind

  • find_first_of,找某个字符串中任意一个,在一个string类中第一次出现的位置。

  • find_last_of,与上面相反,从后往前走

  • find_first_not_of,找不属于某个字符串中任意一个,在一个string类中第一次出现的位置。

  • find_last_not_of,与上面相反

  • substr,切取string类的一段

  • getline,读一行,(遇到空格继续读),也可以自行控制结束标志。
    如果要读一个字符串,它其中有空格的话,用cin是读不到的,遇到空格,就会把剩下的东西放到缓冲区,下次读才能读到剩下的。

  • stoi/stod/…/to_string

  • rfind

  • find_first_of,找某个字符串中任意一个,在一个string类中第一次出现的位置。

  • find_last_of,与上面相反,从后往前走

  • find_first_not_of,找不属于某个字符串中任意一个,在一个string类中第一次出现的位置。

  • find_last_not_of,与上面相反

  • substr,切取string类的一段

  • getline,读一行,(遇到空格继续读),也可以自行控制结束标志。
    如果要读一个字符串,它其中有空格的话,用cin是读不到的,遇到空格,就会把剩下的东西放到缓冲区,下次读才能读到剩下的。

  • stoi/stod/…/to_string

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值