string
- 1.构造函数
- 1.1 不带参构造
- 1.2 带参数的构造函数
- 1.3 合并两个构造函数。
- 2. 析构函数
- 3.拷贝构造函数
- 4. 赋值运算符重载
- 5. size()/capacity()
- 6. 解引用[]
- 8.iterator迭代器
- 7.Print()
- 8.> ==
- 8. push_back()&append()
- 8.1 reserve()
- 9. +=
- 10.insert()
- 10.1 任意位置插入一个字符
- 10.2 在任意位置插入字符串
- 11. resize()
- 12 erase()
- 13. swap()
- 14.find()
- 14.1 查找字符
- 14.2 查找字符串
- 15 <<和>>
- 15.1 流提取
- 15.2 流插入
框架:
namespace abc
{
class string
{
private:
char* _str;
size_t _size; //有效字符大小
size_t _capacity; //总容量
};
}
1.构造函数
因为string类构造函数有多种形式,这里只实现两个最常用的。不带参数的,带参数的。
1.1 不带参构造
示例1·:cout对空指针解引用报错
string()
:_str(nullptr)
,_size(0)
,_capacity(0)
{}
char* c_str()
{
return _str;
}
void Test1()
{
abc::string s1;
cout << s1.c_str() << endl;//该行报错
}
报错原因:首先s1的字符串指向的为空指针。c_str会返回一个c格式的字符串,但是cout<<s1.c_str()会自动识别类型,识别为字符串类型,打印就会解引用。造成空指针访问报错。
改正:如果换成标准库里的string就不会报错,因为它赋的不是空指针,是空字符串。
改1:
如果这样直接赋值的化,成员变量_str为非const变量,会出现权限放大的错误。这样做也不可行。
改2:
申请一个字符的空间,然后函数体里面初始化。
string()
:_str(new char[1])
, _size(0)
, _capacity(0)
{
_str[0] = '\0';
}
修改成这样就可以了。这样后续既可以修改字符串内容,也可以打印空字符串。
1.2 带参数的构造函数
示例2:
string(const char* str) //加const是因为常量字符串必须用const接收,不然会在传参时出错
:_str(str) // *******该行会报错
,_size(strlen(str))
,_capacity(strlen(str)+1)
{}
void Test1()
{
abc::string s2("hello world");
cout << s2.c_str() << endl;
}
报错原因:
因为str为const类型,而成员变量_str为非const类型,赋值产生权限放大。给_str加const不可取,会导致后续没法修改字符串内容。
改1:还是开空间,能存上常量字符串(”hello“),并且还能保证能修改内容,初始化列表初始化不方便,选择在函数体初始化。
string(const char* str)
: _size(strlen(str))
{
_capacity = _size;//容量就是能装有效字符的个数
_str = new char[_capacity + 1]; //开的空间要多包含一个\0
strcpy(_str, str);//拷贝字符串内容
}
1.3 合并两个构造函数。
看起来是两个无参有参的构造函数,其实可以合并成一个,因为第一个无参的就是一个空字符串。
示例:
string(const char* str = "") //使用缺省函数来合并
: _size(strlen(str))
{
_capacity = _size;//容量就是能装有效字符的个数
_str = new char[_capacity + 1]; //开的空间要多包含一个\0
strcpy(_str, str);//拷贝字符串内容
}
2. 析构函数
构造函数写完对应写析构函数,只需要保证new和delete符号匹配即可。
示例:
//析构函数
~string()
{
delete[] _str; //都用带括号的
_str = nullptr;
_size = _capacity = 0;
}
3.拷贝构造函数
拷贝构造函数逻辑上不难。
示例:
//拷贝构造函数
string(const string& str)
:_size(str._size)
, _capacity(str._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, str._str);
}
void Test1()
{
abc::string s1;
abc::string s2("hello world");
abc::string s3(s1);
abc::string s4(s2);
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << s3.c_str() << endl;
cout << s4.c_str() << endl;
}
程序运行正确,没有报错。
4. 赋值运算符重载
示例:(经典标0)
//赋值运算符重载
string& operator=(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
return *this;
}
void Test2()
{
abc::string s1;
abc::string s2("hello world");
abc::string s3("i love you peter");
s2 = s3;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << s3.c_str() << endl;
}
问题:
1.首先s2的空间没有释放,导致内存泄露问题。
2. 没有考虑拷贝多少的问题,比如相等空间可以i直接赋值,大空间给小空间,小空间给大空间的问题。
优化1:首先要将s2的内存释放掉,然后申请一块新空间,赋给s2。(这样可以不用考虑原因2的三种情况,简化逻辑)
string& operator=(const string& s)
{
delete[] _str;//释放空间
_str = new char[s._capacity + 1]; //申请新空间
strcpy(_str, s._str); //拷贝
_size = s._size;
_capacity = s._capacity;
return *this;
}
问题:
3.如果内存申请失败,该版本会弄丢s2的原有值。
优化2:
使用临时变量开空间,开成功再赋回去。
string& operator=(const string& s)
{
char* tem = new char[s._capacity + 1]; //先申请新空间
strcpy(tem, s._str); //拷贝
//没有抛异常,往下执行
delete[] _str;//释放空间
_str = tem; //赋回来
_size = s._size;
_capacity = s._capacity;
return *this;
}
问题:如果自己给自己赋值,原地不动就好了。
优化3:
string& operator=(const string& s)
{
if (this != &s)
{
char* tem = new char[s._capacity + 1]; //先申请新空间
strcpy(tem, s._str); //拷贝
//没有抛异常,往下执行
delete[] _str;//释放空间
_str = tem; //赋回来
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
5. size()/capacity()
示例:
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
void Test3()
{
const abc::string s1;
const abc::string s2("hello world");
abc::string s3("i love you peter");
cout << s1.size() << endl; //报错
cout << s2.capacity() << endl; //报错
}
问题:调用的两个函数,都出现了权限放大的错误。
改正:给this加上const即可。
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
6. 解引用[]
示例:
char& operator[](size_t pos)
{
assert(pos < _size); //pos位置得合法
return _str[pos];
}
void Test3()
{
abc::string s2("hello world");
for (size_t i = 0; i < s2.size(); i++)
{
cout << (s2[i]) << " ";
}
}
问题:如果const对象解引用,会产生权限放大的错误,得把this加const,解决问题。但const对象返回值也得是const(防止对象被修改)。这就与非const对象产生矛盾。
改正:再次重载一个适合const对象的引用函数,即可解决问题。(运算符重载yyds)
char& operator[](size_t pos)
{
assert(pos < _size); //pos位置得合法
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size); //pos位置得合法
return _str[pos];
}
8.iterator迭代器
迭代器是一种比较方便的访问有序对象的一种通用方法。
示例:
public:
//迭代器
typedef char* iterator;
public:
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//测试代码
void Test4()
{
abc::string s2("hello world");
abc::string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 范围for
for (auto ch : s2)
{
cout << ch << " ";
}
}
解释:因为范围for的底层就是迭代器,如果迭代器底层实现好了的话,范围for也可以用。
问题:这个代码同样仅仅考虑了非const的问题,因此再写一组重载。
改:加一组const迭代器就可以了。
public:
//迭代器
typedef const char* const_iterator; //const型
public:
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
void Test4()
{
const abc::string s2("hello world");
abc::string::const_iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 范围for
for (auto ch : s2)
{
cout << ch << " ";
}
}
7.Print()
该函数实现不难,仅仅需要注意区分const和非const即可。
示例:
void Print()const
{
// 范围for
for (auto ch : *this)
{
cout << ch << " ";
}
}
void Test5()
{
const abc::string s2("hello world");
s2.Print();
}
因为前面已经实现好const和非const迭代器,这样Print函数,const对象调用const迭代器,非const对象调用非const迭代器。能成功运行。
8.> ==
string类的比大小遵循c语言的方式,使用strcmp复现。
示例:注意const和非const对象。
bool operator>(const string& s)const
{
return strcmp(_str, s._str) > 0;
}
bool operator==(const string & s)const
{
return strcmp(_str, s._str) == 0;
}
bool operator>=(const string& s)const
{
return _str > s._str || _str == s._str;
}
bool operator<(const string& s)const
{
return !(_str>=s._str);
}
bool operator<=(const string& s)const
{
return _str < s._str || _str == s._str;
}
bool operator!=(const string& s)const
{
return !(_str==s._str);
}
注意:仅仅写出来>+==就可以把其他都复用出来了。
8. push_back()&append()
该算法实现一个字符串增和一个字符增。
示例:
void reserve(size_t n)
{
char* tem = new char[n + 1];//多开一个空间存\0
strcpy(tem, _str);
delete[] _str;
_str = tem;
_capacity = n;
}
void append(const char* str)
{
size_t len = strlen(str);
//需要扩容
if(_size + len > _capacity)
{
//防止new_capacity比len小
size_t new_capacity = (2 * _capacity) > (_size + len) ? (2 * _capacity) : (_size + len);
reserve(new_capacity);
}
strcpy(_str + _size, str);
_size += len;
}
void push_back(char ch)
{
//容量不够需要扩容
if (_size + 1 > _capacity)
{
reserve(2 * _capacity);
}
_str[_size++] = ch;
_str[_size] = '\0'; // 别忘了\0
}
void Test7()
{
abc::string s2("hello world");
const char ch = 'a';
s2.push_back(ch);
s2.append("xxxxaaa");
}
问题:但是当字符串为空的时候,如果添加字符的话,push_back会报错,因为capacity为0,导致扩容还是0。
改:为了简单起见,我们只需要保证capacity不为0即可,可以在构造函数中修改。如下:
string(const char* str = "")
: _size(strlen(str))
{
_capacity = (_size == 0 ? 3 : _size);//容量就是能装有效字符的个数
_str = new char[_capacity + 1]; //开的空间要多包含一个\0
strcpy(_str, str);//拷贝字符串内容
}
这样即可解决问题。
8.1 reserve()
该函数的实现发方法仍然有一些问题,就是当要保留的空间小于原有的空间。函数不做改动。
改:加一个判断语句即可。
void reserve(size_t n)
{
if (n > _capacity)
{
char* tem = new char[n + 1];//多开一个空间存\0
strcpy(tem, _str);
delete[] _str;
_str = tem;
_capacity = n;
}
}
9. +=
使用上面的append和push,实现此函数轻而易举。
示例:
string& operator+=(const char* str)
{
this->append(str);
return *this;
}
string& operator+=(char ch)
{
this->push_back(ch);
return *this;
}
void Test7()
{
abc::string s2("hello world");
const char ch = 'a';
//s2.push_back(ch);
//s2.append("xxxxaaa");
s2 += "hello world";
s2 += 'a';
}
10.insert()
10.1 任意位置插入一个字符
示例:
在pos位置插入一个字符ch。
//在第pos位置插入
void insert(size_t pos, char ch)
{
//位置合法
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(2 * _capacity);
}
size_t end = _size - 1;
while (end >= pos)
{
_str[end + 1] = _str[end];
end--;
}
_str[pos] = ch;
_size++;
}
问题:
1.插入一个字符串后,结尾没加\0。
2.最好不要用end>=pos,因为都是无符号数,当pos和end同时等于0,end–,end变成了-1(但是是无符号数),因此会导致越界访问。
改正:
1.不能让end等于0,让end到1就结束,从前往后赋值。
2.最后加上\0。
void insert(size_t pos, char ch)
{
//位置合法
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(2 * _capacity);
}
size_t end = _size;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size++;
_str[_size] = '\0'; //没有写\0
}
10.2 在任意位置插入字符串
示例:
//插入字符串
void insert(size_t pos, const char* str)
{
assert(str);
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t new_capacity = (2 * _capacity) > (_size + len) ? (2 * _capacity) : (_size + len);
reserve(new_capacity);
}
//从后往前一个字符一个字符移动,记得最后不\0,到插入位置结束
size_t end = _size - 1;
while (end >= pos&&end!=-1)
{
_str[end + len] = _str[end];
end--;
}
int i = 0; //计数
size_t count = len;
while (count--)
{
_str[pos++] = str[i++];
}
_size += len;
_str[_size] = '\0';
}
问题:
1.同样是end和pos的关系,应该从前往后赋值,然后让后面的作为结束条件,这样就防止end越界了。
改正:
1.使用后面的下标作为结束条件,值得学习。
2.挪动完数据后,需要拷贝,可以使用strncpy进行拷贝。
//插入字符串
void insert(size_t pos, const char* str)
{
assert(str);
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t new_capacity = (2 * _capacity) > (_size + len) ? (2 * _capacity) : (_size + len);
reserve(new_capacity);
}
//从后往前一个字符一个字符移动,记得最后不\0,到插入位置结束
size_t end = _size + len;
while (end > pos+len-1)
{
_str[end] = _str[end - len];
end--;
}
//按字节拷贝
strncpy(_str + pos, str, len);
_size += len;
}
11. resize()
当容量小于size时,直接赋\0;当容量>size&&<capacity时,直接填充指定字符;当容量>capacity时,先扩容再填充指定字符。
示例:
//比_size小,就直接阶段、比_capacity大就用\0填充
void resize(size_t n, char ch = '\0')
{
if (n <= _size)
{
_str[n] = '\0';
}
//需不需要异地扩容
else
{
if (n <= _capacity)
{
memset(_str + _size, ch, n - _size);
//最后再加上\0;
_str[n] = '\0';
}
else
{
//char* tem = new char[n+1]; //要多申请一个空间
//strcpy(tem, _str);
//delete[] _str;
//_str = tem;
reserve(n);
memset(_str + _size, ch, n - _size);
_str[n] = '\0';
_capacity = n;
}
}
_size = n;
}
优化:
此代码逻辑有些冗余,可以修改成如下代码,更加简洁。
//比_size小,就直接阶段、比_capacity大就用\0填充
void resize(size_t n, char ch = '\0')
{
if (n <= _size)
{
_str[n] = '\0';
}
//需不需要异地扩容
else
{
if (n > _capacity)
{
reserve(n);
_capacity = n;
}
memset(_str + _size, ch, n - _size);
_str[n] = '\0';
}
_size = n;
}
12 erase()
擦除给定位置的n个字符。
// pos为位置
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos||len>=_size-pos)
{
_str[pos] = '\0';
_size = pos + 1;
}
else
{
size_t end = pos;
while (end + len <= _size)
{
_str[end] = _str[end + len];
end++;
}
_size -= len;
}
}
问题:else的代码有一些冗余,可以使用strcpy来简化
优化:
// pos为位置
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || len >= _size - pos)
{
_str[pos] = '\0';
_size = pos + 1;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
13. swap()
交换两个string对象的内容。
//1. swap(s1,s2)
//2. s1.swap(s2)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
对于两种swap的方式,很明显第二种更高效,第一种需要拷贝构造,第二种自己实现的方式则不需要。
14.find()
14.1 查找字符
size_t find(char ch)
{
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
14.2 查找字符串
使用strstr()库函数,复现。
size_t find(const char* str, size_t pos = 0)
{
assert(str);
char* p = strstr(_str + pos, str); //查找字符串的库函数
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
15 <<和>>
15.1 流提取
因为this指针的原因,不能将其定义为成员函数。
ostream& operator<< (ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
void Test8()
{
abc::string s1("hello world");
s1 += '\0';
s1 += "aaaaaaaaaa";
cout << s1 << endl; //hello worldaaaaaaaaaa
cout << s1.c_str() << endl; //hello world
}
注意:这里要简单提一下:为什么两个输出的函数不一样。
第一个s1是我们自己实现的函数,它是根据字符串的个数打印的。
而第二个s1.c_str返回的是指针,编译器会根据指针来打印,遇到\0就停止!
15.2 流插入
示例:
istream& operator>> (istream& in, string& s)
{
char ch;
in >> ch;
while (ch != ' ' && ch != '\n')
{
s += ch;
in >> ch;
}
return in;
}
报错:输入“hello world”
该代码不能完成功能,因为in>>ch,读不进去空格和回车。所以根本跳不出循环。
改正:换一种方式获取缓冲区数据。使用get()函数。
istream& operator>> (istream& in, string& s)
{
char ch = in.get(); //可以读到空格
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
问题:1.频繁+=会导致频繁扩容。
2.连续两次读取数据不会清除第一次的数据。
优化:1.使用缓冲区的概念,一下+=一个缓冲区。
2.每次提取之前,要清空字符。
void clear()
{
_str[0] = '\0';
_size = 0;
}
istream& operator>> (istream& in, string& s)
{
s.clear(); //每次读取前,清空s
char buf[128]; //申请一个缓冲区
char ch = in.get();
int i = 0;
while (ch != ' ' && ch != '\n')
{
buf[i++] = ch;
if (i == 127) //剩一个位置给\0
{
buf[i] = '\0';
s += buf;
i = 0;
}
ch = in.get();
}
if (i != 0) //缓冲区有内容,再加上
{
buf[i] = '\0';
s += buf;
}
return in;
}
以上就是string类部分库函数实现。