为什么学习string类?
C 语言中的字符串C 语言中,字符串是以 '\0' 结尾的一些字符的集合,为了操作方便, C 标准库中提供了一些 str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
标准库中的string类
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_string<char, char_traits, allocator>string;4. 不能操作多字节或者变长字符的序列
string类的常用接口说明
下来我们对其进行实现展示
#include<iostream>
#include<string>//含string的包
#include<Windows.h>
using namespace std;
//4个默认的成员函数
void test_string1()
{
string s1;//构造空的string类对象,即空字符串,s1
string s2("hello");//用C-string来构造string类对象s2
string s3("hello", 2);//拷贝前两个字符赋给s3
string s4(s2);//拷贝构造函数
string s5(s2, 1, 8);//提取s2从1到8位置的字符,字符小于8则输出剩下全不自负
string s6(s2, 1, string::npos);//提取从1到字符末尾
string s7(10,'a');//构造10个a
cout << "s1: " << s1 << endl;
cout << "s2: " << s2 << endl;
cout << "s3: " << s3 << endl;
cout << "s4: " << s4 << endl;
cout << "s5: " << s5 << endl;
cout << "s6: " << s6 << endl;
s1 = s7;
cout << "s7: " << s7 << endl;
cout << "s1: " << s1 << endl;
}
int main()
{
test_string1();
system("pause");
return 0;
}
string类对象的访问及遍历操作
string类对象的方式问方式大体可以分为3种operator[]+下标,我们使用最多的访问方式,迭代器访问方式,范围for()访问方式,下面我们来一一介绍
operator[]+下标
void test_string2()//[]+下标的遍历方式,推荐用法
{
string s1("hello");
s1 += ' ';
s1 += "world";
cout << s1 << endl;
//写
for (size_t i = 0; i < s1.size(); i++)//对字符+1后移1个字符
{
s1[i] += 1;
}
//读
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;
}
这种方式基本就是我们在数组操作字符串时进行遍历读写的操作,这个操作输出了一个hello world,而后我们对于字符遍历进行了+1操作,再次遍历输出,得到的结果是i f m m p x p s m e对所有的字符进行了+1,在进行遍历输出
[]+下标的方式也是我们最为熟悉的
迭代器
字符迭代器是一种相对模板化的一种字符在操作方式格式为 string::iterator it = s1.begin();类似与这样,当然,如果我们想调用其他的迭代器比如vector,只需要将string换为vector,而后后面再<类型>即可
//迭代器
//写
string::iterator it = s1.begin();
//auto it = s1.begin();也可以套用这种方式,之前学习的auto自动判断类型
while (it != s1.end())
{
*it -= 1;
++it;
}
//读
it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
因为我们刚在上面改变了hello world的字符,所以我们在这里用迭代器先将其改回来--对*it-=1,对每个字符-1,而后++it遍历,完成整个字符的-1,而后遍历字符串,进行输出
我们可以看到迭代器中的操作,很像指针,暂时我们可以将其当做指针
不过这只是最普通最常用的一种迭代器,还有三种迭代器
void test_string3()
{
string s1("hello world");
//倒着遍历
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit;
}
string num("12345");
cout << string2int(num) << endl;
}
这种是reverse_iterator倒序迭代器,不过我们操作还是++进行操作的,内部自动转化为倒序
这段代码利用倒序迭代器完成了将hello world,与12345倒序输出的功能
还有const修饰的迭代器,无法对原来的数据进行改变
int string2int(const string& str)
{
int val = 0;
//只能读,不能写
string::const_iterator it = str.begin();
while (it != str.end())
{
val *= 10;
val += (*it - '0');
++it;
}
//val = 0;
/*string::const_reverse_iterator rit = str.rbegin();
while (rit != str.rend())
{
val *= 10;
val += (*rit - '0');
++rit;
}*/
return val;
int main()
{
string num("12345");
//test_string2();
//test_string3();
cout << string2int(num) << endl;;
system("pause");
return 0;
}
这段代码完成了字符串对于int型的转化,正着转化和倒着转化
从顺序看迭代器分为两种,正序和倒序,从属性来看,迭代器分为const和普通,自由组合一共4种
范围for
范围for
C++11->原理其实是迭代器
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
范围for其实就是用我们从前的知识,对s1进行依次遍历,不过这种方式底层原理也是迭代器,这便是三种遍历string的方式
string类对象的容量操作
这些是基本的一些字符串操作函数,下面我们对capacity()进行剖析
我们可以看到,当我们不断地去加入字符时,容量的大小变化,从0开始,差不多是每次变化1.5倍左右进行扩大得到,我们还可以推出,到了后面增容会越来越慢,随着基数的增大每次扩大的容量也会增加,而且增容是有代价的,不能频繁去增加
下面我们再来了解一个关键字,reserve,这个关键字的含义是直接开辟定量的空间
此时我们会发现,就没有扩容了,直接将容量增到了100,其实我们在开辟时,显示的是开辟了100个空间,实际上是101个,因为还会腾出一个空间去给\0
在了解reserve后,我们有个与之相对应的resize()
这种是仅有一个int参数的情况,起到的作用就是开辟了100个字符串这种是reserve重载的方式,开辟了100个字符串并赋值
那么我们再来看下这种情况
当我们reserve了5个大小的空间,但是字符串不够放进去,所以就只有前五个放进去了,而后我们进行开辟20个,放入x,也不会再将world放入,全为x
这是我们的字符串插入操作,两种都可以完成操作,不过推荐第二种,方便可读
1. size() 与 length() 方法底层实现原理完全相同,引入 size() 的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size() 。2. clear() 只是将 string 中有效字符清空,不改变底层空间大小。3. resize(size_t n) 与 resize(size_t n, char c) 都是将字符串中有效字符个数改变到 n 个,不同的是当字符个数增多时:resize(n) 用 0 来填充多出的元素空间, resize(size_t n, char c) 用字符 c 来填充多出的元素空间。注意:resize 在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。4. reserve(size_t res_arg=0) :为 string 预留空间,不改变有效元素个数,当 reserve 的参数小于string的底层空间总大小时, reserver 不会改变容量大小。
这是我们的插入删除
还有一个小小的接口,c_str(),获取字符数组首地址,用C形式遍历,其实最主要的差距就是中间有无\0,对于C++的遍历方式而言,遇到\0并不会停止遍历,而对于C语言,碰到\0就停止了
还有一个substr(pos)函数,访问从pos位到结束的所有子串,以及我们的find('s')函数,找到第一个与s相同的字符
1. 在 string 尾部追加字符时, s.push_back(c) / s.append(1, c) / s += 'c' 三种的实现方式差不多,一般情况下string 类的 += 操作用的比较多, += 操作不仅可以连接单个字符,还可以连接字符串。2. 对 string 操作时,如果能够大概预估到放多少字符,可以先通过 reserve 把空间预留好。
void Teststring()
{
string str;
str.push_back(' '); // 在str后插入空格
str.append("hello"); // 在str后追加一个字符"hello"
str += 'b'; // 在str后追加一个字符'b'
str += "it"; // 在str后追加一个字符串"it"
cout << str << endl;
cout << str.c_str() << endl; // 以C语言的方式打印字符串
// 获取file的后缀
string file1("string.cpp");
size_t pos = file.rfind('.');
string suffix(file.substr(pos, file.size() - pos));
cout << suffix << endl;
// npos是string里面的一个静态成员变量
// static const size_t npos = -1;
// 取出url中的域名
sring url("http://www.cplusplus.com/reference/string/string/find/");
cout << url << endl;
size_t start = url.find("://");
if (start == string::npos)
{
cout << "invalid url" << endl;
return;
}
start += 3;
size_t finish = url.find('/', start);
string address = url.substr(start, finish - start);
// 删除url的协议前缀
pos = url.find("://");
url.erase(0, pos + 3);
cout << url << endl;
}
这是string一些接口的常用用法
还有一个需要我们注意的点是cin与getline,cin遇到空格或者换行都会停下来,而我们的getline只遇到换行会停下,空格不会,这在需要输入一些带空格的字符串时很有用
String的模拟实现
在这里我们对String类进行模拟实现,加深我们对其的理解
构造函数
我们之前在其它类中定义构造函数的时候是怎么定义的呢?我们在这里试一下
我们会发现,一般的写法对于读取字符串是可行的,但当我们想利用[ ]去改变它时,就会出现报错,原因是我们的s1存在栈中,去调用这个构造函数时会指向存在代码区的“hello”,而我们的operator[ ]返回的是字符串本身第i个位置的字符,因为存在代码段的字符串无法被修改,所以在我们想去修改字符串时会报错
所以我们就不能把字符串存储在常量区中,在这里我们选择堆,因为堆可以自行开辟大小
而我们若想建立一个空对象,也不能直接取用nullptr,因为strlen时出现了空指针,找不到\0,所以我们在建立空对象时需要在堆上开辟一个大小的空间,附上\0
下面我们展示全缺省合并写法
析构函数
析构函数就正常的资源清理与指针指控即可
拷贝构造
我们先来试试如果我们不写拷贝构造会发生什么呢?
我们先对c_str进行了重载,随后拷贝构造了s2,发现也可以实现拷贝构造,但是程序给崩溃了,这是什么原因呢,其实当我们没有重载拷贝构造时,系统自带的拷贝构造就会完成浅拷贝,也就是在栈中将s1中存的字符串地址按字节将值拷贝到同在栈中的s2,此时s1,s2指向堆中的同一个字符串,这里没有什么问题,但是最后在析构的时候,会对s1,s2分别进行析构,又因为他们指向同一地址空间,所以同一块空间被析构了两次,引起报错
接下来看看正确做法
正确的做法就是在堆中在开辟一块一模一样大的空间,再将值拷进去,进行深拷贝
赋值
我们用一般的方法进行赋值
发现同样的,会引起崩溃,这与上面的拷贝构造是一样的,系统自动浅拷贝,会在最后析构的时候出现问题,所以也需要我们对其进行重载
具体做法就是在堆中开辟一块与s3相同的tmp空间,将s3的拷贝给这个tmp,再将原来的s1空间释放掉,再将s1指向tmp,就完成了赋值
下面我们展示代码
namespace wxy
{
class string
{
public:
/*string()
:_str(nullptr)//而我们在调用无参时也不能这样调,会有空指针的问题,无法找到\0结束
{}*/
/* string(char* str)
:_str(str)//不能这么写,因为string对象中存的是指针,指针指向的数组中存储字符,字符无法修改
{}*/
//string()//可行方案
// :_str(new char[1])
//{
// _str[0] = '0';
//}
//string(char* str)//将空间开辟在堆上,str+1是为了给\0空间
// :_str(new char[strlen(str + 1)])//开辟一个str+1大小的新空间,而后用strcpy函数将在常量区的str拷到堆区的_str中
//{
// strcpy(_str, str);
//}
//将他们两个合并
string(char* str = " ")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
~string()
{
delete[] _str;
_str = nullptr;
}
//string s2(s1)
string(const string& s)
:_str(new char[strlen(s._str)+1])
{
strcpy(_str, s._str);
}
size_t size()
{
return strlen(_str);
}
char& operator[](size_t i)
{
return _str[i];
}
const char* c_str()
{
return _str;
}
//s1=s3
//s1=s1
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
return *this;
}
}
private:
char* _str;
};
void test_string2()
{
string s1("hello");
string s2(s1);
/*cout << s1.c_str() << endl;
cout << s2.c_str() << endl;*/
string s3("world");
s1 = s3;
cout << s1.c_str() << endl;
cout << s3.c_str() << endl;
}
}
void test_string1()
{
string s1("hello");
string s2;
for (size_t i = 0; i < s1.size(); ++i)
{
s1[i] += 1;//
cout << s1[i] << " ";
}
cout << endl;
for (size_t i = 0; i < s2.size(); ++i)
{
s1[i] += 1;//
cout << s2[i] << " ";
}
cout << endl;
}
我们的深拷贝其实可以有一种现代写法
//深拷贝-现代写法
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(_str, tmp._str);
}
//s1=s2
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(_str, tmp._str);
}
return *this;
}
//最简单版本
string& operator=(const string& s)
{
swap(_str, s._str);
return *this;
}
说明:上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
接下来我们想实现一个更加复杂的string类
namespace wxy
{
class string
{
public:
typedef char* iterator;//迭代器
iterator begin()//迭代器起点
{
return _str;
}
iterator end()//迭代器终点
{
return _str + _size;
}
string(const char* str = " ")//构造函数,堆上开辟空间将字符串拷进去
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
~string()//析构函数
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
size_t size()const//计算有效字符个数
{
return _size;
}
size_t capacity()const//计算字符串容量,\0不算
{
return _capacity;
}
char& operator[](size_t i)//[]重载
{
assert(i < _size);
return _str[i];
}
const char& operator[](size_t i)const
{
assert(i < _size);
return _str[i];
}
const char* c_str()//c语言形式输出字符
{
return _str;
}
void reserve(size_t n)//开空间函数
{
if (n > _capacity)//当要开的空间大于当前总容量时
{
char* newstr = new char[n + 1];//堆上开辟n+1
strcpy(newstr, _str);//原字符串拷进堆
delete[] _str;//析构原字符串
_str = newstr;//堆上开的新空间赋回字符串
_capacity = n;//容量改为n
}
}
void resize(size_t n, char ch = '\0')//改有效字符个数
{
if (n < _size)//当要改的有效字符个数小于当前有效字符个数
{
_str[n] = '\0';//将第n号下标元素改为\0,系统会自动识别到\0,剩下的就不识别了
_size = n;//有效字符个数改为n
}
else
{
if (n > _capacity)//当有效字符个数大于整个容量
{
reserve(n);//开空间到n
}
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;//将剩下空出来的位置赋成字符ch,缺省时为\0
}
_size = n;//有效字符个数改为n
_str[_size] = "\0";//最后一个位置上改为\0,好让系统识别结束
}
}
void push_back(char ch)//尾插字符
{
//空间满,进行增容
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void append(const char* str)//尾插字符串
{
size_t len = strlen(str);//计算字符串长度
if (_size + len > _capacity)//当最终插入后的字符串长度大于容量
{
reserve(_size + len);//扩容到应该的长度
}
strcpy(_str + _size, str);//将字符拷贝到增容后的空间
_size += len;//有效字符个数变为len
}
//s1+='a'
string& operator+=(char ch)//+=重载
{
this->push_back(ch);
return *this;
}
//s1+="aaaa"
string& operator+=(const char* str)
{
this->append(str);
return *this;
}
string& intsert(size_t pos, char ch)//插入字符
{
assert(pos < _size);//当插入位置小于有效字符个数断言
if (_size == _capacity)//容量不够,扩容
{
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
reserve(newcapacity);
}
int end = _size;//size开始移动
while (end >= pos)//从后往前循环全部后移1
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;//腾出来的位置插入ch
++_size;//有效字符个数+1
}
string& insert(size_t pos, const char* str)
{
assert(pos < _size);
size_t len = strlen(str);//计算字符串长度
if (_size + len > _capacity)
{
reserve(_size + len);
}
int end = _size;
while (end >= pos)
{
_str[end + len] = _str[end];//全部后移len位
--end;
}
/*for (size_t i = 0; i < len; ++i)
{
_str[pos] = str[i++];
}*/
strncpy(_str + pos, str, len);//拷贝len位
_size += len;
}
void erase(size_t pos, size_t len = npos)//删除pos开始之后长为len的字符串
{
assert(pos < _size);
if (len >= _size - pos)//字符串长度大于等于有效字符长度-删除的位置
{
_str[pos] = '\0';//删除的位置变为\0
_size = pos;//有效长度变为前面没删的部分
}
else//当有效字符长度大于字符串长度+pos时
{
size_t i = pos + len;//删除位置+字符串长度=删后字符串长度i,i后数据要挪到前面
while (i <= _size)
{
_str[i-len] = _str[i];//将i后数据挪到前len位置
++i;
}
_size -= len;//最终长度
}
}
size_t find(char ch, size_t pos = 0)//从pos开始寻找ch
{
for (size_t i = pos; i < _size; i++)//
{
if (_str[i] == ch)
{
return i;//找到
}
}
return npos;//未找到
}
size_t find(const char* str, size_t pos = 0)
{
char* p = strstr(_str, str);//在_str中寻找str
if (p == nullptr)//未找到
{
return npos;
}
else
{
return p-_str;//返回
}
}
bool operator<(const string& s)
{
int ret = strcmp(_str, s._str);//s与_str比较,_str大返回>0,相等=0,小<0
return ret < 0;
}
bool operator==(const string& s)
{
int ret = strcmp(_str, s._str);
return ret == 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 !(*this == s);
}
private:
char* _str;
size_t _size;//已有多少个有效字符
size_t _capacity;//可以存储多少个有效字符 \0不是有效字符
static size_t npos;
};
size_t string::npos = -1;
//getlin遇到空格不结束
istream& operator>>(istream& in, string& s)
{
while (1)
{
char ch;
//in >> ch;
ch = in.get();
if (ch == ' ' || ch == '\n')
{
break;
}
else{
s += ch;
}
}
}
ostream& operator<<(ostream& out,const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i];
}
return out;
}
void test_string1()
{
string s1;
string s2("hello");
string::iterator it2 = s2.begin();
while (it2 != s2.end())
{
cout << *it2 << " ";
++it2;
}
cout << endl;
//范围for,本质迭代器
for (auto e : s2)
{
cout << e << " ";
}
cout << endl;
}
}
对于string的学习还需要在不断地练习中精进