• STL中string类的实现


    目录

    引入

    构造 | 析构函数

    构造函数

    析构函数

    返回指针的c_str()

    求字符大小的size()

    operator[]

    普通对象调用:

    const对象调用:

    迭代器的实现

    范围for

    深浅拷贝

    浅拷贝的不足

    实现深拷贝

    赋值的深拷贝

    传统写法与现代写法

    传统写法

    现代写法

    练习

    增删查改

    增容reserve()

    改变大小+初始化的resize()

    尾插字符push_back()

    追加字符串append()

    贼好用的operator+=

    指定位置插 insert()

    删除erase()

    查找find()

    比大小组合

    流插入与流提取

    引入

    三点说明:返回值、形参与实现方法

    流插入的实现

    流提取的实现

    总代码

    📖string.h

    📖test.cpp


    你知道在STL库中,string是怎么实现的吗?其实我们也能写!

    本篇将手把手实现string类。通过自己的实现,我们能更好地去理解string类的底层原理。

    引入

    首先,在string.h里把string的框架搭好:

    1. #pragma once
    2. namespace jzy   //为了和STL里的string区分,我们把string放进自定义的命名空间里
    3. {
    4. class string
    5. {
    6. public:
    7. private:
    8. char* _str;
    9. size_t _size;   //也可以用int,但库里面一般用size_t
    10. size_t _capacity; //_capacity是有效字符的空间数,不包括\0
    11. };
    12. }

    构造 | 析构函数

    构造函数

    构造函数的形参有两种情况,一是有参,二是无参,这两个都要实现。

    ➡️有参:

    1. string(char* str)     //有参:传字符串首元素的地址过来
    2. :_str(new char[strlen(str) + 1])   //记得为末尾的\0开一份空间
    3. , _size(strlen(str))
    4. , _capacity(strlen(str))
    5. {
    6. strcpy(_str, str);
    7. }

    然而,这个函数在测试时却报错了:

    1. #include"string.h"
    2. using namespace jzy;
    3. void test1()
    4. {
    5. string s1("abc");
    6. }
    7. int main()
    8. {
    9. test1();
    10. return 0;
    11. }

    报错:

    这其实是因为:

    abc是位于常量区的常量字符串,被const修饰,不可修改。这就是说,权限比较小。

    而它要调用的string函数,形参未被const修饰,是可修改的,权限较大。

    就像公司的上下级关系,权限小的不可以调用权限大的,它只能调用平级。

    所以,string的形参也要被const修饰。

    这个故事告诉我们,能用const就尽量用。

    ➡️修改后的有参:

    1. class string
    2. {
    3. public:
    4. string(const char* str)  
    5. :_str(new char[strlen(str) + 1])  
    6. , _size(strlen(str))
    7. , _capacity(strlen(str))
    8. {
    9. strcpy(_str, str);
    10. }

    ➡️无参

    1. string()
    2. :_str(new char[1]) //尽管无参,仍要为\0开空间
    3. ,_size(0)
    4. ,_capacity(0)
    5. {
    6. _str[0] = '\0';
    7. }

    或者,用缺省值将有参/无参 合二为一:

    1. string(const char* str = "\0")   //注:这里不能用'\0',得用"\0",//单引号表示的是单个字符,类型为char而非char*        
    2. :_str(new char[strlen(str) + 1])    
    3. , _size(strlen(str))
    4. , _capacity(strlen(str))
    5. {
    6. strcpy(_str, str);
    7. }

    补:这里的"\0"其实有点画蛇添足。用""就可以了,里面隐含了\0。只要是常量字符串。都暗含了\0,只是看不见。

    那这里的"\0"能不能换成nullptr呢?

    不能!因为strlen不会检查判空,而是直接访问字符串,直到找到'\0'才会结束。

    如果不传参,那默认为nullptr的话,strlen就会访问空指针,使程序崩溃。

    析构函数

    1. ~string()
    2. {
    3. delete[] _str;
    4. _str = nullptr;
    5. _size = _capacity = 0;
    6. }

    返回指针的c_str()

    函数c_str()的作用?

    "在C语言中,使用printf直接输出string类型的字符串可能会出现乱码。这是因为printf函数的%s格式化符号期望传入一个char类型的参数,而string类型的字符串实际上是一个对象,不是一个字符指针。所以在使用printf输出string类型的字符串时,应该使用s.c_str()方法将string类型转换为char类型。

    而在C++中,使用cout输出string类型的字符串是没有问题的。cout对string类型有特殊的处理方式,可以直接输出string类型的字符串。

    此外,也可以通过循环遍历string的每个字符,使用printf逐个输出字符,或者使用cout逐个输出字符,都可以得到相同的结果。

    需要注意的是,如果没有包含头文件,那么默认情况下是不能使用cout输出string类型的字符串的,此时需要使用c_str()方法将string类型转化为char*类型。"

    (源自 c知道)

    简单来说,string类是无法直接被cout或者printf输出的,它需要被转化成char类型才可以。那c_str()做的就是这样一个转化的工作。

    c_str()返回的是字符串的首字母地址,此地址只读不写,因此要用const来修饰:

    1. char* c_str()const  
    2. {
    3. return _str;
    4. }

    有了字符串的首元素地址,我们就可以cout输出stirng字符串了。

    测试一下:

    1. void test2()
    2. {
    3. string s1("abcdef");
    4. cout << s1.c_str() << endl;
    5. }

    求字符大小的size()

    1. size_t size()const   //只读不写 就加上const保护
    2. {
    3. return _size;
    4. }

    operator[]

    operator[]是非常好用的接口,它能把字符串当数组一样使用。这么好用的接口,实现起来其实很简单。

    普通对象调用:

    1. char operator[](size_t pos)
    2. {
    3. return *(_str + pos);
    4. }

    🤔等等……直接传值返回的话,会有什么弊端吗?

    有的!我们无法直接修改字符,来测试一下:

    1. void test3()
    2. {
    3. string s1("abcdef");
    4. cout << ++s1[0] << endl;
    5. }

    传值返回s1[0],我们得到的并不是s1里的'a',而是它的拷贝,这导致我们无法修改真正的a。

    如果想要对a做修改,那就要传引用返回。

    更新版的operator[]:

    1. char& operator[](size_t pos)
    2. {
    3. return *(_str + pos);
    4. }

    const对象调用:

    我们知道,const权限小,不能调用权限大的函数。因此,要再写一份const版的operator[]函数:

    1. const char& operator[](size_t pos) const
    2. {
    3. return *(_str + pos);
    4. }

    经过const的保护,这个版本的operator[]是只读不写的。

    迭代器的实现

    之前我们对迭代器的认识为“像指针一样的东西”,那它究竟是不是指针呢?两者又有什么关联呢?别急,实现一遍我们就知道了。

    iterator:

    begin():返回第一个字符的位置。

    end():返回最后一个字符的下一个位置。

    1. typedef char* iterator;
    2. iterator& begin() //&可加,也可不加
    3. {
    4. return _str;
    5. }
    6. iterator end()   //注意:这里不能加&!
    7. {
    8. return _str + _size;
    9. }

    这里说明一下,end()为什么不能加&。因为end()指向的不是最后一个字符,而是它的后一个,也就是\0,所以end()处是开区间,是不能取到的。

    测试一下:

    1. void test4()
    2. {              
    3. string s1("hello");
    4. string::iterator it = s1.begin();
    5. while (it != s1.end())
    6. {
    7. cout << *it << " ";   //可读
    8. it++;
    9. }
    10. cout << endl;
    11. for (auto ch : s1)
    12. {
    13. cout << ++ch << " "; //可写
    14. }
    15. }

    上面实现的是普通的迭代器,是可读可写的。

    现在再实现const_iterator,只读不写的:

    const_iterator:

    1. typedef const char* const_iterator;
    2. const_iterator begin()const
    3. {
    4. return _str;
    5. }
    6. const_iterator end()const
    7. {
    8. return _str + _size;
    9. }

    范围for

    之前我们说过,范围for看起来很高级,实际上底层原理很简单,现在我们就来揭秘一下。

    其实,只要写了迭代器,那直接就能用范围for,它甚至不需要你去实现。

    现在,我们直接在刚刚实现的迭代器后面,使用范围for:

    1. void test4()
    2. {
    3. string s1("hello");
    4. string::iterator it = s1.begin();  
    5. while (it != s1.end())
    6. {
    7. cout <<*it<< " ";
    8. it++;
    9. }
    10. cout << endl;      
    11. for (auto ch : s1) //再遍历一遍
    12. {
    13. cout << ch << " ";
    14. }
    15. }

    这时因为:范围for语句的底层原理是通过迭代器来实现的。编译器会用迭代器 来替换范围for。

    范围for会自动调用 对象的begin()和end()方法 来获取迭代器的起始、结束位置,然后通过迭代器来遍历。

    迭代器是一个对象,用于遍历和访问元素。范围for通过迭代器来遍历集合,不用再显式地操作指针,使代码更简洁易读。

    可见,范围for的确没啥含金量……

    深浅拷贝

    浅拷贝的不足

    之前我们了解过,浅拷贝就是值拷贝,对于内置类型,是按字节的方式直接拷贝的。对于自定义类型,是调用其拷贝构造函数完成拷贝的。

    浅拷贝真的够用吗?

    答案当然是否定的。如果有成员变量是指针,那拷贝时,仅仅是复制了指针的值而不复制指针指向的空间。

    如图,string的浅拷贝:

    可见,俩指针指向同一片空间。这就导致,当其中一个指针释放空间时,另一个指针也受到影响。

    所以说,有指针成员时,就需要进行深拷贝了。

    实现深拷贝

    深拷贝是由我们自己实现的,拷贝指针时,不仅仅是复制值,更是要复制一份空间。

    现在我们来实现下string的深拷贝:

    1. string(const string& s)
    2. :_str(new char[strlen(s._str) + 1])
    3. ,_size(s._size)
    4. ,_capacity(s._capacity)
    5. {
    6. strcpy(_str,s._str);
    7. }

    可以看到,深拷贝的确是新开了空间:

    赋值的深拷贝

    其实赋值和刚刚说的拷贝构造是一个道理。很多时候,默认的赋值运算符就够用了,

    但当涉及资源管理,如指针,就会出现两个指针指向同一块空间的情况。

    来看string赋值的崩溃现场:

    1. void test5()
    2. {
    3. string s1("hello");
    4. string s2("111111111111111111111111111");
    5. s1 = s2;
    6. }

    那这种场景就需要实现赋值的深拷贝。

    我们来实现一下:

    1. string& operator=(const string& s)
    2. {
    3. delete[] _str;                       //先释放旧空间
    4. _str = new char[strlen(s._str) + 1];   //再开新空间
    5. strcpy(_str, s._str);
    6. _size = s._size;
    7. _capacity = s._capacity;
    8. return *this;
    9. }

    测试下:

    1. void test5()
    2. {
    3. string s1("hello");
    4. string s2("111111111111111111111111111");
    5. s1 = s2;
    6. }

    可见,赋值成功。

    ❗但是,这并非 赋值运算符 的最终形态。因为还没检测 是否自己给自己赋值。

    🚩这里要注意一个点:在实现赋值的深拷贝时,需要检测是否自己给自己赋值。

    先来看看如果给自己赋值,会发生什么,

    1. void test5()
    2. {
    3. string s1("hello");
    4. string s2("111111111111111111111111111");
    5. s2 = s2;    
    6. }

    s2中的字符居然无效了!(被置成了随机值)

    这是因为,this 和 形参s 都是s2。一上来this的_str空间就被释放,所以,此时s的 _str空间也被释放了,这俩现在都是随机值。所以,再把s拷给this,就会出现随机值的状况。

    所以说,需要检查 是否自己给自己赋值 的情况。

    赋值深拷贝的最终形态:

    1. string& operator=(const string& s)
    2. {
    3. if (this != &s)
    4. {
    5. delete[] _str;                       //先释放旧空间
    6. _str = new char[strlen(s._str) + 1];   //再开新空间
    7. strcpy(_str, s._str);
    8. _size = s._size;
    9. _capacity = s._capacity;
    10. return *this;
    11. }
    12. }

    传统写法与现代写法

    在stirng这里,我们就要学会写同一个功能的两种写法,即传统写法与现代写法。

    现代写法 较传统写法的优势目前可能表现不出来,但等我们学到了vector、list时,现代写法就方便多了。

    所以,这两种写法,都是有必要掌握的!

    下面就用拷贝构造来举例:

    传统写法

    传统写法,就是老老实实地打工搬砖:开空间、初始化、拷贝:

    1. string(const string& s)
    2. :_str(new char[strlen(s._str) + 1])
    3. ,_size(s._size)
    4. ,_capacity(s._capacity)
    5. {
    6. strcpy(_str,s._str);
    7. }

    现代写法

    现代写法,则精明多了。这些累活我不自己干,我雇打工人tmp来干:

    1. string(const string& s)
    2. {
    3. string tmp(s._str);     //先构造个tmp
    4. swap(_str, tmp._str);   //把tmp交换给我
    5. swap(_size, tmp._size);
    6. swap(_capacity, tmp._capacity);
    7. }

    但是这样写,有一个隐患:

    this._str未经初始化,里面是随机值。经过swap,把随机值给了tmp. _str。

    在delete tmp时,对随机值指向的空间进行释放,可能会引发崩溃。

    如果 _str置空的话,delete就不会释放空指针。所以,要给this. _str初始化。

    经过改造:

    1. void swap(string&tmp)       //这是写在jzy类域里的swap
    2. {
    3. ::swap(_str, tmp._str);   //用::调用全局的swap函数
    4. ::swap(_size, tmp._size);
    5. ::swap(_capacity, tmp._capacity);
    6. }
    7. string(const string& s)
    8. :_str(nullptr)   //初始化,更安全
    9. ,_size(0)
    10. ,_capacity(0)
    11. {
    12. string tmp(s._str);
    13. swap(tmp);    
    14. }

    注:这俩swap不一样,不是函数重载。函数重载的前提是在同一作用域。而这俩swap,一个是类里面的,一个是全局的。

    所以说,全局的swap在调用时要加::,不然它会优先去局部域找,找到我们写的那个swap(string&tmp)之后,会认为参数不匹配。

    总结一下,现代写法的本质是”拷贝构造新对象+将自己和新对象进行交换“。

    练习

    现在,在学习了现代写法的思想之后,我们来练习写operator=的现代写法:

    1. void swap(string& tmp)  
    2. {
    3. ::swap(_str, tmp._str);  
    4. ::swap(_size, tmp._size);
    5. ::swap(_capacity, tmp._capacity);
    6. }
    7. string& operator=(const string& s)
    8. {
    9. if (this != &s)
    10. {
    11. string tmp(s._str);
    12. swap(tmp);
    13. return *this;
    14. }
    15. }

    增删查改

    增容reserve()

    reserve”保留“:开若干个空间,先保留在那里,即增容。

    1. void reserve(size_t size)
    2. {
    3. if (size > _capacity) //先检查下要不要增容
    4. {
    5. char* p = new char[size + 1]; //开新空间(为\0多开一个空间)
    6. strcpy(p, _str);               //拷数据
    7. delete[] _str;                 //释放旧空间
    8. _str = p;  
    9. _capacity = size;
    10. }
    11. }

    改变大小+初始化的resize()

    刚刚的reserve是处理容量,现在的resize是处理大小的。

    resize能将size变大 / 变小(容量不变),size变大的同时 还能顺便给初始化了。

    库里面的resize分两种:给初始值 和 不给初始值的。我们一会实现,就用缺省值的方式 合二为一。

    1. string& resize(size_t n, char c = '\0') //n是新的_size,ch是初始化的值
    2. {
    3. if (n < _size) //小 就截断
    4. {
    5. _str[n] = '0';
    6. _size = n;
    7. }
    8. else
    9. {
    10. if (n > _capacity) //大 就扩容
    11. {
    12. reserve(n);
    13. }
    14. memset(_str + _size, c, n - _size);
    15. _str[n] = '\0';
    16. _size = n;
    17. }
    18. return *this;
    19. }

    尾插字符push_back()

    1. void push_back(const char c)
    2. {
    3. if (_size == _capacity)   //先检查要不要扩容
    4. {
    5. reserve(_capacity == 0 ? 4 : 2 * _capacity);
    6. }
    7. _str[_size++] = c;
    8. _str[_size] = '\0';   //别忘了最后得加上'\0'
    9. }

    追加字符串append()

    1. void append(const char* ch)
    2. {
    3. //考虑扩容
    4. size_t total_size = _size + strlen(ch);   //先判断容量
    5. if(_capacity < total_size)
    6. {
    7. reserve(total_size);
    8. }
    9. strcpy(_str + _size, ch);   //直接把ch拷到\0的位置
    10. _size += strlen(ch);
    11. }

    贼好用的operator+=

    在了解stirng类的方法时,我们就惊叹过,operator+=真的好好用,既能追加字符,又能加字符串。

    实际上,追加字符 和 追加字符串 是构成重载的两个函数,现在我们来实现下。

    1. //追加字符
    2. string& operator+=(const char c)
    3. {
    4. push_back(c);   //复用了push_back()
    5. return *this;
    6. }
    7. //追加字符串
    8. string& operator+=(const char* ch)
    9. {
    10. append(ch);   //复用了append()
    11. return *this;
    12. }

    指定位置插 insert()

    插字符:

    1. string& insert(size_t pos, char c)
    2. {
    3. assert(pos <= _size);
    4. if (_size == _capacity)
    5. {
    6. reserve(_capacity == 0 ? 4 : 2 * _capacity);
    7. }
    8. //先把后面的数据往后挪
    9. size_t end = _size - 1;
    10. while (end > pos)
    11. {
    12. _str[end] = _str[end - 1];
    13. end--;
    14. }
    15. //再插入
    16. _str[pos] = c;
    17. _size++;
    18. _str[_size] = '\0';
    19. return *this; //其实这个返回值意义不大
    20. }

    插字符串:

    1. string& insert(size_t pos, const char* ch)
    2. {
    3. assert(pos <= _size);
    4. size_t total_size = _size + strlen(ch);
    5. if (_capacity < total_size)   //先算出一共需要多少空间,不够就开
    6. {
    7. reserve(total_size);
    8. }
    9. size_t end = _size ;   //挪数据
    10. size_t span = strlen(ch);
    11. while (end > pos)       //这个循环很容易写错! 不能写end>=pos,减成负数也就是无穷大,很容易越界!
    12. {
    13. _str[end + span] = _str[end];
    14. end--;
    15. }
    16. _str[pos+span] = _str[pos];
    17. strncpy(_str+pos, ch, strlen(ch));
    18. _size += strlen(ch);
    19. return *this;
    20. }

    删除erase()

    1. void erase(size_t pos, size_t len = npos)
    2. {
    3. assert(len < _size);
    4. if (len == npos||len>_size-pos) //当删到末尾或者不够删时
    5. {
    6. _str[pos] = '\0';
    7. }
    8. else
    9. {
    10. strcpy(_str + pos, _str + pos + len);
    11. _str[_size - len] = '\0';
    12. }
    13. _size -= len;
    14. }

    关于npos:

    npos的类型为size_t,它被设为-1,因为size_t表示无符号数,-1在无符号数中表示 最大值。

    npos在字符串中,意味着直到字符串的末尾。在容器中,表示不存在的位置。

    我们要在类里声明静态的npos,在类外定义:

    1. //类里
    2. private:
    3. char* _str;
    4. size_t _size;  
    5. size_t _capacity;  
    6. static size_t npos;
    7. //类外
    8. size_t string::npos = -1;

    或者,在类里这样写:

    1. private:
    2. char* _str;
    3. size_t _size;  
    4. size_t _capacity;  
    5. const static size_t npos = -1;   //const static在C++中是语法的特殊处理,直接可以当成定义初始化

    查找find()

    查找字符:

    1. size_t find(char c, size_t pos = 0)const   //从pos位置开始找c,找到返回下标
    2. {
    3. assert(pos < _size);
    4. for (; pos < _size; pos++)
    5. {
    6. if (_str[pos] == c)
    7. {
    8. return pos;
    9. }
    10. }
    11. return npos;
    12. }

    查找字符串:

    1. size_t find(const char* s, size_t pos)const   //第一个const一定要加!!
    2. {
    3. assert(pos < _size);
    4. const char* ret = strstr(_str + pos, s);
    5. if (ret == nullptr)
    6. {
    7. return npos;
    8. }
    9. return ret-_str;
    10. }

    这里说明一下,关于左操作数的const为什么一定要加。

    在测试时,我们给的例子为:s1.find("day", 0),这里的”day“是常量字符串,类型为const char*,而不是char *,

    所以左操作数的类型也要严格为const char*,才能与常量字符串匹配。

    比大小组合

    1. bool operator>(const string& s1, const string& s2)
    2. {
    3. return strcmp(s1.c_str(), s2.c_str()) > 0;
    4. }
    5. bool operator==(const string& s1, const string& s2)
    6. {
    7. return strcmp(s1.c_str(), s2.c_str()) == 0;
    8. }
    9. bool operator>=(const string& s1, const string& s2)
    10. {
    11. return s1 > s2 || s1 == s2;
    12. }
    13. bool operator<(const string& s1, const string& s2)
    14. {
    15. return !(s1 >= s2);
    16. }
    17. bool operator<=(const string& s1, const string& s2)
    18. {
    19. return !(s1 > s2);
    20. }
    21. bool operator!=(const string& s1, const string& s2)
    22. {
    23. return !(s1 == s2);
    24. }

    流插入与流提取

    引入

    不知道你注意到没有,我们用流插入<<运算符 时,string类型的对象是没法直接输出的。

    1. void test8()
    2. {
    3. string s1("happy");
    4. int a = 4;
    5. cout << a << endl;           //√,输出4
    6. cout << s1.c_str() << endl;   //√,将string类型转化成char*类型,输出happy
    7. cout << s1 << endl;           //×
    8. }

    为啥string类的对象不能用<<呢?

    我们先厘清几个问题:

    1.cout是什么?

    cout是iostream中定义的ostream类中的对象

    "iostream一个库,用于输入输出流的操作。ostream是iostream库中的一个类,它是用来进行输出操作的。而cout是ostream类的一个对象,它是在iostream头文件中定义的。"

    2.<<是什么?

    <<是运算符,它之所以能用在cout上,是因为在iostream中对<<进行了重载。

    3.cout<<4是什么?

    其实就是对象cout在调用函数operator<<()

    相当于cout.operator<<(4)

    所以,为啥string类的对象不能用流插入、流提取呢,因为string类型中没有重载operator<<()函数!

    现在,我们就来重载一下<<和>>,让string类也能用上!

    三点说明:返回值、形参与实现方法

    ➡️operator<<的返回值类型是什么?

    直接说答案:返回cout的类型,即ostream&。

    我们通过下面这行代码来说明原因:

    cout<<4<<" days";   //连续的流插入

    此代码的实质是:

    cout.operator<<(4).operator<<("days");

    所以说,operator<<返回值为ostream&类型,因为这样可以支持连续的流插入。该返回值又可作为对象,继续调用<<。

    ➡️形参:>> / <<是双目运算符,且第一个形参(左操作数)必须为cin或者cout,即istream类或ostream类。

    说明原因:

    cin和cout应该作为左操作数,因为根据我们的理解和使用习惯,我们是将输入的内容存进变量a中:cin>>a; 将a插入输出流中:cout<

    这俩都是cin、cout在左,而变量a在右。

    如果cin/cout做右操作数,那用起来就是a<>cin,这不符合我们的理解,非常怪异。

    所以,可知流插入、流提取的声明为:

    1. ostream& operator<<(ostream& out, const string& s); //流插入
    2. istream& operator>>(istream& in, string& s); //流提取

    ➡️实现方法:

    ⭐1.不能用成员函数重载,只能用类外的函数重载。

    如果用成员函数重载,那左操作数默认就是this。而刚刚我们说明了,>> / <<的左操作数必须为istream类或ostream类,这就冲突了。

    所以不能放在类里实现,得在类外。

    2.如果涉及到访问私有成员,那得用友元函数重载。

    当需要输入/ 输出类的私有成员时,就将重载的<>函数设为友元,予以访问权限。

    流插入的实现

    注:这里不需要设为友元函数,因为我们直接就能访问out的数据,没必要再设友元了。

    在类外实现:

    1. ostream& operator<<(ostream& out, const string& s)
    2. {
    3. for (auto ch : s)
    4. {
    5. out << ch;
    6. }
    7. return out;
    8. }

    思路很简单:将string的内容依次插入对象out中,再返回out。

    流提取的实现

    1. istream& operator>>(istream& in, string& s)
    2. {
    3. char ch;
    4. ch = in.get(); //先获取输入流的首个字符
    5. while ( ch! = ' ' && ch != '\n')   //若首字符不是换行符or空格,则进入循环
    6. {
    7. s += ch;         //将该字符存入变量s中
    8. ch = in.get();   //继续获取输入流的字符
    9. }
    10. return in;
    11. }

    思路:

    step1 获取in(输入流)的首字符

    step2 当首字符不是换行符/空格,进入循环:存字符进变量、继续获取字符

    step3 返回in

    注意:这里不能用in>>ch;来获取输入流的字符。因为在cin>>操作符获取不到字符间的空格or换行。

    (图源:c知道)

    总代码

    📖string.h

    1. #pragma once
    2. #define _CRT_SECURE_NO_WARNINGS 1
    3. #pragma warning(disable:6031)
    4. #include
    5. #include  
    6. using std::cin;   //为什么不直接using namespace std;?
    7. using std::cout;   //因为那样会产生冲突,我定义的string和std里的string,编译器不知道用哪个好
    8. using std::endl;
    9. using std::swap;
    10. using std::ostream;
    11. using std::istream;
    12. namespace jzy   //为了和STL里的string区分,我们把string放进自定义的命名空间里
    13. {
    14. class string
    15. {
    16. public:
    17. //构造函数
    18. string(const char* str = "\0")  
    19. :_str(new char[strlen(str) + 1])  
    20. , _size(strlen(str))
    21. , _capacity(strlen(str))
    22. {
    23. strcpy(_str, str);
    24. }
    25. //析构
    26. ~string()
    27. {
    28. delete[] _str;
    29. _str = nullptr;
    30. _size = _capacity = 0;
    31. }
    32. //c_str()
    33. char* c_str()const  
    34. {
    35. return _str;
    36. }
    37. //size()
    38. size_t size()const   //只读不写 就加上const保护
    39. {
    40. return _size;
    41. }
    42. //operator[]
    43. char& operator[](size_t pos)
    44. {
    45. return *(_str + pos);
    46. }
    47. const char& operator[](size_t pos) const  
    48. {
    49. return *(_str + pos);
    50. }
    51. //普通迭代器
    52. typedef char* iterator;
    53. iterator& begin() //&可加,也可不加
    54. {
    55. return _str;
    56. }
    57. iterator end()   //注意:这里不能加&!
    58. {
    59. return _str + _size;
    60. }
    61. //const迭代器
    62. typedef const char* const_iterator;
    63. const_iterator begin()const
    64. {
    65. return _str;
    66. }
    67. const_iterator end()const
    68. {
    69. return _str + _size;
    70. }
    71. //拷贝构造的深拷贝——传统写法
    72. /*string(const string& s)
    73. :_str(new char[strlen(s._str) + 1])
    74. ,_size(s._size)
    75. ,_capacity(s._capacity)
    76. {
    77. strcpy(_str,s._str);
    78. }*/
    79. //现代写法
    80. void swap(string&tmp)  
    81. {
    82. ::swap(_str, tmp._str);  
    83. ::swap(_size, tmp._size);
    84. ::swap(_capacity, tmp._capacity);
    85. }
    86. string(const string& s)
    87. :_str(nullptr)   //初始化,更安全
    88. ,_size(0)
    89. ,_capacity(0)
    90. {
    91. string tmp(s._str);
    92. swap(tmp);    
    93. }
    94. //赋值——传统写法
    95. //string& operator=(const string& s)
    96. //{
    97. // if (this != &s)
    98. // {
    99. // delete[] _str;                       //先释放旧空间
    100. // _str = new char[strlen(s._str) + 1];   //再开新空间
    101. // strcpy(_str, s._str);
    102. // _size = s._size;
    103. // _capacity = s._capacity;
    104. // return *this;
    105. // }
    106. //}
    107. //赋值——现代写法
    108. string& operator=(const string& s)
    109. {
    110. if (this != &s)
    111. {
    112. string tmp(s._str);
    113. swap(tmp);
    114. return *this;
    115. }
    116. }
    117. //增容
    118. void reserve(size_t size)
    119. {
    120. if (size > _capacity) //先检查下要不要增容
    121. {
    122. char* p = new char[size + 1]; //开新空间
    123. strcpy(p, _str);               //拷数据
    124. delete[] _str;                 //释放旧空间
    125. _str = p;  
    126. _capacity = size;
    127. }
    128. }
    129. //尾插字符
    130. void push_back(const char c)
    131. {
    132. if (_size == _capacity)   //先检查要不要扩容
    133. {
    134. reserve(_capacity == 0 ? 4 : 2 * _capacity);
    135. }
    136. _str[_size++] = c;
    137. _str[_size] = '\0';   //别忘了最后得加上'\0'
    138. }
    139. //尾插字符串
    140. void append(const char* ch)
    141. {
    142. //考虑扩容
    143. size_t total_size = _size + strlen(ch);   //先判断容量
    144. if(_capacity < total_size)
    145. {
    146. reserve(total_size);
    147. }
    148. strcpy(_str + _size, ch);   //直接把ch拷到\0的位置
    149. _size += strlen(ch);
    150. }
    151. //operator+=
    152. //追加字符
    153. string& operator+=(const char c)
    154. {
    155. push_back(c);   //本质是复用了push_back()
    156. return *this;
    157. }
    158. //追加字符串
    159. string& operator+=(const char* ch)
    160. {
    161. append(ch);
    162. return *this;
    163. }
    164. //指定位置插
    165. string& insert(size_t pos, char c)
    166. {
    167. assert(pos <= _size);
    168. if (_size == _capacity)
    169. {
    170. reserve(_capacity == 0 ? 4 : 2 * _capacity);
    171. }
    172. //先把后面的数据往后挪
    173. size_t end = _size;
    174. while (end > pos)       //这个看似不起眼的循坏,却很容易写错!
    175. {
    176. _str[end] = _str[end - 1];
    177. end--;
    178. }
    179. //再插入
    180. _str[pos] = c;
    181. _size++;
    182. _str[_size] = '\0';
    183. return *this; //这个返回值意义不大
    184. }
    185. string& insert(size_t pos, const char* ch)
    186. {
    187. assert(pos <= _size);
    188. size_t total_size = _size + strlen(ch); //没引string.h头文件,是怎么用起来strlen的? 因为iostream中包含了stdio.h(其中包含了strlen)
    189. if (_capacity < total_size)   //先算出一共需要多少空间,不够就开
    190. {
    191. reserve(total_size);
    192. }
    193. size_t end = _size ;   //把后面的数据挨个往后挪
    194. size_t span = strlen(ch);
    195. while (end > pos)       //这个循环很容易写错! 不能写end>=pos,变成负数也就是无穷大,很容易越界!
    196. {
    197. _str[end + span] = _str[end];
    198. end--;
    199. }
    200. _str[pos+span] = _str[pos];
    201. strncpy(_str+pos, ch, strlen(ch));
    202. _size += strlen(ch);
    203. return *this;
    204. }
    205. //删字符——初始版
    206. //void erase(size_t pos, size_t len = npos)  
    207. //{
    208. // assert(len < _size);
    209. // size_t num = _size - pos - len; //算出要挪几个数
    210. //
    211. // size_t start = pos + len;     //从下标为start的数开始挪
    212. // while (num)   //挪数据
    213. // {
    214. // _str[pos++] = _str[start++];
    215. // num--;
    216. // }
    217. // _str[_size - len] = '\0';
    218. // _size -= len;
    219. //}
    220. //删字符——进阶版
    221. void erase(size_t pos, size_t len = npos)
    222. {
    223. assert(len < _size);
    224. if (len == npos||len>_size-pos) //当删到末尾或者不够删时
    225. {
    226. _str[pos] = '\0';
    227. _size = strlen(_str);
    228. }
    229. else
    230. {
    231. strcpy(_str + pos, _str + pos + len);
    232. _str[_size - len] = '\0';
    233. _size -= len;
    234. }
    235. }
    236. //查找
    237. size_t find(char c, size_t pos = 0)const //从pos位置开始找c
    238. {
    239. assert(pos < _size);
    240. for (; pos < _size; pos++)
    241. {
    242. if (_str[pos] == c)
    243. {
    244. return pos;
    245. }
    246. }
    247. return npos;
    248. }
    249. size_t find(const char* s, size_t pos)const   //第一个const一定要加!!
    250. {
    251. assert(pos < _size);
    252. const char* ret = strstr(_str + pos, s);
    253. if (ret == nullptr)
    254. {
    255. return npos;
    256. }
    257. return ret-_str;
    258. }
    259. private:
    260. char* _str;
    261. size_t _size;     //也可以用int,但库里面一般用size_t
    262. size_t _capacity;   //_capacity是有效字符的空间数,不包括\0
    263. static size_t npos;  
    264. };
    265. size_t string::npos = -1;
    266. //流插入
    267. ostream& operator<<(ostream& out, const string& s)
    268. {
    269. for (auto ch : s)
    270. {
    271. out << ch;
    272. }
    273. return out;
    274. }
    275. //流提取
    276. istream& operator>>(istream& in, string& s)
    277. {
    278. char ch;
    279. //in >> ch;   //cin获取不到数据间的空格or换行
    280. ch = in.get(); //先获取输入的首个字符
    281. while (ch!= ' ' && ch != '\n') //若首字符不是换行符or空格,则进入循环
    282. {
    283. s += ch;         //将该字符存入变量s中
    284. ch = in.get();   //继续获取输入的字符
    285. }
    286. return in;
    287. }
    288. }

    📖test.cpp

    1. #include"string.h"
    2. using namespace jzy;
    3. void test1()
    4. {
    5. string s1("abc");
    6. string s2;
    7. }
    8. void test2()
    9. {
    10. string s1("abcdef");
    11. cout << s1.c_str() << endl;
    12. }
    13. void test3()
    14. {
    15. string s1("abcdef");
    16. cout << ++s1[0] << endl;
    17. const string s2("abcdef");
    18. cout << s2[0]<< endl;
    19. for (int i = 0; i < s2.size(); i++)
    20. {
    21. cout << s2[i] << " ";
    22. }
    23. }
    24. void test4()
    25. {
    26. /*string s1("hello");
    27. string::iterator it = s1.begin();
    28. while (it != s1.end())
    29. {
    30. cout <<*it<< " ";
    31. it++;
    32. }
    33. cout << endl;
    34. for (auto ch : s1)
    35. {
    36. cout << ch << " ";
    37. }
    38. cout << endl;*/
    39. const string s1("hello");
    40. string::const_iterator it = s1.begin();
    41. //cout << ++s1[0] << " ";   //不可修改
    42. for (auto ch : s1)  
    43. {
    44. cout << ++ch << " "; //为什么这里可以修改s1的内容?❓
    45. }
    46. }
    47. void test5()
    48. {
    49. string s1("hello");
    50. string s2("111111111111111111111111111");
    51. s2 = s2;
    52. }
    53. void test6()
    54. {
    55. string s1("happy");
    56. string s2(s1);
    57. s1=s2 = "hi";
    58. }
    59. void test7()
    60. {
    61. string s1("happy");
    62. //s1.reserve(100);
    63. s1.push_back('A');
    64. cout << s1.c_str() << endl;
    65. s1.append(" day");  
    66. cout<< s1.c_str() << endl;
    67. s1 += 'y';
    68. cout << s1.c_str() << endl;
    69. s1 += " zz";
    70. cout << s1.c_str() << endl;
    71. s1.insert(0, 'k');
    72. cout << s1.c_str() << endl;
    73. s1.insert(4, "kkk");
    74. cout << s1.c_str() << endl;
    75. s1.erase(4, 4);
    76. cout << s1.c_str() << endl;
    77. cout << s1.find('p', 2) << endl;
    78. cout << s1.find("day", 0) << endl;
    79. }
    80. void test8()
    81. {
    82. string s1("happy");
    83. cout << s1 << endl;
    84. string s2;
    85. cin >> s2;
    86. cout << s2 << endl;
    87. }
    88. int main()
    89. {
    90. //test1();   //构造函数
    91. //test2();   // c_str
    92. //test3();   // size() | operator[]
    93. //test4();   //迭代器 | 范围for
    94. //test5();     //深浅拷贝 | 赋值
    95. //test6();     //拷贝构造 / 赋值的现代写法
    96. //test7();     //reserve | push_back | append | operator+= | insert | 删字符 | 查找
    97. test8();       //流插入 | 流提取
    98. return 0;
    99. }

  • 相关阅读:
    Vue.2x秘籍(下)
    软考 - 系统架构设计师 - 基于口令的认证方式和基于公钥体系的认证方式
    C语言:用函数打印质数
    《QT实用小工具·五十八》模仿VSCode的可任意拖拽的Tab标签组
    Netty游戏服务器消息分发技巧(channelRead处处理)
    springboot项目(maven构建)添加子模块,子模块中的接口无法访问,报404异常
    K8s复习笔记7--Redis单机和Redis-cluster的K8S实现
    HTML+CSS美食静态网页设计——简单牛排美食餐饮(9个页面)公司网站模板企业网站实现
    1 .python 接口测试框架封装(一)
    使用match-lstm和答案指针进行机器理解
  • 原文地址:https://blog.csdn.net/2301_76540463/article/details/133305558