

声明:以下知识相关资料来自比特官网和小编手搓~
string类++1、为什么学习string类?++
++2、标准库中的string类++
2.1、auto 和 范围for
2.2、string类的常用接口说明
++3、string类的模拟实现++
3.1、经典的string类问题
3.2、浅拷贝
3.3、深拷贝
3.3.1、String类的传统写法&现代写法
3.4、写时拷贝(了解)
1、为什么学习string类?
字符串常常出现在我们日常的学习和做算法题里面,而之前C语言里面,对于字符串的处理并没有形成体系,祖师爷在设计C++的时候,为了让字符串的处理更加方便、符合OOP的思想,就设计出了string类,用来统筹字符串的一切。其实string类的出世,是要早于后来设计的模板类的,所以,string类里面的接口会有些冗余,没办法,这是时代遗留的问题。
2、标准库中的string类
2.1、auto 和 范围for
auto关键字:
早期:使用auto修饰的变量,是具有自动存储器的局部变量。
C++11:变废为宝,赋予新的含义。auto作为一个新的类型指示符用来指示编译器,让编译器在编译时期完成对类型的推导。
1、用auto声明指针类型时,用 auto / auto*,是没有区别的;但是对于引用类型的变量,必须必须加 &,用 auto& 。
2、用 auto 连续声明多个变量时,变量的类型要保持一致,否则会报错,因为编译器只推导第一个变量的类型,用推导来的类型定义其他变量。
3、auto 不能作为参数,但是可以做返回值,建议谨慎使用。(这设计真逆天)
4、auto 不能用来直接声明数组。
cppauto aa = 1, bb = 2; // 编译报错:error C3538: 在声明符列表中,"auto"必须始终推导为同一类型 auto cc = 3, dd = 4.0; // 不能做参数 void func2(auto a) {} // 可以做返回值,但是建议谨慎使用 auto func3() { return 3; } // 编译报错:error C3318: "auto []": 数组不能具有其中包含"auto"的元素类型 auto array[] = { 4, 5, 6 };auto的用武之地: --->auto用在对的地方是真的省事
cpp#include<iostream> #include <string> #include <map> using namespace std; int main() { std::map<std::string, std::string> dict = { { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} }; // auto的用武之地 //std::map<std::string, std::string>::iterator it = dict.begin(); auto it = dict.begin(); while (it != dict.end()) { cout << it->first << ":" << it->second << endl; ++it; } return 0; }
范围for:
针对于一个有范围的集合,范围for就像是一个快捷键,方便、格式固定、不容易出错,for循环后括号内由冒号分为两部分:冒号前一部分:范围内用于迭代的变量;冒号的后一部分:被迭代的范围。
具体可以用于对数组和容器进行全范围的遍历。
范围for的底层其实就是迭代器,所以一般有迭代器的,就是支持范围for的。
要改变迭代到的一个位置的值,可以用引用呦(&)。
cpp// C++11的遍历 for (auto& e : array) e *= 2; for (auto e : array) cout << e << " " << endl;
2.2、string类的常用接口说明
1.string类对象的常见构造
--- string() 构造空的string类对象,即空字符串
--- string(const char*) 用C语言里面的常量字符串构造string类对象
--- string(size_t n, char c) 构造有n个字符c的string类对象
--- string(const string& s) 用已有的string对象拷贝构造
cppvoid Teststring() { string s1; // 构造空的string类对象s1 string s2("hello bit"); // 用C格式字符串构造string类对象s2 string s3(s2); // 拷贝构造s3 }
2.string类对象的容量操作
--- size 返回字符串的有效字符个数
--- length 返回字符串的有效字符个数
--- capacity 返回空间总大小
--- empty 判断是不是空字符串,是空,返回true,不是空,返回false
--- clear 清空有效字符
--- reserve 为字符串预留空间
--- resize 重新定义有效字符个数,多出的空间用字符c填充
注意:
1、size和length这两个接口的功能是一样的,但是,为了和后面的容器接口保持一致,常用size。
2、clear只是清空有效字符个数,并不会改变底层空间容量大小。
3、reserve(size_t res_arg = 0) 为string预留空间,不会改变有效字符个数,当预留空间小于底层空间时,就不会改变容量大小;大于那当然会改变了。
4、resize(size_t n):当重新定义的有效字符个数大于原有的有效字符个数时,多出的空间用0填;resize(size_t n, char c):当重新定义的有效字符个数大于原有的有效字符个数时,多出的空间用字符c填。resize重新定义的有效字符个数比总空间大小还要大的话,是要扩容到n的。
3.string类对象的访问及遍历操作--- operator\[\] 返回pos位置的字符,const string类对象的调用(只读)
--- begin + end begin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
--- rbegin + rend rbegin获取最后一个字符的迭代器 + rend获取第一个字符前一个位置的迭代器
--- 范围for C++11支持的新的更加简洁的遍历方式
4.string类对象的修改操作--- push_back 在字符串后尾插字符c
--- append 在字符串后追加一个字符串
--- operator+= 在字符串后追加一个字符串str
--- c_str 返回C语言格式的字符串
--- find + npos 从字符串的pos位置开始往后寻找想要的字符,找到就返回该字符的下标,找不到就返回 npos,即-1。
--- rfind 从字符串的pos位置往前寻找指定字符,返回该字符在字符串中的位置
--- substr 从字符串的pos位置开始,截取长度为n的子串,然后返回
注意:
1、在日常,不管给string尾部追加字符,还是字符串,常用的是 +=
2、在对string操作时,可以用reserve先预留一定的空间,减少麻烦
5.string类非成员函数--- operator+ 尽量少用,因为传值拷贝,会频繁触发对象的拷贝(尤其是深拷贝),造成性能浪费
--- operator>> 输入运算符重载
--- operator<< 输出运算符重载
--- getline 获取一行字符串,可以读取空格,遇到'\n'停止,也可以传参指定遇到什么字符再停止
--- relational operators 支持了string的大小比较
6.vs和g++下string结构的说明声明:均是32位,32位下指针是4字节。
VS:一个联合体:除了常见的指针、有效元素大小、容量大小(char* str,size,capacity),还有一个buff数组,大小是16,因为常用的字符串长度基本都是在16以内,超出就去堆上申请,所以VS下string大小为 16 + 4 + 4 + 4 = 28。
g++:string在该环境下是通过写时拷贝实现的,真实的内部只有一个指针,所以总共就 4 字节,该指针在将来会指向堆的一块空间,该空间包含(空间总大小、有效字符个数、引用计数)。
3、string类的模拟实现
3.1、经典的string类问题
这有一段代码,看看犯了啥经典错误:
cpp// 为了和标准库区分,此处使用String class String { public: /*String() :_str(new char[1]) {*_str = '\0';} */ //String(const char* str = "\0") 错误示范 //String(const char* str = nullptr) 错误示范 String(const char* str = "") { // 构造String类对象时,如果传递nullptr指针,可以认为程序非 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类的实现中只写了构造和析构,并没有显示实现拷贝构造和赋值运算符重载,所以编译器自动生成了默认拷贝构造和赋值运算符重载,在执行 String s2(s1); 这个语句时,编译器调用的是默认拷贝构造,完成s2的初始化,但是,这是浅拷贝,所以完成初始化后,s1、s2都是指向同一块资源空间的,在编译器调用析构函数时,就会对一个空间调用两次析构函数了,程序会崩溃的。
3.2、浅拷贝
浅拷贝:又名位拷贝,编译器只是将对象中的值一个字节一个字节的拷贝过来,对于有资源申请的对象来说,调用默认拷贝构造或是默认的赋值运算符重载,会导致多个对象共享一块资源,对象各自析构互不知情,所以就会发生访问违规。
3.3、深拷贝
深拷贝对于资源的处理,不再是浅拷贝那般,简单的值拷贝,存在安全隐患,而是,给你搞一块一摸一样的资源来,但不是同一个奥!
对于涉及到资源管理:拷贝构造函数、赋值运算符重载、析构函数,都是要显示实现的!
3.3.1、String类的传统写法&现代写法
传统写法:
cppString(const String& s) : _str(new char[strlen(s._str) + 1]) { strcpy(_str, s._str); } String& operator=(const String& s) { if (this != &s) { char* pStr = new char[strlen(s._str) + 1]; strcpy(pStr, s._str); delete[] _str; _str = pStr; } return *this; }现代写法:
cppString(const String& s) : _str(nullptr) { String strTmp(s._str); swap(_str, strTmp._str); } // 对比下和上面的赋值那个实现比较好?---上面 String& operator=(String s) { swap(_str, s._str); return *this; } //不好的原因: //参数传递方式:const String& s 是const 引用传递,不会直接拷贝对象。 //核心逻辑:在函数内部手动创建临时对象 strTmp,再交换资源。 //额外操作:需要手动写 if(this != &s) 来处理自赋值情况。 /* String& operator=(const String& s) { if(this != &s) { String strTmp(s); swap(_str, strTmp._str); } return *this; } */现代写法:纯纯就是把形参当作黑奴,不仅抢他吃的,还让他帮我把我吃完的餐盘洗了。
3.4、写时拷贝
写时拷贝本质上是一种偷懒的做法,当只用拷贝来的值,不进行修改,写时拷贝就可以用来偷懒,但是触及到资源内部调动,其实还是要写深拷贝的,这其实就是侥幸心理,赌你不对资源进行操作,只要值。
写时拷贝 = 浅拷贝 + 引用计数
引用计数:用来记录资源使用者的个数,在构造时,计数就+1,每增加一个使用者就+1,当某个对象销毁时,计数-1,再检查是否需要释放、清理资源,如果减完不为0,就是还有人使用就不需要清理、释放资源。



