C++ STL详解(一) ———— string类的模拟实现

                           ———— string类的使用参考文章——————

目录

                           ———— string类的使用参考文章——————

一.默认成员函数

1.类的成员

2.构造函数

3.拷贝构造

4.赋值运算符重载 

5.析构函数

二. 迭代器相关函数

1.迭代器的定义

2.begin和end

三.容量和大小相关函数

1.size 和 capacity

2.resize 和 reserve

3.empty           

四. 字符串内容修改函数

1.push_back

2.append

3.operator+= (函数复用)

4.insert (尽量少用insert,因为底层实现是数组,头部或者中间插入需要挪动数据)

5.erase

6.clear 

7.swap

8.c_str 

五. 访问字符串相关函数

1.operator[] 

2.find(正向查找)

六. 关系运算符重载

七. 输入>> 和 输出<<运算符的重载以及getline函数 

①>>重载

②<<重载

③getline函数 


一.默认成员函数

1.类的成员

private:
		char* _str;  //指向一个char类型的空间

		size_t _size; //字符串长度(有效个数)
		size_t _capicity; //字符串容量

		static const size_t npos; //类外初始化
}

const size_t string::npos = -1;

                 

2.构造函数

string(const char* str = "")//含缺省参数
{
	_size = strlen(str); //算出原字符串的个数,如果没有默认是0
	_capicity = _size;     //_capicity 表示的是有效字符
	_str = new char[_capicity + 1]; //多的一个空间是给 \0 的 
	strcpy(_str, str);   //将原数组空间的字符串拷贝一份到新的空间
}

                         

3.拷贝构造

两个概念:

浅拷贝(值拷贝):拷贝出来的目标对象的指针和源对象的指针指向同一块内存空间。其中一个对象的改动会对另一个对象造成影响。
深拷贝:深拷贝是指源对象与拷贝对象互相独立,各自有各自的空间。其中任何一个对象的改动不会对另外一个对象造成影响。

 深拷贝的两个写法:

①传统写法 (什么都自己做 , 相当于自己在家做饭)  

//传统写法
string(const string& s)
	:_str(new char[strlen(s._str) + 1]) //_str申请一块刚好可以容纳s._str的空间
	, _size(0)
	, _capacity(0)
{
	strcpy(_str, s._str);    //将s._str拷贝一份到_str
	_size = s._size;         //_size赋值
	_capacity = s._capacity; //_capacity赋值
}

                

②现代写法(我们用别人的 , 相当于我们点外卖) 

//现代写法
string(const string& s) //拷贝构造
	:_str(nullptr)
	, _size(0)
	, _capicity(0)
{
	string temp(s._str); //调用构造函数实例化一份string对象
	swap(temp); //两个对象里面的内容进行交换
} 


void swap(string& s)
{
	// ::指定在全局找 swap
	::swap(_str, s._str);
	::swap(_size, s._size);
	::swap(_capicity, s._capicity);
}

                         

4.赋值运算符重载 

①传统写法

//传统写法
string& operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		delete[] _str; //将原来_str指向的空间释放
		_str = new char[strlen(s._str) + 1]; //重新申请一块刚好可以容纳s._str的空间
		strcpy(_str, s._str);    //将s._str拷贝一份到_str
		_size = s._size;         //_size赋值
		_capacity = s._capacity; //_capacity赋值
	}
	return *this; //支持连续赋值
}

                         

②现代写法

赋值运算符重载函数的现代写法是通过采用“值传递”的方法,让编译器自动调用拷贝构造函数,构造出一个临时对象,然后与拷贝出来的临时对象进行交换,等函数调用完成拷贝出来的对象进行析构回收,这时我们已经将资源进行了转移。

//法1 (常用)
string& operator=(string s) //我们传的是对象的一份拷贝
{
	::swap(_str, s._str);
	return *this;
}

但这种写法无法避免自己给自己赋值,就算是自己给自己赋值这些操作也会进行,虽然操作之后对象中_str指向的字符串的内容不变,但是字符串存储的地址发生了改变,为了避免这种操作我们可以采用下面这种写法: (实际上很少遇见自己给自己赋值的情况)

//法2
string& operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		string tmp(s); //用s拷贝构造出对象tmp
		swap(tmp); //交换这两个对象
	}
	return *this; //支持连续赋值
}

                 

5.析构函数

完成资源的回收和清理工作,string类的析构函数需要我们进行编写,因为每个string对象中的成员_str都指向堆区的一块空间这是我们new出来的,当对象销毁时堆区对应的空间并不会自动销毁,为了避免内存泄漏,我们需要使用delete手动释放堆区的空间。

//析构函数
~string()
{
	delete[] _str;  //释放_str指向的空间
	_str = nullptr; //及时置空,防止非法访问
	_size = 0;      //大小置0
	_capacity = 0;  //容量置0
}

二. 迭代器相关函数

1.迭代器的定义

string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已,因为string操作的是字符,所以这里的迭代器类型是普通的指针,并不是所有的迭代器都是内置类型的指针。

typedef char* iterator;
 typedef const char* const_iterator;

                

2.begin和end

iterator begin()
{	
	return _str; //返回字符串的首地址
}

iterator end()
{
	return _str + _size; //返回字符串中最后一个字符的下一个地址(‘\0’的地址)
}

 ①+ const的好处 const对象可以调用,非const的对象也可以调用该函数  

 ②成员函数const 修饰的是 *this,本质是保护成员变量在函数体内不会被修改,相当于是this指向的对象成员被保护,不能修改。    

const_iterator begin() const  
{                            
	return _str;
}

const_iterator end() const	
{
	return _str + _size;
}

                           

补充:迭代器的使用

string s("XiaoMI_MIUI");
string::iterator it = s.begin();
while (it != s.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;

用范围for来遍历string,范围for + auto 看起来很神奇其实底层就是迭代器支持的,代码编译的时候底层会进行替换,对迭代器有严格要求 ,begin() ,ene() ,iterator 必须是这样的规范。         

string s("XiaoMi_MIUI");
//编译器将其替换为迭代器形式
for (auto e : s)
{
	cout << e << " ";
}
cout << endl;

                

三.容量和大小相关函数

1.size 和 capacity

int size , int capacity 被定义成私有,外面无法直接访问,我们通过函数返回这两个值。 

//大小
size_t size()const
{
	return _size; //返回字符串当前的有效长度,不包含('\0')
}
//容量
size_t capacity()const
{
	return _capacity; //返回字符串当前的容量
}

                        

2.resize 和 reserve

reserve规则:1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
                 2、当n小于对象当前的capacity时,什么也不做。

void reserve(size_t i)
{
	if (i > _capicity)
	{
		char* temp = new char[i + 1]; //开辟新空间
		strncpy(temp, _str, _size + 1); //不能用strcpy 有可能\0也是我们的有效字符 (多拷贝一个'\0')
		delete[] _str; //释放旧空间
		_str = temp;
		_capicity = i;
	}
			
}

  attention:代码中使用strncpy进行拷贝对象的字符串而不是strcpy,是为了防止对象的字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝字符遇到第一个’\0’就结束了)。


              

                 

resize规则:1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认
               2、当n小于当前的size时,将size缩小到n。

//改变大小
void resize(size_t n, char ch = '\0') //默认字符‘\0’
{
	if (n <= _size) //n小于当前size
	{
		_size = n; //将size调整为n
		_str[_size] = '\0'; //在第n个字符后面放上'\0'
	}
	else //n大于当前的size
	{
		if (n > _capacity) //判断是否需要扩容
		{
			reserve(n); //扩容
		}

		for (size_t i = _size; i < n; i++) //将size扩大到n,扩充的字符为ch
		{
			_str[i] = ch;
		}

		_size = n; //size更新
		_str[_size] = '\0'; //字符串后面放上'\0'
	}
}

                        

3.empty

//判空
bool empty()
{
	return strcmp(_str, "") == 0;
}

                 

四. 字符串内容修改函数

1.push_back

push_back函数的作用就是在当前字符串的后面尾插上一个字符,尾插之前首先需要判断是否需要增容,若需要,则调用reserve函数进行增容,然后再尾插字符,注意尾插完字符后需要在该字符的下一个位置上设置’\0’,否则打印字符串的时候会出现非法访问,因为尾插的字符后方不一定就是’\0’。

//1.尾插字符
void push_back(char ch)
{
	if (_size == _capacity) //判断是否需要增容
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍
	}

	_str[_size] = ch; //将字符尾插到字符串
	_str[_size + 1] = '\0'; //字符串后面放上'\0'
	_size++; //字符串的大小加一
}


//2.尾插字符
void push_back(char ch)
{
	insert(_size, ch); //在字符串末尾插入字符ch
}

注意:这里的增容是以二倍的形式进行增容,避免多次调用push_back函数时每次都需要调用reserve函数。你也可以有不同的方式进行增容,增容的倍数过小会增大增容次数,浪费性能;增容的倍数过大可能造成空间的浪费。(进行权衡)

                        

2.append

append函数的作用是在当前字符串的后面尾插一个字符串,尾插前需要判断当前字符串的空间能否容纳下尾插后的字符串,若不能,则需要先进行增容,然后再将待尾插的字符串尾插到对象的后方,因为待尾插的字符串后方自身带有’\0’,所以我们无需再次设置’\0’。

//1.尾插字符串
void append(const char* str)
{
	size_t len = _size + strlen(str); //尾插str后字符串的大小(不包括'\0')
	if (len > _capacity) //判断是否需要增容
	{
		reserve(len); //增容
	}

	strcpy(_str + _size, str); //将str尾插到字符串后面
	_size = len; //字符串大小改变
}


//2.尾插字符串
void append(const char* str)
{
	insert(_size, str); //在字符串末尾插入字符串str
}

                

3.operator+= (函数复用)

+=运算符的重载是为了实现字符串与字符、字符串与字符串之间能够直接使用+=运算符进行尾插。
 

//+=运算符重载  +=字符
string& operator+=(char ch)
{
	push_back(ch); //尾插字符
	return *this; //支持连续+=
}
//+=运算符重载 +=字符串
string& operator+=(const char* str)
{
	append(str); //尾插字符串
	return *this; //支持连续+=
}

                

4.insert (尽量少用insert,因为底层实现是数组,头部或者中间插入需要挪动数据)

①插入字符

insert函数用于插入字符时,首先需要判断pos的合法性,若不合法则无法进行操作,紧接着还需判断当前对象能否容纳插入字符后的字符串,若不能则还需调用reserve函数进行扩容。插入字符的过程也是比较简单的,先将pos位置及其后面的字符统一向后挪动一位,给待插入的字符留出位置,然后将字符插入字符串即可。

//在pos位置之前插入字符
string& insert(size_t pos, char ch)
{
	assert(pos <= _size); //检测下标的合法性

	if (_size == _capacity) //判断是否需要增容
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2); 
	}

	char* end = _str + _size;

	//将pos位置及其之后的字符向后挪动一位
	while (end >= _str + pos)
	{
		*(end + 1) = *(end);
		end--;
	}

	_str[pos] = ch; //pos位置放上指定字符
	_size++; //size更新

	return *this;
}

②插入字符串

insert函数用于插入字符串时,首先也是判断pos的合法性,若不合法则无法进行操作,再判断当前对象能否容纳插入该字符串后的字符串,若不能则还需调用reserve函数进行扩容。插入字符串时,先将pos位置及其后面的字符统一向后挪动 len位(len为待插入字符串的长度),给待插入的字符串留出位置,然后将其插入字符串即可。

//在pos位置之前插入字符串
string& insert(size_t pos, const char* str)
{
	assert(pos <= _size); //检测下标的合法性

	size_t len = strlen(str); //计算需要插入的字符串的长度(不含'\0')
	if (len + _size > _capacity) //判断是否需要增容
	{
		reserve(len + _size); //增容
	}

	char* end = _str + _size;

	//将pos位置及其之后的字符向后挪动len位
	while (end >= _str + pos)
	{
		*(end + len) = *(end);
		end--;
	}

	strncpy(_str + pos, str, len); //pos位置开始放上指定字符串

	_size += len; //size更新
	return *this;
}

                         

5.erase

①pos位置及其之后的有效字符都需要被删除,这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。

②pos位置及其之后的有效字符只需删除一部分。这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾就有’\0’了,更新size

//删除pos位置开始的len个字符 
string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size); //检测下标的合法性
	size_t n = _size - pos; //pos位置及其后面的有效字符总数

	if (len >= n) //说明pos位置及其后面的字符都被删除
	{
		_size = pos; //size更新
		_str[_size] = '\0'; //字符串后面放上'\0'
	}
	else //说明pos位置及其后方的有效字符需要保留一部分
	{
		strcpy(_str + pos, _str + pos + len); //用需要保留的有效字符覆盖需要删除的有效字符
		_size -= len; //size更新
	}

	return *this;
}

6.clear 

clear函数用于将对象中存储的字符串置空,实现时直接将对象的_size置空,然后在字符串后面放上’\0’

//清空字符串
void clear()
{
	_size = 0; //size置空
	_str[_size] = '\0'; //字符串第一个位置放'\0'

7.swap

swap函数用于交换两个对象的数据,直接调用标准库里的swap模板函数将对象的各个成员变量进行交换即可。但我们若是想在这里调用库里的swap模板函数,需要在swap函数之前加上“::”(作用域限定符),告诉编译器优先在全局范围寻找swap函数,否则编译器编译时会认为你调用的是正在实现的swap函数(就近原则)。

//交换两个对象的数据
void swap(string& s)
{
	//调用库里的swap
	::swap(_str, s._str); //交换两个对象的C字符串
	::swap(_size, s._size); //交换两个对象的大小
	::swap(_capacity, s._capacity); //交换两个对象的容量
}

8.c_str 

返回C语言形式的字符指针

//返回C类型的字符串
const char* c_str()const //加const防止对指针指向的内容进行改变
{
	return _str;
}

                        

五. 访问字符串相关函数

1.operator[] 

[ ]运算符的重载是为了让string对象能像C字符串一样,通过[ ] +下标的方式获取字符串对应位置的字符。在C字符串中我们通过[ ] +下标的方式可以获取字符串对应位置的字符,①并可以对其进行修改 ②只能读,实现[ ] 运算符的重载时只需返回对象C字符串对应位置字符的引用即可,这样便能实现对该位置的字符进行读取和修改操作了,但需要注意在此之前检测所给下标的合法性

//1.[]运算符重载(可读可写)
char& operator[](size_t i)
{
	assert(i < _size); //检测下标的合法性
	return _str[i]; //返回对应字符
}


//2.[]运算符重载(只读)
const char& operator[](size_t i)const
{
	assert(i < _size); //检测下标的合法性
	return _str[i]; //返回对应字符
}

2.find(正向查找)

①查找字符,可以根据传递的pos,从pos处向后查找,找到的是第一个相同的字符下标。

//正向查找第一个匹配的字符
size_t find(char ch, size_t pos = 0)
{
	assert(pos < _size); //检测下标的合法性
	for (size_t i = pos; i < _size; i++) //从pos位置开始向后寻找目标字符
	{
		if (_str[i] == ch)
		{
			return i; //找到目标字符,返回其下标
		}
	}

	return npos; //没有找到目标字符,返回npos
}

②查找字符串 ,调用strstr函数在字符串中查找子字符串

//正向查找第一个匹配的字符串
size_t find(const char* str, size_t pos = 0)
{
	assert(pos < _size); //检测下标的合法性
	const char* ret = strstr(_str + pos, str); //调用strstr进行查找

	if (ret) //ret不为空指针,说明找到了
	{
		return ret - _str; //返回字符串第一个字符的下标
	}
	else //没有找到
	{
		return npos; //返回npos
	}
}

③使用


六. 关系运算符重载

//>运算符重载
bool operator>(const string& s)const
{
	return strcmp(_str, s._str) > 0;
}
//==运算符重载
bool operator==(const string& s)const
{
	return strcmp(_str, s._str) == 0;
}

 四个关系运算符的重载,就可以通过复用这两个已经重载好了的关系运算符来实现了。

//>=运算符重载
bool operator>=(const string& s)const
{
	return (*this > s) || (*this == s);
}
//<运算符重载
bool operator<(const string& s)const
{
	return !(*this >= s);
}
//<=运算符重载
bool operator<=(const string& s)const
{
	return !(*this > s);
}
//!=运算符重载
bool operator!=(const string& s)const
{
	return !(*this == s);
}

                         

七. 输入>> 和 输出<<运算符的重载以及getline函数 

①>>重载

重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到 ’  ‘ 或是 ’\n’ 便停止读取。 

//>>运算符的重载
istream& operator>>(istream& in, string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != ' '&&ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}

	return in; //支持连续输入
}

 ②<<重载

重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印。实现时我们可以直接使用范围for对对象进行遍历即可

//<<运算符的重载
ostream& operator<<(ostream& out, const string& s)
{
	//使用范围for遍历字符串并输出
	for (auto e : s)
	{
		cout << e;
	}
	return out; //支持连续输出
}

③getline函数 

getline函数可以用于读取一行含有空格的字符串。实现时于>>运算符的重载基本相同,只是当读取到’\n’的时候才停止读取字符。

//读取一行含有空格的字符串
istream& getline(istream& in, string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}

	return in;
}

面试题: 写一个简洁版的string


 1. 考察实现string 的四个默认成员函数 , 深浅拷贝问题
 2. 可以写传统写法,也可以写现代写法

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


		// 传统写法

		/*
		
		 string(const string& s)
			:_str(new char[strlen(s._str) + 1])
		{
			strcpy(_str, s._str);
		}

		string& operator = (const string& s)
		{
			if (&s != this)
			{
				delete[] _str;
				_str = new char[strlen(s._str) + 1];
				strcpy(_str, s._str);
			}
		}

		*/

		
		//现代写法
		string(const string& s) //拷贝构造
			:_str(nullptr) //这里如果不初始化为空则是一个随机值,最后temp调用析构函数释放空间,会报错
		{
			string temp(s._str);
			swap(_str, temp._str);
		}
		

		string& operator=(string s) //重载=
		{
			swap(_str, s._str); 
			return *this;
		}
		

		const char* c_str()
		{
			return _str;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

	private:
		char* _str;

    };

}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值