✨✨小新课堂开课了,欢迎欢迎~✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++:由浅入深篇
小新的主页:编程版小新-CSDN博客
前言:
前面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。这里我们也会介绍一些常见接口的模拟实现。
string类各函数接口总览:
#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using namespace std;namespace fu
{class string{//短小又频繁调用的函数可以直接定义在类内,默认是inlinetypedef char* iterator;typedef const char* const_iterator;//默认成员函数string(const char* str = "");//构造函数string(string& s);//拷贝构造string& operator=(const string& s);//赋值运算符重载~string();//析构函数//迭代器相关的函数iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//容量和大小相关的函数size_t size();size_t capacity();void reserve(size_t n);void resize(size_t n, char ch = '\0');bool empty();//修改字符串相关的函数void push_back(char ch);void append(const char* str); string& operator+=(char ch);string& operator+=(const char* str);string& insert(size_t pos, char ch);string& insert(size_t pos, const char* str);string substr(size_t pos = 0, size_t len = npos);string& erase(size_t pos, size_t len);void clear();void swap(string& s);const char* c_str()const;//为啥没有参数?//访问字符串相关函数char& operator[](size_t i);const char& operator[](size_t i)const;size_t find(char ch, size_t pos = 0) const;size_t find(const char* str, size_t pos = 0) const;//关系运算符重载bool operator>(const string& s)const;bool operator>=(const string& s)const;bool operator<(const string& s)const;bool operator<=(const string& s)const;bool operator==(const string& s)const;bool operator!=(const string& s)const;private:char* _str; //储存字符串size_t size;//记录字符串当前有效长度的大小size_t capacity;//记录字符串当前的容量static const size_t npos;};//<<和>>运算符重载istream& operator >> (istream& in, string& s);ostream& operator << (ostream & out, const string& s);istream& getline(istream& in, string& s);
}
注意:这里我们把string类放在一个命名空间域里,防止与标准库库里的string类产生命名冲突。
默认成员函数
构造函数
给缺省值的时候,不能给成nulllptr,因为strlen(str)这里会让程序崩溃崩溃。可以给成\0,但是没有必要,这样字符串就会有两个\0了,常量字符串默认会带有\0,给成空字符串是最合适的。
string(const char* str="")//构造函数
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];//多开一个位置给'\0'strcpy(_str, str);
}
除了上面这种写法,我们也可以分开写,这里也有需要注意的点。
//默认成员函数string():_str(nullptr), _size(0), _capacity(0){}string(const char* str){_size = strlen(str);//初始化时,长度为有效字符的长度_capacity = _size;//初始化时,容量为有效字符的长度_str = new char[_capacity + 1];//多开一个位置给'\0'strcpy(_str, str);}
上面的程序是存在错误的,我们在初始化_str时不能给成nullptr,因为_str是char*,我们要输出_str时存在对空指针解引用,会使得程序崩溃。
#include"string.h"namespace fu
{void test_string1(){string s1;string s2("hello world");cout << s1.c_str() << endl;cout << s2.c_str() << endl;}
}
s1会调用无参的构造函数,被初始化为nullptr (因为还没有模拟实现流插入和流提取,先用C字符串输出也没有太大区别),在输出的时候,就会对空指针解引用。
解决法案:
string():_str(new char[1]{'\0'}), _size(0), _capacity(0)
{}string(const char* str)
{_size = strlen(str);//初始化时,长度为有效字符的长度_capacity = _size;//初始化时,容量为有效字符的长度_str = new char[_capacity + 1];//多开一个位置给'\0'strcpy(_str, str);
}
拷贝构造函数
在模拟实现拷贝构造之前,我们要先了解深拷贝和浅拷贝的区别。
浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
下面我们给出两种深拷贝的写法。
传统写法:
开辟一个能足够容纳源字符串大小的空间,将源对象指向的字符串拷贝过去,并将其容量和大小信息进行更新。
string(string& s)//拷贝构造
{_str = new char[s._capacity+ 1];strcpy(_str, s._str);_capacity = s._capacity;_size = s._size;
}
现代写法:
先根据源字符串的C字符串格式调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。
//现代写法
string(string& s)//拷贝构造
{string tmp(s._str);//中间变量确保原始对象的状态不会改变,也保证了自赋值的正确性swap(tmp);
}
赋值运算符重载函数
与拷贝构造类似,在模拟实现的过程中涉及资源的管理,我们也要采用深拷贝。
下面提供两种深拷贝的写法。
传统写法:
不是自己给自己赋值的情况下,除了要先释放原来的旧空间,其他的与拷贝构造的模拟实现并无不同。
string& operator=(const string& s)//赋值运算符重载
{if (this != &s){delete[]_str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}return *this;//支持连续赋值
}
现代写法:
这里的写法与拷贝构造的现实写法十分类似,不同的是,拷贝构造是先通过调用构造函数实例化出一个对象,然后用该对象与拷贝的对象进行交换;而赋值运算符重载则是采用“值传递”接收右值的方法,让编译器自动调用拷贝构造函数,然后我们再将拷贝出来的对象与左值进行交换即可。
//现代写法string& operator= ( string& s){swap(s);return *this;//支持连续赋值}
析构函数
由于string对象的成员变量_str指向一块空间,我们需要手动写析构函数,避免内存泄漏。
~string()//析构函数
{if(_str){delete[]_str;_str = nullptr;_size = _capacity = 0;}
}
迭代器相关的函数
在实现string类是,我们用到的迭代器就是字符指针,只是给字符指针起了一个新的名字。要注意的是并不是所有的迭代器本质上都是指针类型。
typedef char* iterator;
typedef const char* const_iterator;
begin
begin函数的作用就是返回字符串中第一个字符的地址:
iterator begin()
{return _str;
}
const_iterator begin() const
{return _str;
}
end
end函数的作用就是返回字符串最后一个字符的地址,即‘\0’的地址。
iterator end()
{return _str + _size;
}const_iterator end() const
{return _str + _size;
}
容量和大小相关的函数
size和capacity
由于_size和_capacity都是成员变量,受访问限定符的限制,在类外不能访问,size和capacity函数就是为了在类外获取成员变量而设置的。
size_t size()
{return _size;
}
size_t capacity()
{return _capacity;
}
reserve和resize
reserve和resize的区别一定要区分。
reserve:
当n>capacity的时候,要扩容
当n<capacity的时候,不会缩容,capacity不会缩为n
void string:: reserve(size_t n)
{if (n >_capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[]_str;_str = tmp;_capacity = n;}
}
resize:
当n>capacity的时候,会扩容多出来的部分用字符ch填充,如果ch 为给,默认是\0。
当n<capacity的时候,也不会缩容,但是字符串的有效长度会减到n。
void string::resize(size_t n, char ch )
{if (n < _capacity){_size = n;_str[_size] = '\0';}else{if (n > _capacity){reserve(n);//扩容}for (size_t i = _size; i < n; i++){_str[i] = ch;//填充数据}_size = n;//更新_size_str[n] = '\0';}
}
empty
字符串的有效长度为0时就为空了,不需要容量capacity也为0。
bool empty()
{return _size == 0;
}
修改字符串的相关函数
push_back
这个实现逻辑就是尾插一个字符,尾插前要先判断空间是否足够,不够进行扩容,十分重要的一点的是不要忘记更新size,并且要手动添加\0。
void string::push_back(char ch)
{if (_size == _capacity)//扩容{reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_size++;_str[_size] = '\0';
}
append
这里的实现逻辑就是尾插一个字符串,插入的第一步还是要先判断空间大小,不够就扩容,之后将要插入对的字符串插入到源对象的字符串末尾即可,这里因为要插入的字符串带有\0,就不需要我们手动添加\0了。
void string::append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){//小于2倍按2倍扩,大于2倍,按需扩reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);}strcpy(_str + len, str);_size += len;
}
operator+=
这里直接复用上面已经实现好的功能即可。
string& string::operator+=(char ch)
{push_back(ch);return *this;
}string& string::operator+=(const char* str)
{append(str);return *this;
}
insert
在指定位置插入一个字符或者字符串。第一步判断pos位置的合法性和空间是否够用,第二步挪动数据,第三步,插入数据,第四步更新size。
string& string::insert(size_t pos, char ch)
{assert(pos <= _size);//等于的时候有意义,尾插if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size + 1;//从\0开始挪while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;
}
在指定位置插入一个字符串。首先也是判断pos的合法性,再判断是否需要扩容。插入字符串时,先将pos位置及其后面的字符统一向后挪动len位(len为待插入字符串的长度),给待插入的字符串留出位置,然后将其插入字符串即可。
string& string::insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){//小于2倍按2倍扩,大于2倍,按需扩reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);}size_t end = _size + len;while (end > pos+len-1){_str[end] = _str[end - 1];end--;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;}
erase
erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,删除字符串有两种情况。
第一种是pos位置及其之后的有效字符都需要被删除。这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。
第二种是pos位置及其之后的有效字符只需删除一部分。这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾有’\0’了。
string& string::erase(size_t pos, size_t len)
{assert(pos < _size);if (len > _size - pos){_str[pos] = '\0';_size = pos;}else{for (size_t i = pos + len; i < _size; i++){_str[i - len] = _str[i];}_size -= len;}return *this;
}
clear
将源对象的字符串的第一个字符改为\0,就能达到清空字符串的效果了,不要忘了更新size哦。
void string:: clear()
{_str[0] = '\0';_size = 0;
}
swap
调用库里面的交换函数即可。
void swap(string& s)
{std::swap(_str, s._str);std::swap(_capacity, s._capacity);std::swap(_size, s._size);
}
c_str
返回一个C格式的字符串。
const char* c_str()const
{return _str;
}
访问字符串相关函数
operator[]
通过下标获取字符串对应位置的字符,也可以对其进行修改操作,当不需要修改时,我们就走下面那个,这里也要注意下标的合法性。
char& string:: operator[](size_t i)//可读可写
{assert(i < _size);return _str[i];
}const char& string:: operator[](size_t i)const//只读
{assert(i < _size);return _str[i];
}
find
用于在字符串中查找一个字符或者字符串。find是从指定位置开始往后找,找到了返回对应位置的下标,找不到就返回npos。
size_t string::find(char ch, size_t pos = 0) const
{assert(pos < _size);for (size_t i = 0; i < _size; i++){if (_str[i] == ch)return i;}return npos;
}
size_t string::find(const char* str, size_t pos = 0) const
{assert(pos < _size);const char* ptr = strstr(_str + pos, str);if (ptr == nullptr)return npos;elsereturn ptr - _str;
}
关系运算符重载
bool string::operator<(const string& s)const
{return strcmp(_str, s._str) < 0;
}bool string::operator<=(const string& s)const
{return _str < s._str || _str == s._str;
}bool string::operator>(const string& s)const
{return !(_str <= s._str);
}
bool string::operator>=(const string& s)const
{return !(_str < s._str);
}
bool string::operator==(const string& s)const
{return strcmp(_str, s._str) == 0;
}
bool string::operator!=(const string& s)const
{return !(_str == s._str);
}
<<和>>运算符重载
>>
重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取。
istream& operator >> (istream& in, string& s)
{s.clear();//清空字符串char ch = in.get();//读取一个字符while (ch != ' ' || ch != '\n')//遇到空格或者换行结束{s += ch;//尾插ch = in.get();//继续读}return in;//支持连续读取
}
<<
重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印。实现时我们可以直接使用范围for对对象进行遍历即可。
ostream& operator << (ostream& out, const string& s)
{for (auto ch : s)//范围for{out << ch;}return out;//支持连续输出
}
getline
getline与scanf,getchar等不同的是,getline能够读取空格,遇到换行符才会停止。与>>的实现逻辑相同。
istream& getline (istream& is, string& str, char delim);在string类的里的这个delim也可以指定字符,当遇到该指定的字符时停止。
istream& getline(istream& in, string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != '\n') //遇到换行结束{s += ch; //尾插ch = in.get(); //继续读取字符}return in;}
总结:
除了string类模拟实现比较重要的默认成员函数,我们也实现了很多常见的接口,感兴趣的话也可以自己对着string类的将剩下的接口模拟实现一下
感谢各位的观看,创作不易,还请一键三连哦 ~