C++STL:string类的模拟实现

Posted 山舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++STL:string类的模拟实现相关的知识,希望对你有一定的参考价值。

前言

【C++】STL(一)string类的使用一文中已经对string类进行了简单的介绍,一般来说只要会正常使用即可,下面来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。


一、四个默认成员函数

1.构造函数

构造函数的实现思路就是给_str新开一片空间,并把参数中的字符串拷贝到新开的空间内。

代码如下:

class String
{
public:
	String(const char* str = "")
		:_str(new char[strlen(str) + 1])//开空间,空间的大小+1是为了给\\0留位置
	{
		strcpy(_str, str);
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

注意参数的默认值不能给nullptr,因为若不传参使用默认值时,strlen(str)会对空指针进行解引用操作从而报错,这里最好用"",也即一个仅有\\0的空字符串。


另一种写法如下

代码如下:

class String
{
public:
	String(const String& s)
		:_str(nullptr)
	{
		//用s的_str产生一个临时的String对象
		String tmp(s._str);
		//交换tmp的_str和*this的_str即可达到目的
		swap(_str, tmp._str);
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

注意这是构造函数,所以新产生的对象在初始化前_str内存储着随机值,交换后tmp(临时对象,生命周期仅在这个函数体内)出作用域时调用析构函数会释放一段随机空间,这是非法访问,所以在初始化列表内给_str以nullptr,这样在析构函数调用时对nullptr进行delete[]没有问题。


2.析构函数

析构函数比较简单,只需释放空间并将指针置空即可。

代码如下:

class String
{
public:
	~String()
	{
		delete[] _str;
		_str = nullptr;
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

3.拷贝构造

这里会设计到深浅拷贝的问题。

1.浅拷贝

先看一下若不写拷贝构造函数,使用编译器默认生成的是否能满足要求。

代码如下:

int main()
{
	String s1("hello world");
	String s2(s1);

	return 0;
}

运行后程序会崩溃,那么原因是什么呢?

运行如下:


调试会发现,s1、s2中各自的_str指向同一片空间,也就是说在析构函数时会对同一块空间delete[]两次,这显然是不合法的,根本原因在于编译器给出的默认构造函数只进行值拷贝,即把s1中_str的值拷贝给s2中的_str,这样一来两个String对象中的_str指向同一块空间,不仅修改时会互相影响,在调用析构函数时程序更是会崩溃。

调试结果如下:

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一个对象的资源已经被释放,当继续对资源进项操作时,就会发生发生了访问违规。


2.深拷贝

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

代码如下:

class String
{
public:
	//注意不要与构造函数混淆
	String(const String& s)
		:_str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

这样一来,两个String对象的_str指向的空间便成了独立的两块,互不影响。

调试结果如下:


另一种写法如下:

代码如下:

class String
{
public:
	//                注意这里不传引用
	String& operator=(String s)
	{
		swap(_str, s._str);
		return *this;
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

形参列表里的s是一个临时产生的String对象,在该函数结束后会调用析构函数,将s与*this的_str交换后,this内的_str便和希望得到的内容一样,而原来this的_str内的内容随着对象s析构函数的调用消失。


4.赋值运算符重载

若不写而使用编译器默认生成的赋值运算符重载,则同样会发生上面浅拷贝的问题,所以这里仍要考虑深拷贝。

代码如下:

class String
{
public:
	String& operator=(const String& s)
	{
		if (this != &s)//防止自己给自己赋值
		{
			delete[] _str;//释放原来的空间
			_str = new char[strlen(s._str) + 1];//开辟新空间
			strcpy(_str, s._str);//拷贝字符串
		}
		return *this;
	}
private:
	char* _str;
};

二、string类的其它函数

上面的四个默认成员函数主要是操作_str,基本不涉及_size和_capacity,下面的函数则需要用到,所以在private的成员变量中需要添加。

则上面的四个函数也需要稍微改动。

代码如下:

class String
{
public:
	String(const char* str = "")
	{
		_size = strlen(str);
		_capacity = _size;
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	
	String(const String& s)
		:_str(nullptr)
		, _size(0)
		, _capacity(0)
	{
		String tmp(s._str);
		//交换三个成员变量
		swap(_str, tmp._str);
		swap(_size, tmp._size);
		swap(_capacity, tmp._capacity);
	}

	String& operator=(String s)
	{
		//交换三个成员变量
		swap(_str, s._str);
		swap(_size, s._size);
		swap(_capacity, s._capacity);
		return *this;
	}
	
	~String()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}
private:
	char* _str;
	size_t _size;//当前字符的个数
	size_t _capacity;//最多能存储的字符个数
};

1.一些简单的函数

(1)size、capacity、c_str和clear

这几个函数非常简单,这里不作过多解释。

代码如下:

class String
{
public:
	size_t size()
	{
		return _size;
	}
	
	size_t capacity()
	{
		return _capacity;
	}
	
	const char* c_str() const
	{
		return _str;
	}
	
	//删除_str内的所有内容
	void clear()
	{
		_size = 0;
		_str[_size] = '\\0';
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

(2)比较函数

下面这些函数时比较两个string类的(实质是用strcmp比较两个对象的_str),代码较短且常用,所以可以设计为内联函数。

代码如下:

inline bool operator<(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) > 0;
}

inline bool operator==(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;
}

inline bool operator<=(const string& s1, const string& s2)
{
	return (s1 < s2) || (s1 == s2);
}

inline bool operator>(const string& s1, const string& s2)
{
	return !(s1 <= s2);
}

inline bool operator>=(const string& s1, const string& s2)
{
	return !(s1 < s2);
}

inline bool operator!=(const string& s1, const string& s2)
{
	return !(s1 == s2);
}

2.三种遍历方式

(1)下标+[]

要用到[]则需要进行重载,这样便可通过下标访问字符串。

代码如下:

class String
{
public:
	//返回引用保证可读可写
	char& operator[](size_t i)
	{
		assert(i < _size);
		return _str[i];
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

(2)迭代器

string类的迭代器就是一个char*指针(但其它容器不一定是),所以实现起来比较简单。

代码如下:

class String
{
public:
	//typedef为char*改名
	typedef char* Iterator;
	//返回字符串第一个位置
	Iterator begin()
	{
		return _str;
	}
	
	//返回字符串最后一个字符的下一个位置
	Iterator end()
	{
		return _str + _size;
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

(3)范围for

对于范围for的遍历方式,只要支持迭代器就可以使用,因为范围for的遍历方式最终会被编译器转换为迭代器。同时由于这个原因,模拟实现的迭代器命名必须与标准一致,begin、end名称不可修改,随意更换begin、end的大小写或单词拼写都将时范围for无法运行


3.reserve和resize

(1)reserve

reserve会改变字符串能存储的最大有效字符个数。

注意拷贝字符串时要用strncpy把_size个字符全部拷贝,不能用strcpy,因为strcpy遇到’\\0’拷贝即结束,但string类字符串的结束是_size限定的。

代码如下:

class String
{
public:
	//开空间,扩容_capacity
	void reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];//注意给'\\0'留位置
			strncpy(tmp, _str, _size + 1);//不能用strcpy
			delete[] _str;
			_str = tmp; 
			_capacity = n;
		}
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

(2)resize

resize会改变字符串存储的有效字符个数,同时可能会改变_capacity。若有多余的空间会将其初始化为指定的内容。

代码如下:

class String
{
public:
	//开空间+初始化。改变_size,可能改变_capacity且初始化
	void resize(size_t n, char val = '\\0')//默认值给'\\0'
	{
		//n < _size时直接修改_size并截止字符串
		if (n < _size)
		{
			_size = n;
			_str[_size] = '\\0';
		}
		else
		{
			if (n > _capacity)
				reserve(n);
			//将多余的位置用val填充
			for (size_t i = _size; i < n; i++)
				_str[i] = val;
			_size = n;//修改_size
			_str[_size] = '\\0';//结束字符串
		}
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

4.插入数据

(1)push_back插入字符

代码如下:

class String
{
public:
	void push_back(const char ch)
	{
		//空间不够则扩容
		if (_size == _capacity)
		{
			//若原来空间为0则设为4,否则将容量翻倍
			size_t newcapacity = (_capacity == 0) ? 4 : (2 * _capacity);
			reserve(newcapacity);
		}
		
		//直接添加到_str后即可
		_str[_size] = ch;
		_size++;
		_str[_size] = '\\0';
	}
	
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

(2)append添加字符串

代码如下:

class String
{
public:
	void append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len >= _capacity)//空间不够则扩容
			reserve(_size + len);
		//将str拷贝到_str之后
		strcpy(_str + _size, str);
		_size += len;
	}
	
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

(3)operator+=

这里注意代码复用。

代码如下:

class String
{
public:
	String& operator+=(const char ch)
	{
		push_back(ch);
		return *this;
	}

	String& operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

(4)insert在任一位置插入字符或字符串

代码如下:

class String
{
public:
	String& insert(size_t pos, const char ch)
	{
		assert(pos <= _size);

		if (_size == _capacity)//空间不够则扩容
		{
			size_t newcapacity = (_capacity == 0) ? 4 : (2 * _capacity);
			reserve(newcapacity);
		}
		size_t i = 0;
		//腾出一个位置来填充字符
		for (i = _size + 1; i > pos; i--)
			_str[i] = _str[i - 1];
		_str[pos] = ch;
		_size++;

		return *this;
	}
	
	//在pos之前插入字符串
	String& insert(size_t pos, const char* str)
	{
		assert(pos <= _size);

		size_t len = strlen(str);
		if (len + _size > _capacity)//空间不够则扩容
			reserve(len + _size);
		size_t i = 0, j = 0;
		//先挪动数据,腾出len个空间
		for (i = len + _size; i >= pos + len; i--)
			_str[i] = _str[i - len];
		//将str的内容补充到腾出的len个空间
		for (i = pos, j = 0; j < len; j++)
			_str[i++] = str[j];
		_size += len;

		return *this;
	}
	
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

5.erase

代码如下:

class String
{
public:
String& erase(size_t pos, size_t len = npos)
	{
		assert(pos < _size);
		size_t leftLen = _size - pos;//从pos位置到字符串结尾剩余的长度
		//若要求删除的长度比leftLen长,则全部删除 
		if (len >= leftLen)
		{
			_str[pos] = '\\0';
			_size = pos;
		}
		else
		{
			//从pos开始删除len个字符
			for (size_t i = pos; i < pos + len; i++)
				_str[i] = _str[i + len];
			_size -= len;
		}
		return *this;
	}
	
private:
	char* _str;
	size_t _size;
	size_t _capacity;
	
	static const size_t npos;//添加一个静态成员变量
};

//设置为无符号整型的最大值
const size_t String::npos = -1;

6.find

代码如下:

class String
{
public:
	//从pos开始查找字符ch,找不到返回npos,找到返回下标
	size_t findC++STL详解—— string类的模拟实现

[C/C++]详解STL容器1--string的功能和模拟实现(深浅拷贝问题)

C++初阶:STL —— stringstring类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现

C++初阶:STL —— stringstring类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现

C++从青铜到王者第八篇:STL之string类的模拟实现

STL--线性容器 string