STL——string模拟实现(一)

目录

构造函数的实现

拷贝构造

赋值重载

 const问题

 迭代器打印

范围for打印

运算符重载 

reserve模拟

插入数据

push_back

 append


构造函数的实现

先贴出一段错误代码:

#include<iostream>
#include<assert.h>
namespace zzl//避免与库冲突
{
	class string
	{
	public:
		string()
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{}
		string(const char* str)
			:_str(str)//_str要为const类型
			, _size(strlen(_str))
			, _capacity(strlen(_str))
		{}
	private:
		const char* _str;
		size_t _size;
		size_t _capacity;
	};
}

输出c风格的字符串:

    const char* c_str()//外部不加const,指针可以改变
		{
			return _str;//返回常量字符串
		}

11bafbfef1bf42e3bdfe44e73dd10d66.png大家能根据string.h文件的代码说出错误的地方在哪吗?

这个错误涉及对空指针解引用,也就是说,在我们通过c_str得到字符串内容的过程中,涉及对空指针解引用引发了这样的错误。

当我们使用cout输出一个指针变量时,默认情况下会输出该指针变量所指向的内存地址,而不是指针本身的值。
如果我们想输出指针的地址,可以将其强制转为(void*)使其打印地址。

除此之外,它还存在一些问题,我们再增加两个成员函数

    char& operator[](size_t pos)/支持改变返回值a[i]++
		{
			assert(pos < size)//\0不存有效数据
			return _str[pos];
		}
		size_t size()//模拟size()
		{
			return _size;
		}

在定义_str时,为了成功初始化也加上了const,但这样的问题就是无法改变其值。

我们定义的构造函数不能扩容这是问题之二。

对单参构造函数的改动:

string(const char* str)
			:_size(strlen(_str))
		{
			_capacity = _size;
			_str = new char[_capacity + 1];
            strcpy(_str, str);
		}

这里我只初始化了_size,目的是避免依赖带来的初始化顺序不匹配问题,没有多次用strlenO(n)提高了效率,接着扩容(不要忘了\0!),并复制内容。而我们的const成员变量现在也可以去掉啦。

对无参构造的改动:

    string()
			:_str(new char[1])//匹配析构
			,_size(0)
			,_capacity(0)
		{
			_str[0] = '\0';
		}
		~string()
		{
			delete[] _str;
			_capacity = _size = 0;
		}

为了析构与扩容匹配,这里用[ ]。因为无参不存储有效数据,所以我们用\0初始化

成功输出: 

923450916ebc4ca38ba7029eb9fd9913.png

 根据我们学过的缺省知识,构造函数还可以再进行简化

如果我们不传参,默认传空串

string(const char* str = "\0")//""也可以
			:_size(strlen(str))
		{
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

可以看到逻辑是没有错的。为空串时,capacity和size都为0 

注意这里不要把值定为nullptr,后面strlen会解引用出错,也不要传递单个字符(strlen用来计算字符串)

拷贝构造

 先来看看编译器默认生成的拷贝构造 677b9e904820475893054e66fe6e620e.png

 成功实现了浅拷贝!

两大问题:1.析构两次程序崩溃 2.指针指向同一块空间,有篡改的风险

实现拷贝构造:

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

 成功实现深拷贝!

赋值重载

与拷贝构造稍微有点不同的是,赋值重载是对两个已经初始化后的操作数的运算,这就存在一个问题,两个操作数数据元素不同的问题,可能一方的数据远大于另一方,一方数据远小于另一方,当然也不排除两者数据差不多的情况。如果是前两种情况,会造成空间的大量浪费,所以我们可以使用提前释放空间的方式进行赋值。

string& operator=(const string& s)
		{
			delete[] _str;
			_str = new char[s._capacity + 1];
			_size = s._size;
			_capacity = s._capacity;//释放后更改的capacity
			strcpy(_str, s._str);
			return *this;
		}

这段代码还存在一个问题:

c994cf8b60cc4515a44c4dfc33be6478.png 由于释放了空间。自己给自己赋值将会是随机值

 所以我们加上判断:

	    if (this != &s)
			{}
			return *this;

注意不要用 ==判断,因为我们没有写对应的重载,用地址判断十分巧妙(引用的特性)。

开辟空间失败怎么办?可以抛异常的方式解决,抛异常意味着空间已经被delete,这样的后果是原空间不能被正常使用了。如果想避免这种情况也可以通过中间指针的方式解决。

string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;//指向堆区
				_size = s._size;
				_capacity = s._capacity;
			}

运行结果:

1071a5c685514b698cf7efc49312925d.png

 const问题

现在我们给定一个函数,让它打印string的值,观察现象

89d6987eb57d46d398cd75c61179b38c.png

 在函数体中调用[ ]重载发生了典型的权限放大问题,我们在学习库里的string时发现[ ]重载有两种类型,const和非const,这样做的好处就是支持一些情况下可读,可写

    const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}

注意这里两个const的用法:第一个const与引用构成常量引用返回,返回值_str[pos]作为一块堆上的空间(不在堆上申请也存在),在返回其别名后就不能修改了,而第二个const修饰this指针,与可写[]构成重载,且不能在体内改变其指向对象的值。

编译器会根据所传递的参数选择最适合的对象。

这提醒我们:在写一些可写类型的成员函数时,要注意是否需要提供它的仅读版本,这点很重要!

 迭代器打印

类内:

	typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str+size();
		}

因为是一块连续的空间,所以我们可以暂且用指针的方式模拟迭代器。
类外:

zzl::string::iterator it = s2.begin();
	while (it != s2.end())
	{
		cout << *it << ' ';
		++it;
	}
	cout << endl;
}

 需要添加 string::表明使用的是string内重定义的char*

范围for打印

和内置类型不同,我们使用auto输出自定义类型的数据时,它所调用的底层是调用我们自己写的迭代器。

        for (auto& ch : s)
			{
				cout << ch << ' ';
			}

类似define的直接替换,如果我将begin换成start它就会报错。

如果我把他放进我们刚才写的Print函数内又会出现一个新的问题——

权限放大

我们观察这段代码更容易理解:

	void Print(const string& s)
		{
            string::iterator it = s2.begin();
        }    

char* = const char*,明显的权限放大,所以迭代器也支持const版本进行readonly

typedef const char* const_iterator;
        const_iterator begin()const
		{
			return _str;
		}
		const_iterator end()const
		{
			return _str + size();
		}

运行结果: 

eec6f9b705d04c7099a5fd6bbbc967de.png

 注意这里的const修饰的是指针指向的内容,所以指针是可以++或--进行迭代的。

运算符重载 

思考一下,string的比较是比较二者的size大小还是capacity?

答案都不是。它们甚至连\0也不放过 ~_~

正确答案是比较ascii码值,我们可以用c中的strcmp函数复用,省区我们写的功夫。

66e52a56ded54933b6fe050aaf165344.png

	bool operator==(const string& s)const
		{
			return !strcmp(_str, s._str);
			//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;
			return *this > s || s == *this;
		}

		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);
		}

希望大家在复用strcmp完后不要跟我一样比较错对象,后面的比较是两个string对象的比较而不是两个str指针的比较。同时注意加上const,以应付不时之需。

运行结果:

 注意流提取优先级高于比较运算符优先级得加上括号

reserve模拟

void reserve(size_t n)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	

推荐使用中间变量的方法扩容,扩容失败不至于原数据丢失。

插入数据

push_back

	void push_back(char ch)
		{
			if (_size + 1 > _capacity)
			{
				reserve(_capacity * 2);//复用
			}
	        _str[_size] = ch;//\0处插入
			_size++;
			_str[_size] = '\0';
		}

这里采用2倍扩策略,注意当用这种方式尾插时需要手动添加\0,以免无界出现乱码

这里还有一个隐藏很深的问题,如果一开始是无参的string传递的capacity为0就会造成capacity*2=0的情况,以至于我们添加的\0造成数组越界。为了避免这种情况,有两种解决方式

在构造时判断

_capacity = _size==0? 3:_size;

 在传参时判断

	reserve(_capacity == 0 ? 3: _capacity * 2);
    //reserve(_capacity*2+1);

 append

	void append(const char* s)
		{
			int len = strlen(s);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
            strcpy(_str + _size, s);
			_size += len;
		}

由于不知道字符串大小,就开辟相应字符串长度的大小即可,由于我们在reserve里为\0预留了空间,所以这里不再加1。

这里不推荐用strcat遍历数组查找\0的方式追加,我们知道要追加的位置直接用strcpy更便捷。

 运行结果:

 +=

直接复用我们这里实现的两个尾插接口:

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

今天就先讲到这,下次我们对string模拟进行一个收尾。

猜你喜欢

转载自blog.csdn.net/dwededewde/article/details/130932642