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]; //开的空间要多包含一个\0strcpy(_str, str);//拷贝字符串内容}
1.3 合并两个构造函数。
看起来是两个无参有参的构造函数,其实可以合并成一个,因为第一个无参的就是一个空字符串。
示例:
string(const char* str = "") //使用缺省函数来合并: _size(strlen(str)){_capacity = _size;//容量就是能装有效字符的个数_str = new char[_capacity + 1]; //开的空间要多包含一个\0strcpy(_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;// 范围forfor (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;// 范围forfor (auto ch : s2){cout << ch << " ";}}
7.Print()
该函数实现不难,仅仅需要注意区分const和非const即可。
示例:
void Print()const{// 范围forfor (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];//多开一个空间存\0strcpy(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]; //开的空间要多包含一个\0strcpy(_str, str);//拷贝字符串内容}
这样即可解决问题。
8.1 reserve()
该函数的实现发方法仍然有一些问题,就是当要保留的空间小于原有的空间。函数不做改动。
改:加一个判断语句即可。
void reserve(size_t n){if (n > _capacity){char* tem = new char[n + 1];//多开一个空间存\0strcpy(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 worldaaaaaaaaaacout << 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(); //每次读取前,清空schar 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类部分库函数实现。