【C++】string类 (模拟实现详解 上)

        我们不仅要会使用strng的接口,还要模拟实现,更深地理解strng的底层逻辑。这里我们最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数这些比较核心的接口

1.准备工作

        我们依旧采用声明和定义分离的方式模拟实现string,跟之前模拟实现Stack、顺序表那些是同样的操作,建三个文件,一个头文件string.h,两个源文件test.cpp和string.cpp。

string.h中用命名空间分隔一下,因为c++库里面也有string,避免冲突。string.h里面写一些会用到的头文件,一个string类,string类的成员变量都是老朋友了,size和capacity也是介绍过的,和模拟实现顺序表差不多的,前面的博文说过,string可以认为是char类型的顺序表

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

namespace lyj //用命名空间与库里的string分隔开
{
	class string
	{
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

2.基础接口

短小而且频繁被调用的函数,就直接放在string类里面,就不做声明和定义分离了。

2.1 无参构造

还是写在string.h里的string类,作为string类的成员函数。

class string
{
public:
	string() //无参构造
		:_str(new char[1]{'\0'}) //不能是nullptr
		,_size(0)
		,_capacity(0)
	{}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

无参构造走初始化列表,一个一个初始化,不传参_size和_capacity都是0,但是_str不可以为nullptr,这里要开辟一个空间,初始化为'\0'

2.2 带参构造

写在string类里面。带参构造就不要走初始化列表初始化了,因为不是很方便,我们就在函数体里实现。

class string
{
public:
	string() //无参构造
		:_str(new char[1]{'\0'})
		,_size(0)
		,_capacity(0)
	{}
	string(const char* str) //带参构造
	{
		_size = strlen(str);
		_capacity = _size; //capacity大小不包括\0
		_str = new char[_capacity + 1]; //开空间时多开一个
		strcpy(_str, str);//拷贝 
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

传参的时候_size就是传过来的str的大小,_capacity也初始化的和size一样大,切记,_capacity大小是不包括'\0'的,但是我们开空间的时候要多开一个,所以new char的大小是_capacity+1。空间开好之后就把str的数据拷贝到_str去,就初始化好了。

test.cpp中测试一下带参构造和无参构造。

#include "string.h"

namespace lyj //命名空间保持一致
{
	void test1(void)
	{
		string s1; //不传参
		string s2("hello world");//传参
	}
}

int main()
{
	lyj::test1(); //指定命名空间调用函数

	return 0;
}

2.3 无参构造和带参构造结合

两种构造函数可以结合成一个构造函数,如下。

string(const char* str = "") //给缺省值,什么都没有的字符串
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);//拷贝 
}

什么都没有的字符串自带\0,strlen求的是\0之前的字符长度,如果不传参,strlen(str)的结果就是缺省值的空字符串大小,为0, _capacity = _size = 0,new的大小为1;如果传参就是带参构造的结果。

test.cpp中测试一下这个结合的构造函数。

#include "string.h"

namespace lyj //命名空间保持一致
{
	void test1(void)
	{
		string s1; //不传参
		string s2("hello world");//传参
	}
}

int main()
{
	lyj::test1(); //指定命名空间调用函数

	return 0;
}

结果是正确的。 

2.3 析构函数

写在string类里面。

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

 析构函数没啥多说的。

2.5 c_str

写在string类里面。这个接口也算是打印函数,我们还没实现流插入流提取函数,暂时用这个。

const char* c_str()
{
	return _str;
}

test.cpp中测试一下。

namespace lyj //命名空间保持一致
{
	void test1(void)
	{
		string s1; //不传参
		string s2("hello world");//传参
        //打印
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
	}
}

int main()
{
	lyj::test1(); //指定命名空间调用函数

	return 0;
}

2.6 size 、capacity 和 operator[]

都写在string类里面。

size_t size() const
{
	return _size;
}
size_t capacity() const
{
	return _capacity;
}

operator提供两个版本,普通对象和const对象。 普通对象返回引用,可以修改,const对象返回const引用,不可修改。

char& operator[](size_t pos) //普通对象
{
	assert(pos < _size);//断言,防止越界
	return _str[pos];
}
const char& operator[](size_t pos) const //const对象
{
	assert(pos < _size);//断言,防止越界
	return _str[pos];
}

2.7 拷贝构造

还是写在string.h里的string类,作为string类的成员函数。

//假设用s1拷贝s2,即s2(s1)
string(const string& s)//拷贝构造
{
	_str = new char[s._capacity]+ 1; //s2开和s1一样的大小
	strcpy(_str, s._str);//拷贝数据
	_size = s._size;
	_capacity = s._capacity;
}

 有资源的申请是深拷贝。

2.8 operator=  赋值

还是写在string.h里的string类,作为string类的成员函数。

//s2 = s1;
string operator=(const string& s) //赋值
{
	delete[] _str;
	_str = new char[s._capacity + 1];
	_size = s._size;
	_capacity = s._capacity;
	return *this;
}

但是这个代码,自己赋值给自己就会出问题。所以我们要做一个改动。

string operator=(const string& s) //赋值
{
	if (this != &s) //不是自己给自己赋值时
	{
		delete[] _str;
		_str = new char[s._capacity + 1];
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

 

3.迭代器和范围for

先实现迭代器。

typedef char* iterator; //给char*换个名字叫iteraator
iterator begin()
{
	return _str;
}
iterator end()
{
	return _str + _size;//_str + _size是'\0'的位置
}

但是不可以认为迭代器就是指针,没有这么简单。 只能在string里面用指针这样实现一下。

test.cpp里测试。

namespace lyj //命名空间保持一致
{
	void test1(void)
	{
		string s1; //不传参
		string s2("hello world");//传参
		//迭代器遍历
		string::iterator it = s2.begin();
		while (it != s2.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

	}
}

int main()
{
	lyj::test1(); //指定命名空间调用函数

	return 0;
}

 范围for可以直接用,因为我们说过,范围for其实底层就是迭代器

如果我们现在把迭代器的实现注释掉,范围for也不能用。

除了普通迭代器,还有const迭代器。

typedef const char* const_iterator; //给const char*换个名字叫const_iteraator
const_iterator begin() const
{
	return _str;
}
const_iterator end() const
{
	return _str + _size;
}

到这我们就大概实现了string的大概框架,增删查改的接口我们下次再说,本篇就到这里,拜拜~

猜你喜欢

转载自blog.csdn.net/2402_82757055/article/details/143091877