string模拟实现
🐎前言
首先说明一下,string是c++中的类,他有很多接口函数,string类和所有的类一样,有public的函数,也有private的成员,当我们调用string的子函数时, 这里我先把private的成员给出来,所有的函数都是为了让成员使用而创造的。
下面介绍string的成员
_str:一个字符串
_capacity:用来记录容量
_size:用来记录字符串的字符个数
npos:这是一个数字,代表string的最大值,大概有四亿九千万。在一些函数中如果不给参数,就用npos作为缺省值。
private:
char* _str;
//这里_str是char*类型的,因为_str的目的是存储字符串,用指针存储,并且存储的是字符串所以用char
size_t _capacity;
size_t _size;
const static size_t npos;
为了更好的帮助大家理解,我画了一张图。
框里的hello world是_str。
请添加图片描述
🐎构造函数
构造函数的作用是给成员初始化。
给一个空字符作为缺省值,不传参时,构造的就是空字符串,传入参数时,就用传入的字符串。
直接上代码!
//构造函数
12 //构造函数的目的是初始化
//,把size置成字符串的长度的个数
13 //初始化把capacity置成和size一样的
//缺省值是空字符串
14 string(const char* str = "")
15 : _size(strlen(str))
16 ,_capacity(_size)
17 {
18 _str = new char [_size+1];//给_str开辟空间
19 strcpy(_str, str);//拷贝过来,把外面的str传给里面的_str
20 }
21
🐎 析构函数
清理内存的一些资源。
//析构函数
49 ~string()
50 {
51 _size = _capacity=0;//size和capacity置成0
52 delete[]_str;//释放空间
53 _str = nullptr;//置成null,防止出现野指针
54
55 }
🐎拷贝构造函数
写拷贝构造函数之前,你需要了解深浅拷贝。这里我简单介绍一下
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,可能就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,但是其他的对象不知道该资源已经被释放了,以为资源还有效,所以他们会继续对这个资源进行访问。这时就发生了访问违规。为了解决浅拷贝的问题,深拷贝就出现了。
深拷贝:如果一个类中涉及到资源的管理,其拷贝的构造函数,赋值运算符重载以及析构函数必须要显式给出。(就是要手动写,不能用编译器自动生成的)。一般这种情况都是按照深拷贝方式提供。
上代码!
//s2(s1)拷贝构造函数
//s2=s1
string(const string& s)
: _size(strlen(s._str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
以下是图解。
🐎operator=赋值运算符重载
画张图理解这个函数是干什么用
过图片可以看出,s2原本的值是‘xy’,通过调用函数,s2变成了和s1相同的值。
//operator=
30 string& operator=(const string& s)
31 {
32 if (this != &s)//防止自己给自己赋值
33 //这里我的想法是写成this!=&s._str
34 //这个想法是是错误的,this是一个类,s._str是类的对象,根本无法比较
35 {
36 char* tmp = new char[s._capacity+1];
37 strcpy(tmp, s._str);
38 delete[]_str;
39 _str =tmp;
40 _size = s._size;
41 _capacity = s._capacity;
42 }
43 return *this;
44 }
47
以下是图解,大家可以根据图更好理解代码实现。
🐎返回首元素地址(c_str)
我们直接返回首元素的地址即可(注意这里返回指针,其实返回的是他的首元素的地址)。
//返回首元素的地址
48 //这里为什么加const,因为他可能被其他的函数调用,并且这个函数不会改变值,
49 //所以加上const防止有的函数不能调用它
50 const char* c_str()const
51 {
52 return _str;
53 }
54
🐎operator[]
这里传过来一个pos,就是下标,我们返回下标对应的值即可。
实现它是为了我们可以像访问数组一样访问字符串。
//operator[]
55 char& operator[](size_t pos)
56 {
57 assert(pos < _size); //这两种写法都是可以的,断言pos不会超过字符串的长度即可
58 // assert(pos < strlen(_str));
59 return _str[pos];
60 }
61
🐎size
我们直接返回 _size即可,不包括\0
这里可能有的同学会问,为什么要实现一个函数size,直接返回成员_size不行吗?
注意成员_size是private类型的,我们不能直接访问私有类型,所以实现一个公有的函数间接访问_size.
//size
62 size_t size()
63 {
64 return _size;
65 }
66
🐎capacity
我们直接返回 _size即可,不包括\0
这里和size一样,我们通过函数间接访问私有的成员。
//cpaacity
67 size_t capacity() const
68 {
69 return _capacity;
70 }
71
🐎operator+=
这是插入字符函数,可以插入一个或者多个字符,这两个函数复用了push_back和append,这两个函数我将会在后面实现。
//operator +=
72 //插入一个字符
73 string& operator+=(const char ch)
74 {
75 push_back(ch);
76 return *this;
77 //判断空间够不够
78 }
79 //operator +=
80 //插入多个字符
81 string& operator+=(const char* str)
82 {
83 append(str);
84 return *this;
86 }
87
🐎扩容函数(reserve)
调整容量大小到n,我们先new一个n+1的新空间,然后把原来的数据拷贝到新空间上,再释放原来的空间,再交换两个指针的指向,再将_capacity置成n即可。
//reserve 扩容函数
88 string& reserve(size_t n)
89 {
90 if(n>_capacity)
91 {
92 char* tmp = new char[n+1];
93 strcpy(tmp, _str);
94 delete[]_str;
95 _str = tmp;
_capacity=n;
96 }
97
98 }
99
下面来看看图解。
🐎resize
resize会出现两种情况
- 给定的size的值n小于_size,这种情况数据会被截断,只保留前面的n个
- 给定的size的值n大于_size,这种情况又可以分成两种情况
2.1 _size<n<_capacity
这种情况需要改变__capacity的值
2.2 n>_capacity
这种情况需要增容,扩大_capacity
//改变size,如果新的size的比capacity还大,就补成\0
100 //如果新的size比size小,就截短即可
101 void resize(size_t n, char ch ='\0')
102 {
103 if (n < _size)
104 {
105 _size = n;
106 _str[_size] = '\0';
107 }
108 else// n>=_size
109 {
110 if (n > _capacity)
111 {
112 reserve(n);
113 }
114 for (size_t i = 0; i < n; i++)
115 {
116 _str[i] = ch;
117 }
118 _size = n;
119 _str[_size] = '\0';
120 }
121
122 }
123
下面看看图解
🐎push_back
push_back就是在一个字符串尾部拼接上字符,我们首先要考虑空间是否足够,若不够需要扩容,可以利用reserve函数,扩大2倍,然后再插入数据,最后在末尾填上\0
这个函数也可以通过复用insert函数实现(insert函数我将在后面实现)
//push back 插入一个字符
125 void push_back(char ch)
126 {
127 //先判断空间够不够
128 //如果不够,就进行增容
129 if (_capacity == _size)
130 {
131 reserve(_capacity == 0 ? 4 : _capacity * 2);
132
133 }
134 _str[_size] = ch;
135 ++_size;
136 _str[_size] = '\0';
137
138
139 //也可以复用insert函数,insert函数我将在后面实现
140 // 这样可以让函数更加健壮,复用也会让函数更加简洁
141 // insert(_size,ch);
142 }
143
🐎append
append其实和push_back类似,只不过append是拼接上多个字符,也就是一个字符串。这里的问题就出现在字符串,因为我们不知道字符串的长度是多少,就不知道要扩容到多少,是扩容到2倍还是3倍?这里要看的是传入的字符串的个数,我们的空间至少要让开到_size+len(字符串的长度),让空间能够满足最低的情况,让所有字符都有地方住。开好空间后,利用strcpy把字符串里面的数据拷贝过来即可。
//append 插入多个字符
145 void append(const char* str)
146 {
//这里我把字符串的长度和原来的长度加在一起作为len
//,也就是合起来的字符串的长度是len
147 size_t len = strlen(str)+_size;
148 if (len > _capacity)
149 {
150 reserve(len + 1);
151 }
152 strcpy(_str + len, str);
153 _size = len;
154
155 //也可以复用insert函数,insert函数我将在后面实现
156 // 这样可以让函数更加健壮,复用也会让函数更加简洁
157 // insert(_size,str);
158 }
159
🐎insert
insert分为两种情况,插入一个字符或者插入多个字符
//insert 插入一个字符
161 string& insert(size_t pos, char ch)
162 {
163 assert(pos < _capacity);
164 if (_capacity == _size)
165 {
166 reserve(_capacity == 0 ? 4 : _capacity * 2);
167 }
168
169 size_t end = _size+1;
170 while(pos<end)
171 {
172 _str[end ] = _str[end-1];
173 --end;
174 }
175 _str[pos] = ch;
176 _size++;
177 return *this;
178
179 }
180
//insert 插入多个字符
182 string& insert(size_t pos, const char* str)
183 {
184 assert(pos < _capacity);
185 size_t len = strlen(str);
186 if (_size+len> _capacity)
187 {
188 reserve(_size+len);
189 }
190 size_t end = _size + len;
191
192 //往后挪动n个字符
193 while(end>pos+len-1)
194 {
195 _str[end] = _str[end -len];
196 --end;
197 }
198 strncpy(_str + len, str, len);
199 _size += len;
200
201 return *this;
202 }
203
204
以下是插入多个字符的图解,插入单个字符的也是同样的道理。
🐎earse
我们要删除pos位置开始的len个字符
分成两种情况
- pos位置之后的所有位置都被删除,则需要把\0置于pos位置,_size=pos即可
- pos位置之后的字符只删除一部分,删除的部分形成的空位需要让后面的字符补上。
string& earse(size_t pos, size_t len=npos)
228 {
229 assert(pos < _capacity);
230 //从pos开始,直接全部删完,删到最后
231 if (len == npos || pos + len >= _capacity)
232 {
233 reserve(pos);
234 _str[pos] = '\0';
235 }
236 //只删一部分
237 else
238 {
239 size_t begin = pos + len;
240 while(begin<=_size)
241 {
242 _str[pos] = _str[pos + len];
243 begin++;
244 //pos++;我认为这里是pos也可以,把后面的字符往前覆盖就行
245 }
246 _size -= len;
247 }
248 return *this;
249 }
250
下面看一下图解
🐎find
利用的是strstr函数查找字符串,找到了就返回字符串的第一个字符的地址,没有找到返回npos
分成两种情况:
- 找一个字符
1.1 对于这种情况,直接返回pos即可 - 找一个字符串
2.2 对于这种情况,找到字符串后,我们需要返回第一个字符的下标,通过指针差值确定目标字符串的位置。
//find
252 //找一个字符,从0开始找,如果能找到就返回他的下标,如果找不到就返回npos
253 size_t find(const char ch,size_t pos=0)
254 {
255 for (; pos < _size; ++pos)
256 {
257 if (_str[pos] == ch)
258 return pos;
259 }
260 return npos;
261 }
262
//find
263 //找一个字符串,从0开始找,如果能找到就返回他的下标,如果找不到就返回npos
264 size_t find(const char* str, size_t pos = 0)
265 {
266 const char* p = strstr(_str + pos, str);
267 //strstr找一个字符串中某个子串是否存在,如果存在就返回这个子串的地址,不存在就返回null
268 //定义:strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串
269 //如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。
270 if (p == nullptr)
271 return npos;
272 else
273 return p-_str;
274 //为什么要减去_str
275 //答:因为这个函数最后要返回的是这个字符串的首元素的下标
276 // 直接返回p是一个地址,而指针减指针就是字符个数
277 // 之前有一个求字符串的长度的题 就用到了指针减指针
278 }
279
🐎比较大小函数
这里我把所有的比较类函数放在一起实现,他们的核心思想都是一样的
- strcmp函数比较字符串的大小,他把字符转换成对应的ASCLL码值,然后比较大小。如果前面的字符大则返回一个正数,如果相等就返回0,如果后面的字符大就返回一个负数。
- 这里复用了c_str函数,极大的提高了程序的健壮性,如果需要修改某处,只要修改<和=即可,因为其他的都是复用出来的。
bool operator<(const string& s1, const string& s2)
321 {
322 //strcmp比较字符串的大小,如果s1大就返回1,s1小就返回-1,相等就返回0
323 return strcmp(s1.c_str(),s2.c_str())<0;
324 }
325
326 bool operator==(const string& s1, const string& s2)
327 {
328 return strcmp(s1.c_str(), s2.c_str()) == 0;
329 }
330
331 bool operator<=(const string& s1, const string& s2)
332 {
333 return s1 < s2 || s1==s2 ;
334 }
335
336 bool operator>(const string& s1, const string& s2)
337 {
338 return !(s1 <= s2) ;
339 }
340
341 bool operator>=(const string& s1, const string& s2)
342 {
343 return !(s1<s2);
344 }
345
346 bool operator!=(const string& s1, const string& s2)
347 {
348 return !( s1 == s2);
349 }
350