string的使用和模拟实现
- string的成员变量
 - string的构造方法
 - 用法
 - 无参的构造方法的实现
 - 全缺省的构造参数的实现
 
- strcpy的模拟实现
 - 为什么这里的_size要+1?
 - 为什么这里是默认传空字符串?
 
- 赋值运算符重载
 
- 析构函数
 - 遍历字符串
 - operator[]
 - 使用
 - 传统c语言字符串下标遍历的缺点
 
- 模拟实现
 - 迭代器
 - 使用
 
- 范围for
 
- 成员函数
 - reserve
 - reserve的使用
 - reserve的模拟实现
 
- push_back
 - push_back的使用
 - push_back的模拟实现
 
- append
 - apeend的模拟实现
 
- insert
 - insert的使用
 - insert的模拟实现
 - 插入字符
 
- 插入字符串
 
- erase
 - erase的使用
 - erase的模拟实现
 
- resize
 - resize的使用
 - rise的模拟实现
 
- substr
 - substr的使用
 - substr的模拟实现
 
- find
 - 使用
 - 模拟实现
 - strstr的模拟实现
 
- swap
 - 使用
 - 改写
 
- string的赋值运算符重载
 - ==
 - strcmp的模拟实现
 
- <
 - >>
 - 改进
 
- 拷贝构造函数的现代写法
 - 赋值运算符重载的现代写法
 - 谢谢观看
 
我们模拟实现一个string 不是为了造一个比库里面更好的,而是熟悉语法,学习底层原理,复习数据结构等。
string的成员变量
string 就是有\0的顺序表,所以和顺序表的成员一样有,size,capacity,和一个指针
- 因为string的底层是字符串数组
所以我们需要一个char*的指针指向数组 - size记录存放字符的个数
不包括/0 - capacity作用是反应现在的容量以便后续扩容
 
string的构造方法
常用的就是第一个和第四个所以我们模拟实现也是这两个

用法
std::string s1; // 无参
std::string s2("hello word"); //字符串构造
 
无参的构造方法的实现

这样初始化可以吗?不可以 因为用空指针初始化_str则_str无法解引用,打印_str数组时会空指针异常。
 怎么解决这个问题呢?可以给str开一个空间放/0
 string():_str(new char[1]),_size(0),_capacity(0){_str[0] = '/0';}
 
我们如果想传一个c字符串来构造字符串怎么做呢?
全缺省的构造参数的实现
就是用c字符串str的长度给_str开空间,再把str拷贝_str
 string(const char* str=""):_size(strlen(str)){_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);
}
 
可能有的同学已经忘了strcpy的原理了我简单复习下
strcpy的模拟实现
就是挨个拷贝,连同反斜杠/0一起,因为没有/0就不能称为一个c串了。
char* my_strcpy(char* dest, const char* src) 
{assert(dest != nullptr && src != nullptr);char* ret = dest;while (*src != '\0') {*(dest++ ) = *(src++);}*dest = *src;return ret;
}
 
为什么这里的_size要+1?
因为strlen算出的字符串大小不包括/0,所以开辟的空间的大小要_size+1
为什么这里是默认传空字符串?
如果传空指针解strlen(str)没有/0作为字符串结束标志则会崩溃,不知道在哪结束所以不能传空指针,而空字符串默认是有一个/0的。
赋值运算符重载
 void  operator=(const string& s1)
{_size = s1._size;_capacity = s1._capacity;char* tmp = new char[_capacity + 1];strcpy(tmp, s1._str);delete[]_str;_str = tmp;
}                     
 
析构函数
~string() 
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
 
遍历字符串
operator[]
这个有个非常进步的地方,就是它会越界检查了,c字符串越界的读不会检查而越界的写是抽查。
使用
s1[2]  // 非常的方便
 
传统c语言字符串下标遍历的缺点
越界读不报错
 
 越界写报错但是是抽查的,在边界检查严格些
模拟实现
 char& operator[](size_t pos) {assert(pos< _size);return _str[pos];}
 
有了断言这样不管是读还是写都会检查了
迭代器
使用
 string s2("hello word");string::iterator it;it = s2.begin();while (it != s2.end()) {cout << *it<<" ";it++;}
 
注意这个end是\0的那个位置,而不是最后一个字符,因为是最后一个字符的话 it!=s2.end()就不打印最后一个字符了
不同类的迭代器可能不一样,我们用指针简单的实现一个string的迭代器
typedef  char* iterator;
///
iterator begin() 
{return _str;
}
iterator end() 
{return _str + _size;
}
 
范围for
当我们把迭代器写好后,范围for自动就成了。
 因为范围for底层就是调用迭代器
 
成员函数
reserve

 功能是给字符串的容量增长到n个字符的空间,当n<_capacity时不起作用。
reserve的使用
reserve(100);
 
reserve的模拟实现
void reserve(int n) 
{if (_capacity < n) {char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}
 
push_back

 功能追加一个字符给字符串
push_back的使用
string s1("hello word");
s1.push_back('a');
 
push_back的模拟实现
 void push_back(char ch) {if (_size >= _capacity) {reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_str[_size+1] = '\0';_size++;}
 
append
appen的功能是把字符串变长通过在现存的字符串的末尾后面添加额外的字符串。
 
apeend的模拟实现
 void append(const string& s) {int len = strlen(s._str);if (_size > _capacity - len){reserve(_size + len);}strcpy(_str + _size, s._str);} 
insert

 功能是在pos位置之前插入其他字符/字符串
insert的使用
 std::string s("hello");std::string b(" word");s.insert(0, b);cout << s;
 
insert的模拟实现
插入字符
 void 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 insert(size_t pos, char c) 
{assert(pos <= _size);if (_size >= _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}int end = _size;while (end >= (int)pos) //不强转pos end变成无符号数 一直>=0 {_str[end+1] = _str[end];end--;}_str[pos] = c;_size++;}
 
注意这里的pos一定要强制类型转换不然end会整型提升 当头插时pos = 0 end被提升为无符号数了 一直大于pos 0 所以会出错
插入字符串
 void  insert(size_t pos, string& s) {int len = s._size;assert(pos <= _size);if (_size+len  >= _capacity){reserve(_size + len);}size_t end = _size + len;while (end >= pos + len){_str[end] = _str[end - len];end--;}strncpy(_str + pos, s.c_string(),len);_size += len;}
 
注意这里要有strncp目的是不让\0拷贝下来
erase
功能:擦除pos位置起len长度的字符串
 
 第一个用得很多我们模拟实现第一个
erase的使用
   std::string s1("hello word");s1.erase(1,2);
 
erase的模拟实现
首先判断pos位置是否合法 pose小于size才行 因为等于size就把\0擦除了。
 判断 npos = -1 || len + pos > size 成功就全部擦除把o位置的值赋值成\0
 或者把pos位置赋值成\0
 再把 pos+len的位置拷贝到 pos处
 size-= len
 void erase(size_t pos, size_t len = npos) {assert(pos < _size);if (len == npos || pos > _size - len) {_str[pos] = '\0';_size = pos;}else{strcpy(_str +pos, _str + len+pos);_size -= len;}}
 
resize
功能是改变字符串的长度
 如果当前的size小于n,就缩短到n
 若n大于size,如果指定了字符就在后面插入指定的字符否则插入\0以达到n的长度
 
resize的使用
  std::string s1("hello word");s1.resize(100,'a');cout << s1;
 
rise的模拟实现
首先判断size和n的关系
 n小于size 则把n位置的字符赋值\0
 否则 扩容到n的长度
 把下标size到下标n-1的字符全部赋值成ch
 size 改为 n
void resize(size_t n, char ch ='\0')
{if (n <= _size) {_str[n] = '/0';_size = n;}else {reserve(n);for (size_t i = _size; i < n; i++) {_str[i] = ch;}_str[n] = '\0';_size = n;}
}
 
substr

功能:返回从主串截取的从pos位置长度为len的子串
substr的使用
std::string s1("hello word");
cout << s1.substr(0, 5);
 
substr的模拟实现
因为要返回一个新的字符串,所以我们先定义个新串
 然后一个+=循环 ,循环条件从pos到下标pos+len-1
string substr(size_t pos = 0, size_t len = npos)
{string sub;//if (len == npos || len >= _size-pos)if (len >= _size - pos){for (size_t i = pos; i < _size; i++){sub += _str[i];}}else{for (size_t i = pos; i < pos + len; i++){sub += _str[i];}}return sub;
 
find
寻找子串在主从中从pos位置开始第一个出现的位置
 或者找一个字符在字符串pos后的第一个位置
 没有找到则返回npos
使用
     string s1("hello word");int ret1 = s1.find("word", 3);int ret2 = s1.find("w",3); 
模拟实现
size_t find(const string& str, size_t pos = 0) const 
{assert(pos < _size);char * ret= strstr(_str + pos, str.c_string());if(ret)return ret - _str;return npos;
}size_t 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;
}
 
这里我们直接用的库函数strstr 可能有的同学忘了ststr的功能
 我简单复习一下 就是在主串中在子串第一个出现的位置然后返回匹配到的第一个位置的指针。没有找到则返回空指针
strstr的模拟实现
这里我们使用暴力查找的方法简单的模拟一下
     char* my_strstr(const char* str, const char* substr) {int len = strlen(substr);int i = 0;while (i < strlen(str)) {size_t j = 0;for (; j < len; j++){if (substr[j] == str[i]){i++;}else{break;}}if (j == len) {return (char*) str+i - len;}i++;}return nullptr;}
 
swap
交换两个字符串
使用
string s1("hello word");
string s2;
s2.swap(s1);
 
改写

 我们直接把T变为 string string c(a); a =b ;b = c;
 这里会走一次拷贝构造 两次赋值运算符重载 总共三次构造 还外加一次析构 c 代价太大了 所以必须重写
  void swap(string s1){std::swap(_size, s1._size);std::swap(_capacity, s1._capacity);std::swap(_str, s1._str);}
 
为了防止别人调用库里面的swap(T a,T b)我们需要在类外面写一个相同参数的swap,这样调用的时候就优先调用我们写了的。
 因为函数模板有现成的吃现成的。
	void swap(string& x, string& y){x.swap(y);}
 
string的赋值运算符重载
==
    bool operator==(const string& s1, const string& s2) {return strcmp(s1.c_string(), s2.c_string()) ==0;}
 
可能有的同学忘了strcmp的原理
 就是两个字符串从头开始比较 相等就继续走 如果遇到串1的第一个字符大于串2的第一个字符 return 1 小于则return -1 全部走完了则return 0
strcmp的模拟实现
    int my_strcmp(const char* a, const char* b) {while (*a == *b && *a != '\0' && *b != '\0'){a++;b++;}if (*a > *b)return 1;if (*a < *b)return -1;return 0;}
 
<
bool operator<(const string& s1, const string& s2){int ret = strcmp(s1.c_str(), s2.c_str());return ret < 0;}
 
>>
这里不能用 scanf 和 cin 因为 cin 和scanf 都把空格和 回车作为分隔符 读不到 则程序永远不会结束。
istream& operator>>(istream& in,  string& s) 
{char ch;s.clear();ch = in.get();while (ch != ' ' && ch != '\n') {s += ch;ch = in.get();}return in;
}
 
改进
我们这个+=需要频繁扩容不太好。
 如果我们用reserve 开个很大的空间 则输入很小的时候又浪费了
 所以我们用一个数组暂存字符 然后根据数组的大小一次性开辟好空间,就不用频繁扩容了。
 istream& operator>>(istream& in,  string& s) {char ch;s.clear();char buff[128];ch = in.get();int i = 0;while (ch != ' ' && ch != '\n') {//s += ch;buff[i++] = ch;if (i == 127) {buff[127] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0) {buff[i] = '\0';s += buff;}return in;}
 
什么叫现代写法呢?就是假借他人的手完成相应的功能
拷贝构造函数的现代写法

 借构造函数的手 构造一个和s1一样的字符串 然后和 this 交换
         /* string(const string &s1) {_size = s1._size;_capacity = s1._capacity;char* tmp = new char[_size + 1];strcpy(tmp, s1._str);this->_str = tmp;}*/string(const string& s1) {string tmp(s1.c_string());swap(tmp);}
 
赋值运算符重载的现代写法
第三个版本是由第二个版本而进过来的
 第二个版本借助 s 拷贝构造 字符串ss
 第三个版本 我们没有用引用编译器帮我们调用了拷贝构造
// version 1/*string& operator=(const string& s){string ss(s);swap(ss);return *this;}*/// version 2/*string& operator=(const string& s){string ss(s);swap(ss);return *this;}*/// version 3string& operator=( string ss){swap(ss);return *this;}