string类的实现
文章目录
默认成员函数
构造函数
构造函数设置为缺省参数,若不传入参数,则默认构造为空字符串。字符串的初始大小和容量均设置为传入C字符串的长度(不包括’\0’)。
public:
string(const char* str="")
{
_size=strlen(str);
_capacity=_size;//这里不是_size+1因为'\0'不是有效字符
_str=new char[_capacity+1];
strcpy(_str,str);
}
private:
char* _str;
size_t _size;//已经有多少个有效字符
size_t _capacity;//能存多少个有效字符 [!!!'\0'不是有效字符]
static size_t npos=-1;
}
拷贝构造函数
在拷贝构造中,存在类的默认拷贝构造函数是浅拷贝的问题。
浅拷贝是按字节序拷贝,因为对于指针类型变量进行拷贝会导致指向同一块空间,会对同一块区域析构两次造成内存问题。
而拷贝构造函数有两种写法。
传统写法
写法一:传统写法
:
//s2(s1)---->深拷贝--传统写法
string(const string& s)
:_str(new char[strlen(s._str)+1])
,_size(0)
,_capacity(0);
{
strcpy(_str,s._str);
_size = s._size;
_capacity = s._capacity;
}
现代写法
写法二:现代写法
可以理解为空手套白狼。
//交换两个对象的数据
void swap(string& s)
{
//调用全局的swap
::swap(_str, s._str); //交换两个对象的C字符串
::swap(_size, s._size); //交换两个对象的大小
::swap(_capacity, s._capacity); //交换两个对象的容量
}
//s2(s1)---->深拷贝--现代写法
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);//string tmp对象是在栈上,里面的_str对象是在堆上
swap(tmp);
}
赋值运算符重载
与拷贝构造函数类似,赋值运算符重载函数的模拟实现也涉及深浅拷贝问题,我们同样需要采用深拷贝。
传统写法
写法一:传统写法
:
string& operator&=(const string& s)
{
if(this != &s)
{
char* ptr=new char[strlen(s._str)+1])
if(ptr == nullptr) perror("error");
delete[] _str;
_str=ptr;
strcpy(_str,s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
现代写法
写法二:现代写法1
:
//交换两个对象的数据
void swap(string& s)
{
//调用全局的swap
::swap(_str, s._str); //交换两个对象的C字符串
::swap(_size, s._size); //交换两个对象的大小
::swap(_capacity, s._capacity); //交换两个对象的容量
}
string& opeartor=(string s)
{
swap(s);//交换两个对象
return *this;
}
但这种写法无法避免自己给自己赋值,就算是自己给自己赋值这些操作也会进行,虽然操作之后对象中_str指向的字符串的内容不变,但是字符串存储的地址发生了改变,为了避免这种操作我们可以采用下面这种写法:
现代写法2
:
//交换两个对象的数据
void swap(string& s)
{
//调用全局的swap
::swap(_str, s._str); //交换两个对象的C字符串
::swap(_size, s._size); //交换两个对象的大小
::swap(_capacity, s._capacity); //交换两个对象的容量
}
string& operator=(const string& s)
{
if(this != s){
string tmp(s);
swap(s);
}
return *this;
}
但实际中很少出现自己给自己赋值的情况,所以采用“现代写法1”就行了。
析构函数
当对象销毁时堆区对应的空间并不会自动销毁,防止内存泄漏,我们需要使用delete手动释放堆区的空间。
~string()
{
delete[] _str;
_str=nullptr;
_capacity = 0;
_size = 0;
}
迭代器相关函数
在string
类中,迭代器只是字符指针,而到了list
中,迭代器就是封装好的iterator
类。
begin和end
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
const_iterator begin()const
{
return _str;
}
typedef char* iterator;
typedef const char* const_iterator;
iterator end()
{
return _str+_size;
}
const_iterator end() const
{
return _str+_size;
}
看到了string类中迭代器的底层实现,再看迭代器遍历string的代码,其实是字符串的指针遍历。
string s = "hello";
string::iterator it = s.begin();
while( it != s.end() )
{
cout<<*it<<" ";
it ++ ;
}
cout<<endl;
而我们常用的范围for
实际上就是auto
替换成迭代器
string s = "hello";
for( auto&e : s){
cout<< e <<" ";
}
cout<<endl;
容量和大小相关函数
size和capacity
size
用于获得当前字符串的有效长度
size_t size() const
{
return _size;
}
capacity
函数用于获取字符串当前容量
size_t capacity() const
{
return _capacity;
}
reserve和resize
reserve规则:
1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
2、当n小于对象当前的capacity时,什么也不做。
//reserve
void reserve(size_t n){
if (n < _capacity) return; //空间足够时不开辟
char* tmp = new char[n + 1]; //多的1是给'\0'
if (!tmp) perror("reserve");
strncpy(tmp,_str,_size+1);
delete[] _str;
_str = tmp;
_capacity = n;
}
Tips:这里的要用strncpy
,因为如果字符串中途有\0
会导致拷贝提前停止,是个小细节
resize(size_t n)与resize(size_t n,char c)都是将字符串中有效字符个数改变到n个,不同的是当字符增多时
- resize(size_t n)是用0来填充多出的元素空间,另一个使用char c
- resize 在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小。如果将元素个数减少,底层空间的总大小不变
resize规则:
1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\0’。
2、当n小于当前的size时,将size缩小到n。
resize
的测试代码
void test_string5()
{
string s;
//s.reserve(100);//提前开空间
s.resize(100);//放100个字符
size_t sz = s.capacity();
cout << "making s grow:" << endl;
for (int i = 0; i < 100; i++)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << "\n";
}
}
}
void test_string6()
{
string s("hello world");
s.resize(5);//hello
s.resize(20, 'x');//helloxxxxxxxxxxx
}
// 开空间+初始化,扩展capacity 并且初始化空间。size也要动
void resize(size_t n, char val = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; ++i)
{
_str[i] = val;
}
_str[n] = '\0';
_size = n;
}
}
empty
判空函数
bool empty()
{
return _size ==0;
}
字符串修改相关函数
insert
实现了insert
接口之后插入的函数都可以复用insert
的
insert
是在pos位置之前插入
string& insert(size_t pos,char ch)
{
assert(pos<=_size);
if(_size==capacity){
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
char* end = _str + _size;
while(end>=_str +pos){
*(end+1) = *end;
end--;
}
_str[pos]=ch;
++_size;
return *this;
}
string& insert(size_t pos,const char* str)
{
assert(pos<=_size);///_size是最后一个字符的下一个位置
size_t len=strlen(str);
if(_size+len>_capacity)
{
reverse(_size+len);
}
char* end = _str + _size;
while (end >= _str + pos)
{
*(end + len) = *end;
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
注意:插入字符串的时候使用strncpy,不能使用strcpy,否则会将待插入的字符串后面的’\0’也插入到字符串中。
push_back
void push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_str[_size + 1] = '\0';
++_size;
}
复用之后
void push_back(char ch)
{
insert(_size,ch);
}
append
复用之前
void append(const char* str)
{
size_t len = _size + strlen(str);
if (len > _capacity)
{
reserve(len);
}
strcpy(_str + _size, str);
_size = len;
}
复用之后
void append(const char* str)
{
insert(_size,str);
}
operator+=
string& operator+=(const char* str)
{
append(str);
return *this;
}
erase
和insert
类似,所以一般来说实现了insert
和erase
之后剩下的增加和删除的接口就可以快速复用了。
string& erase(size_t pos,size_t len=npos)
{
assert(pos<_size);
if(len>=_size-pos){
_str[pos]='\0';
_size=pos;
}
else{
size_t i=pos+len;
//_str[size]存的实际是'\0'
while(i<=_size){
_str[i-len]=_str[i];
++i;
}
_size-=len;
}
}
clear
清空但是并不意味着释放空间
//清空字符串
void clear()
{
_size = 0; //size置空
_str[_size] = '\0'; //字符串后面放上'\0'
}
swap
若想让编译器优先在全局范围寻找某函数,则需要在该函数前面加上“::”(作用域限定符)。
//交换两个对象的数据
void swap(string& s)
{
//调用库里的swap
::swap(_str, s._str); //交换两个对象的C字符串
::swap(_size, s._size); //交换两个对象的大小
::swap(_capacity, s._capacity); //交换两个对象的容量
}
字符串访问相关函数
c_str
c_str函数用于获取对象C类型的字符串。
//返回C类型的字符串
const char* c_str()const
{
return _str;
}
front
char& front() const {
return _str[0];
}
back
char& back() const {
assert(_size > 0);
return _str[_size - 1];
}
operator[]
[ ]运算符的重载是为了让string对象能像C字符串一样,通过[ ] +下标的方式获取字符串对应位置的字符。
//[]运算符重载(可读可写)
char& operator[](size_t i)
{
assert(i < _size); //检测下标的合法性
return _str[i]; //返回对应字符
}
//[]运算符重载(只读)
const char& operator[](size_t i)const
{
assert(i < _size); //检测下标的合法性
return _str[i]; //返回对应字符
}
find
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
const char* ret = strstr(_str + pos, str);
if (ret)
{
return ret - _str;
}
else
{
return npos;
}
}
rfind
//反向查找第一个匹配的字符串
size_t rfind(const char* str, size_t pos = npos)
{
string tmp(*this); //拷贝构造对象tmp
reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的C字符串
size_t len = strlen(str); //待查找的字符串的长度
char* arr = new char[len + 1]; //开辟arr字符串(用于拷贝str字符串)
strcpy(arr, str); //拷贝str给arr
size_t left = 0, right = len - 1; //设置左右指针
//逆置字符串arr
while (left < right)
{
::swap(arr[left], arr[right]);
left++;
right--;
}
if (pos >= _size) //所给pos大于字符串有效长度
{
pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
}
pos = _size - 1 - pos; //将pos改为镜像对称后的位置
size_t ret = tmp.find(arr, pos); //复用find函数
delete[] arr; //销毁arr指向的空间,避免内存泄漏
if (ret != npos)
return _size - ret - len; //找到了,返回ret镜像对称后再调整的位置
else
return npos; //没找到,返回npos
}
关系运算符重载
bool operator<(const string& s)
{
int ret=strcmp(_str,s._str);
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);
}
流运算符重载
输出
重载输出
:
ostream& operator<<(ostream& out,const string& s)
{
for(int i=0;i<s.size();i++){
cout<<s[i];
}
return out;
}
输入
重载输入
:
///写一个重载输入
istream& operator>>(istream& in,const string&s){
s.clear();//库的string虽然初始化好了重新输入的时候是新的内容。
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
getline
//getline
istream& getline(istream& in,string&s){
clear();
char ch;
ch = in.get();
while (ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
补充
关于string的一些实现方案讨论
c++标准只规定了string要实现的接口功能,具体如何实现,那是各个库的实现人自己决定的
- vs系列的编译器,微软工程师实现
- 比如VS把长度<=16的字符串并没有放开出来的堆上而是放在数组里
- gcc/g++ gnu工程师
- clang
int main()
{
std::string s1("hello");
YCB::string s2("hello");
cout<<sizeof(s1)<<endl;
cout<<sizeof(s2)<<endl;
std::string s1("hello world");
std::string s4(s3);
printf("%p\n",s1.c_str());//在VS和linux下的头文件
printf("%p\n",s4.c_str());//
s1[0]='x';
printf("%p\n",s1.c_str());
printf("%p\n",s4.c_str());
string s6="hello";//隐式类型转化--expliect
}
- 关于string的深浅拷贝的解决方案
- 深拷贝
- 引用计数浅拷贝+写时拷贝
- 一般情况下,推荐深拷贝的方案
- 引用计数的写时拷贝,设计相对复杂并且在特殊情况下存在缺陷。
const使用的小结
-
有些在函数内要修改的就不用const 修饰*this
- 本身就是修改数据的接口,如:push_back、insert、erase是不能加const
-
有些接口同时也要给const对象用就要用const修饰*this
- 不修改成员变量的函数,都应该加const
-
有些函数要实现const和非const两个版本
-
迭代器
-
operator[]
-
string s1("hello"); s1[1]; s1[1]='x'; const string s2("world"); s2[1];//只读
-
-
- 总结
- 只读接口函数+ 加const
- 只写接口函数+ 不加const
- 可读可写接口 +加const版本和不加const版本,也就是具体加不加要看函数接口的功能性
简易版string
如果时间紧迫就不必要写一个增删查改完善的string
类,就写一个基本的即可,提前把insert
和erase
两个接口写好然后复用
namespace YCB
{
class string
{
public:
///写一个全缺省的
string()
:_str(new char str[1])
{ _str[0]='\0'; }
string(char* str)
:_str(new char[strlen(str)+1])
{
strcpy(_str,str);
}
string(char* str="")/// ""是'\0'
:_str(new char[strlen(str)+1])
{
strcpy(_str,str);
}
//s2(s1)---->深拷贝
string(const string& s)
:_str(new char[strlen(s._str)+1])
{
strcpy(_str,s._str);
}
string& operator=(const string& s)
{
if(this!=&s)
{
char* ptr=new char[strlen(s._str)+1])
if(ptr==nullptr)
{
perror("operator=");
}
delete[] _str;
_str=nullptr;
_str=ptr;
strcpy(_str,s._str);
}
return *this;
}
const char* c_str()
{
return _str;
}
size_t size()
{
return strlen(_str);
}
char& operator(size_t i)
{
return _str[i];
}
~string()
{
delete[] _str;
_str=nullptr;
}
private:
char* _str;
};
void test_string1()
{
string s1("hello");
string s2;
for(size_t i=0;i<s1.size();i++)
{
s1[i]+=1;
cout<<s1[i]<" ";
}
for(size_t i=0;i<s2.size();i++)//Runtime error
{
s2[i]+=1;
cout<<s2[i]<<" ";
}
}
void test_string_2()
{
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;
}
}
较完整的string
发觉之前的实现中存在一些细节问题,等一段时间后修正。