• 【C++ • STL】探究string的源码



    ヾ(๑╹◡╹)ノ" 人总要为过去的懒惰而付出代价ヾ(๑╹◡╹)ノ"


    一、深浅拷贝

    浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
    浅拷贝:(1)析构两次,造成程序崩溃(2)一个对象修改影响另外一个

    如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

    编译器默认生成的拷贝构造,是浅拷贝,会是两个对象指向同一块空间,当程序结束的时候,那么两个对象都会进行销毁,那么一块空间就会进行多次释放,从而引起崩溃。

    深拷贝:给每一个对象分配资源,保证多个对象之间不会因为共享资源而导致多次释放造成程序崩溃。

    二、传统版写法的string类(简单)

    #pragma once
    #include 
    using namespace std;
    #include 
    
    namespace yyqx//为了与库里面的string进行区分
    {
    	//仅仅实现一个简单的string,仅仅考虑资源管理深浅拷贝问题
    	class string
    	{
    	public:
    		//构造函数
    		string(const char* str)
    			:_str(new char[strlen(str) + 1])//这里的+1,是为了'\0'开辟空间
    		{
    			strcpy(_str, str);//拷贝的时候'\0'也拷贝了
    		}
    
    		//拷贝构造(深拷贝)
    		//s2(s1)
    		string(const string& s)
    			:_str(new char[strlen(s._str) + 1])
    		{
    			strcpy(_str, s._str);
    		}
    
    		//赋值,也会有深浅拷贝的问题
    		string& operator=(const string& s)
    		{
    			if (this != &s)//避免自己给自己赋值,会导致值被释放,就会变成随机值
    			{
    				//delete[] _str;//首先进行释放
    				//_str = new char[strlen(s._str) + 1];//C++的new是不需要检查是否开辟空间
    				会抛异常
    				//strcpy(_str, s._str);
    
    				//为了避免开辟空间失败,而本来的空间也被我们释放,可以先开启空间,
    				//进行拷贝,然后再释放
    				char* tmp = new char[strlen(s._str) + 1];
    				strcpy(tmp, s._str);
    				delete[] _str;
    				_str = tmp;
    			}
    			return *this;
    		}
    
    
    		//析构函数
    		~string()
    		{
    			if (_str)
    			{
    				delete[] _str;
    			}
    		}
    		
    		//目的为了输出字符串
    		const char* c_str() const
    		{
    			return _str;
    		}//返回c格式的字符串
    
    		//重载[]
    		char& operator[](size_t pos)
    		{
    			assert(pos < strlen(_str));//注意这里的范围
    			return _str[pos];
    		}
    
    		size_t size()
    		{
    			return strlen(_str);
    		}
    	private:
    		char* _str;
    	};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    赋值运算符重载也会有深浅拷贝的问题。赋值,对象本身是有值的【拷贝的时候,如果空间小,就会不够,空间大,就会造成资源浪费】

    三、string类的模拟实现

    string的增删查改以及使用string【传统】
    基本框架

    #pragma once
    #include 
    using namespace std;
    #include 
    
    namespace yyqx//为了与库里面的string进行区分
    {
    	class string
    	{
    	public:
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    构造函数+析构函数
    写法1

    //构造函数
    		string(const char* str)
    			:_size(strlen(str))
    			,_capacity(_size)
    		{
    			_str = new char[strlen(str) + 1];//这里的+1,是为了'\0'开辟空间
    			strcpy(_str, str);//拷贝的时候'\0'也拷贝了
    		}
    
    		string()//注意,这里不是给的空,而是给了一个空的字符串//标准库里的就是给了一个""
    			:_size(0)
    			,_capacity(0)
    		{
    			_str = new char[1];
    			_str[0] = '\0';
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 构造函数:初始化列表,初始化的顺序并不是初始化列表的顺序,而是成员变量在类中的声明次序。
    • 构造函数:注意默认的构造函数【编译器自动生成、缺省、函数重载】,默认的构造函数这里选择写一个同名函数,注意这里并不是给一个空指针,而是给了一个空字符串。
      写法2:(最优写法)
    		string(const char* str = "")//这里默认值不能给nullptr,strlen以及拷贝strcpy会崩溃
    			:_size(strlen(str))
    			,_capacity(_size)
    		{
    			_str = new char[strlen(str) + 1];//这里的+1,是为了'\0'开辟空间
    			strcpy(_str, str);//拷贝的时候'\0'也拷贝了
    		}
    		
    		//析构函数
    		~string()
    		{
    			if (_str)
    			{
    				delete[] _str;
    				_str = nullptr;//好习惯
    				_size = 0;
    				_capacity = 0;
    			}
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 缺省值这不能给nullptr,strlen以及拷贝strcpy时程序会崩溃
    • 注意初始化列表
    • strcpy注意,拷贝的时候’\0’也拷贝了
    • new开空间的时候,一定要多开一个给’\0’

    拷贝构造+赋值重载函数+其他

    		//拷贝构造(深拷贝)
    		//s2(s1)
    		string(const string& s)
    			:_size(strlen(s._str))
    			,_capacity(_size)
    		{
    			_str = new char[_capacity + 1];
    			strcpy(_str, s._str);
    		}
    
    		//赋值,也会有深浅拷贝的问题
    		string& operator=(const string& s)
    		{
    			if (this != &s)//避免自己给自己赋值,会导致值被释放,就会变成随机值
    			{
    				char* tmp = new char[s._capacity + 1];
    				strcpy(tmp, s._str);
    				delete[] _str;
    				_str = tmp;
    				_size = s._size;
    				_capacity = s._capacity;
    			}
    			return *this;
    		}
    		//目的为了输出字符串
    		const char* c_str() const
    		{
    			return _str;
    		}//返回c格式的字符串
    		
    		char& operator[](size_t pos)//这里仅仅可以传入对象,不能传入const对象,如果是const对象,就会报错
    		{
    			assert(pos < _size);//注意这里的范围
    			return _str[pos];
    		}
    		const char& operator[](size_t pos) const//这里就可以传入const对象
    		{
    			assert(pos < _size);
    			return _str[pos];
    		}
    
    		//这里的const修饰的是this指针指向的对象const string s;
    		size_t size() const//写const,普通对象以及const对象都可以调用,如果不加const对象就不可以调用
    		{
    			return _size;
    		}
    		size_t capacity() const//写const,普通对象以及const对象都可以调用
    		{
    			return _capacity;
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    添加

    		string& operator+=(char ch)
    		{
    			push_back(ch);
    			return *this;
    		}
    
    		string& operator+=(const char* str)
    		{
    			append(str);
    			return *this;
    		}
    
    		void reverse(size_t n)//一个扩容的作用
    		{
    			if (n > _capacity)
    			{
    				char* tmp = new char[n + 1];
    				strcpy(tmp, _str);
    				delete[] _str;//注意这里的释放不是free
    				_str = tmp;
    				_capacity = n;
    			}
    		}
    
    		void resize(size_t n, char ch = '\0')
    		{
    			if (n < _size)
    			{
    				_size = n;
    				_str[_size] = '\0';
    			}
    			else
    			{
    				if (n > _capacity)
    				{
    					reverse(n);
    				}
    				for (size_t i = _size; i < n; i++)
    				{
    					_str[i] = ch;
    				}
    				_size = n;
    				_str[_size] = '\0';
    			}
    		}
    
    		void push_back(char ch)
    		{
    			if (_size == _capacity)
    			{
    				reverse(_capacity == 0 ? 4 : _capacity * 2);//如果是一个空字符串,就会导致并没有扩容,
    				//扩容要注意刚开始没有容量的情况下
    			}
    			_str[_size] = ch;
    			_size++;
    			_str[_size] = '\0';//注意\0,容易遗漏
    		}
    
    		//append插入的字符个数是未知的,扩容二倍也不一定足够
    		void append(const char* str)
    		{
    			size_t len = _size + strlen(str);
    			if (len > _capacity)
    			{
    				reverse(len);
    			}
    			strcpy(_str + _size, str);
    			_size = len;
    		}//但是我们一般用+=
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 判断容量是否满,如果 _size= _ capacity,容量扩2倍,new一个新容量的空间,释放旧空间,最后指针指向新的空间。
    • append (append插入的字符个数是未知的,扩容二倍也不一定足够:解决办法:reverse预留空间【一个扩容的作用】)
    • reverse 为string预留空间,避免多次扩容(提高效率)
    • resize用处:扩空间+初始化;删除数据保留前n个
      插入
    string& insert(size_t pos, char ch)
    		{
    			assert(pos <= _size);//这里的=_size相当于尾插
    			//注意,这里容易忘记,size_t就已经大于等于0了,所以在这里我们主要保证pos是小于_size即可
    			if (_size == _capacity)
    			{
    				reverse(_capacity == 0 ? 4 : 2 * _capacity);
    			}
    			//不可以用strcpy,这里不可以是同一块地址,对导致内容不是我们想要的
    			//最后一个未知的字符移到_size然后就是倒数第二位移动,从后向前移动
    			size_t end = _size + 1;
    			//注意这里如果end=_size,当头插的时候,进入循环end会变成-1,因为是size_t所以又会进入循环,导致错误
    			while (end > pos)
    			{
    				_str[end] = _str[end - 1];
    				--end;
    			}
    			_str[pos] = ch;
    			_size++;
    			return *this;
    		}
    
    		//插入\0,用c_str(遇到\0停止打印)打印显示在屏幕的字符串长度会减小或者不变,但是_size会变大
    		//用范围for或者迭代器可以打印出来
    
    		string& insert(size_t pos, const char* str)
    		{
    			assert(pos <= _size);
    			size_t len = strlen(str);
    			if (_size + len > _capacity)
    			{
    				reverse(_size + len);
    			}
    			size_t end = _size + len;
    			while (end > pos + len - 1)//这里注意
    			{
    				_str[end] = _str[end - len];
    				--end;
    			}
    			strncpy(_str + pos, str, len);//防止为了遇见\0就不拷贝了(strcpy遇见\0就不拷贝了)
    			_size += len;
    			return *this;
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    插入字符:

    • 不可以用strcpy,在字符进行向后移的时候,不可以是同一块地址,对导致内容不是我们想要的,最后一个未知的字符移到_size然后就是倒数第二位移动,从后向前移动
    • end=_size,当头插的时候,进入循环end会变成-1,因为是size_t,又是大于0所以又会进入循环,导致代码错误

    插入字符串:

    • 防止为了遇见\0就不拷贝了,所以用的是strncpy(strcpy遇见\0就不拷贝了)

    删除

    //删除
    		string& erase(size_t pos, size_t len = npos)
    		{
    			assert(pos < _size);
    			//删除的数据大于等于_size
    			if (len == npos || pos + len >= npos)
    			{
    				_str[pos] = '\0';
    				_size = pos;
    			}
    			else
    			{
    				size_t begin = pos + len;
    				while (begin <= _size)
    				{
    					_str[begin - len] = _str[begin];
    					++begin;
    				}
    				_size -= len;
    			}
    			return *this;
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    注意:npos类中静态成员的初始化,必须在类外,类和对象(下)本篇文章中有详细说明。【const在定义的时候必须初始化,但是静态成员的变量初始化又在外面】

    查找

    		size_t find(char ch, size_t pos = 0)
    		{
    			for (; pos < _size; ++pos)
    			{
    				if (_str[pos] == ch)
    				{
    					return pos;
    				}
    			}
    			return npos;
    		}
    
    		size_t find(const char* str, size_t pos = 0)
    		{
    			const char* p = strstr(_str + pos, str);
    			if (p == nullptr)
    			{
    				return npos;
    			}
    			else
    			{
    				return p - _str;
    			}
    			
    		}
    		
    		void clear()
    		{
    			_str[0] = '\0';
    			_size = 0;
    		}
    	private:
    		char* _str;
    		size_t _size;//有效字符的个数
    		size_t _capacity;//存储有效字符的空间大小
    		const static size_t npos;//正确的写法是在类外进行初始化
    		//const static size_t npos = -1;//这种写法也可以,但是违背了正确的写法,要注意
    	};
    	const size_t string::npos = -1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • strstr返回的是指针,没有找到返回空指针。

    流插入和流提取

    /流插入和流提取
    	//在类外
    	//不可以用c_str(),因为遇见\0会停止
    	//'\0'是不可以见字符,不会显示
    
    	//流插入
    	ostream& operator<<(ostream& out, const string& s)
    	{
    		for (auto ch : s)
    		{
    			out << ch;
    		}
    		return out;
    	}
    	//流提取,字符从面板提取到s
    	istream& operator>>(istream& in, string& s)
    	{
    			s.clear();
    		//要把对象里面的字符清理掉,否则当对象不是空的时候,会导致字符直接加到已有对象的后面。
    		//但是我们想要的是,对象是我们输入的字符串
    	
    		//第一种思路(缺点:频繁的+=,字符串过大,会导致频发的扩容,影响效率)
    		/*char ch;
    		ch = in.get();
    		if (ch != ' ' && ch != '\n')
    		{
    			s += ch;
    			ch = in.get();
    		}
    		return in*/
    
    		//第二种思路(这种思路比较优,无论大小都可以避免频繁扩容)
    		char ch;
    		ch = in.get();
    		char buff[128] = { '\0' };
    		size_t i = 0;
    		if (ch != ' ' && ch != '\n')
    		{
    			buff[i++] = ch;
    			if (i == 127)
    			{
    				s += buff;
    				memset(buff, '\0', 128);
    				i = 0;
    			}
    			ch = in.get();
    		}
    		s += buff;
    		return in;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • '\0’是不可以见字符,不会显示
    • clear()要把对象里面的字符清理掉,否则当对象不是空的时候,会导致字符直接加到已有对象的后面。但是我们想要的是,对象是我们输入的字符串

    运算符重载

    	//运算符重载
    	//比较大小
    	//全局函数.可以类比日期类
    	bool operator<(const string& s1, const string& s2)
    	{
    		return strcmp(s1.c_str(), s2.c_str()) < 0;
    	}
    
    	bool operator==(const string& s1, const string& s2)
    	{
    		return strcmp(s1.c_str(), s2.c_str()) == 0;
    	}
    
    	bool operator<=(const string& s1, const string& s2)
    	{
    		return s1 < s2 || s1 == s2;
    	}
    
    	bool operator>(const string& s1, const string& s2)
    	{
    		return !(s1 <= s2);
    	}
    
    	bool operator>=(const string& s1, const string& s2)
    	{
    		return s1 > s2 || s1 == s2;
    	}
    
    	bool operator!=(const string& s1, const string& s2)
    	{
    		return !(s1 == s2);
    	}
    }//这个是yyqx的大括号
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    这里是在全局变量,没有在类里面,是在类外

    迭代器
    string类private里面:

    public:
    		//迭代器
    		typedef char* iterator;
    		typedef const char* const_iterator;
    
    		const_iterator begin() const
    		{
    			return _str;
    		}
    
    		const_iterator end() const
    		{
    			return _str + _size;
    		}
    		
    		iterator begin() 
    		{
    			return _str;
    		}
    
    		iterator end() 
    		{
    			return _str + _size;
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    四、现代版写法的string类

    拷贝构造和赋值的现代写法

    		//拷贝构造(深拷贝)
    		//s2(s1)//现代写法,剥削行为,要完成深拷贝,
    		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)//这里要进行初始化,否则交换后,局部变量的销毁(随机值销毁,不可以)
    		{
    			//构造一个对象tmp,tmp里所有的东西this想要。this和tmp
    			string tmp(s._str);//局部变量,出了作用域会销毁
    			swap(tmp);
    			//tmp出了作用域会销毁
    		}
    
    		//赋值,也会有深浅拷贝的问题
    		//现代写法
    		//第一种
    		//string& operator=(const string& s)
    		//{
    		//	if (this != &s)//避免自己给自己赋值,会导致值被释放,就会变成随机值
    		//	{
    		//		string tmp(s._str);
    		//		swap(tmp);//把tmp给this,出了作用域把this给tmp的值进行销毁
    		//	}
    		//	return *this;
    		//}
    
    		//第二种
    		string& operator=(string s)//传值传参,拷贝构造,拷贝的值给this,并不会导致s的实参发生变化
    		{
    			swap(s);
    			return *this;
    		}
    		//掌握现代写法
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    补充知识点
    遍历方式中有一个是范围for(范围for的底层实现是迭代器,如果没有迭代器的程序,代码会进行报错)

    代码展示:

    	yyqx::string s("hello 12345");
    	for (auto ch : s)
    	{
    		cout << ch << " ";
    	}
    	cout << endl;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在c语言中,我们用atoi。
    在这里插入图片描述
    string中的两个常用函数


    五、总结

    以上就是今天要讲的内容,本文详细的介绍了浅拷贝、浅拷贝和string的模拟实现。本文以及一文带你走进string详细的介绍了string的相关知识,希望给友友们带来帮助!

  • 相关阅读:
    【问题篇】浏览器get请求带token
    【1++的Linux】之进程(四)
    uni-app - 左右垂直分类列表(类似商城商品分类,左侧菜单右侧列表内容)
    OA项目之会议排座和送审
    在键盘输入一个数输出的是该数二进制的个数--c语言
    禅道bug统计并发送钉钉通知
    世界互联网大会|美创科技新一代 灾备一体化平台(DRCC v3.0)重磅亮相
    Drone-Yolo:一种高效的无人机图像目标检测神经网络方法
    Keras Sequential 模型
    除了labview你还知道哪些工业控制领域的软件?
  • 原文地址:https://blog.csdn.net/m0_57388581/article/details/132518110