C++——类的模拟实现

Posted 小倪同学 -_-

tags:

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

string类接口总览

namespace nzb
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		//默认成员函数
		string(const char* str = "");         //构造函数
		string(const string& s);              //拷贝构造函数
		string& operator=(const string& s);   //赋值运算符重载函数
		~string();                            //析构函数

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

		//容量和大小相关函数
		size_t size();
		size_t capacity();
		void reserve(size_t n);
		void resize(size_t n, char ch = '\\0');
		bool empty()const;

		//修改字符串相关函数
		void push_back(char ch);  // 尾插字符
		void append(const char* str);  // 尾插字符串
		string& operator+=(char ch);
		string& operator+=(const char* str);
		string& insert(size_t pos, char ch);  // 在指定位置插入
		string& insert(size_t pos, const char* str);
		string& erase(size_t pos, size_t len);  // 删除指定位置
		void clear();
		void swap(string& s);
		const char* c_str()const;  

		//访问字符串相关函数
		char& operator[](size_t i);
		const char& operator[](size_t i)const;
		size_t find(char ch, size_t pos = 0)const;  // 查找
		size_t find(const char* str, size_t pos = 0)const;

	private:
		char* _str;       //存储字符串
		size_t _size;     //记录字符串当前的有效长度
		size_t _capacity; //记录字符串当前的容量
		static const size_t npos; //静态成员变量(整型最大值)
	};
	const size_t string::npos = -1;
	
	//关系运算符重载函数
	bool operator>(const string& s)const;
	bool operator>=(const string& s)const;
	bool operator<(const string& s)const;
	bool operator<=(const string& s)const;
	bool operator==(const string& s)const;
	bool operator!=(const string& s)const;

	//<<和>>运算符重载函数
	istream& operator>>(istream& in, string& s);
	ostream& operator<<(ostream& out, const string& s);
	istream& getline(istream& in, string& s);
}

注意:为了防止和标准库中的string类产生命名冲突,建议在自己的命名空间中模拟实现

默认成员函数

构造函数

		string(char* str = "")
			:_size(strlen(str))//记录字符串长度
			, _capacity(_size)//设定容量
		{
			_str = new char[_capacity + 1];
			//为存储字符串开辟空间(多开一个用于存放'\\0')
			strcpy(_str, str);//将字符串拷贝到已开辟好的空间
		}

拷贝构造函数

模拟实现拷贝构造函数前,我们应该首先了解深浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源。当其调用析构函数时会报错,原因如下

当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。

深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。

传统写法

传统写法思路较为简单,就是新建一个string类,再将原string类拷贝过去即可

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

现代写法

现代写法较为巧妙,调用构造函数新建一个string类,再交换原string类和新string类,出作用域时,新建的string类被销毁,原string类完成拷贝构造

string(const string& s)
	:_str(nullptr)
{
	string tmp(s._str);
	swap(tmp);
}

赋值运算符重载函数

赋值运算符重载函数和拷贝构造函数类似,分为传统和现代写法,思路大致相同

传统写法

赋值运算符重载函数的传统写法和拷贝构造函数传统写法相比多了销毁原先的空间和检查是否自己给自己赋值

string& operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		delete[] _str; //将原来_str指向的空间释放
		_str = new char[strlen(s._str) + 1];
		strcpy(_str, s._str);    //将s._str拷贝一份到_str
		_size = s._size;        
		_capacity = s._capacity; 
	}
	return *this; //返回左值
}

现代写法

通过采用“值传递”接收右值的方法,让编译器自动调用拷贝构造函数,然后我们再将拷贝出来的对象与左值进行交换

string& operator=(string s)
{
	swap(s);
	return *this;
}

可以和传统写法一样添加判断是否自己给自己赋值,但是现实中很少出现这种情况,不添加判断也可以。

析构函数

因为在堆上开辟了空间,所以析构函数需要自己写,避免开辟的空间无法释放,造成内存泄漏

~string()
{
	delete[] _str;  // 释放开辟的空间
	_str = nullptr;  // 指针赋空 
}

迭代器相关函数

string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已。

typedef char* iterator;
typedef const char* const_iterator;

begin和end

返回第一个字符或最后一个字符

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

容量相关函数

size和capacity

size记录字符的数量,capacity记录容量大小

size_t size() const
{
	return _size;
}

size_t capacity() const
{
	return _capacity;
}

reserve和resize

实现reserve和resize时一定要注意两者的区别
reserve:

  1. 当n大于对象当前的capacity时,将capacity扩大到n或大于n。
  2. 当n小于对象当前的capacity时,什么也不做。
void reserve(size_t n)
{
	if (n > _capacity)// 当n大于对象当前的capacity时才执行操作
	{
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;

		_capacity = n;
	}
}

resize:

  1. 当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\\0’。
  2. 当n小于当前的size时,将size缩小到n。
void resize(size_t n, char ch = '\\0')
{
	if (n < _size)
	{
		_size = n;
		_str[_size] = '\\0';
	}
	else
	{
		if (n>_capacity)//判断是否需要扩容
		{
			reserve(n);
		}
		for (int i = _size; i < n; i++)//ch赋值给字符串
		{
			_str[i] = ch;
		}
		_size = n;
		_str[_size] = '\\0';
	}
}

empty

判断_size是否为0

bool empty()const
{
	return _size == 0;
}

修改字符串相关函数

push_back

push_back函数的作用就是在当前字符串的后面尾插上一个字符,先判断容量是否充足,不充足就先增容,再尾插字符串并在末位加上‘\\0’

void push_back(char ch)
{
	if (_size >= _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		//二倍增容
		reserve(newcapacity);
	}

	_str[_size] = ch;// 尾插字符
	_size++;// 字符量加1
	_str[_size] = '\\0';// 结尾添加‘\\0’
}

append

append的作用是尾插字符串,思路和push_back一样

void append(const char* str)
{
	size_t len = strlen(str);// 计算尾插字符串长度
	if (_size + len >= _capacity)// 增容
	{
		reserve(_size + len);
	}
	strcpy(_str + _size, str);
	_size += len;
	// 因为字符串中有'\\0'所以不需要额外添加
}

operator+=

+=运算符实现字符串与字符之间的尾插直接调用push_back函数

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

+=运算符实现字符串与字符串之间的尾插直接调用append函数

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

insert

insert作用是在指定位置插入字符或字符串

insert函数用于插入字符时,首先需要判断pos的合法性,再判断当前对象能否容纳插入字符后的字符串,若不能则还需调用reserve函数进行扩容。插入字符时,先将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++; 
	return *this;
}

insert函数用于插入字符串和插入字符思路大致相同

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--;
	}
	// 插入字符串
	for (size_t i = 0; i < len; i++)
	{
		_str[i + pos] = str[i];
	} 
	_size += len; 
	return *this;
}

erase

erase函数的作用是删除字符串任意位置开始的n个字符。

  1. pos位置及其之后的有效字符都需要被删除。
    只需在pos位置添加‘\\0’
  2. pos位置及其之后的有效字符删除一部分
    将后面字符往前移即可
string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);//检测下标的合法性
	if (len == npos || len + pos>_size)// 字符串不够删
	{
		_str[pos] = '\\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		// 用有效字符串覆盖要删除的字符串
		_size -= len;
	}
	return *this;
}

clear

clear作用是清空类中字符串

void clear()
{
	_str[0] = '\\0';
	_size = 0;
}

swap

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

void swap(string& s)
{
	//调用库里的swap
	::swap(_str, s._str);
	::swap(_size, s._size); 
	::swap(_capacity, s._capacity); 
}

注意:若想让编译器优先在全局范围寻找某函数,则需要在该函数前面加上“::”(作用域限定符)。

c_str

将类中字符串以C语言形式输出

const char* c_str()
{
	return _str;
}

访问字符串相关函数

operator[ ]

[ ]运算符的重载是为了让string对象能像C字符串一样,通过[ ] +下标的方式获取字符串对应位置的字符。

// 可读可写
char& operator[](size_t i)
{
	assert(i < _size); //检测下标的合法性
	return _str[i]; //返回对应字符
}
// 只能读
const char& operator[](size_t i)const
{
	assert(i < _size); 
	return _str[i]; 
}

find

  1. 正向查找第一个匹配的字符
    首先判断所给pos的合法性,然后遍历找目标字符,若找到,则返回其下标;若没有找到,则返回npos。(npos是string类的一个静态成员变量,其值为整型最大值)
size_t find(char ch, size_t pos = 0) const
{
	assert(pos < _size);//检测下标的合法性
	while (pos < _size)// 遍历寻找字符
	{
		if (_str[pos] == ch)
		{
			return pos;
		}
		pos++;
	}
	return npos;// 没找到返回npos
}
  1. 正向查找第一个匹配的字符串。
    首先也是先判断所给pos的合法性,然后我们可以通过调用strstr函数进行查找。(strstr函数若是找到了目标字符串会返回字符串的起始位置,若是没有找到会返回一个空指针)若是找到了目标字符串,我们可以通过计算目标字符串的起始位置和对象C字符串的起始位置的差值,进而得到目标字符串起始位置的下标。
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
	}
}

关系运算符重载函数

关系运算符有 >、>=、<、<=、==、!= 这六个,我们可以实现其中的>和==,剩下的六个调用这两个即可

// operator> 重载
bool operator>(const string& s1, const string& s2)
{
	size_t i1 = 0, i2 = 0;
	// 两个字符串从前往后依次比较
	while (i1 < s1.size() && i2 < s2.size())
	{
		if (s1[i1]>s2[i2])
		{
			return true;
		}
		else if (s1[i1] < s2[i2])
		{
			return false;
		}
		else
		{
			i1++;
			i2++;
		}
	}
	
	// 如果il先到结尾为真,否则为假
	if (i1 == s1.size())
	{	
		return false;
	}
	else
	{
		return true;
	}
}
// operator== 重载
bool operator==(const string& s1, const string& s2)
{
	

以上是关于C++——类的模拟实现的主要内容,如果未能解决你的问题,请参考以下文章

C++ string类的模拟实现

C++初阶---string类的模拟实现

C++初阶第九篇——string类(string类中一些常见接口的用法与介绍+string类的模拟实现)

C++——类的模拟实现

C++ 中string类的三种模拟实现方式

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