008-C++String
string类
1. string类存在的原因
1.1 C语言中的字符串
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP(面向对象)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
1.2 C++的string类
C++STL库中将字符串封装成了string类,并提供了相应的结构,这样我们对字符串的使用就能更加方便快捷,并且不用担心越界等风险。
2. 标准库中的string类
2.1 string类
C++文档中的介绍:string - C++ Reference — string - C++ Reference
Strings are objects that represent sequences of characters.
The standard string
class provides support for such objects with an interface similar to that of a standard container of bytes, but adding features specifically designed to operate with strings of single-byte characters.
The string
class is an instantiation of the basic_string class template that uses char
(i.e., bytes) as its character type, with its default char_traits and allocator types (see basic_string for more info on the template).
Note that this class handles bytes independently of the encoding used: If used to handle sequences of multi-byte or variable-length characters (such as UTF-8), all members of this class (such as length or size), as well as its iterators, will still operate in terms of bytes (not actual encoded characters).
总结:
-
string是表示字符串的字符串类
-
该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
-
string在底层实际是:basic_string模板类的别名,
typedef basic_string<char, char_traits, allocator> string
; -
不能操作多字节或者变长字符的序列。
在使用string类时必须包含头文件#include <string>
,string类也在std命名空间中。
2.2 string类的常用结构
-
string类常见构造
函数名称 功能说明 string() 构造空的string类对象,即空字符串 string(const char* s) 用C的字符串来构造string类对象 string(size_t n, char c) string类对象中包含n个字符c string(const string& s) 拷贝构造函数 int main() {string s1; // 空字符串string s2("hello string"); // 使用C语言字符串进行构造string s3(9, 'a'); // 创建一个包含9个'a'字符的字符串string s4(s2); //拷贝构造return 0; }
-
string类对象的容量操作
函数名称 功能说明 size 返回字符串有效字符长度 length 返回字符串有效字符长度 capacity 返回空间总大小 empty 检测字符串释放为空串,是返回true,否则返回false clear 清空有效字符 reserve 为字符串预留空间 resize 将有效字符的个数改成n个,多出的空间用字符c填充,如果未指定字符,则填充0 int main() {string s1("hello string");cout << s1.size() << endl;cout << s1.length() << endl;cout << s1.capacity() << endl;cout << s1.empty() << endl;s1.clear();cout << s1.empty() << endl;s1.reserve(20);cout << s1.capacity() << endl;s1.resize(30, '0');cout << s1 << endl;return 0; }
注意:
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
- clear()只是将string中有效字符清空,不改变底层空间大小。
- resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- reserve(size_t n=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
-
string类对象的访问及遍历操作
函数名称 功能说明 operator[] 返回字符串对应下标位置的引用(与C语言字符串功能类似) begin、end begin()获取一个字符的迭代器、end()获取最后一个字符下一个位置的迭代器 rbegin、rend rbegin()获取一个字符的反向迭代器、rend()获取最后一个字符下一个位置的反向迭代器 cbegin、cend 与begin、end不同的是,这个是const修饰的迭代器,也就是不允许修改指向的内容 crbegin、crend const修饰的反向迭代器。 关于迭代器:
- 暂时可以将它理解成一个指针,可以进行解引用操作。
- begin就是第一个字符串中第一个位置的指针,end就是指向字符串中最后一个位置下一个位置的指针(左闭右开),rbegin和rend正好相反,rbegin是指向最后一个位置的指针,rend是指向第一个位置的前一个位置的指针。
- 对正向迭代器进行++操作会将迭代器往后移一个位置,对反向迭代器++则是将迭代器往前移一个位置。
- string中正向迭代器的类型是
string::iterator
、反向迭代器则是string::reverse_iterator
int main() {string s1("hello string");cout << s1[0] << endl;auto it = s1.begin(); // 因为迭代器类型较长,这里可以使用auto自动识别,更加方便一些while (it != s1.end())cout << *it++;cout << endl;auto rit = s1.rbegin();while (rit != s1.rend())cout << *rit++;cout << endl;return 0; }
补充:string类的对象也同样支持前面说过的范围for(详见:001-C++入门-CSDN博客文章中9. 基于范围的for循环)
-
string类对象的修改操作
函数名称 功能说明 push_back 在字符串后尾插字符 append 在字符串后追加一个字符串 operator+= 在字符串后追加一个字符串 c_str 返回C格式字符串(const char*) find 从字符串指定位置开始往后找字符c或字符串,不传指定位置默认从第一个位置开始找,返回该字符在字符串中的位置,没找到返回npos(size_t的最大值,转成int本质上就是-1)。 rfind 从字符串指定位置开始往前找字符c,不传指定位置默认从最后一个位置开始找,返回该字符在字符串中的位置,没找到返回npos substr 在str中从pos位置开始,截取n个字符,然后将其返回。 int main() {string s;s.push_back('a');s.append("bb");s += "ccc";printf("%s\n", s.c_str());cout << s.find('a') << ' ' << (int)s.find('d') << endl;cout << s.rfind('c') << ' ' << (int)s.rfind('d') << endl;cout << s.substr(0, 3) << endl;return 0; }
说明:
- 在string尾部追加字符时,
s.push_back(c) / s.append(1, c) / s += 'c'
三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。 - 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好,这样可以减少底层扩容的次数,提升效率。
- 在string尾部追加字符时,
-
string类非成员函数
函数名称 功能说明 operator+ 将两个字符串拼接称一个字符串返回,尽量少用,因为传值返回,导致深拷贝效率低 operator>> 输入运算符重载(读取到空格就停止) operator<< 输出运算符重载 getline 从指定流中获取一行字符串(读取到换行符才停止) relaticonal operators(关系运算符重载,也就是<、<=、>、>=、==、!=的重载) 大小比较 int main() {string s1("aaa");string s2("bbb");cout << s1 + s2 << endl;string s3;cout << "输入:";getline(cin, s3);cout << s3 << endl;return 0; }
-
vs和g++下string结构的说明
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
-
vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:
- 当字符串长度小于16时,使用内部固定的字符数组来存放
- 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty { // storage for small buffer or pointer to larger onevalue_type _Buf[_BUF_SIZE];pointer _Ptr;char _Alias[_BUF_SIZE]; // to permit aliasing } _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量。
最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。
-
g++下string的结构
g++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
- 空间总大小
- 字符串有效长度
- 引用计数
struct _Rep_base {size_type _M_length;size_type _M_capacity;_Atomic_word _M_refcount; }
- 指向堆空间的指针,用来存储字符串。
-
3.string类的模拟实现
这里我们只实现部分上面提及的常用接口。
String.hpp
#pragma once#include <iostream>namespace my
{class String{friend void swap(String& s1, String& s2);public:typedef char* iterator;typedef const char* const_iterator;String(const char* s = ""):_size(strlen(s)){_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, s);_str[_size] = '\0';}String(size_t n, char c):_capacity(n),_size(n){_str = new char[_capacity + 1];for (int i = 0; i < _size; i++)_str[i] = c;_str[_size] = '\0';}String(const String& s){String tmp(s._str);swap(*this, tmp);}String& operator=(String s){swap(*this, s);return *this;}~String(){delete[] _str;}size_t size(){return _size;}size_t length(){return _size;}size_t capacity(){return _capacity;}bool empty(){return _size == 0;}void clear(){_str[0] = '\0';_size = 0;}void reserve(size_t n){if (n > _capacity || (n < _capacity && n > _size)){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void resize(size_t n, char c = '\0'){if (n < _size){_str[n] = '\0';_size = n;return;}if (n > _capacity)reserve(n);while (_size < n)_str[_size++] = c;_str[_size] = '\0';}char& operator[](size_t n){return _str[n];}const char& operator[](size_t n) const{return _str[n];}const char* c_str() const{return _str;}iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator cbegin() const{return _str;}const_iterator rend() const{return _str + _size;}void push_back(char c){if (_capacity == _size){size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;char* tmp = new char[newCapacity + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;}_str[_size++] = c;_str[_size] = '\0';}String& operator+=(const char* s){size_t sz = strlen(s);reserve(sz + _size);for (int i = 0; i < sz; i++)push_back(s[i]);return *this;}String& operator+=(char c){push_back(c);return *this;}String& operator+=(const String& s){reserve(s._size + _size);for (int i = 0; i < s._size; i++)push_back(s[i]);return *this;}size_t find(char c, size_t pos = 0){for (int i = pos; i < _size; i++)if (_str[i] == c)return i;return npos;}size_t find(const char* s, size_t pos = 0){char* p = strstr(_str + pos, s);if (p != nullptr) return p - _str;return npos;}String substr(size_t pos, size_t len = npos){String sub;len = std::min(len, _size - pos);for (int i = pos; i < pos + len; i++)sub += _str[i];return sub;}private:char* _str;size_t _capacity;size_t _size;public:static const size_t npos;};const size_t String::npos = -1;String operator+(String s1, String& s2){s1 += s2;return s1;}std::istream& operator>>(std::istream& in, String& s){s.clear();char c;char tmp[128];int i = 0;c = in.get();while (c != '\n' && c != ' '){tmp[i++] = c;if (i == 127){tmp[i] = '\0';s += tmp;i = 0;}c = in.get();}if (i != 0){tmp[i] = '\0';s += tmp;}return in;}std::ostream& operator<<(std::ostream& out, String& s){out << s.c_str();return out;}bool operator<(String& s1, String& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;}bool operator==(String& s1, String& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator<=(String& s1, String& s2){return s1 < s2 || s1 == s2;}bool operator>(String& s1, String& s2){return !(s1 <= s2);}bool operator>=(String& s1, String& s2){return !(s1 < s2);}bool operator!=(String& s1, String& s2){return !(s1 == s2);}std::istream& getline(std::istream& in, String& s){s.clear();char c;char tmp[128];int i = 0;c = in.get();while (c != '\n'){tmp[i++] = c;if (i == 127){tmp[i] = '\0';s += tmp;i = 0;}c = in.get();}if (i != 0){tmp[i] = '\0';s += tmp;}return in;}void swap(String& s1, String& s2){std::swap(s1._str, s2._str);std::swap(s1._capacity, s2._capacity);std::swap(s1._size, s2._size);}
}