既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
1、string是表示字符串的字符串类
2、该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3、string在底层实际是:basic_string模板类的别名
typedef basic_string<char, char_traits, allocator> string;
4、不能操作多字节或者变长字符的序列。
3.string类的常见构造及模拟实现
3.1string类对象的常见构造
(constructor)函数名称 | 功能说明 |
string() | 构造空的string类对象,即空字符串 |
string(const char*s) | 用C-string来构造string类对象 |
string(size_t n,char c) | string类对象中包含n个字符c |
string(const string& s) | 拷贝构造函数 |
int main()
{
string s1; //构造空的s1 等价于string s1("");
string s2("hello");//用C格式字符串构造
string s3(s2);
return 0;
}
我们来自己模拟实现一下string类的构造函数
3.2 string类的构造函数
class string
{
public:
//全缺省的构造函数
string(const char* str = "")
:_size(strlen(str))
,_capacity(_size)
{
//实际上要多开一个 留给'\0'
_str = new char[_capacity + 1];
strcpy(_str, str);//会把'\0也拷过去'
}
private:
char* _str;
size_t _size;//有效字符个数 不算'/0';
size_t _capacity;//实际存储有效字符的空间
};
想必这段代码大家肯定是可以看的懂得。
3.3 string类的拷贝构造
基本写法
string(const string& s)
:_size(strlen(s._str))
,_capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
现代写法
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//现代写法
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
}
基本写法想必大家肯定很好理解,但是现代写法可能会有点疑惑,我们在下面的赋值拷贝就会分析。
3.4 string类的赋值构造
3.4.1 常规解法
我们在写任何一个类型的赋值运算符重载函数时一定要考虑到下面这几个方面:
1》是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(*this)。只有返回一个引用,才能支持连续赋值。否则,如果函数的返回值是void,在使用该赋值运算符将不能支持连续赋值。这与我们日常使用是违背的。
2》是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次复制拷贝函数。把参数声明为引用可以避免这样无谓消耗。能提高代码效率。同时,我们在赋值运算符函数内不会改变传入的实例的状态,因此应该为传入的引用参数加上const关键字。
string& operator=(const string& s)
3》是否释放实例自己已有的内存。如果我们忘记在分配新内存之前释放自身已有的空间,则程序会出现内存泄漏
4》判断传入的参数和当前的实例(*this)是不是同一个实例。如是同一个,则不进行赋值操作,直接返回即可。如果实现不做判断就进行赋值,那么在释放实例自身空间内存的时候就会导致严重的问题:当*this和传入的参数是同一个实例时,一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。
结合以上4点,我们可以写出:
string& operator=(const string& s)
{
if (this == &s)
return *this;//自己赋值自己 直接返回
delete[] _str;
_str = nullptr;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
return *this;
}
这种经典解法已经能够解决问题了,但是我们如果在极端情况下考虑如果delete释放_str后,内存不足导致new char抛出异常,则_str将是一个空指针,这样很容易导致程序崩溃。也就是说,一旦在赋值运算符函数内部抛出一个异常,string的实例不再保持有效的状态,这就违背了异常安全原则。
3.4.2考虑异常安全法的解法:
我们有两种解法:
1》一种简单的办法是我们先用new分配新内容,再用delete释放已有的内容。这样只在分配内容成功之后才会释放原来的内容,也就是当分配内存失败时我们能确保string的实例不会被修改。
2》第二种解法是我们先创建一个临时实例,再交换临时实例和原来的实例。由于我们创建的临时实例时一个局部变量,一旦程序除了该局部变量的作用域,就会自动调用析构函数,把这个临时空间所指向的内存释放掉。由于这个临时空间指向的内存就是我们原来的实例的内存。这就相当于自动调用析构函数释放实例的内存。
并且我们在string的构造函数里面用new分配内存,如果由于内存不足抛出异常,但是我们还没有修改原来实例的状态,因此实例的状态还是有效的,这也就保证了异常安全性。
下面是考虑异常安全的代码:
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);
std::swap(_str, tmp._str);
std::swap(_size, tmp._size);
std::swap(_capacity, tmp._capacity);
}
return *this;
}
我们使用我们自己模拟实现的看看效果:
int main()
{
//std库
//std::string s1; //构造空的s1 等价于string s1("");
//std::string s2("hello");//用C格式字符串构造
//std::string s3;
//自己模拟实现
s::string s1("hello world");
s::string s2;
s::string s3;
s3 = s2 = s1;
return 0;
}
4.全部代码
以下是本篇文章模拟实现的string类相关代码。
![img](https://img-blog.csdnimg.cn/img_convert/3b851db5f6bda81fe19495ff8de2be59.png)
![img](https://img-blog.csdnimg.cn/img_convert/5d35a93d69fb5262821a8cdbabc22355.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
[外链图片转存中...(img-rQg8ry16-1715531698087)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**