构造函数设置为缺省参数,若不传入参数,则默认构造为空字符串。字符串的初始大小和容量均设置为传入C字符串的长度(不包括’\0’)。
string(const char* str = "")
: _str(new char[strlen(str) + 1])//为存储字符串开辟空间('\0'也要开辟一个空间)
, _size(strlen(str))//初始化为字符串大小
, _capacity(strlen(str))//初始化为字符串大小
{
strcpy(_str, str);//将参数拷贝进开辟的空间中
}
每个string对象中的成员_str都指向堆区的一块空间,当对象销毁时堆区对应的空间并不会自动销毁,为了避免内存泄漏,我们需要使用delete手动释放堆区的空间。
~string()
{
delete[] _str;//释放_str指向空间
_str = nullptr;//置空
_size = _capacity = 0;//置为0
}
首先我们得了解深浅拷贝的问题:
1.浅拷贝:浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
2.深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。
namespace gtt
{
class string
{
public:
string(const char* str = "")
{
_size = strlen(str);//初始化为字符串大小
_capacity = _size;//初始化为字符串大小
_str = new char[_capacity + 1];//为存储字符串开辟空间('\0'也要开辟一个空间)
strcpy(_str, str);//将参数拷贝进开辟的空间中
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
void TestString1()
{
string s("hello world");
string s1(s);//系统调用默认拷贝构造函数,发生浅拷贝
}
}
对于string类来说,我们必须显示来实现拷贝构造函数,否则就会出现深浅拷贝的问题,如果我们显示写拷贝构造函数,系统就会调用默认拷贝构造函数,此时就会发生浅拷贝,浅拷贝就会出现一个问题,s,s1的_str指向了同一块空间,在调用析构函数后这块空间被释放,就会造成野指针的问题。
所以我们在我们的拷贝构造函数就要以深拷贝的方式实现:
方法1:
开辟一块新的空间,将源字符串拷贝过去,再将其他变量耶拷贝过去。
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;
}
方法2:
利用构造函数创建一个中间变量tmp,然后将tmp的值与_strj进行交换,这样s._str与_str指向的就是不同的空间:
void swap(string& tmp)
{
::swap(_str, tmp._str);
::swap(_size, tmp._size);
::swap(_capacity, tmp._capacity);
}
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str);//利用构造函数构造tmp
swap(tmp);//交换_str与tmp
}
方法1:
重新开辟一块空间tmp,将源字符串s._str复制到新开辟的空间tmp中,在释放掉_str指向的空间,最后令_str指向新开辟的空间
string& operator=(const string& s)
{
if (this != &s)//防止自己给自己赋值
{
char* tmp = new char[s._capacity + 1];//开辟新空间,空间大小为源原字符串大小+'\0'
strcpy(tmp, s._str);//复制数据
delete[] _str;//释放掉原来的空间
_str = tmp;//指向新开辟空间
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
void TestString2()
{
string s("hello world");
string s1("hello people");
s = s1;
}
方法2:
利用拷贝构造函数构造一块新空间tmp,交换tmp与_str数据:
string& operator=(const string& s)
{
if (this != &s)//防止自己给自己赋值
{
string tmp(s);//利用拷贝构造函数构造一块新空间tmp
swap(tmp);//进行交换
}
return *this;
}
begin和end
- begin
返回字符串中第一个字符的地址。- end
返回字符串中最后一个字符的后一个字符的地址(即’\0’的地址)。
typedef char* iterator;
typedef const char* const_iterator;
//返回字符串第一个字符的地址
iterator begin()
{
return _str;
}
//返回字符串第一个字符的const地址
const_iterator begin()const
{
return _str;
}
//返回字符串最后一个字符下一位的地址
iterator end()
{
return _str + _size;
}
//返回字符串最后一个字符下一位的const地址
const_iterator end()const
{
return _str + _size;
}
在这儿我们需要了解的是编译器会自动将范围for转换为迭代器的形式,我们已经实现了迭代器,范围for自然可以使用。
capacity
返回字符串的容量大小
size_t capacity()const
{
return _capacity;//返回容量大小
}
size
返回字符串大小
size_t size()const
{
return _size;//返回容量大小
}
empty
判断字符串是否为空,可以使用strcmp函数来实现:
bool empty()
{
return strcmp(_str, "") == 0;//源字符串与空字符串比较为0,就为空
}
reserve
1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
2、当n小于对象当前的capacity时,什么也不做。
void reserve(size_t n = 0)
{
if (n > _capacity)//判断n与capacity大小关系
{
char* tmp = new char[n + 1];//开辟n+1个新空间(包括\0)
strcpy(tmp, _str);//将数据复制过去
delete[] _str;//释放原空间
_str = tmp;//指向新空间
_capacity = n;
}
}
resize
1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\0’。
2、当n小于当前的size时,将size缩小到n。
void resize(size_t n, char ch = '\0')
{
if (n > _size)//判断是否需要扩容
{
reserve(n);//扩容
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;//插入数据
}
_size = n;//改变_size大小
_str[_size] = '\0';//最后一位改为‘\0’
}
//不需要扩容
else
{
_str[n] = '\0';//n位置置为'\0'
_size = n;//改变size大小
}
}
find
1、正向查找第一个匹配的字符。
首先判断所给pos的合法性,然后通过遍历的方式从pos位置开始向后寻找目标字符,若找到,则返回其下标;若没有找到,则返回npos。
size_t find(char ch, size_t pos = 0)const
{
assert(pos < _size);//判断pos合法性
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;//找到返回下标
}
}
return npos;//没找到返回npos;
}
2、正向查找第一个匹配的字符串。
首先也是先判断所给pos的合法性,然后我们可以通过调用strstr函数进行查找。strstr函数若是找到了目标字符串会返回字符串的起始位置,若是没有找到会返回一个空指针。
//正向查找第一个字符串
size_t find(const char* sub, size_t pos = 0) const
{
assert(sub);//判断字符串合法性
assert(pos < _size);//判断pos合法性
const char* str = strstr(_str + pos, sub);//查找子串
if (str == nullptr)
{
return npos;//找不到返回npos
}
else
{
return str - _str;//找到了返回该位置该位置起始下标
}
substr
返回一个新构造的字符串对象,他是此对象的子字符串的副本
string substr(size_t pos, size_t len = npos) const
{
assert(pos < _size);//判断pos合法性
size_t realLen = len;//求len的长度
if (len == npos || pos + len > _size)
{
realLen = _size - pos;
}
string sub;//构造一个空字符串
for (size_t i = 0; i < realLen; i++)
{
sub += _str[pos + i];//追加字符串
}
return sub;
}
push_back
尾插数据:也就是在原字符串尾部插入数据,如果空间不够,就调用reserve函数进行扩容,尾插完成以后,还需要将’\0’放在最后的位置。
void push_back(char ch)
{
if (_size == _capacity)//判断是否需要扩容
{
reserve(_capacity == 0 ? 4 : _capacity * 2);//进行扩容
}
_str[_size] = ch;//尾插数据
_size++;
_str[_size] = '\0';//_size位置置为‘\0’
}
append
append(const char * str)
在当前字符串后面尾插一个字符串,如果空间不够,就需要进行扩容,但是不需要考虑’\0’的问题,因为插入字符串本身就是以’\0’结尾。
append (size_t n, char c)
在当前字符串后面尾插n个字符,可以直接就扩容。
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)//判断是否需要扩容
{
reserve(_size + len);//扩容
}
strcpy(_str + _size, _str);//从尾部开始拷贝数据
_size += len;//重新定义大小
}
void append(size_t n, char ch)
{
reserve(_size + n);//扩容
for (int i = 0; i < n; i++)
{
push_back(ch);//尾插字符
}
}
operator+=
+=运算符的重载是为了实现字符串与字符、字符串与字符串之间能够直接使用+=运算符进行尾插。
+=运算符实现字符串与字符之间的尾插直接调用push_back函数即可,字符串与字符串之间的尾插直接调用append函数即可。
string& operator+=(const char* str)
{
append(str);//调用append函数尾插字符串
return *this;
}
string& operator+=(char ch)
{
push_back(ch);//调用push_back函数尾插字符
return *this;
}
insert
在字符串任意位置插入字符或者是字符串
首先需要判断pos位置的合法性,然后判断插入是否需要进行扩容,最后就是将pos位置后面的字符均向后挪动一位。
string& insert(size_t pos, char ch)
{
assert(pos <= _size);//判断pos位置的合法性
if (_size == _capacity)//判断是否需要扩容
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
//挪动pos后面位置的每一个字符
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;//在pos位置插入ch
_size++;
return *this;
}
push_back就可以优化为:
void push_back(char ch)
{
insert(_size, ch);
}
在进行字符串插入时,其他条件不变,将pos位置后的每个字符同意向后移动len位,然后pos位置插入字符串即可。
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);//判断pos位置的合法性
size_t len = strlen(str);//求插入字符串大小
if (_size + len > _capacity)//判断是否需要扩容
{
reserve(_size + len);
}
size_t end = _size + len;
//挪动pos后面位置的每一个字符
while (end >= pos + len)
{
_str[end] = _str[end - len];
end--;
}
//在pos位置插入字符串str
strncpy(_str + len, str, len);
_size += len;
return *this;
}
append就可以优化为:
void append(const char* str)
{
insert(_size, str);
}
erase
删除任意位置开始的第n个字符,需要先判断位置的合法性,删除会出现两种情况:
1.删除pos位置后面全部的字符,删除后需要将最后一个位置置为’\0’;
2.删除pos位置后面一部分的字符,只需将删除字符后面的全部字符依次往前进行覆盖即可;
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);//判断坐标合法性
if (len == npos || pos + len >= _size)//判断是否是删除pos位置后的全部字符
{
_str[pos] = '\0';//最后位置置为'\0'
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);//覆盖删除部分的字符
_size -= len;
}
return *this;
}
clear
将对象中存储的字符串置空,实现时直接将对象的_size置空,然后在字符串后面放上’\0’即可
void clear()
{
_size = 0;
_str[_size] = '\0';
}
c_str
用于获取对象C类型的字符串,实现时直接返回对象的成员变量_str即可
const char* c_str()const
{
return _str;
}
operator[]
[ ]运算符的重载是为了让string对象能像C字符串一样,通过[ ] +下标的方式获取字符串对应位置的字符。
1.在C字符串中我们通过[ ] +下标的方式可以获取字符串对应位置的字符,并可以对其进行修改,实现[ ] 运算符的重载时只需返回对象C字符串对应位置字符的引用即可,这样便能实现对该位置的字符进行读取和修改操作了,但需要注意在此之前检测所给下标的合法性。
char& operator[](size_t pos)//可读可改
{
assert(pos < _size);//判断合法性
return _str[pos];//返回该位置字符
}
2.在某些场景下,我们可能只能用[ ] +下标的方式读取字符而不能对其进行修改。例如,对一个const的string类对象进行[ ] +下标的操作,我们只能读取所得到的字符,而不能对其进行修改。所以我们需要再重载一个[ ] 运算符,用于只读操作。
const char& operator[](size_t pos)const//可读不可改
{
assert(pos < _size);//判断合法性
return _str[pos];//返回该位置字符
}
重载>>是为了让string对象像内置类型对象一样可以进行直接进行输入,要注意输入遇见空格就会自动停下来,所以这儿需要使用到get()函数,为了避免刚开始重复的开辟空间,我们可以先开辟一个数组,只需要将数据存此数组,在进行追加就可以。
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
const size_t N = 32;
char Buff[N];//开辟一个数组
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
Buff[i++] = ch;
if (i == N - 1)
{
Buff[i] = '\0';
s += Buff;
i = 0;
}
ch = in.get();
}
Buff[i] = '\0';
s += Buff;
return in;
}
重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
关系运算符有 >、>=、<、<=、==、!= 这六个,但是对于C++中任意一个类的关系运算符重载,我们均只需重载其中的两个,剩下的四个关系运算符可以通过复用已经重载好了的两个关系运算符来实现。
//==重载
bool operator==(const string& s)
{
return strcmp(_str, s._str) == 0;
}
//>重载
bool operator>(const string& s)
{
return strcmp(_str, s._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 !(*this == s);
}