string的底层简单实现(造轮子)

文件:String.h ----- 头文件

           String.cpp ----- 源文件

           Test.cpp ----- 源文件

实现细节:

实现带参构造:

在实现带参构造建议不使用初始化列表,初始化去写不太好:

:_str(new char[strlen(str)+1])

用初始化列表要在这new空间,String要开自己的空间,字符串初始化的时候,把字符串拷贝给str,因为这样才能修改与扩容,+1的原因是为了多开一个空间给\0

,_size(strlen(str))
,_capacity(_size)

注意:这样的话,_size初始化也要strlen,_capacity也可以要strlen

那如果先初始化_size(strlen(str)),再用_size取初始化_str呢?

这就犯了一个致命错误:初始化列表出现的顺序并不是真正的顺序,是按声明顺序,_str(_size)先走,给了个随机值,这就坑死了,那去改声明顺序,就显得不太好,所以,建议在这不使用初始化列表,直接建立在函数体里面,就不会受这些乱七八糟的影响了

String(const char* str)
{
	_size = strlen(str);
	_capacity = _size;//_capacity不包含\0,不要+1,所以我们在开空间的时候才会多开一个
	_str = new char[_capacity + 1];
	strcpy(_str, str);//将str拷贝给到_str
}

注意:_capacity不包含\0,不要+1,所以我们在开空间的时候才会多开一个

测试函数的位置:

在类外面定义函数的话,在编译时会发生链接错误:

//String.h 文件
namespace my_home
{
	class String
	{
	public:
		//无参
		String()
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	void test_string1()
	{
		string s1;
		string s2("hello world");
	}

}

1.类里面的默认是内联,内联不会放到符号表,就不会冲突;
2.在类外面定义全局的函数,因为头文件(String.h)在String.cpp文件包含了一份,在Test.cpp也包含了一份,那么这个函数就会在两个cpp生成的目标文件各自有一份,最后链接合到一起的时候有两份就冲突了(链接错误);

解决链接错误的方法:
1.用static修饰函数,静态的,只有在当前文件可建,相当于不进入符号表
2.内联也可以解决问题,与静态达到的效果是类似的,内联还有另外一层展开,本质也是只在当前文件可建,不进入符号表
3.最标准的还是去做声明和定义的分离

无参构造的空指针解引用: 

其实上面无参的实现存在问题:空指针解引用

在String类的public添加函数:

const char* c_str()
{
	return _str;
}

因为当前还没有重载<<,所以仅仅打印字符串:

void test_string1()
{
	String s1;//s1是空的string对象,使用空指针进行初始化的
	String s2("hello world");
	cout << s1.c_str() << endl;//空指针char*在打印的时候,char*会认为是字符串,会直接解引用,遇到\0才会终止,造成了空指针的解引用
	cout << s2.c_str() << endl;
}

程序崩溃 

1.s1是空的string对象,使用空指针进行初始化的;
2.空指针char*在打印的时候,char*会认为是字符串,会直接解引用,遇到\0才会终止,造成了空指针的解引用;

所以,我们应该对无参构造进行修改:

String()
	: _str(new char[1]{'\0'}) // _str(nullptr)
	, _size(0)
	, _capacity(0)
{}

为了美观,我们可以将无参和带参进行整合·:全缺省构造函数

String(const char* str = "")//不要写成nullptr问题与上面一样,字符串有\0,没必要写成"\0"这样其实是两个\0
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

注意:不要写成nullptr问题与上面一样,字符串有\0,没必要写成"\0"这样其实是两个\0 

类中的声明与定义分离

短小,频繁调用的函数我们直接写在类里面,默认是内联函数
下面几个是适合做内联的函数:

String(const char* str = "")
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}
~String()
{
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}
const char* c_str() const
{
	return _str;
}
size_t size() const
{
	return _size;
}
size_t capacity()const
{
	return _capacity;
}
char& operator[](size_t pos)
{
	assert(pos < _size);//防止越界
	return _str[pos];
}
const char& operator[](size_t pos) const
{
	assert(pos < _size);//防止越界
	return _str[pos];
}

那么当前的程序支持范围否吗?

显然,当前的情况并不支持范围for:因为其实范围for的底层就是迭代器 

简单迭代器的实现:(在某些场景下会有缺陷)

//在类里面定义类型有两种方式,一种是内部类,另一种是typedef
typedef char* iterator;//在String类里typedef了iterator,类型char*,属于String这个类域
//其实迭代器模拟的是指针的行为
typedef const char* const_iterator;
iterator begin()
{
	return _str;
}
iterator end()
{
	return _str + _size;
}
const_iterator begin()const
{
	return _str;
}
const_iterator end()const
{
	return _str + _size;
}
//主要是底层是一个物理数组

1.在类里面定义类型有两种方式,一种是内部类,另一种是typedef ,这样就是为什么说有的容器的迭代器都一个名,但是不全是指针实现的,list就不是由原生指针;
2.在String类里typedef了iterator,类型char*,属于String这个类域;
3.其实迭代器模拟的是指针的行为;
4.迭代器是种封装的体现,屏蔽了底层实现的细节,提供了统一的类似访问容器的方式,不需要关心容器底层结构和实现细节;

//范围for
for (auto e : s2)
{
	cout << e << " ";
}
cout << endl;
//迭代器
String::iterator it = s2.begin();
while (it != s2.end())
{
	cout << *it << endl;
	++it;
}

这时候范围for便也可以遍历,侧面说明了范围for底层就是迭代器

但是for是傻瓜式模仿,一但begin()写成Begin()就行不通

浅拷贝问题:
class String
{
public:
	String(const char* str = "")
	{
		// 构造String类对象时,如果传递nullptr指针,可以认为程序非
		if (nullptr == str)
		{
			assert(false);
			return;
		}
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};
// 测试
void TestString()
{
	String s1("hello bit!!!");
	String s2(s1);
}

说明:上述 String 类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认 的,当用 s1 构造 s2 时,编译器会调用默认的拷贝构造。最终导致的问题是, s1 s2 共用同一块内 存空间,在释放时同一块空间被释放多次而引起程序崩溃 ,这种拷贝方式,称为浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来 。如果 对象中管理资源 ,最后就会 导致 多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该 资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规

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

深拷贝:

传统写法:自己开空间,自己拷贝

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

现代写法:让别人帮自己拷贝(交换)

交换_str,如果_str是随机值(因为对于内置类型编译器未必会初始化),交换给tmp,tmp作为局部对象,出了作用域要被销毁,就相当于释放野指针了,因此,可以在声明时给缺省值

void swap(String& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
String(const String& s)
{
	String tmp(s.c_str());
	swap(tmp);
}
流赋值拷贝: (深拷贝)

知晓了拷贝构造,其实复制拷贝跟拷构造差不多,只不过赋值拷贝是运算符重载, 复制拷贝的问题不仅仅是浅拷贝,还导致了之前的数据被覆盖,导致内存泄漏,需要我们自行实现:

传统写法:自己开空间

String& operator=(const String& s)
{
    //防止自己把自己释放了
	if (this != &s)
	{
		delete[] _str;

		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}

	return *this;
}

现代写法:抢夺资源(与拷贝构造不同,复制拷贝还将tmp给析构了,而拷贝构造只是tmp指向nullptr)(代码块也是一个域,出了域调用析构函数)

String& operator=(const String& s)
{
	//防止自己把自己释放了
	if (this != &s)
	{
		String tmp(s);//调用拷贝构造
		swap(tmp);
	}

	return *this;
}

最新版本:s传给tmp会调用拷贝构造(现代写法的浓缩)(只用拷贝构造才必须用引用)

String& operator=(String tmp)
{
	swap(tmp);
	return *this;
}
swap:
void test_string7()
{
	String s1("hello world");
	String s2;
	std::swap(s1, s2);
	s1.swap(s2);
}

对于库里面的swap与我们自行实现得swap有什么区别吗?

库中swap实现:

 

C++98的库的swap模板,会深拷贝3次,所以我们自行实现的是面对C++98版本更推荐的

为了防止这个问题,其实库里面就已经为我们解决了这个问题:

交换字符串对象 x 和 y 的值,这样在调用此函数后,x 的值是调用之前位于 y 上的值,y 的值是 x 的值
这是泛型算法交换的重载,它通过相互转移对其内部数据的所有权到另一个对象(即,字符串交换对其数据的引用,而不实际复制字符)来提高其性能:它的行为就像调用了 x.swap(y)

面对模板和实例化共存,是优先走实例化的(有现成吃现成的),所以以上两种其实效率一样 

流提取:分隔问题

in会跳过   ' '   '\n'  , 有分隔符的概念,读整数,浮点数,字符...

那么,在实现String的流提取时,需要读到分隔符达到结束的标志,然而单单的in会直接不读空格和换行。在C语言中,可以用getc()来解决问题,C++其实也有解决的办法:get()

这时候虽然到达了输入的目的,但是如果String有东西,输入的内容直接接在原来的String变量后面,这时候我们应该自行写一个clear(),清除数据,保留原来空间:

void clear()
{
	_str[0] = '\0';
	_size = 0;
}
istream& operator>>(istream& in, String& s)
{
	s.clear();
	char ch;//C语言用getc()解决
	ch = in.get();//C++中,有get(),他就不用管什么分隔符,因为他不涉及任何类型,会一个字符一个字符的读//in会跳过' ,'\n',有分隔符的概念,读整数,浮点数,字符...
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

优化:面对输入大数据,+=会不断地扩容,会有消耗,效率降低

解决:提供一个buffer,每次来的字符不往String变量(s)进行+=,而是将每次来的字符都把它放到buffer里面,如果buffer差一个满了(最后一个放\0),用Stringd变量进行+=buffer,相当于把buffer当成一个缓冲区:(buffer是栈上的,开得快,且临时)

istream& operator>>(istream& in, String& s)
{
	s.clear();
	const int N = 256;
	char buffer[N];
	int i = 0;
	char ch;//C语言用getc()解决
	ch = in.get();//C++中,有get(),他就不用管什么分隔符,因为他不涉及任何类型,会一个字符一个字符的读//in会跳过' ,'\n',有分隔符的概念,读整数,浮点数,字符...
	while (ch != ' ' && ch != '\n')
	{
		buffer[i++] = ch;
		if (i == N - 1)
		{
			buffer[i] = '\0';
			s += buffer;
			i = 0;
		}
		ch = in.get();
	}
	if (i > 0)//说明buffer还有字符没有+=到s
	{
		buffer[i] = '\0';
		s += buffer;
	}
	return in;
}

实现代码:

头文件:String.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
//#include<string>
#include<assert.h>

using namespace std;
namespace home
{
	class String
	{
	public:
		//在类里面定义类型有两种方式,一种是内部类,另一种是typedef
		typedef char* iterator;//在String类里typedef了iterator,类型char*,属于String这个类域
		//其实迭代器模拟的是指针的行为
		typedef const char* const_iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		const_iterator begin()const
		{
			return _str;
		}
		const_iterator end()const
		{
			return _str + _size;
		}
		String(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		void swap(String& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		//显示提供拷贝构造
		String(const String& s)
		{
			String tmp(s.c_str());
			swap(tmp);
		}
		String& operator=(String tmp)
		{
			swap(tmp);
			return *this;
		}
		~String()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
		const char* c_str() const
		{
			return _str;
		}
		size_t size() const
		{
			return _size;
		}
		size_t capacity()const
		{
			return _capacity;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);//防止越界
			return _str[pos];
		}
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);//防止越界
			return _str[pos];
		}
		//声明和定义分离
		void reserve(size_t n);
		void push_back(char ch);
		void append(const char* str);
		String& operator += (char ch);
		String& operator +=(const char* str);
		
		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void erase(size_t pos, size_t len = npos);

		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);
		String substr(size_t pos = 0, size_t len = npos);

	private:
		//内置类型未必初始化
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;

		static const size_t npos = -1;//在这可以写,核心原因是有const
		
		//static const double N = 1.1;//只有整型才可以

		/*static const int N = 1;
		int buff[N];*/
	};
	//const size_t String::npos = -1;
	//为了支持不属于String的比较(但其实可以隐式类型转换(调用构造)的,感觉没必要)(必须要有一个类类型变量)
	bool operator<(const String& s1, const String& s2);
	bool operator<=(const String& s1, const String& s2);
	bool operator>(const String& s1, const String& s2);
	bool operator>=(const String& s1, const String& s2);
	bool operator==(const String& s1, const String& s2);
	bool operator!=(const String& s1, const String& s2);
	//不写成成员函数是因为String会抢占cout位置
	ostream& operator<<(ostream& out, const String& s);
	istream& operator>>(istream& in, String& s);

	void test_string1();
	void test_string2();
	void test_string3();
	void test_string4();
	void test_string5();

}

源文件:String.cpp

#include"String.h"
namespace home
{
	//const size_t String::npos = -1;
	void String::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];//为了\0
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}
	void String::push_back(char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';//注意
	}
	void String::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}
		strcpy(_str + _size, str);
		_size += len;
	}

	String& String::operator += (char ch)
	{
		push_back(ch);
		return *this;
	}
	String& String::operator +=(const char* str)
	{
		append(str);
		return *this;
	}
	void String::insert(size_t pos, char ch)
	{
		assert(pos <= _size);

		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}

		// 挪动数据//注意有些代码的隐式类型转化
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}

		_str[pos] = ch;
		++_size;
	}

	void String::insert(size_t pos, const char* s)
	{
		assert(pos <= _size);

		size_t len = strlen(s);
		if (_size + len > _capacity)
		{
			// 大于2倍,需要多少开多少,小于2倍按2倍扩
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}

		size_t end = _size + len;
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];
			--end;
		}

		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = s[i];
		}

		_size += len;
	}

	void String::erase(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			for (size_t i = pos + len; i <= _size; i++)
			{
				_str[i - len] = _str[i];
			}

			_size -= len;
		}
	}

	size_t String::find(char ch, size_t pos)
	{
		assert(pos < _size);

		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}

		return npos;
	}

	size_t String::find(const char* str, size_t pos)
	{
		assert(pos < _size);

		const char* ptr = strstr(_str + pos, str);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}

	String String::substr(size_t pos, size_t len)
	{
		assert(pos < _size);

		// len大于剩余字符长度,更新一下len
		if (len > _size - pos)
		{
			len = _size - pos;
		}

		String sub;
		sub.reserve(len);
		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}

		return sub;
	}
	bool operator<(const String& s1, const String& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) < 0;
	}
	bool operator<=(const String& s1, const String& s2)
	{
		return s1 < s2 || s1 == s2;
	}
	bool operator>(const String& s1, const String& s2)
	{
		return !(s1 <= s2);
	}
	bool operator>=(const String& s1, const String& s2)
	{
		return !(s1 < s2);
	}
	bool operator==(const String& s1, const String& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
	bool operator!=(const String& s1, const String& s2)
	{
		return !(s1 == s2);
	}

	ostream& operator<<(ostream& out, const String& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}
	istream& operator>>(istream& in, String& s)
	{
		s.clear();
		const int N = 256;
		char buffer[N];
		int i = 0;
		char ch;//C语言用getc()解决
		ch = in.get();//C++中,有get(),他就不用管什么分隔符,因为他不涉及任何类型,会一个字符一个字符的读//in会跳过' ,'\n',有分隔符的概念,读整数,浮点数,字符...
		while (ch != ' ' && ch != '\n')
		{
			buffer[i++] = ch;
			if (i == N - 1)
			{
				buffer[i] = '\0';
				s += buffer;
				i = 0;
			}
			ch = in.get();
		}
		if (i > 0)//说明buffer还有字符没有+=到s
		{
			buffer[i] = '\0';
			s += buffer;
		}
		return in;
	}
	

}

  • 17
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值