【C++闯关笔记】STL:string的学习和使用(万字精讲)
系列文章目录
第零篇:从C到C++入门:C++有而C语言没有的基础知识总结-CSDN博客
第一篇:【C++闯关笔记】封装①:类与对象-CSDN博客
第二篇:【C++闯关笔记】封装②:友元与模板-CSDN博客
第三篇:【C++闯关笔记】STL:string的学习和使用(万字精讲)-CSDN博客
目录
系列文章目录
前言
一、什么是string类?
二、使用string:string类的常用函数
1.创建string对象
2.修改string对象
3.遍历string对象
1)operator[ ]
2)迭代器遍历
3)范围for
4.string类对象容量相关函数
5.其他常用函数
1.查找字符或字符串:find和rfind
2.截取子字符串:substr
3.返回C格式字符串:c_str
4.获取一行字符:getline
三、模拟实现string
1.private成员
2.构造函数与析构函数
2.修改类函数
3.遍历类函数
4.其他常用函数
完整代码
总结
前言
什么是STL?
STL是C++标准库的重要组成部分,是一个可复用的组件库,它里面包罗有众多数据结构与算法。STL由六大组件构成:容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中容器与算法最为重要,后面 4 部分都是为它两服务的。这些组件相互协作,共同实现了STL的强大功能。
为什么要学习STL?
STL是C++中的优秀作品代表,如果能够熟练的使用STL乃至了解底层原理,那么许多底层的数据结构以及算法都不需要自己重新编码,可以直接复用前人的优秀成果,相当于站在巨人的肩膀上,大幅提升开发速度。
本系列将从STL的各个容器与算法着手,先详细介绍它们使用方法,然后通过自实现的方式理解它们的底层逻辑。
本文主要介绍string的学习与使用。
一、什么是string类?
在C语言中,字符串是char[ ]数组以'\0'结尾的一些字符的集合,C标准库有一些str系列的库函数专门用以操作这些字符串。 但是这些库函数与字符串是分离开的,不太符合C++面对对象的思想。
于是在C++中将原C语言中的字符串与相关函数并结合数据结构顺序表,将它们整合打造成为一个类——用以专门表示字符串的字符串类即string类,以便使用。
对于刚接触string的新人而言,可以将string先理解为一种特化的顺序表——专门为存储和操作字符数据而设计。结合数据结构顺序表的原理,先在脑海中有个大致的模型,之后在学习string相关接口时更加游刃有余。
string类与C语言的数组有什么区别?
①char s[ ]本质上还是数组,长度固定;string长度不限;
②string本质是类,其中包含有大量函数,方便操作;
③string中重载了如‘+’ ’>' '<"等符号,便于操作;
④可以将string理解为包含了大量函数的char s[ ]。
二、使用string:string类的常用函数
在使用string类时,必须包含#include<string>头文件以及using namespace std;
1.创建string对象
在日常编码中,我们常用到的四种方法创建string对象。
string类对象的常见构造函数
构造函数 | 功能说明 |
string() | 构造空的string类对象,即空字符串 |
string(const char* s) | 用C语言的字符串来构造string类对象 |
string(size_t n, char c) | 创建的string类对象中包含n个字符c |
string(const string&s) | 拷贝构造函数,将已有的对象拷贝给一个新的对象 |
示例
string s1;string s2("hello String");string s3(7, 'a');string s4(s3);
解释:string s3(7,‘a'),这里将’a‘字符复制了7遍后存入字符串对象s3.
string的构造函数除了上述最常用的四种外还包括:
2.修改string对象
string类对象的常见修改函数
①增加类型函数
函数名称 | 功能说明 |
push_back('c') | 在字符串后尾插字符c |
appden(“字符串” ) | 在字符串末尾追加一个字符串 |
operator+= | 将两个字符串合并 |
示例
int main()
{string s1("he");string s2("String");s1.push_back('l');s1.append("lo ");s1 += s2;cout << s1 << endl;//打印结果为“hello String”return 0;
}
②减删类型函数
函数名称 | 功能说明 |
pop_back( ) | 删除字符串末尾字符 |
erase(size_t pos,size_t n); erase(size_t pos) | 从索引pos位置开始,往后删除n个字符(包括pos); 将pos位置及以后的字符删除。 |
clear( ) | 清空整个字符串 |
示例
int main()
{string s1("hello string");s1.pop_back();s1.erase(7, 4);s1.erase(5);s1.clear();return 0;
}
细节注意:
1.确保你的删除位置pos在
[0,size()]
范围内。2.调用
pop_back()
之前,最好检查一下检查一下字符串是否为空 !str.empty( );
3.遍历string对象
string对象常用的遍历方法包括三种:
函数名称 | 功能说明 |
operator[pos ] | 返回pos位置的字符 |
begin+end; rbegin+rend; | 迭代器遍历 |
for(auto:str) | 范围for |
1)operator[ ]
string重载了[ ],可以通过下标访问。
使用示例
2)迭代器遍历
这里的迭代器可以理解为封装过后的“指针”;
(1)正着遍历:begin+end
使用语法
string::iterator 变量名 = 对象.begin( );(end同理)
如
string::iterator it = s.begin();//iterator为关键字,it为变量名
使用示例
(2)反向遍历:rbegin+rend
使用语法
string::reverse_iterator 变量名 = 对象.rbegin( );(rend同理)
如
string::reverse_iterator rit = s.rbegin();//reverse_iterator为关键字
使用示例
3)范围for
语法
for(auto 变量名 : 对象)
auto,自动推测变量类型,中间由‘ :’隔开
(这里简述auto与范围for,主要目的是能介绍这种遍历方法,有兴趣的读者可参看:)从C到C++入门:C++有而C语言没有的基础知识总结-CSDN博客
范围for的底层是由迭代器实现的。
使用示例
4.string类对象容量相关函数
函数名 | 功能 |
size( ) | 返回字符串有效字符长度 |
length( ) | 返回字符串有效字符长度 |
capacity( ) | 返回该对象占用空间大小 |
empty() | 检测该string对象是否为空,是返回true,否则返回false |
reserve(size_t n) | 将字符串空间拓展为n |
resize(size_t n,char c) | 将有效字符的个数改成成n个,多出的空间用字符c填充 |
说明:
①size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一 致,一般情况下基本都是用size();
②resize与reserve的核心区别
-
resize(n,c)
:改变字符串的“逻辑长度” (即size()
)。 它会直接让你看到的字符串内容变长或变短。当 n 比目前字符串长度长时,多出空间用字符c填充;当 n 比目前字符串长度短时,会截取字符串长度至n。 -
reserve(n)
:改变字符串的“总容量” (即capacity()
)。 它只是为你将来要放入的字符预先申请一大块内存(可能不会严格申请至 n 空间),但不会改变你当前看到的字符串内容。
使用示例
int main()
{string s("hello");cout <<s <<' ' << s.size() << ' ' << s.capacity() << endl;s.reserve(64);//将总空间大小拓展为64,多出的部分为空白;s.resize(32, 'a');//将实际使用空间拓展至32,多出部分填字符'a';cout << s << ' ' << s.size() << ' ' << s.capacity() << endl;return 0;
}
陷阱注意
①reserve()
不会初始化内存
reserve()只会申请空间,不会将申请出来的空间赋值。如果通过 []
操作符访问 [size(), capacity())
范围内的内存是未定义行为。
②reserve()
只增不减
reserve(n)
是将容量拓展至 至少是 n。如果你传入一个比当前 capacity()
还小的值,是不会实现的。
③resize(n)
如果未指定初始化字符,则新添加的字符会被默认初始化为空字符 '\0'。
5.其他常用函数
1.查找字符或字符串:find和rfind
作用:在字符串中寻找带查找字符或字符串出现的位置,找到则返回其首下标,没有则返回-1。
使用方法:
s.find("你要找到字串",pos);pos,指从该位置往后找,若没有输入pos则默认从头开始找。
s.rfind("你要找到字串",pos);pos是指从该位置往前找,若没有输入pos则默认从头开始找。
示例:
注意:若没有输入pos,则find默认从首字符开始找;rfind从尾字符开始找。
2.截取子字符串:substr
作用:截取子字符串,当长度超过原字符串时,截取已有剩余的全部字符。
使用方法:
s.substr(pos,len);len是你要截取的长度,若不给len,则默认从pos位置取完剩下的。
示例:
3.返回C格式字符串:c_str
为了保持与C语言的兼容性,c_str函数便孕育而出了。
作用:返回一个指向当前字符串内容的、以空字符 '\0'
结尾的 C风格字符串(即 const char*
)。
使用方法:
const char *s =对象.c_str();
示例:
4.获取一行字符:getline
在平常通过 cin 输入字符串时,如果遇到空格就会停止输入,如
int main()
{string s;//输入“hello world”;cin >> s;//只打印出“hello”cout << s << endl;return 0;
}
getline函数的作用就是将一整行输入都存入字符串。
使用方法:
getline(cin,string对象);//注意getline并非string类的成员函数!
示例
三、模拟实现string
1.private成员
类似于数据结构顺序表:
char* _s用于存储字符串;
size_t _capacity表示在堆上申请的空间数量;
size_t _size表示实际使用了多少空间。
size_t 可以看成没有负数的int型。
namespace karsen
{class string{private:size_t _size;size_t _capacity;char* _s;};
}
2.构造函数与析构函数
两种构造函数以实现:①空对象;②括号内赋初值;③创建的string类对象中包含n个字符c。
以及拷贝构造函数,析构函数。
注意:
①new 与 delete,new[ ]与delete[ ]配合使用,否则无法正确释放空间。
②必须实现拷贝构造函数,因为默认拷贝构造是按字节拷贝的“浅拷贝”,他会将原对象的数据原模原样的拷贝进新对象,当析构函数释放空间时就会造成释放两次导致报错。
namespace karsen
{class string{public://传入字符串构建string(const char* s = nullptr){//空对象给它两个空间if (!s || s[0] == '\0'){_s = new char[2];_s[0] = '\0';_size = 0;_capacity = 2;return;}_size = strlen(s);_capacity = _size + 1;_s = new char[_size + 1];strcpy(_s, s);}//创建的string类对象中包含n个字符cstring(int n, char c){if (n > 0){_size = n;_s = new char[_size + 1];for (int i = 0; i < n; ++i)_s[i] = c;_s[_size] = '\0';}}string(const string& s){_size = s._size;_capacity = s._capacity + 1;_s = new char[_capacity];strcpy(_s, s._s);}~string(){delete[] _s;_size = _capacity = 0;_s = nullptr;}private:size_t _size;size_t _capacity;char* _s;};
2.修改类函数
注意在push_back中,当_size==_capacity-1时就要扩容,因为push之后需要设置_s[_size] = '\0';防止越界。
erase函数使用了函数重载,以满足在不传入参数二时的删除工作。
void push_back(char c){if (_size == _capacity-1){size_t newcapa = _capacity * 2;reserve(newcapa);}_s[_size++] = c;_s[_size] = '\0';}void reserve(size_t n){if (n > _capacity){_capacity = n;char* news = new char[_capacity];strcpy(news, _s);delete[] _s;_s = news;}}void pop_back(){if (_size > 0){_size--;}}void append(const char* s){if (s){size_t len = strlen(s);size_t newsize = len + _size;if (newsize > _capacity){reserve(newsize);}strcpy(_s + _size, s);_size += len;//_s[_size] = '\0';//+=那可能用到}}string& operator+=(const char* s){append(s);return *this;}string& operator+=(const char s){push_back(s);return *this;}void erase(size_t pos){_size = pos;_s[_size] = '\0';}void erase(size_t pos, int n){if (n > 0){size_t end = pos + n;if (end < _size){while (end < _size){_s[pos++] = _s[end++];}_size = pos;_s[_size] = '\0';}else{erase(pos);}}else return;}void clear(){_size = 0;_s[_size] = '\0';}
3.遍历类函数
注意:范围for底层调用的是迭代器。
char& operator[](size_t n){if (n < _size){return _s[n];}else{//越界抛异常throw std::out_of_range("Index out of range");}}//用于const型对象const char& operator[](int& n)const{if (n < _size){return _s[n];}else{//越界抛异常throw std::out_of_range("Index out of range");}}typedef char* iterator;//迭代器iterator begin(){return _s;}iterator end(){return _s + _size;}
4.其他常用函数
解释:
①resize分两大类:n是否大于capacity。在n小于capacity一类中又可分为:n是否大于size。
②通过重载find,以达到查找单个字符与字符串的目的。在第二个find函数中,利用指针变量本质存储的是地址数据且每个地址空间正好等于char大小,将前后地址相减取得间距。
③在substr函数的第一个if中,如果len的值没变还是-1,或者pos+len的大小超过实际对象中存储字符串的大小,那么将pos之后的字符串(_s+pos)传递给一个临时变量返回。
如果不满足第一个if,那么po与len之间一定在已有的数据中,将这些数据push进临时变量再返回。
注意:不要返回临时变量的引用。
size_t size(){return _size;}size_t capacity(){return _capacity;}bool empty(){if (_size > 0)return true;else return false;}void resize(size_t n, char c){if (n > _capacity){reserve(n);}if (n > _size){while (_size < n){_s[_size++] = c;}}else{_size = n;_s[_size] = '\0';}}int find(const char a,int pos = 0){if (pos < 0 || pos > _size)return -1;for (int i = pos; i < _size; ++i){if (_s[i] == a)return i;}return -1;}int find(const char* a, int pos = 0){if (pos < 0 || pos > _size)return -1;const char* temp = strstr(_s + pos, a);if (!temp)return -1;else return (int)(temp - _s);}//会传下标int rfind(const char a, int pos){if (pos < 0 || pos > _size){return -1;}for (int i = pos; i > -1 ; --i){if (_s[i] == a)return i;}return -1;}//不会传下标int rfind(const char a, size_t pos = 1){pos = _size;for (size_t i = pos; i > -1; --i){if (_s[i] == a)return (int)i;}return -1;}const char* c_str(){return _s;}string substr(int pos, int len = -1){if (pos<0 || pos>_size)return nullptr;if (len == -1 || pos + len >=_size){string ret(_s + pos);return ret;}else{string ret;for (int i = pos; i < pos + len; ++i){if (i < _size)ret.push_back(_s[i]);else break;}return ret; }}//重载输入输出符std::ostream& operator<<(std::ostream& out, const string& s){out << s._s;return out;}std::istream& operator>>(std::istream& in, string& s){s.clear(); char c;while (in.get(c)){if (c == '\n' || c ==' ')break;s.push_back(c);}//在push_back中已经处理过了,这里就不用,也不能(越界)//s[s._size] = '\0';return in;}
完整代码
如果不需要namespace,可以直接将其中的代码复制到外部。
//#define _CRT_SECURE_NO_WARNINGS//这个宏用于去除vs编译器的不安全提醒
#pragma once
#include<iostream>namespace karsen
{typedef char* iterator;typedef char* reverse_iterator;class string{friend std::ostream& operator<<(std::ostream& out, const string& s);friend std::istream& operator>>(std::istream& in,string& s);public://传入字符串构建string(const char* s = nullptr){if (!s || s[0] == '\0'){_s = new char[2];_s[0] = '\0';_size = 0;_capacity = 2;return;}_size = strlen(s);_capacity = _size + 1;_s = new char[_size + 1];strcpy(_s, s);}//创建的string类对象中包含n个字符cstring(int n, char c){if (n > 0){_size = n;_s = new char[_size + 1];for (int i = 0; i < n; ++i)_s[i] = c;_s[_size] = '\0';}}string(const string& s){_size = s._size;_capacity = s._capacity + 1;_s = new char[_capacity];strcpy(_s, s._s);}void push_back(char c){if (_size == _capacity-1){size_t newcapa = _capacity * 2;reserve(newcapa);}_s[_size++] = c;_s[_size] = '\0';}void pop_back(){if (_size > 0){_size--;}}size_t size(){return _size;}size_t capacity(){return _capacity;}void append(const char* s){if (s){size_t len = strlen(s);size_t newsize = len + _size;if (newsize > _capacity){reserve(newsize);}strcpy(_s + _size, s);_size += len;//_s[_size] = '\0';//+=那可能用到}}string& operator+=(const char* s){append(s);return *this;}string& operator+=(const char s){push_back(s);return *this;}char& operator[](size_t n){if (n < _size){return _s[n];}else{//越界抛异常throw std::out_of_range("Index out of range");}}const char& operator[](int& n)const{if (n < _size){return _s[n];}else{//越界抛异常throw std::out_of_range("Index out of range");}}void erase(size_t pos){_size = pos;_s[_size] = '\0';}void erase(size_t pos, int n){if (n > 0){size_t end = pos + n;if (end < _size){while (end < _size){_s[pos++] = _s[end++];}_size = pos;_s[_size] = '\0';}else{erase(pos);}}else return;}void reserve(size_t n){if (n > _capacity){_capacity = n;char* news = new char[_capacity];strcpy(news, _s);delete[] _s;_s = news;}}void clear(){_size = 0;_s[_size] = '\0';}//迭代器iterator begin(){return _s;}iterator end(){return _s + _size;}//方向迭代需要重载++变--//reverse_iterator rend()//{// return _s;//}//reverse_iterator rbegin()//{// return _s + _size-1;//}bool empty(){if (_size > 0)return true;else return false;}void resize(size_t n, char c){if (n > _capacity){reserve(n);}if (n > _size){while (_size < n){_s[_size++] = c;}}else{_size = n;_s[_size] = '\0';}}int find(const char a,int pos = 0){if (pos < 0 || pos > _size)return -1;for (int i = pos; i < _size; ++i){if (_s[i] == a)return i;}return -1;}int find(const char* a, int pos = 0){if (pos < 0 || pos > _size)return -1;const char* temp = strstr(_s + pos, a);if (!temp)return -1;else return (int)(temp - _s);}//会传下标int rfind(const char a, int pos){if (pos < 0 || pos > _size){return -1;}for (int i = pos; i > -1 ; --i){if (_s[i] == a)return i;}return -1;}//不会传下标int rfind(const char a, size_t pos = 1){pos = _size;for (size_t i = pos; i > -1; --i){if (_s[i] == a)return (int)i;}return -1;}const char* c_str(){return _s;}string substr(int pos, int len = -1){if (pos<0 || pos>_size)return nullptr;if (len == -1 || pos + len >=_size){string ret(_s + pos);return ret;}else{string ret;for (int i = pos; i < pos + len; ++i){if (i < _size)ret.push_back(_s[i]);else break;}return ret; }}~string(){delete[] _s;_size = _capacity = 0;_s = nullptr;}public://以下作为组件函数使用private:size_t _size;size_t _capacity;char* _s;};std::ostream& operator<<(std::ostream& out, const string& s){out << s._s;return out;}std::istream& operator>>(std::istream& in, string& s){s.clear(); char c;while (in.get(c)){if (c == '\n' || c ==' ')break;s.push_back(c);}//在push_back中已经处理过了,这里就不用,也不能(越界)//s[s._size] = '\0';return in;}}
总结
本文先详细介绍了什么是STL以及string是什么,然后再介绍了string的一些常用函数的使用方法,最后通过模拟实现string以求深入理解string。
整理不易,希望能帮到你。
读完点赞,手留余香~