目录
一、string类的介绍
二、string类的常用接口
1、构造函数
2. string类对象的容量操作
3. string类对象的访问及遍历操作
4. string类对象的修改操作 (重点)
5. string类非成员函数
6. vs和g++下string结构的说明
三、string类的模拟
1. 浅拷贝问题
2. 深拷贝
3. string类常用库函数的实现
4、写时拷贝(了解)
一、string类的介绍
二、string类的常用接口
1、构造函数
函数 | 功能 |
string( ) | 构造一个空的string类对象,即空字符串 |
string(const char* s) | 用字符串来构造一个string类对象 |
string(size_t n, char c)
|
string
类对象中包含
n
个字符
c
|
string(const string&s)
| 拷贝构造函数 |
代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1;
string s2("hello qingshan");
string s3(3, 'x');
string s4(s3);
return 0;
}
2. string类对象的容量操作
函数 | 功能 |
size | 返回字符串有效长度 |
capacity | 返回空间总大小 |
lenth | 返回字符串有效长度 |
empty |
检测字符串是否为空串,是返回true,否则返回false
|
clear | 清空有效字符 |
reverse | 为字符串预留空间 |
resize |
将有效字符的个数该成n个,多出的空间用字符c填充
|
size 和 lenth
其实效果相同,只是由于某些原因创造出了一些多余的接口。
capacity
string底层原理是一个字符数组,capacity代表的就是这个字符数组的容量大小。
int main()
{
string s1;
string s2("hello qingshan");
cout << s2.size() << " " << s2.length() << endl;
cout << s2.capacity() << endl;
return 0;
}
clear
就是将string中的字符全部清空。
可以看到clear之后s2变成了空。
reverse
一般是开更大的空间,值得说的是,一般都是异地扩容,将string类对象的容量扩容到指定个数(或者更大)。如果想要缩容的话,是不支持的。
resize
分为三种,重置的大小大于容量,那么就需要扩容,然后将扩容后的空间用字符c填充;如果小于容量但是大于有效字符长度,那么就直接用字符填充;如果小于有效字符长度,那么就直接缩减长度。(具体后文实现的时候会详细叙述)
注意:resize的第二个参数是将有效空间的字符都变成第二个参数中的字符。
int main()
{
string s1;
string s2("hello qingshan");
cout << s2.size() << " " << s2.length() << endl;
cout << s2.capacity() << endl;
s2.clear();
s2.reserve(20);
s2.resize(20, 'q');
cout << s2.capacity() << endl;
cout << s2 << endl;
return 0;
}
这里编译器是按照2倍数扩容了。
3. string类对象的访问及遍历操作
operator[ ]
返回pos位置的字符
代码:
int main()
{
string s1;
string s2("hello qingshan");
cout << s2[1] << endl;
return 0;
}
begin+ end
一般来说迭代器可能指针,但有时候也可能不是。
int main()
{
string s1;
string s2("hello qingshan");
string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
迭代器就是遍历的另一种方式
rbegin + rend
int main()
{
string s1;
string s2("hello qingshan");
string::reverse_iterator it = s2.rbegin();
while (it != s2.rend())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
这其实调用的是反向迭代器,所以遍历的时候也是反向遍历。
范围for
int main()
{
string s1;
string s2("hello qingshan");
for (auto ch : s2)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
ch每次取一个s1中的元素。
其实,范围for看着很神奇,底层用的也是迭代器。
4. string类对象的修改操作 (重点)
push_back
尾部插入字符
append
在尾部追加
可以看到用法有很多,最常用的其实就是追加字符和追加字符串。
代码:
int main()
{
string s1;
string s2("hello qingshan");
s2.append("!");
s2.append("hahaha");
cout << s2 << endl;
return 0;
}
opeartor +=
在字符串后追加
追加分为三种:1、字符。 2、字符数组。3、字符串。
c_str
返回c格式字符串
也就是返回底层的那个指向字符数组的字符指针
int main()
{
string s1;
string s2("hello qingshan");
cout << s2.c_str() << endl;
return 0;
}
find函数+ npos
查找一般默认是从0位置开始。找到了返回该位置,没找到返回npos。
int main()
{
string s1;
string s2("hello qingshan");
cout << s2.find("n") << endl;
return 0;
}
rfind
int main()
{
string s1;
string s2("hello qingshan");
cout << s2.rfind("n") << endl;
return 0;
}
这里返回的pos还是按照正向顺序来的,并不是反向从0开始数。
substr
int main()
{
string s1;
string s2("hello qingshan");
size_t pos = s2.rfind("q");
cout << s2.substr(pos, 8) << endl;
return 0;
}
5. string类非成员函数
operator+
operator>>
operator<<
getline
第一个参数必须是输入流,第二个才是填string类对象。
代码:
int main()
{
string s1;
string s2("hello qingshan");
getline(cin, s1);
cout << s1 << endl;
return 0;
}
relational operators
也就是string类对象的比较函数
代码:
#include <iostream>
#include <vector>
int main ()
{
std::string foo = "alpha";
std::string bar = "beta";
if (foo==bar) std::cout << "foo and bar are equal\n";
if (foo!=bar) std::cout << "foo and bar are not equal\n";
if (foo< bar) std::cout << "foo is less than bar\n";
if (foo> bar) std::cout << "foo is greater than bar\n";
if (foo<=bar) std::cout << "foo is less than or equal to bar\n";
if (foo>=bar) std::cout << "foo is greater than or equal to bar\n";
return 0;
}
6. vs和g++下string结构的说明
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
· g++下string的结构
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
三、string类的模拟
1. 浅拷贝问题
我们看下面这段代码:
class String
{
public:
String(const char* str = "")
{
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
// 测试
void TestString()
{
String s1("hello bit!!!");
String s2(s1);
}
说明:上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
2. 深拷贝
//s2(s1)
string(const string& s)
{
_str = new char[s._capacity + 1];
_capacity = s._capacity;
_size = s._size;
strcpy(_str, s._str);
}
其实也就是调用拷贝构造函数的时候给新创的对象开辟一块空间。
3. string类常用库函数的实现
先确定string类私有的成员变量为
_str指的是开辟的字符数组, _size代表有效字符个数,_capacity代表字符数组中的容量。npos则是无符号整型的最大值。为什么static的的成员可以在类内定义呢?这是因为C++标准规定了const类型的成员可以在类内给一个初始值。
构造函数 、拷贝构造、析构函数
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
string(const string& s)
{
_str = new char[s._capacity + 1];
_capacity = s._capacity;
_size = s._size;
strcpy(_str, s._str);
}
~string()
{
delete[] _str;
_str = nullptr;
_capacity = _size = 0;
}
获取size和capacity
size_t size() const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
获取c_str
const char* c_str() const
{
return _str;
}
获取第n个元素(重载 [ ])
这里有可能是const类型的对象和普通的对象调用,而普通对象需要对数据进行修改,所以这里将函数重载一下。
//普通对象
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//const对象
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
resize和reserve的实现
因为reserve只做扩容操作,所以我们需要判断传入的开辟大小是否大于本身拥有的空间容量
而resize就需要分情况了,如果是是扩容,就先用reserve开辟空间,然后默认填入 '\0' 到开辟的空间中。如果是缩小有效字符长度,直接在该位置填入 '\0' 即可。
void reserve(size_t n)
{
//c++中扩容只能重新开辟空间拷贝过去
if (n > _capacity)
{
char* temp = new char[n + 1];
//拷贝过来再删除
strcpy(temp, _str);
delete[] _str;
_str = temp;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0')
{
if (n > _size)
{
reserve(n);
for (int i = _size; i > _size; --i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
else
{
_str[n] = '\0';
_size = n;
}
}
迭代器
迭代器一般情况下都是指针,但不全都是。在string类中就是一个char*类型,只不过被我们用typedef包装成了iterator。
begin() 返回字符数组的开头,end() 返回字符数组的尾部。
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
push_back、append、+=
尾插的话还是经典的先检查再插入数据。
append需要先算好扩容后的空间大小然后扩容,最后strncpy把需要追加的字符串追加到原有的字符串后面就行了。重载+=号服用前面两个函数就够了。
void push_back(char ch)
{
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
_capacity = newcapacity;
}
_str[_size] = ch;
++_size;
//结尾加上\0
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
//如果扩容了两倍也有可能超出
if (_size + len > _capacity)
{
reserve(_size + len + 1);
}
strncpy(_str + _size, str, len);
_size += len;
}
string& operator+=(const char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
insert的实现
有两种情况:插入字符和插入字符串。
插入字符需要先检查扩容,然后挪动数据,这里需要注意边界问题。如果我们是把end位置的数据挪动到end+1的位置,那么到0的时候再--,因为这里的pos是一个无符号整形,0就会变成npos,陷入死循环。
解决方法:1、可以把pos强制类型转换为int类型。
// 挪动数据
int end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
2、循环到1的时候就停下来,那么我们就可以看成是end位置来获取前一个位置的值。
//挪动数据
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
插入字符串也是同理,因为字符串是有长度的,可能是len个,我们这里也需要关注边界问题,循环里end+len之后还要-1,因为需要排除最后一个。
代码:
string& insert(size_t pos, char ch)
{
assert(pos < _size);
//检查容量
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
_capacity = newcapacity;
}
//挪动数据
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos < _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len ;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
while(len > 0)
{
_str[pos -1] = str[len-1];
len --;
}
return *this;
}
erase
先断言一下pos位置是否在有效范围内。
接下里我们需要判断的是删除的元素个数是否超出了有效字符个数。
当删除的元素大于剩余有效字符长度,那么就可以直接把该位置的元素改为 '\0' ,然后修改_size的数据就行了。
当删除的元素小于剩余有效字符长度,用strcpy将后面的元素拷贝到前面来就行了,因为strcpy是会把 ‘\0’ 也一并拷贝过来的,所以使用起来非常方便。
string& erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len == npos || _size - pos <= len)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
find实现
查找分为:查找字符和查找字符串。
查找字符串这里偷个懒,用strstr函数直接查找一下。
因为strstr返回的是指针,所以我们用if判断一下,如果返回的是空指针,那么就返回npos,如果不为空,那么返回找到的ptr - _str 就是位置了。
size_t find(const char ch, size_t pos = 0)const
{
assert(pos < _size);
while (pos < _size)
{
if (_str[pos] == ch)
{
return pos;
}
++pos;
}
return npos;
}
size_t find(const char* str, size_t pos = 0)const
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
clear、operator<<、operator>>
clear函数作用是清空有效字符。
void clear()
{
_str[0] = '\0';
_size = 0;
}
因为操作符第二个参数才是string类对象,所以需要在类外定义。
operator<<就是一个循环输出所有字符。
operator>>由于我们不知道输入多少内容,开少了需要频繁扩容,开大了浪费空间。
我们可以设置一个有128给元素的数组buff,用get函数来获取输入的字符,每次填入一个字符到数组中,每次满了128就将数组添加到string类对象的字符数组中,并将buff数组重置。最后一次如果不满128个是不会追加到string类对象的字符数组中的,我们手动追加一下。因为每次输入的东西都是要覆盖前一次的,所以我们每次输入前都需要清空一下原来的数据,所以我们调用一下clear函数。
ostream& operator<<(ostream& out, const string& s)
{
for (int i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
istream& operator>>(istream& in, string& s)
{
//先清理s
s.clear();
char buff[128] = { '\0' };
size_t i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
//满了
if (i == 127)
{
s += buff;
i = 0;
}
buff[i++] = ch;
ch = in.get();
}
//剩下不满一组的
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
4、写时拷贝(了解)
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
参考文章:
C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell
C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell