C++STL容器之——模拟实现String类

       

目录

 一. 首先先来看看String类的成员结构:

 二.接下来的普通函数接口的实现:

 三. 其次就是模拟String类对象的扩容机制:

 四.增删改查

         push_back、append、+=重载函数增添数据:

         insert函数——在数组的任意位置添加数据: 

         删:

         查:

         改:

 五:拷贝构造与赋值重载:

        5.1传统写法

        5.2现代写法:

 六.流插入/流提取重载函数:

 七:迭代器部分实现:

 String类代码.h文件:


         String作为C++的字符序列类,可以对字符串数据进行一系列的增删查改,下面来看看String类中多个常用成员函数的底层实现 :

 一. 首先先来看看String类的成员结构:

class String {
public:
    //构造
	String(const char* str = "") 
     {
	    }

    //析构
    ~String(){
	}

private:
	char* _arr;
	size_t _size;
	size_t _capacity;
	const static size_t npos = -1;
};

该类的底层是一个连续的存储空间,相当于一个字符数组,该类中共有四个成员变量:

        1.其中有一个是字符指针_arr,它指向堆区空间的一块地址,在该地址中存放着对象的字符串内容),因为栈区的内存空间很少,所以需要开辟堆区空间去存放数据 ;

        2._size是指在当前数组中已经存储的字符总个数,不包括'\0'字符,我们每次在增删数据时,都需要用到它;

        3._capacity是指当前数组的容量有多少,_capacity表示的是数组的上限存储空间,一旦超过了这个容量,就相当于是越界了,'\0'字符不算在_capacity中!

        4.最后就是这个npos变量了,它是const静态成员,不可被修改,且被该类的所有成员所共享。

二.接下来的普通函数接口的实现:

class String{	
 public:
        //拷贝构造
       string(const char* str="") {
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity+1];
			strcpy(_str, str);
		}

		//析构
		~string() {
			_size = _capacity = 0;
			delete[] _str;
			_str = nullptr;
		}
        //求字符串的长度
		 size_t size() const{
			return _size;
		}
		//求字符串的数组容量大小
		size_t capacity() const{
			return _capacity;
		}
		//c_str内容
		char* c_str() const{
			return _str;
		}
        //判断该对象数组中是否为空
        bool empty() const {
		    return _size == 0;
	    }
   
        //获取数组所能存放数据的最大容量
	    size_t Max_size() {
		    return npos;
    	}
        //清空对象所有内容
	    void clear() {
		    _size = 0;
		    _arr[0] = '\0';
	    }

 private:
    char* _arr;
	size_t _size;
	size_t _capacity;
	const static size_t npos = -1;
    };

        对于构造函数来说,形参我使用了缺省值“空字符串”去替代,假如创建对象在不初始化的情况下都是无内容的,所以用空字符串更符合;若是初始化就已经赋值的话,可以获取到所赋的字符串的大小,依据大小去开辟空间,进而适配_size、_capacity等成员变量。然后就是在_arr初始化赋值时多new了一个空间,该空间用于存放'\0'终止符,该终止符不算在_size和_capacity成员变量中。

        对于析构函数来说,就是释放堆区空间还给操作系统,将剩余成员变量清零即可。

        1.在上面这些成员函数中,大多都加上了const,const修饰变量的作用是不允许该变量在后续操作中被修改;而const修饰成员函数的作用也是如此,使得函数在做返回值时避免其成为左值被修改。放在类中的每个非静态成员函数中的第一个形参都是隐藏的this指针,而const修饰的就是这个this指针,意味着this指针就不能再进行修改其成员变量(_size,_arr,_capacity)了。

        2.以上这些成员函数几乎都是对成员变量的封装,封装提高了底层成员变量的安全性,不会暴露在外面,被别人随意使用。

 注:下面的函数都是放在类中的成员函数!

  三. 其次就是模拟String类对象的扩容机制:

//扩容机制
	void reserve(size_t n){
		if (n > _capacity) {
			//会重新开辟一块更大的新空间
			char* tmp = new char[n + 1];  //扩容的时候多开一个空间,为'\0'开
			strcpy(tmp, _str);
			//销毁原来的旧空间
			delete[] _str;
			_str = tmp;		//将临时空间再赋值给类成员_str
			_capacity = n;	//更新容量
			}
		}

        扩容是在原空间容量_capacity不够的情况下进行的,而堆区空间创建空间又是随机性的,所以扩容系统会根据该空间后面是否有空闲空间去扩,若该空间后面有空闲空间,则是原地扩——直接在该空间后面增加所需要的字节空间;另一种就是异地扩——重新选择一块合适大小的地方去开辟空间供其使用。 

        而我们并不清楚系统是按照异地还是原地去扩容,所以选择一个临时指针(打工人)去帮我们做这些事,等事情做完(扩容完毕)后,我们在从打工人那里获取成果即可。

        注:异地扩会导致忘记释放原来的堆区空间,所以要记得销毁~

四.增删改查

        push_back、append、+=重载函数增添数据:

    //插入字符
	void push_back(char c) {
		//在插入字符时,需要注意对象可能是空字符串,需要手动扩容
		if (_size == _capacity) {
			size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
			reserve(newcapacity);
			}
			_str[_size++] = c;
			_str[_size] = '\0';
		}
        
        //插入字符串
		void append(const char* str){
			size_t len = strlen(str);
			
			if (_size+len > _capacity) {
				reserve(_size+len);	
			}
            //方法1:
			/*for (int i = 0; i < len; i++) {
				_str[_size++] = str[i];
			}
			_str[_size] = '\0';*/
			
			//方法2:
			strcpy(_str +_size, str);
				_size += len;
		}

        //插入字符+=
        string& operator+=(char c) {
			push_back(c);
			return *this;
		}
        //插入字符串+=
		string& operator+=(const char* str) {
			append(str);
			return *this;
		}
         对于push_back、append函数来说,它们都属于尾插,尾插的效率对于数组来说是最高的,不需要挪动数据!
        其次+=运算符重载函数也是尾插函数,直接复用push_back和append即可。

        insert函数——在数组的任意位置添加数据: 

//在某个位置插入字符
		string& insert(size_t pos,char c) {
			assert(pos <= _size);
			//若该string类对象是空字符串时,需要手动扩容
			if (_size == _capacity) {
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			//方法1:
			size_t end = _size+1;
			while (end > pos) {
				_str[end] = _str[end - 1];
				--end;
			}
			_str[pos] = c;
			_size++;

			//方法2:
			/*int end = _size;
			while (end >= (int)pos) {
				_str[end + 1] = _str[end];
				--end;
			}
			_str[pos] = c;
			_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);
			}
            //方法1:
			size_t end =_size + len;
			while (end >=pos+len) {
				_str[end] = _str[end -len];
				--end;
			}
            //方法2同上——不展示了

			strncpy(_str+pos, str,len);    
			_size+=len;
			return *this;
		}

        对于insert函数来说,可以在任意位置插入数据,这时需要考虑三种情况:数组末尾插入数据、数组头部插入数据、数组中间插入数据。

        对于头插和中间插来说,就需要挪动数据,为插入的位置留下足够的空间,又因为数组挪动数据的时间复杂度为O(N),效率很低,所以insert函数很少被使用。 

        而以上增添数据的函数,在每次插入前都需要对容量进行检查,查看数组是否满了,是否需要扩容! 

         删:

//删除字符串
		string& erase(size_t pos, size_t len=npos) {									
			if (len == npos || pos + len == _size) {
				_str[pos] = '\0';
				_size=pos;
			}
			else {
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

        删除数据也是需要考虑三种情况:头删、尾删、中间删,由于形参npos缺省值的特殊性,需要做特别情况处理。

        查:

//查找字符
	size_t find(char c,size_t pos=0) {	
		assert(pos < _size);
		while (pos < _size) {
			if (_str[pos] == c) {
				return pos;
			}
			++pos;
		}
			//若找不到,则返回-1
		return npos;
	}

	//查找字符串
	size_t find(const char* str, size_t pos = 0) {
		assert(pos < _size);
		const char* ptr = strstr(_str + pos, str);
		if (ptr == nullptr) {
			return npos;
		}
		else {
			return ptr - _str;	
		}
	}

        查找函数就很好写了,查找字符可以利用循环遍历的方式一个一个字符的对比进行,成功了则返回该字符的下标。 

        而查找字符串,则是用到了strstrC库函数,该函数的作用是扫描指定字符串,成功了则返回指针,不成功则返回空。ptr是查找成功返回的字符串,利用指针-指针=数字的方式可以定位该字符串在整个类对象数组中的下标位置!

         改:

//寻找字符串的某个pos位置字符
		char& operator[](size_t pos) {
			assert(pos < _size);
			return _str[pos];
		}

        重载了[ ]运算符后,我们可以在主函数中使用循环的方式对该对象的数据进行遍历修改! 

五:拷贝构造与赋值重载:

        5.1传统写法:

string(const string& s) {
		_str = new char[s._capacity + 1];
		_size = s._size;
		_capacity = s._capacity;
		strcpy(_str, s._str);
		}

string& operator=(const string& s) {
	if (this != &s) {	//加if条件是因为,可能有自己给自己赋值的操作,需要考虑这一情况
		char* tmp = new char[s._capacity + 1];
		strcpy(_str, s._str);
		delete _str;
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
		}
		return *this;
	}

      1. 拷贝构造函数和赋值重载函数本质上都是将一个对象的数据赋值/拷贝给另一个对象!

   

          2.不写拷贝构造和赋值重载函数都是由于_arr所指向的是堆区空间,拷贝会发生浅拷贝,会导致两个类对象指向同一块堆区空间,析构时会析构两次导致系统崩溃,所以拷贝构造和赋值重载必须亲自写,为了避免浅拷贝,就必须让被拷贝被赋值的对象拥有一块自己的堆区空间,只拷贝_size和_capacity两个成员变量即可。

       

        3.拷贝构造和赋值重载函数的形参和返回值都尽量使用引用传递,这样可以减少实参和新参的拷贝次数,提升运行效率!

5.2现代写法:

//拷贝构造——现代写法(为了代码的简洁性)
    void swap(string& s) {
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
		    }
    //拷贝构造——  String s3(s1);
	string(const string& s)
	:_str(nullptr)
	,_size(0)
	,_capacity(0){
		string tmp(s._str);
		this->swap(tmp);	
		}
    //赋值重载—— s3=s1;
    string& operator=(const string& s) {
		if (this != &s) {	
			string tmp(s);
			this->swap(tmp);
            tmp._arr=nullptr;    //老板给打工人钱
			}
		return *this;
		}

现代写法要比传统写法更具有代码简洁性,可读性更好。

        现代写法的核心就是将左边对象和右边对象(形参)的所有数据进行交换swap(库函数),但在交换前都是通过创建一个临时对象,让该对象与临时对象进行数据交换,这样就不会发生浅拷贝,不会发生同一块空间析构两次的情况。

        赋值重载函数代码解析:是用临时对象tmp去拷贝构造形参对象s(形参对象s是类对象s3的别名,使用引用传递就不在该函数中建立空间,直接传的是s3的地址过来,但是this指针没办法去直接拷贝地址的数据,所以通过新创建临时的对象tmp,实体化空间去拷贝s3的数据继而让this指针去拷贝tmp,tmp好比是打工人,替老板做事,事成之后,老板就可以窃取tmp的成果,也给了钱(将tmp的_arr地址置空,释放的时候就不会释放野指针了),相安无事。

六.流插入/流提取重载函数:

class String{
   public:
    friend ostream& operator<<(ostream& out,  string& s);
    friend istream& operator>>(istream& in, string& s);
    };


	//流插入
	ostream& operator<<(ostream& out,  string& s) {
		for (size_t i = 0; i <s.size(); i++) {
			out << s[i];
		}
		return out;
	}


	//流提取
	istream& operator>>(istream& in, string& s) {
		s.clear();
		char buff[128] = { '\0' };
		char ch = in.get();		//get函数用来提取每一个字符
		size_t i = 0;
		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;
	}

        流提取和流插入函数只能放在类外,原因是放在类内的话这俩函数的第一个参数都会是隐藏this指针。若是放在类内,测试时使用:cout<<s1;编译器是无法识别该语句的,只能写成s1<<cout; 但没人会这样写,于是只好放在类外。

        虽说放在类外,但是由于类内私有了成员变量,类外不能访问,于是友元函数声明解决了这一大问题,类外的函数通过friend关键字在类内进行声明,便可以在类外访问类的的成员!

        没看懂的小伙伴可以看这篇文章,里面讲述了关于类的流插入流提取重载运算符放在类外的讲解

七:迭代器部分实现:

typedef char* iterator;
	public:
		//迭代器
		iterator begin() {
			return _str;	//begin会指向字符串的首个字符位置
		}

		iterator end() {
			return _str+_size;	//end会指向最后一个有效字符的下一个位置
		}

typedef const char* const_iterator;
		//迭代器
		const_iterator cbegin() const{
			return _arr;	//begin会指向字符串的首个字符位置
		}

		const_iterator cend() const{
			return _arr+_size;	//end会指向最后一个有效字符的下一个位置
		}

        迭代器iterator类型名称,是由char*重命名而成,迭代器中的begin、end都是指针,指向类对象数组的开头和结尾。


String类代码.h文件:

using namespace std;
#include<string.h>
#include<iostream>
#include<assert.h>

namespace Cheng {
	class string {
		typedef char* iterator;
	public:
		//迭代器
		iterator begin() {
			return _str;	//begin会指向字符串的首个字符位置
		}

		iterator end() {
			return _str+_size;	//end会指向最后一个有效字符的下一个位置
		}

	    typedef const char* const_iterator;
	    //const迭代器
	    const_iterator cbegin() const {
		    return _arr;	
	    }

	    const_iterator cend() const {
		    return _arr + _size;	
	    }

		//类对象的构造函数,str=""是缺省值,若使用者不给参数,则是默认使用缺省值——无参构造
		//若是给参数,则按给参数构造,缺省值失效
		string(const char* str="") {
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity+1];
			strcpy(_str, str);
		}

		//析构
		~string() {
			_size = _capacity = 0;
			delete[] _str;
			_str = nullptr;
		}

		//拷贝构造——传统写法
		/*
		string(const string& s) {
			_str = new char[s._capacity + 1];
			_size = s._size;
			_capacity = s._capacity;
			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);
			this->swap(tmp);
			
		}

		//赋值——传统写法
		/*
		string& operator=(const string& s) {
			if (this != &s) {	//加if条件是因为,可能有自己给自己赋值的操作,需要考虑这一情况
				char* tmp = new char[s._capacity + 1];
				strcpy(_str, s._str);
				delete _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}
		*/

		//赋值——现代写法
		//s1=s3;
		string& operator=(const string& s) {
		if (this != &s) {	
				string tmp(s);
				this->swap(tmp);
			}
			return *this;
		}

		//求字符串的长度
		const size_t size() const{
			return _size;
		}
		//求字符串的数组容量大小
		size_t capacity() {
			return _capacity;
		}

		//寻找字符串的某个pos位置字符
		//普通对象:可读可写
		char& operator[](size_t pos) {
			assert(pos < _size);
			return _str[pos];
        }
		//c_str内容
		char* c_str() {
			return _str;
		}
        
        //判断该对象数组中是否为空
        bool empty() const {
		    return _size == 0;
	    }

	    void shrink_to_fit() {
		    _capacity = _size;
		    _arr[_size] = '\0';
	    }
        
        //获取数组所能存放数据的最大容量
	    size_t Max_size() {
		    return npos;
    	}

		//扩容机制
		void reserve(size_t n) {
			if (n > _capacity) {
				//会重新开辟一块更大的新临时空间
				char* tmp = new char[n + 1];	//扩容的时候多开一个空间,为\0开
				strcpy(tmp, _str);
				//销毁原来的旧空间
				delete[] _str;
				_str = tmp;		//将临时空间再赋值给类成员_str
				_capacity = n;	//更新容量
			}
		}

	    void resize(size_t n, char ch = '\0') {
		    if (n <= _size) {
			    _arr[n] = '\0';
			    _size = n;
		    }

		    else {
			    if (n <= _capacity) {
				    for (size_t i = _size; i < n; ++i) {
				    	_arr[_size++] = ch;
				    }
		    	}
			    else{
			    	reserve(_capacity*2);
				    for (size_t i = _size; i < n; ++i) {
				    	_arr[_size++] = ch;
				    }
		    	}
		    	_arr[_size] = '\0';
		    }
	    }

		//插入字符
		void push_back(char c) {
			//在插入字符时,需要注意对象可能是空字符串,需要手动扩容
			if (_size == _capacity) {
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size++] = c;
			_str[_size] = '\0';
		}

		void append(const char* str){
			size_t len = strlen(str);
			
			if (_size+len > _capacity) {
				reserve(_size+len);	
			}
			//方法1:
			/*for (int i = 0; i < len; i++) {
				_str[_size++] = str[i];
			}
			_str[_size] = '\0';*/
			

			//方法2:
			strcpy(_str +_size, str);	//使用strcpy会把字符串的斜杆0也拷贝过来,那么最后就不需要再加斜杠0了
			//_str指针指向字符串的首元素,_str+_size就会让指针指向字符串的最后一个元素的下一个位置
			//那么会在\0位置开始拷贝想要尾插的新字符串
				_size += len;
		}

		string& operator+=(char c) {
			push_back(c);
			return *this;
		}

		string& operator+=(const char* str) {
			append(str);
			return *this;
		}

		//在某个位置插入字符
		string& insert(size_t pos,char c) {
			assert(pos <= _size);
			//若该string类对象是空字符串时,需要手动扩容
			if (_size == _capacity) {
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			//挪动数据
			//情况1:若在头部插入时,即pos=0,那么end只能是>pos的,否则会死循环
			//造成死循环原因,size_t不为负,若它为-1,会隐式提升成42亿多
			//方法1:
			size_t end = _size+1;
			while (end > pos) {
				_str[end] = _str[end - 1];
				--end;
			}
			_str[pos] = c;
			_size++;

			//方法2:不建议用
			/*int end = _size;
			while (end >= (int)pos) {
				_str[end + 1] = _str[end];
				--end;
			}
			_str[pos] = c;
			_size++;*/

			return *this;
		}

		//在某个位置插入字符串
		string& insert(size_t pos, const char* str) {
			assert(pos <= _size);

			//若该string类对象是空字符串时,需要手动扩容
			size_t len = strlen(str);

			if (_size + len > _capacity) {
				reserve(_size + len);
			}

			size_t end =_size + len;
			while (end >=pos+len) {
				_str[end] = _str[end -len];
				--end;
			}
			strncpy(_str+pos, str,len);
			_size+=len;
			return *this;
		}

		//删除字符串
		string& erase(size_t pos, size_t len=npos) {	//len=npos为缺省参数,若使用的时候mai'n中不给第二个参数
														//代表从pos位置会直接删到'\0'结束
			if (len == npos || pos + len == _size) {
				_str[pos] = '\0';
				_size=pos;
			}
			else {
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

		//查找字符
		size_t find(char c,size_t pos=0) {	//pos又给缺省参数,因为C++库中的string类find函数,pos可以不给参数,默认为0
			assert(pos < _size);
			while (pos < _size) {
				if (_str[pos] == c) {
					return pos;
				}
				++pos;
			}
			//若找不到,则返回-1
			return npos;
		}

		//查找字符串
		size_t find(const char* str, size_t pos = 0) {
			assert(pos < _size);
			const char* ptr = strstr(_str + pos, str);
			if (ptr == nullptr) {
				return npos;
			}
			else {
				return ptr - _str;	
			}
		}
		//清空函数
		void clear() {
			_size = 0;
			_str[0] = '\0';
		}

	private:
		size_t _size;
		size_t _capacity;
		char* _str;
		//
		const static size_t npos = -1;
	};

	//流插入
	ostream& operator<<(ostream& out,  string& s) {
		for (size_t i = 0; i <s.size(); i++) {
			out << s[i];
		}
		return out;
	}

	//流提取
	istream& operator>>(istream& in, string& s) {
		s.clear();
		char buff[128] = { '\0' };
		char ch = in.get();		//get函数用来提取每一个字符
		size_t i = 0;
		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;
	}

猜你喜欢

转载自blog.csdn.net/weixin_69283129/article/details/131883775