目录
[1. 成员变量和构造函数,拷贝构造,析构函数](#1. 成员变量和构造函数,拷贝构造,析构函数)
[2. string类对象的容量操作](#2. string类对象的容量操作)
<1>size,capacity,empty,clear(初始化)
[3. string类对象的访问和遍历](#3. string类对象的访问和遍历)
[4. string类对象的修改操作](#4. string类对象的修改操作)
[<1>push_back(尾插), append, operator+=](#<1>push_back(尾插), append, operator+=)
<2>insert(插入),erase(删除),operator=(赋值)
[5. string类非成员函数](#5. string类非成员函数)
浅浅介绍一下什么是STL:
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。STL有六大组件:仿函数,算法,迭代器,空间适配器,容器,配接器
本篇讲的就是STL容器部分的string类
string类需要知道的小知识
auto和范围for:
(1)在本篇的学习中将使用到关键字auto,它的作用是自动识别类型
注意:
1.使用auto声明指针类型,auto和auto*没有任何区别,但如果修饰引用类型则必须加上&
2.auto在对同一行声明多个变量时,变量必须是相同的类型,否则会报错,因为编译器实际只能对第一个类型进行推导,然后用推导出来的类型定义其他变量
//会出现编译报错:在声明符列表中,auto必须始终推导为同一类型
auto a = 1, b = 2.1;//在这之前已经推导出了int
3.auto不能够作为函数的参数,但可以作为返回值,想设为返回值请谨慎使用
4.auto不能设为数组
(2)范围for
C++11中引入了基于范围的for循环,for循环后的括号由冒号" :"分为两部分:第一部分是范围
内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
1.对于一个知道范围的集合而言,就可以使用范围for
2.范围for可以作用到数组和容器上进行遍历
3.范围for的底层实际上就是替换为迭代器
int main()
{
int a[]={1,2,3,4,5};
for(auto& a1:a)
{
cout<<a1<<" ";
}
return 0;
}
string类的常用接口:
string是一个比较早的容器所以在部分设计上会出现冗余的情况,我只会讲一些比较常用的接口,其他的接口请自行查看文档
string类接口详细文档传送门:string - C++ 参考 (cplusplus.com)
1.string类对象的常见构造
string() //构造出一个空的string对象
string(const char* ch) //使用一个字符串进行构造对象
string (size_t n,char c) //string类对象中包含n个字符c,这个基本不实现
string (const string& st) //拷贝构造
2.string类对象的容量操作
size //返回字符串有效长度
capacity //返回空间总大小
empty //判断字符串是否为空
clear //清空字符串
reserve //空间扩容
resize //将字符串大小调整为n个字符的长度,多出的空间用字符c填充
3.string类对象的访问和遍历
operator[] //返回pos位置的字符引用
begin //获取字符串起始位置字符的迭代器
end //获取字符串最后一位的下一位置的迭代器
4.string类对象的修改操作
operator=//将一个对象赋值给另一个对象
push_back //尾插一个字符或者字符串
append //在字符串后追加一个字符串
operator+= //在字符串后面追加一个字符或者字符串c_str //返回C格式字符串
find+pos //在起始位置加上pos后的下标开始往后查找字符或者字符串出现的起始位置
substr //从对象的字符串的pos位置开始向后截取n个字符,然后返回
insert//在pos位置上插入一个字符或者字符串
5.string类非成员函数
operator>> //输入重载
operator<< //输出重载
实现一个string类:
这里都是我自己实现各项功能,实现参数部分可能会和文档有差异,请见谅
1. 成员变量和构造函数,拷贝构造,析构函数
string类主要有三个成员变量,还会设置一个静态成员常量,分别是指向数组的指针char*,记录有效字符数的_size,和容量_capacity,静态成员常量npos
构造函数上我们不使用初始化列表,参数设成一个带有缺省值的char*指针,这样可以同时解决单字符初始化,字符串初始化,空字符串对象三种初始化
注意:
1.npos固定为-1,因为在参数里会使用它作为缺省值,无符号整形的符号位无效所以当缺省值有效时它的大小是:4294967295。它的作用就是给一个很大的值,在后面的函数实现中erase(删除)和substr(复制子字符串)会使用到
2.字符串的结尾都会跟上一个'\0'它也是占空间的,但记录容量的_capacity并不会记录这个空间,所以在new开空间的时候要额外增加一个空间给'\0',如果不开这个空间就有两个结果:
<1>不在字符串尾部增添'\0'导致循环越界访问直至遇到'\0';
<2>增添'\0'的位置已经被别的变量申请走了,越界增添'\0'导致破坏了其他资源的完整性
class string { public: string(const char*ch="") //注意这里给的缺省值是没有有效字符的哦 { size_t size=strlen(ch); _str=new char[size+1]; //加1为'\0'开空间 strcpy(_str,ch); _size=_capacity=size; } private: char* _str; size_t _size; size_t _capacity; static const int npos = -1; }
拷贝构造现在也不应该是浅拷贝,因为出现了空间申请,需要使用深拷贝,现在我们实现一个swap(交换函数)来辅助进行拷贝构造的实现。
注意:直接使用库里的swap对对象(参数是对象)也可以完成交换,但是和我自己实现交换过程有很大差别,库里的实现是对对象的拷贝再交换,这样会导致频繁的触发拷贝构造(深拷贝)导致降低效率,所以不直接交换对象,交换成员变量
出现了资源申请析构函数也不能使用自动生成的了,需要自己实现
void swap(string& ch) { std::swap(_str, ch._str); //交换指针 std::swap(_size, ch._size); //交换大小 std::swap(_capacity, ch._capacity); //交换容量 } string(const string& str) { //std::swap(*this,str); 频繁触发拷贝构造会出现效率降低的问题 string temp(str._str); //这里是用str的字符串初始化一个string对象 swap(temp); //现在交换temp和this指针指向的对象 } ~string() { if(!empty()) { delete[] _str; _str = nullptr; _size = _capacity = 0; } }
2. string类对象的容量操作
<1>size,capacity,empty,clear(初始化)
这四个都是比较简单的成员函数,直接给大家看实现,一看很快就能理解了
size_t size() //返回有效字符个数 { return _size; } size_t capacity() //返回容量大小 { return _capacity; } bool empty() //判空 { return _size==0; } void clear() { _str[0]='\0'; //这里要重载[]返回当前位置的引用才能直接修改 _size=0; }
<2>reserve(扩容),resize
剩下的这两个,reserve负责扩容比较耳熟能详;resize它能调整字符串的长度,如果给的n大于当前字符串长度,则通过在末尾插入所需数量的字符来扩展,直到达到n的长度,如果给的n小于当前长度,那么缩短为当前n个字符,并且删除第n个字符(包括n)以外的字符
下面直接看实现来理解更方便:
void reserve(size_t n) { if(n>_capacity) { char* newstr=new char[n+1]; //这里也不要忘了'\0'的位置 strcpy(newstr,_str); //拷贝一下旧空间 delete[] _str; //删除旧空间 _str=newstr; //指向新空间 _capacity=n; //更新容量 } } void resize(size_t n, char c='0') { if (n > _capacity) //如果容量不够就扩容 { reserve(_capacity * 2); } if (n < size()) { _str[n] = '\0'; //小于的话直接在n位置加'\0' _size = n; //更新有效字符数 } else { for (int i = size(); i <= n; i++) { _str[i] = c; } _size=n; //更新_size _str[_size]='\0'; //不要忘了'\0' } }
3. string类对象的访问和遍历
这里就会涉及到迭代器的使用了。迭代器的主要目的是为了屏蔽底层细节,让代码有更好封装性和模块化,能够让使用者不要直接面对复杂的底层内部实现,只需要通过简化后的接口进行操作,使其更容易使用和理解,它就是封装的一种体现方式
class string { public: typedef char* iterator; //自定义命名char* iterator begin() //返回字符串起始位置的迭代器 { return _str; } iterator end() //返回字符串末尾下一位的迭代器 { return _str+_size; } const iterator begin()const //const迭代器 { return _str; } const iterator end()const { return _str + _size; } }
operator[]:重载方括号方便字符串的访问和修改
char& operator[](size_t pos) //运算符重载 { assert(pos < _size); //当前下标必须有效 return _str[pos]; //返回当前下标字符的引用 }
4. string类对象的修改操作
<1>push_back(尾插), append, operator+=
这三个成员函数在实现上都有着十分相似的情况,甚至可以相互复用所以直接放到一起讲
void push_back(const char ch) { if (_size == _capacity) //判断容量是否足够 { reserve(_capacity == 0 ? 4 : _capacity * 2); } _str[_size++] = ch; //尾插一个字符 _str[_size] = '\0'; } string& operator+=(const char ch) //复用push_back完成尾插一个字符 { push_back(ch); return *this; } string& operator+=(const char* ch) { size_t len = strlen(ch); //计算字符串长度 if (_size+len > _capacity) //判断容量是否足够 { reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2); } memcpy(_str + _size, ch, sizeof(char) * len); //拷贝空间 _size += len; //更新_size _str[_size] = '\0'; //尾部增添'\0' return *this; } void append(const char* str) //在字符串后面增添字符串 { *this+=str; //调用operator+= } void push_back(const char* ch) //这个和append就一样 { *this += ch; //调用operator+= }
在实现完后就可以发现这些函数的功能重叠了好多,append和push_back尾插字符串功能就重叠了,这就是因为比较早出现的原因出现的设计冗余
<2>insert(插入),erase(删除),operator=(赋值)
insert(插入)在代码上容量扩容方面和上面基本一致,但注意不要把insert和前面的功能搞混,insert是插入字符串中的有效字符,上面的三个都是尾插需要在末尾手动添加'\0',假设调用insert时pos位置是尾巴,在代码段末端增加'\0'的代码那就和上面的功能无异,那pos在中间时那不就暴毙了,搁中间加'\0'那就错了,这也是为什么尾插无法复用insert,所以单独拉出来讲
void insert(size_t pos, char ch) //在pos位置插入一个字符 { assert(pos <= _size); //pos位置必须有效 if (_size == _capacity) //判断容量是否足够 { reserve(_capacity == 0 ? 4 : _capacity * 2); } size_t end = _size + 1; //找到'\0'位置的下一个位置 while (end >= pos) //把pos位置后的字符(包括'\0')全部后移 { _str[end] = _str[end - 1]; end--; } _str[pos] = ch; //插入字符 _size++; //_size加1 } void insert(size_t pos, const char* ch) { assert(pos <= _size); //pos位置必须有效 size_t len = strlen(ch); //计算插入字符串长度 if (_size + len > _capacity) //容量大小是否足够 { reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2); } int end = _size + len; //'\0'位置加上字符串长度 while (end >= pos + len ) //将包括'\0'的字符全部后移 { _str[end] = _str[end - len]; end--; } memcpy(_str + pos, ch, sizeof(char) * len); //直接拷贝进去 _size += len; //_size增加插入字符串长度 }
erase(删除)的主要作用:要求在pos位置开始向后删除len个字符,len有缺省值,当缺省值有效时删除pos位置包括pos位置往后的所有字符,因为len此时超级大
void erase(size_t pos, int len=npos) { assert(pos < _size); //pos位置必须有效 if (len >= _size - pos) //如果删除字符数大于剩下字符数 { _str[pos] = '\0'; //直接pos位置给'\0' _size = pos; //更新_size } else { for (size_t i = pos + len; i <= _size; i++) //将pos+len的数据前移 { _str[i - len] = _str[i]; } _size -= len; //更新_size } }
operator=(赋值):这个就很简单了,使用赋值对象构造一个对象,然后交换被赋值对象和构造出来的对象即可
string& operator=(string temp) //这里不用引用直接传参触发拷贝构造 { swap(temp); //交换(形参不影响实参) return *this; }
<3>c_str(返回C格式字符串)和find(查找)
在没有重载输出(cout)之前 cout<<st1(string类对象) 肯定是不行的,因为它是一个对象并不是字符串,c_str就是专门解决这个问题,它会返回字符串的起始位
const char* c_str() { return _str; } //没重载cout之前 cout<<st1<<endl; 无法打印字符串 //cout<<st1.c_str()<<endl; 这样就可以了
find(查找):返回在pos位置往后查找字符或者字符串出现的第一位置的下标,
查找一个字符:直接for循环遍历查找,找到返回下标
查找字符串:调用strstr函数直接查找字符串出现第一个位置的指针,通过这个指针减去起始位就是下标
下面看实现:
size_t find(char ch, size_t pos) { assert(pos < _size); //pos位置必须有效 for (size_t i = pos; i < _size; i++) { if(_str[i]==ch) { return i; //返回下标 } } } size_t find(const char* ch, size_t pos) { assert(pos < _size); const char* fin = strstr(_str + pos, ch); //找到并返回出现的第一个位置的指针 if (fin == nullptr) //空代表没找到 { return npos; //返回npos } else { return fin - _str; //当前指针减去起始位置得出下标返回 } }
<4>substr(复制子字符串)
substr的主要作用:要求指定位置开始往后截取n个字符并返回,如果n没有给指定的长度那么缺省值有效,默认从指定位置开始向后截取全部字符返回,下面看实现理解更方便
string substr(size_t pos, size_t len=npos) { assert(pos < _size); //依旧是pos位置必须有效 //len大于剩余长度,更新len if (len > _size-pos) { len = _size-pos; } string sub; //构造一个新的string类对象 sub.reserve(len); //给len长度的空间 for (size_t i = 0; i < len; i++) //从pos位置开始向后截取len次字符 { sub += _str[pos + i]; //尾插 } return sub; //返回当前类对象 }
5. string类非成员函数
比较主要的就是重载输入和输出了,这个比较简单直接看实现来理解
ostream& operator<<(ostream& out, const string& s) { for(auto ch:s) //使用范围for自动识别类型,自动迭代,自动判断停止 { cout<<ch; } out << endl; return out; } istream& operator>>(istream& in, string& s) { s.clear(); //输入自然要对当前对象进行初始化 char ch; ch=in.get() while(ch != ' ' && ch != '\n') //这个是判断什么时候停下来,我假设 { //遇到空格和换行就停止 s+=ch; ch=in.get(); } return in; }
本篇文章到这里主要内容已经讲完了,希望能够对你产生帮助,感谢阅读
需要看全部实现可以往下翻下面有完整实现
我自己实现的一个string类 :
//头文件
#include <iostream>
#include <assert.h>
using namespace std;
namespace cool
{
class string
{
public:
//简易迭代器
typedef char* iterator;
iterator begin() //普通迭代器
{
return _str;
}
iterator end()
{
return _str+_size;
}
const iterator begin()const //const迭代器
{
return _str;
}
const iterator end()const
{
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;
_capacity = _size=0;
}
void swap(string& str) //交换函数
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
string(const string& str) //拷贝构造
{
string temp(str._str);
swap(temp);
}
const char* c_str()const //返回C格式字符串
{
return _str;
}
size_t size()const //返回有效个数
{
return _size;
}
bool empty() //判空
{
return _size==0;
}
void clear() //初始化
{
_str[0] = '\0';
_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 push_back(char ch); //尾插
void push_back(const char* ch); //尾插的函数重载
string& operator+=(const char ch); //重载+=
string& operator+=(const char* ch); //+=的函数重载
string& operator=(string temp); //重载=
char& operator[](size_t pos) //重载[]
const char& operator[](size_t pos)const; //const类型的[]重载
void append(const char* str); //字符串后增加一个字符串
void insert(size_t pos, char ch); //插入
void insert(size_t pos, const char *ch); //插入的函数重载
void erase(size_t pos,int len=npos); //删除
size_t find(char ch, size_t pos = 0); //查找
size_t find(const char* ch, size_t pos = 0); //查找的函数重载
string substr(size_t pos = 0, size_t len=npos); //复制一个子字符串
private:
char* _str=nullptr;
size_t _size=0;
size_t _capacity=0;
static const size_t npos=-1;
};
//重载输入输出
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
void test();
void test1();
void test2();
void test3();
void test4();
}
//实现文件
namespace cool
{
void string::push_back(const char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
void string::push_back(const char* ch)
{
*this += ch;
}
string& string::operator+=(const char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* ch)
{
size_t len = strlen(ch);
if (_size+len > _capacity)
{
reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
}
memcpy(_str + _size, ch, sizeof(char) * len);
_size += len;
_str[_size] = '\0';
return *this;
}
string& string::operator=(string temp)
{
swap(temp);
return *this;
}
char& string::operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& string::operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
void string::append(const char* str)
{
*this+=str;
}
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = size() + 1;
while (end >= pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size++;
}
void string::insert(size_t pos, const char* ch)
{
assert(pos <= _size);
size_t len = strlen(ch);
if (_size + len > _capacity)
{
reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
}
size_t end = _size + len;
while (pos + len <= end)
{
_str[end] = _str[end - len];
end--;
}
memcpy(_str + pos, ch, sizeof(char) * len);
_size += len;
}
void string::erase(size_t pos, size_t len)
{
assert(pos <= _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* ch, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, ch);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len >_size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i <len; i++)
{
sub += _str[pos + i];
}
return sub;
}
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 256;
char buff[N];
int i=0;
char ch;
ch = in.get();
while (ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
i = 0;
}
return in;
}
//测试用例
void test( )
{
string s1;
string s2("h");
string s3("hello");
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
//尾插测试
void test1()
{
string s1;
s1.push_back('h');
cout << s1 << endl;
s1.push_back("ello");
cout << s1 << endl;
s1.append(" world");
cout << s1 << endl;
}
//插入删除测试
void test2()
{
string s1("hello world");
s1.insert(5, 'x');
cout << s1 << endl;
s1.insert(0, "xxxx");
cout << s1 << endl;
s1.erase(0, 4);
cout << s1 << endl;
}
//拷贝和赋值测试
void test3()
{
string s1("hello world");
string s2 = s1;
string s3;
s3 = s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
//查找和返回字符串测试
void test4()
{
string st("hello.cpp");
size_t fin = st.find('.');
string st1 = st.substr(fin);
cout << st1 << endl;
}
}