• 万字string类总结



    目录

    一、string类的介绍

    二、string类的常用接口

    1、构造函数

    2. string类对象的容量操作

    3. string类对象的访问及遍历操作

    4. string类对象的修改操作 (重点)

    5. string类非成员函数

    6. vs和g++下string结构的说明

    三、string类的模拟

    1. 浅拷贝问题

    2. 深拷贝

    3. string类常用库函数的实现

    4、写时拷贝(了解)


    一、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)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。 
    总结:
    1. string是表示字符串的字符串类
    2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
    3. string在底层实际是:basic_string模板类的别名,typedef basic_stringstring;
    4. 不能操作多字节或者变长字符的序列。
    注:在使用string类时,必须包含#include头文件以及using namespace std;
    详细文章可以参考:string - C++ Reference (cplusplus.com)


    二、string类的常用接口

    1、构造函数

    函数功能
    string( )构造一个空的string类对象,即空字符串
    string(const char* s)用字符串来构造一个string类对象
    string(size_t n, char c)
    string 类对象中包含 n 个字符 c
    string(const string&s) 
    拷贝构造函数

    代码:

    1. #include
    2. #include
    3. using namespace std;
    4. int main()
    5. {
    6. string s1;
    7. string s2("hello qingshan");
    8. string s3(3, 'x');
    9. string s4(s3);
    10. return 0;
    11. }

    2. string类对象的容量操作

    函数功能
    size返回字符串有效长度
    capacity返回空间总大小
    length返回字符串有效长度
    empty
    检测字符串是否为空串,是返回true,否则返回false
    clear清空有效字符
    reverse为字符串预留空间
    resize
    将有效字符的个数该成n个,多出的空间用字符c填充

    size length

    其实效果相同,只是由于某些原因创造出了一些多余的接口。 

    capacity

    string底层原理是一个字符数组,capacity代表的就是这个字符数组的容量大小。

    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. cout << s2.size() << " " << s2.length() << endl;
    6. cout << s2.capacity() << endl;
    7. return 0;
    8. }

    clear

    就是将string中的字符全部清空。

     可以看到clear之后s2变成了空。

    reverse

    一般是开更大的空间,值得说的是,一般都是异地扩容,将string类对象的容量扩容到指定个数(或者更大)。如果想要缩容的话,是不支持的。

    resize

    分为三种,重置的大小大于容量,那么就需要扩容,然后将扩容后的空间用字符c填充;如果小于容量但是大于有效字符长度,那么就直接用字符填充;如果小于有效字符长度,那么就直接缩减长度。(具体后文实现的时候会详细叙述)

    注意:resize的第二个参数是将有效空间的字符都变成第二个参数中的字符。

    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. cout << s2.size() << " " << s2.length() << endl;
    6. cout << s2.capacity() << endl;
    7. s2.clear();
    8. s2.reserve(20);
    9. s2.resize(20, 'q');
    10. cout << s2.capacity() << endl;
    11. cout << s2 << endl;
    12. return 0;
    13. }

     这里编译器是按照2倍数扩容了。

      

    3. string类对象的访问及遍历操作

    operator[ ]

     返回pos位置的字符

    代码:

    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. cout << s2[1] << endl;
    6. return 0;
    7. }

       

    begin+ end

    begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器

    一般来说迭代器可能指针,但有时候也可能不是。

    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. string::iterator it = s2.begin();
    6. while (it != s2.end())
    7. {
    8. cout << *it << " ";
    9. it++;
    10. }
    11. cout << endl;
    12. return 0;
    13. }

     迭代器就是遍历的另一种方式

      

    rbegin + rend

    begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
    代码:
    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. string::reverse_iterator it = s2.rbegin();
    6. while (it != s2.rend())
    7. {
    8. cout << *it << " ";
    9. it++;
    10. }
    11. cout << endl;
    12. return 0;
    13. }

     这其实调用的是反向迭代器,所以遍历的时候也是反向遍历。

      

    范围for

    C++11支持更简洁的范围for的新遍历方式
    代码:
    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. for (auto ch : s2)
    6. {
    7. cout << ch << " ";
    8. }
    9. cout << endl;
    10. return 0;
    11. }

    ch每次取一个s1中的元素。

    其实,范围for看着很神奇,底层用的也是迭代器。

     

    4. string类对象的修改操作 (重点)

    push_back

    尾部插入字符

     
     

    append

    在尾部追加

     可以看到用法有很多,最常用的其实就是追加字符和追加字符串。

    代码:

    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. s2.append("!");
    6. s2.append("hahaha");
    7. cout << s2 << endl;
    8. return 0;
    9. }

       

    opeartor +=

    在字符串后追加

     追加分为三种:1、字符。 2、字符数组。3、字符串。

      

    c_str

    返回c格式字符串

    也就是返回底层的那个指向字符数组的字符指针

    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. cout << s2.c_str() << endl;
    6. return 0;
    7. }

       

    find函数+ npos

    find是查找函数,npos是string类型中的值为-1的 size_t类型的数,size_t其实是无符号整型。

     查找一般默认是从0位置开始。找到了返回该位置,没找到返回npos。

    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. cout << s2.find("n") << endl;
    6. return 0;
    7. }

       

    rfind

    反向查找所要找的内容。
     
    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. cout << s2.rfind("n") << endl;
    6. return 0;
    7. }

     这里返回的pos还是按照正向顺序来的,并不是反向从0开始数。

      

    substr

    在str中从pos位置开始,截取n个字符,然后将其返回
    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. size_t pos = s2.rfind("q");
    6. cout << s2.substr(pos, 8) << endl;
    7. return 0;
    8. }

       

    5. string类非成员函数

    operator+

    尽量少用,因为传值返回,导致深拷贝效率低
      
     

    operator>> 

    >>重载之后我们就可以直接通过cin向string类对象中输入数据了,但是不能有空格,空格会自动截断。

    operator<< 

    <<重载之后就可以直接通过cout输出string类对象的内容了。

    getline 

    获取一行内容

     第一个参数必须是输入流,第二个才是填string类对象。

    代码:

    1. int main()
    2. {
    3. string s1;
    4. string s2("hello qingshan");
    5. getline(cin, s1);
    6. cout << s1 << endl;
    7. return 0;
    8. }

      

    relational operators

    也就是string类对象的比较函数

    代码:

    1. #include
    2. #include
    3. int main ()
    4. {
    5. std::string foo = "alpha";
    6. std::string bar = "beta";
    7. if (foo==bar) std::cout << "foo and bar are equal\n";
    8. if (foo!=bar) std::cout << "foo and bar are not equal\n";
    9. if (foo< bar) std::cout << "foo is less than bar\n";
    10. if (foo> bar) std::cout << "foo is greater than bar\n";
    11. if (foo<=bar) std::cout << "foo is less than or equal to bar\n";
    12. if (foo>=bar) std::cout << "foo is greater than or equal to bar\n";
    13. return 0;
    14. }

    6. vs和g++下string结构的说明

    注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
    · vs下string的结构
    string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字
    符串的存储空间:
    当字符串长度小于16时,使用内部固定的字符数组来存放
    当字符串长度大于等于16时,从堆上开辟空间
    1. union _Bxty
    2. { // storage for small buffer or pointer to larger one
    3. value_type _Buf[_BUF_SIZE];
    4. pointer _Ptr;
    5. char _Alias[_BUF_SIZE]; // to permit aliasing
    6. } _Bx;
    这种设计也是有一定道理的,大多数情况下字符串的长度都小于 16 ,那 string 对象创建好之后,内
    部已经有了 16 个字符数组的固定空间,不需要通过堆创建,效率高。
    其次:还有 一个 size_t 字段保存字符串长度,一个 size_t 字段保存从堆上开辟空间总的容量
    最后:还 有一个指针 做一些其他事情。
    故总共占 16+4+4+4=28 个字节。

    · g++下string的结构

    G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指
    针将来指向一块堆空间,内部包含了如下字段:
            ` 空间总大小
            ` 字符串有效长度
            ` 引用计数
    1. struct _Rep_base
    2. {
    3. size_type _M_length;
    4. size_type _M_capacity;
    5. _Atomic_word _M_refcount;
    6. };

    三、string类的模拟

    1. 浅拷贝问题

    浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

      

    我们看下面这段代码:

    1. class String
    2. {
    3. public:
    4. String(const char* str = "")
    5. {
    6. if (nullptr == str)
    7. {
    8. assert(false);
    9. return;
    10. }
    11. _str = new char[strlen(str) + 1];
    12. strcpy(_str, str);
    13. }
    14. ~String()
    15. {
    16. if (_str)
    17. {
    18. delete[] _str;
    19. _str = nullptr;
    20. }
    21. }
    22. private:
    23. char* _str;
    24. };
    25. // 测试
    26. void TestString()
    27. {
    28. String s1("hello bit!!!");
    29. String s2(s1);
    30. }

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

      

    2. 深拷贝

    如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
    1. //s2(s1)
    2. string(const string& s)
    3. {
    4. _str = new char[s._capacity + 1];
    5. _capacity = s._capacity;
    6. _size = s._size;
    7. strcpy(_str, s._str);
    8. }

    其实也就是调用拷贝构造函数的时候给新创的对象开辟一块空间。

      

    3. string类常用库函数的实现

    先确定string类私有的成员变量为

    _str指的是开辟的字符数组, _size代表有效字符个数,_capacity代表字符数组中的容量。npos则是无符号整型的最大值。为什么static的的成员可以在类内定义呢?这是因为C++标准规定了const类型的成员可以在类内给一个初始值。

    构造函数 、拷贝构造、析构函数

    1. string(const char* str = "")
    2. {
    3. _size = strlen(str);
    4. _capacity = _size;
    5. _str = new char[_capacity + 1];
    6. strcpy(_str, str);
    7. }
    8. string(const string& s)
    9. {
    10. _str = new char[s._capacity + 1];
    11. _capacity = s._capacity;
    12. _size = s._size;
    13. strcpy(_str, s._str);
    14. }
    15. ~string()
    16. {
    17. delete[] _str;
    18. _str = nullptr;
    19. _capacity = _size = 0;
    20. }

    获取size和capacity

    1. size_t size() const
    2. {
    3. return _size;
    4. }
    5. size_t capacity()const
    6. {
    7. return _capacity;
    8. }

      

    获取c_str

    1. const char* c_str() const
    2. {
    3. return _str;
    4. }

    获取第n个元素(重载 [ ])

    这里有可能是const类型的对象和普通的对象调用,而普通对象需要对数据进行修改,所以这里将函数重载一下。

    1. //普通对象
    2. char& operator[](size_t pos)
    3. {
    4. assert(pos < _size);
    5. return _str[pos];
    6. }
    7. //const对象
    8. const char& operator[](size_t pos) const
    9. {
    10. assert(pos < _size);
    11. return _str[pos];
    12. }

    resize和reserve的实现

    因为reserve只做扩容操作,所以我们需要判断传入的开辟大小是否大于本身拥有的空间容量

    而resize就需要分情况了,如果是是扩容,就先用reserve开辟空间,然后默认填入 '\0' 到开辟的空间中。如果是缩小有效字符长度,直接在该位置填入 '\0' 即可。

    1. void reserve(size_t n)
    2. {
    3. //c++中扩容只能重新开辟空间拷贝过去
    4. if (n > _capacity)
    5. {
    6. char* temp = new char[n + 1];
    7. //拷贝过来再删除
    8. strcpy(temp, _str);
    9. delete[] _str;
    10. _str = temp;
    11. _capacity = n;
    12. }
    13. }
    14. void resize(size_t n, char ch = '\0')
    15. {
    16. if (n > _size)
    17. {
    18. reserve(n);
    19. for (int i = _size; i > _size; --i)
    20. {
    21. _str[i] = ch;
    22. }
    23. _size = n;
    24. _str[_size] = '\0';
    25. }
    26. else
    27. {
    28. _str[n] = '\0';
    29. _size = n;
    30. }
    31. }

    迭代器

    迭代器一般情况下都是指针,但不全都是。在string类中就是一个char*类型,只不过被我们用typedef包装成了iterator。

    begin() 返回字符数组的开头,end() 返回字符数组的尾部。

    1. typedef char* iterator;
    2. iterator begin()
    3. {
    4. return _str;
    5. }
    6. iterator end()
    7. {
    8. return _str + _size;
    9. }

       

    push_back、append、+=

    尾插的话还是经典的先检查再插入数据。

    append需要先算好扩容后的空间大小然后扩容,最后strncpy把需要追加的字符串追加到原有的字符串后面就行了。重载+=号服用前面两个函数就够了。

    1. void push_back(char ch)
    2. {
    3. if (_size == _capacity)
    4. {
    5. int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
    6. reserve(newcapacity);
    7. _capacity = newcapacity;
    8. }
    9. _str[_size] = ch;
    10. ++_size;
    11. //结尾加上\0
    12. _str[_size] = '\0';
    13. }
    14. void append(const char* str)
    15. {
    16. size_t len = strlen(str);
    17. //如果扩容了两倍也有可能超出
    18. if (_size + len > _capacity)
    19. {
    20. reserve(_size + len + 1);
    21. }
    22. strncpy(_str + _size, str, len);
    23. _size += len;
    24. }
    25. string& operator+=(const char ch)
    26. {
    27. push_back(ch);
    28. return *this;
    29. }
    30. string& operator+=(const char* str)
    31. {
    32. append(str);
    33. return *this;
    34. }

    insert的实现

    有两种情况:插入字符和插入字符串。

    插入字符需要先检查扩容,然后挪动数据,这里需要注意边界问题。如果我们是把end位置的数据挪动到end+1的位置,那么到0的时候再--,因为这里的pos是一个无符号整形,0就会变成npos,陷入死循环。

    解决方法:1、可以把pos强制类型转换为int类型。

    1. // 挪动数据
    2. int end = _size;
    3. while (end >= (int)pos)
    4. {
    5. _str[end + 1] = _str[end];
    6. --end;
    7. }

    2、循环到1的时候就停下来,那么我们就可以看成是end位置来获取前一个位置的值。

    1. //挪动数据
    2. size_t end = _size + 1;
    3. while (end > pos)
    4. {
    5. _str[end] = _str[end - 1];
    6. --end;
    7. }

    插入字符串也是同理,因为字符串是有长度的,可能是len个,我们这里也需要关注边界问题,循环里end+len之后还要-1,因为需要排除最后一个。

    代码:

    1. string& insert(size_t pos, char ch)
    2. {
    3. assert(pos < _size);
    4. //检查容量
    5. if (_size == _capacity)
    6. {
    7. int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
    8. reserve(newcapacity);
    9. _capacity = newcapacity;
    10. }
    11. //挪动数据
    12. size_t end = _size + 1;
    13. while (end > pos)
    14. {
    15. _str[end] = _str[end - 1];
    16. --end;
    17. }
    18. _str[pos] = ch;
    19. ++_size;
    20. return *this;
    21. }
    22. string& insert(size_t pos, const char* str)
    23. {
    24. assert(pos < _size);
    25. size_t len = strlen(str);
    26. if (_size + len > _capacity)
    27. {
    28. reserve(_size + len);
    29. }
    30. size_t end = _size + len ;
    31. while (end > pos + len - 1)
    32. {
    33. _str[end] = _str[end - len];
    34. --end;
    35. }
    36. while(len > 0)
    37. {
    38. _str[pos -1] = str[len-1];
    39. len --;
    40. }
    41. return *this;
    42. }

    erase

    先断言一下pos位置是否在有效范围内。

    接下里我们需要判断的是删除的元素个数是否超出了有效字符个数。

    当删除的元素大于剩余有效字符长度,那么就可以直接把该位置的元素改为 '\0' ,然后修改_size的数据就行了。

    当删除的元素小于剩余有效字符长度,用strcpy将后面的元素拷贝到前面来就行了,因为strcpy是会把 ‘\0’ 也一并拷贝过来的,所以使用起来非常方便。

    1. string& erase(size_t pos, size_t len)
    2. {
    3. assert(pos < _size);
    4. if (len == npos || _size - pos <= len)
    5. {
    6. _str[pos] = '\0';
    7. _size = pos;
    8. }
    9. else
    10. {
    11. strcpy(_str + pos, _str + pos + len);
    12. _size -= len;
    13. }
    14. return *this;
    15. }

    find实现

    查找分为:查找字符和查找字符串。

    查找字符串这里偷个懒,用strstr函数直接查找一下。

    因为strstr返回的是指针,所以我们用if判断一下,如果返回的是空指针,那么就返回npos,如果不为空,那么返回找到的ptr - _str 就是位置了。

    1. size_t find(const char ch, size_t pos = 0)const
    2. {
    3. assert(pos < _size);
    4. while (pos < _size)
    5. {
    6. if (_str[pos] == ch)
    7. {
    8. return pos;
    9. }
    10. ++pos;
    11. }
    12. return npos;
    13. }
    14. size_t find(const char* str, size_t pos = 0)const
    15. {
    16. assert(pos < _size);
    17. const char* ptr = strstr(_str + pos, str);
    18. if (ptr == nullptr)
    19. {
    20. return npos;
    21. }
    22. else
    23. {
    24. return ptr - _str;
    25. }
    26. }

    clear、operator<<、operator>>

    clear函数作用是清空有效字符。

    1. void clear()
    2. {
    3. _str[0] = '\0';
    4. _size = 0;
    5. }

    因为操作符第二个参数才是string类对象,所以需要在类外定义。

    operator<<就是一个循环输出所有字符。

    operator>>由于我们不知道输入多少内容,开少了需要频繁扩容,开大了浪费空间。

    我们可以设置一个有128给元素的数组buff,用get函数来获取输入的字符,每次填入一个字符到数组中,每次满了128就将数组添加到string类对象的字符数组中,并将buff数组重置。最后一次如果不满128个是不会追加到string类对象的字符数组中的,我们手动追加一下。因为每次输入的东西都是要覆盖前一次的,所以我们每次输入前都需要清空一下原来的数据,所以我们调用一下clear函数。

    1. ostream& operator<<(ostream& out, const string& s)
    2. {
    3. for (int i = 0; i < s.size(); ++i)
    4. {
    5. out << s[i];
    6. }
    7. return out;
    8. }
    9. istream& operator>>(istream& in, string& s)
    10. {
    11. //先清理s
    12. s.clear();
    13. char buff[128] = { '\0' };
    14. size_t i = 0;
    15. char ch = in.get();
    16. while (ch != ' ' && ch != '\n')
    17. {
    18. //满了
    19. if (i == 127)
    20. {
    21. s += buff;
    22. i = 0;
    23. }
    24. buff[i++] = ch;
    25. ch = in.get();
    26. }
    27. //剩下不满一组的
    28. if (i > 0)
    29. {
    30. buff[i] = '\0';
    31. s += buff;
    32. }
    33. return in;
    34. }

    4、写时拷贝(了解)

    写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
    引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

    参考文章:

    C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell
    C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell

  • 相关阅读:
    wiki.js一个开源知识库系统
    罗丹明 PEG 巯基,Rhodamine PEG Thiol,荧光染料标记巯基/硫醇
    2021年软件测试面试题大全
    线程并发安全问题解决方案
    react基础教程学习(一)
    【DaVinci Developer工具实战】05 - DaVinci Developer 功能区概述和介绍
    rsync 远程同步
    CentOS升级为python
    《论文阅读28》OGMM
    面试:KOOM内存泄漏的监控
  • 原文地址:https://blog.csdn.net/qq_65139309/article/details/127975111