目录
🌈前言🌈
📁 为什么学习string
📁 认识string(了解)
📁 string的常用接口
📂 构造函数
📂 string类对象的容量操作
📂 string类对象的访问以及遍历操作编辑
📂 string类对象的修改操作
📁 模拟实现string
📁 总结
🌈前言🌈
欢迎观看本期【C++杂货铺】,本期内容将全面string,包含了解string,如何操作string,最后会模拟实现string。
因为计算机行业的不断发展,许多程序员不仅仅要掌握string这些容器的使用方法,有的公司会要求阅读底层源码,模拟实现这些容器,所以本期内容将会从零开始,带大家了解使用string。
当然,本篇内容的参考文献主要来源于网站:<string> - C++ Reference (cplusplus.com)
如果,你只想掌握string的具体使用方法,可以阅读下面这篇文章:
【C++杂货铺】详解string的接口-CSDN博客
📁 为什么学习string
在C语言中,字符串是以‘\0’结尾的一些字符的结合,为了操作方便,C标准库提供了一些str系列的库函数,但这些库函数与字符串是分开的,底层空间需要用户自己管理,可能造成越界访问等。
在日常生活中,为了简单,方便,快捷,基本都会使用string类,很少会有人去使用C库中的字符串操作函数。
📁 认识string(了解)
string是表示字符串的字符串类。该类的接口与常规容器的接口基本相同,在添加了一系列专门用来操作string的常规操作。
string是basic_string模板类的一个别名,用char来实例化basic_string模板类。
string不能操作多字节或者边长字符的序列。
在使用string时,必须包括#include头文件以及using namespace std;
📁 string的常用接口
string接口有上百种,这里我们只介绍常用的,以及需要了解的。
📂 构造函数
//函数名称 功能说明
string() 重点 构造空的string类对象,即空字符串
string(const string* s) 重点 用C_string来构造string类对象
string(size_t n,char c) string类对象中把包含了n个字符c
string(const string& s) 重点 拷贝构造
string s1; //构造空的string类对象s1
string s2("hello string"); //用C风格字符串构造string类对象s2
string s3(s2); //拷贝构造s3
📂 string类对象的容量操作
1. size()
size函数求的是字符串中元素的个数,不包含‘\0’。
//打印 5
string s("hello");
cout<<s.size()<<endl;
其中length 和 size作用都是一样的,都是求有效字符串的字符长度,不包含 ‘\0’。size和length方法底层实现原理完全相同。引入size的原因是为了和其他容器接口保持一致。基本使用size。
2. reserve()
reverse的作用就是为字符串预留空间,应用场景就是已知数据元素有多少,可以减少扩容的操作,减少消耗。
但reserve()相当于手动扩容,但要注意的是,扩容量不能小于现有的容量。
string s;
size_t cnt = s.capacity();
cout << cnt << endl;
cout<<"change:" << endl;
for (int i = 0;i < 100;i++)
{
s.push_back('a');
if (cnt != s.capacity())
{
cout << s.capacity() << endl;
cnt = s.capacity();
}
}
以上是没有reserve的对象,第一次扩容两倍,之后是扩容1.5倍每次(vs是1.5倍扩容,Linux下按照2倍扩容)。
string s;
s.reserve(100);
size_t cnt = s.capacity();
cout << cnt << endl;
cout<<"change:" << endl;
for (int i = 0;i < 100;i++)
{
s.push_back('a');
if (cnt != s.capacity())
{
cnt = s.capacity();
cout << cnt << endl;
}
}
上图可知reseve的结果并不一定是准确的扩容数,可能会增加一些。
3. resize()
resiz的功能是将有效字符的个数改成n;如果n大于有效字符个数,即n>size,则会插入;如果空间不够,即n>capacity,则会扩容+插入。当然传参没有char c则没有插入。
📂 string类对象的访问以及遍历操作
1. operator[ ]
string类对象支持下标访问,[ ]支持读写pos位置的数据;const修饰为只读,不能修改。
//非const的对象使用[],可读可写
string s1("hello world");
for(int i =0;i<s1.size();i++)
{
s1[i] = 'a';
}
for(int i=0;i<s1.size();i++)
{
cout<<s1[i]<<' '<<endl;
}
//const对象,只读
const string s2("hello world");
for(int i =0;i<s2.size();i++)
{
cout<<s2[i]<<' '<<endl;
}
2. 迭代器 begin + end
迭代器iterator,容器中类似与指针的东西,通过迭代器可以访问容器中的数据,使用方法也类似于指针。迭代器可能是指针,也可能不是。
begin指向容器中第一个数据的位置;end指向容器中最后一个有效字符的下一个位置,即‘\0’( '\0'不算有效字符 )。
string s("hello world");
for (string::iterator it = s.begin();it != s.end();it++)
{
cout << *it << ' ';
}
//打印 h e l l o w o r l d
3. 迭代器 rbegin + rend
rbegin就是reverse_begin的缩写,rbegin和rend主要用于逆序遍历。反向迭代器则是reverse_iterator.
string s("hello world");
for (string::reverse_iterator it = s.rbegin();it != s.rend();it++)
{
cout << *it << ' ';
}
4. 范围for
范围for的底层就是迭代器,将s的迭代器赋值给e(auto类型是编译器自动推导的数据类型)。
string s("hello world");
for (auto e : s)
{
cout << e << ' ';
}
cout << endl;
📂 string类对象的修改操作
1. push_back()
尾部插入一个字符
//打印 hello w
string s("hello ");
s.push_back('w');
cout<<s<<endl;
2. append()
尾部插入一个字符串。
string s1("hello ");
s.append("world");
//打印 hello world
string s2("hello ");
s2.append(10, 'x');
//打印helloxxxxxxxxxx
string s3("hello ");
s3.append(s1.begin(),s1.end());
//打印hello hello world
3. operator+=
尾部插入一个字符或者字符串。
string s1("hello ");
s1 += "world";
string s2("aaaaa");
s2 += ' ';
string s3("bbbbb");
s3 += s2;
4. insert()
前面之前插入字符或者字符串。
5. erase()
删除从pos位置开始,len个字符,如果len没有传参赋初值,npos就代表着有多少删多少。
npos是string里面的一个静态成员变量 static const size_t npos = -1; size_t是无符号整数,所以-1代表整数最大值。
从pos位置开始,删除len个字符。或者从first迭代器开始,到last迭代器结束的字符删除。
string s("a bcd");
s.erase(0,1);
cout << s << endl;
//打印 bcd
6. replace()
将字符替换。
string s1("a bcd");
s1.replace(1,1,"a");
cout << s1 << endl;
//打印aabc
string s2("a bcd");
s2.replace(1,1,1,'a');
cout << s2 << endl;
//打印aabc
insert,earse,replace等函数尽量少用,因为涉及数据元素的移动,效率太低。
7. c_str()
因为C++是要兼容C语言的,在C语言中,表示字符串使用char* 来表示的,所以C语言库中许多操作使用的是char* 来操作的。而在C++中,我们大多数使用的是string,那么如何将string类型转为C语言字符串类型呢?
c_str()的作用就是从string中返回C格式字符串。
string filename("test.txt");
/*
FILE* file = fopen(filename,"r");
filename是string类型,而fopen函数的第一个参数类型是C格式字符串。
*/
FILE* file = fopen(filename.c_str(),"r");
8. find()
find()作用就是查找字符或者字符串,从pos位置开始。默认情况下,pos位置是从0开始。
rfind()作用与find几乎相同,不过是从字符串尾开始查找。
9. sub_str()
在str中从pos位置开始,截取len个字符,将其作为字符串返回。如果len没有给或者大于npos(-1),含义则是从pos位置截取到字符串尾。
#include <iostream>
#include <string>
int main ()
{
std::string str="We think in generalities, but we live in details.";
// (quoting Alfred N. Whitehead)
std::string str2 = str.substr (3,5); // "think"
std::size_t pos = str.find("live"); // position of "live" in str
std::string str3 = str.substr (pos); // get from "live" to the end
std::cout << str2 << ' ' << str3 << '\n';
return 0;
}
📁 模拟实现string
下面会有大量的string接口的模拟实现,模拟实现是为了更好的从里层理解string,使用string。
📂 拓展知识 : 编码
常见的编码有:ASCII编码,GBK编码,UTF编码。
所谓的编码,就是文字在计算机中的存储和表示。我们通过编码将计算机0 1 表示成日常生活中的文字。
但随着越来越多国家的加入,简单的ASCII编码已经不能满足需求。所以有了UTF编码,UTF编码有UTF-8,UTG-16(2B),UTF-32(4B),它们主要的区别就是UTF-8是可变字节,utf-16和utf-32是不变字节。
我们常用的string的底层就是使用utf-8的编码方式的char实例化的。
模拟实现string类,并完成测试
namespace bit
{
class string
{
friend ostream& operator<<(ostream& _cout, const bit::string& s);
friend istream& operator>>(istream& _cin, bit::string& s);
public:
typedef char* iterator;
public:
string(const char* str = "");
string(const string& s);
string& operator=(const string &s);
~string();
//
// iterator
iterator begin();
iterator end();
/
// modify
void push_back(char c);
string& operator+=(char c);
void append(const char* str);
string& operator+=(const char* str);
void clear();
void swap(string& s);
const char* c_str()const;
/
// capacity
size_t size()const
size_t capacity()const
bool empty()const
void resize(size_t n, char c = '\0');
void reserve(size_t n);
/
// access
char& operator[](size_t index);
const char& operator[](size_t index)const;
/
//relational operators
bool operator<(const string& s);
bool operator<=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator==(const string& s);
bool operator!=(const string& s);
// 返回c在string中第一次出现的位置
size_t find (char c, size_t pos = 0) const;
// 返回子串s在string中第一次出现的位置
size_t find (const char* s, size_t pos = 0) const;
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c);
string& insert(size_t pos, const char* str);
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len);
private:
char* _str;
size_t _capacity;
size_t _size;
}
};
📂 默认成员函数的模拟实现
//构造函数
string(const char* str = "")
:_size(strlen(str))
{
_str = new char[_size + 1];
strcpy(_str, str);
_capacity = _size;
}
//拷贝构造函数
string(const string& s)
{
string temp(s._str);
swap(temp);
}
//赋值重载
string& operator=(string temp)
{
swap(temp);
return *this;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
📂 iterator模拟实现
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
📂 insert 和 erase的模拟实现
//再字符串的pos位置插入字符
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0? 4:2 * _capacity);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[end] = ch;
_size++;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
while (end > pos+len-1)
{
_str[end] = _str[end - len];
end--;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
//删除pos位置后len个元素
string& erase(size_t pos=0, size_t len=npos)
{
assert(pos < _size);
if (len >= _size - pos || len == npos)
{
_str[pos] = '\0';
_size = pos;
return *this;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
return *this;
}
}
📂 modify模拟实现
//modify
void swap(string& s)
{
std::swap(this->_str, s._str);
std::swap(this->_size, s._size);
std::swap(this->_capacity, s._capacity);
}
const char* c_str() const
{
return _str;
}
void clear()
{
_size = 0;
_str[_size] = '\0';
}
void push_back(char ch)
{
//扩容2倍
/*if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';*/
insert(_size, ch);
}
void append(const char* str)
{
//扩容
/*size_t len = strlen(str);
if(_size >= _capacity - len)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;*/
insert(_size, str);
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
📂 capacity模拟实现
//capacity
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
bool empty() const
{
return _size == 0;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* temp = new char[n + 1];
strcpy(temp, _str);
delete[] _str;
_str = temp;
_capacity = n;
}
}
void resize(size_t n, const char ch = '\0')
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
for (size_t i = _size;i < n;i++)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
}
📂 relational operators的模拟实现
//relational operators
bool operator<(const string& s)
{
return strcmp(this->c_str(), s.c_str()) < 0;
}
bool operator<=(const string& s)
{
return *this < s || *this == s;
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return !(*this < s);
}
bool operator==(const string& s)
{
return strcmp(this->c_str(), s.c_str()) == 0;
}
bool operator!=(const string& s)
{
return !(*this == s);
}
📂 find 的模拟实现
//返回字符c在字符串中出现的第一次位置
size_t find(char ch, int pos = 0)
{
assert(pos < _size);
for (size_t i = pos;i < _size;i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
//返回子串s在字符串中出现的第一次位置
size_t find(const char* s, size_t pos = 0)
{
assert(pos < _size);
char* ps = strstr(_str + pos, s);
if (ps != nullptr)
{
return ps - _str;
}
else
{
return npos;
}
}
📂 substr的模拟实现
string substr(size_t pos=0, size_t len=npos)
{
string temp;
if (len == npos || _size - pos < len)
{
for (size_t i = pos;i < _size;i++)
{
temp += _str[i];
}
}
else
{
for (size_t i = pos;i < pos + len;i++)
{
temp += _str[i];
}
}
return temp;
}
📁 总结
以上,我们就对string进行了全面的讲解,介绍了string的常用接口,string的底层实现,以及string相关的拓展知识。
如果你能看到这里,恭喜你,你已经对string有了全面的了解,可以说已经上手了string。剩下的就是不断调试模拟实现string了,当然模拟实现只是为了更好的理解string。
如果感觉本期内容对你有帮助,欢迎点赞,收藏,关注。Thanks♪(・ω・)ノ