一、标准库中的string类
1、在使用标准库中的string类时,必须包函头文件,即#include<string>以及使用标准命名空间using namespace std;
2、常见接口说明
①string对象的常见构造
注:string是c++提出的字符串容器,由于要兼容c语言,c语言中的string其实是一个字符数组,以“\0”结尾,所以第二个构造函数是用C-string来构造string类对象。
void test1()
{
string s1;//方法一 构造空字符串
string s2("hello world");//方法二 用c-string构造
string s3(5,'o');//方法三 不常用
string s4(s2);//方法四 拷贝构造
}
②string类对象的容量操作
注:
a、size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一 致,一般情况下基本都是用size()
b、clear()只是将string中有效字符清空,不改变底层空间大小
c、resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。也就是说,resize函数一定会改变原对象的size大小,size变小字符c就不会到;size变大则会用0或者字符c填充。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。也就是说,容量只会变大不会变小。
d、reserve(size_t n=0):扩容操作,为string预留空间,不改变有效元素个数,当reserve的参数小于 string的底层空间总大小时,reserver不会改变容量大小。也就是说只扩容不缩容。
void test2()
{
string s1("hello world");
cout << s1.size() << endl;//11
cout << s1.length() << endl;//11
cout << s1.capacity() << endl;//15
s1.resize(12);
cout << s1 << endl; //hello world\0
cout << s1.size() << endl;//12
cout << s1.capacity() << endl;//15
s1.resize(20, 'x');
cout << s1 << endl;//hello worldxxxxxxxxx
cout << s1.size() << endl;//20
cout << s1.capacity() << endl;//31
s1.resize(5, 'x');
cout << s1 << endl;//hello
cout << s1.size() << endl;//5
cout << s1.capacity() << endl;//31
s1.reserve(20);
cout << s1.size() << endl;//5
cout << s1.capacity() << endl;//31
s1.reserve(12);
cout << s1.size() << endl;//5
cout << s1.capacity() << endl;//31 理论上不会缩容,但有些编译器为了节省空间可能会缩容
s1.reserve(3);
cout << s1.size() << endl;//5
cout << s1.capacity() << endl;//31
}
③string类对象的访问及遍历操作
void test3()
{
string s1("hello world");
cout << s1[0] << s1[1] << endl;//he
cout << *s1.begin() << *(s1.begin() + 1) << endl;//he
string::iterator it = s1.end();
cout << *(it - 1) << *(it - 2) << endl;//dl
for (auto e:s1)
{
cout << e << " ";
}
//h e l l o w o r l d
}
④string类对象的修改操作
注:
a、在string尾部追加字符时,s.push_back(c) / s.append(c) / s += 'c'三种的实现方式差不多,一般 情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
b、 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
void test4()
{
string s1("hello world");
s1.push_back('x');
s1.push_back('xx');//只会插入第一个字符
cout << s1 << endl;//hello worldxx
string s2(s1);
s2.resize(11);
s2.append("xxx");
cout << s2 << endl;//hello worldxxx
s2 += "hello";
cout<< s2 << endl;//hello worldxxxhello
cout << s2.c_str() << endl;//hello worldxxxhello
size_t pos = s2.find('l',5);
s2[pos] = 'd';
cout << s2 << endl; //hello worddxxxhello
string s3 = s2.substr(0, pos);//左闭右开
cout << s3 << endl;//hello wor
}
⑤string类非成员函数
void test5()
{
string s1("hello world");
string s2 = s1 + "!!!";//效率低,是传值返回,调用了拷贝构造(传值调用和传值返回都用到拷贝构造)
cout << s2 << endl;//hello world!!!
//cin >> s2;//输入world
//cout << s2 << endl;//world 如果这里不不注释,按下回车之后下面的getline也会受到影响所以尽量不要混着用
string s4;
getline(cin, s4);//获取一行的内容 输入hello world
cout << s4 << endl;//hello world
char c = getchar();//获取一个字符 输入a (即使输入ac也只会取到a)
cout << c << endl;//a
char c1[20] = {};
printf("请输入:");
scanf("%s", c1);//c语言读入的方式 存在弊端 遇到空格或者回车会结束读入 输入hello world
//scanf一般不会和容器string混合使用,会出错 c语言的取字符方式就配合c语言的类型
cout << c1 << endl;//hello
}
注:relational operators 指的就是< 、>、 =、 !=、<= 、>=等比较符号,比较的是ascill值的大小
⑥VS 和g++下 string底层结构的不同(32位平台下)
a、在vs下,string总共占28个字节。先是有一个联合体,联合体用来定义string中字符串的存储空间:当字符串长度小于16时,使用内部固定的字符数组来存放;当字符串长度大于等于16时,从堆上开辟空间。(这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。)
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;
(该联合体的大小是16)
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量。
最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。
真实结构在vs中调试查看:
b、g++下string的结构
g++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指 针将来指向一块堆空间,内部包含了如下字段:空间总大小 、字符串有效长度 、引用计数
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
写时拷贝和引用计数配合起来可以使内存空间减小。
二、string类的模拟实现
string类的模拟实现,重点在于构造、拷贝构造、赋值运算符重载。
如果说,定义自己的string类后,不显式定义构造函数、拷贝构造、赋值运算符重载,系统会默认生成,这时候往往会引起浅拷贝的问题。
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源(动态开辟空间),最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生访问违规。
深拷贝:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供
注:这里初始化列表中用new动态开辟一个大小和s相同的空间。strlen计算的是到\0为止的长度,所以要+1(string容器的末尾默认要有\0)。一般的string的size不会把\0算进去;在扩容时,reserve(n)时真正开的空间也是n+1.
传统写法和现代写法:现代写法相较于传统写法,区别在于在一些函数的写法上是通过复用一些已经写好的函数实现的。
写时拷贝:写时拷贝是在浅拷贝的基础之上增加了引用计数的方式来实现的。
string类中必有一个私有成员,其是一个char*,用户记录从堆上分配内存的地址,其在构造时分配内存,在析构时释放内存。因为是从堆上分配内存,所以string类在维护这块内存上是格外小心的,string类在返回这块内存地址时,只返回const char*,也就是只读的,如果你要写,你只能通过string提供的方法进行数据的改写。写时拷贝的意思是,例如用已生成的s1来构造s2,s2指向的和s1是同一块空间,只有在s2写入数据的时候才会开辟另一块新的空间。这样做的目的就是为了提高效率。
引用计数
用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象是资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。