string详解(2)— 模拟实现

1.经典的string类实现

最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。

(1) String.h

为了与库里的string进行区分我们使用String:

cpp 复制代码
// String.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include<string.h>
#include<assert.h>
using namespace std;

namespace zyt
{
	class String
	{
	public:
		//迭代器,实现的是封装
		typedef char* iterator;
		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()// 无参
		//	:_str(new char[1] {'\0'})//开一个空间存储'\0',不能直接给nullptr
		//	// 如果对无参对象应用c_str,接口会持续对空指针解引用直到遇到'\0'为止
		//	, _size(0)
		//	, _capacity(0)
		//{}

		//string(const char* str) // 有参
		//{
		//	_size = strlen(str);
		//	_capacity = _size; // 容量不包含'\0'
		//	_str = new char[_capacity + 1]; // 空间要多开一个
		//	strcpy(_str, str);
		//}

		// 短小频繁调用的函数可以直接定义在类里,默认是inline
		// 或者直接写一个全缺省的构造
		String(const char* str = "")//C语言规定常量字符串后面自带一个'\0'
		{//单参数构造函数支持隐式类型转换
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// 深拷贝
		//显示拷贝构造,否则其他函数(substr)会默认调用浅拷贝
		String(const String& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

		//显示赋值重载,否则也会默认调用浅拷贝,导致内存泄漏
		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;
		}

		~String()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
		const char* c_str()const
		{
			return _str;
		}
		// 清空所有数据但不销毁空间
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		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;
		size_t _size;
		size_t _capacity;

		static const size_t npos;
		//static const size_t npos = -1;
		// static const 整形 的类型可以直接定义,其余static类型都要声明和定义分离
	};

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

	ostream& operator<<(ostream& out, const String& s);
	istream& operator>>(istream& in, String& s);

	void test1();
	void test2();
	void test3();
	void test4();
}

(2) String .cpp

cpp 复制代码
// String.cpp

#include"String.h"

namespace zyt
{
	const size_t String::npos = -1;//声明和定义分离

	void String::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			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';// 一定要加终止符
	}
	String& String::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}

	void String::append(const char* str)
	{
		size_t len = strlen(str);
		if (len + _size > _capacity)
		{
			//大于2倍,需要多少开多少,小于2倍按2倍扩
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}
		strcpy(_str + _size, str);
		_size += len;
	}
	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);
		}
		//挪动数据

		//int end = _size;
		// 当操作符两端数据类型不同会发生类型提升(提升成无符号),所以要强转
		//while (end >= (int)pos)
		//{
		//	_str[end + 1] = _str[end];
		//	--end;
		//}
		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* str)
	{
		assert(pos < _size);
		size_t len = strlen(str);
		if (len + _size > _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] = str[i];
		}
		_size += len;
	}
	void String::erase(size_t pos, size_t len)// 缺省参数只能声明的时候给
	{
		assert(pos < _size);
		if (len >= _size - pos)// 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)
	{
		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);

		if (len > _size - pos)
		{ // 更新len
			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();//将s原有的数据清空

		const int N = 256;
		char buff[N];//空间为256的数组用来存储字符串,避免多次扩容和大量空间浪费的问题
		int i = 0;

		char ch;
		//流提取遇到' '和'\0'无法读取
		//in >> ch;
		ch = in.get();//只获取一个字符
		while (ch != ' ' && ch != '\0')
		{
			buff[i++] = ch;//先存储在数组里
			if (i == N - 1)// i = 255
			{
				buff[i] = '\0';//存满了
				s += buff;// +=会给s扩容

				i = 0;
			}
			//in >> ch;
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		
		return in;
	}
	void test1()
	{
		String s1;
		String s2("hello world");
		cout << s2.c_str() << endl;
		for (size_t i = 0; i < s2.size(); i++)
		{
			s2[i] += 2;
		}
		cout << s2.c_str() << endl;

		String::iterator it = s2.begin();
		while (it != s2.end())
		{
			*it -= 2;
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto ch : s2)
		{
			cout << ch << " ";
		}
	}

	void test2()
	{
		String s1("hello world");
		s1 += 'a';
		s1 += 'b';
		cout << s1.c_str() << endl;
		s1 += "123";
		cout << s1.c_str() << endl;
		s1.insert(5, '*');
		cout << s1.c_str() << endl;
		String s2("hello world");
		s2.insert(5, "%%%");
		cout << s2.c_str() << endl;
		s2.insert(0, "%%%");
		cout << s2.c_str() << endl;
		s2.erase(5, 3);
		cout << s2.c_str() << endl;
		s2.erase(5, 100);
		cout << s2.c_str() << endl;
	}

	void test3()
	{
		String s1("hello world");
		size_t pos = s1.find(' ');
		String last = s1.substr(pos);
		cout << last.c_str() << endl;
		String s2(s1);
		cout << s2.c_str() << endl;
		s1 = s2;
		cout << s1.c_str() << endl;
		s2 = s2;
		cout << s2.c_str() << endl;
	}

	void test4()
	{
		String s1("hello world");
		String s2("hello world");
		cout << (s1 < s2) << endl;
		cout << (s1 == s2) << endl;
		//单参数构造函数支持隐式类型转换,所以没有函数实现也支持字符串和string比较
		cout << ("hello world" < s2) << endl;
		cout << (s1 == "hello world") << endl;
		//运算符重载必须有个类类型的参数,所以下面的会被识别成指针的比较
		cout << ("hello world" == "hello world") << endl;
		cout << s1 << " " << s2 << endl;
		String s3;
		cin >> s3;
		cout << s3 << endl;
	}
}

(3)Test.cpp

cpp 复制代码
// Test,cpp

#include"String.h"

int main()
{
	zyt::test4();
	return 0;
}

(4)注意

1. 【static const 整形】的类型可以直接定义,其余static类型都要声明和定义分离。

static const size_t npos = -1;
2. 短小频繁调用的函数(如 类的构造、拷贝构造、赋值运算符重载以及析构函数等**)可以直接定义在类里,默认是inline**
3.全缺省构造函数用的是: String(const char* str = "")

C语言规定常量字符串后面自带一个'\0'****,
//String(const char* str = "\0") 错误示范,因为常量字符串后面自带一个'\0',所以这里重复写了。
//String(const char* str = nullptr) 错误示范,如果对无参对象应用c_str,接口会持续对空指针解引用直到遇到'\0'为止。
4. String类如果没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认
的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是, s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃, 这种拷贝方式,称为浅拷贝。
5. 显示赋值重载,否则也会默认调用浅拷贝,导致内存泄漏
6. 封装: 屏蔽了底层实现细节,提供了统一的类似访问容器的方式,不需要关心容器底部结构和实现细节
7. 当操作符两端数据类型不同会发生类型提升(提升成范围较大的),int类型和size_t类型比较会发生整形提升,两边都是正数不会有影响,但当int类型的数是负数(-1)时,就会出问题。
8. 在实现流输入时,为避免多次扩容和预留空间浪费过多,可以开一个空间为256的数组用来先存储字符串。
9. 实现流输入中,流提取遇到' '空格会被自动跳过,直到遇到下一个非空白字符,遇到'\0'结束读取,所以用到 get()函数get() 函数是 istream 类的一个成员函数,用于从输入流中读取字符,get() 函数不会自动跳过任何空白字符,包括空格和制表符
10. 单参数构造函数支持隐式类型转换,所以没有函数实现也支持字符串和string比较:

String s1("hello world");

cout << (s1 == "hello world") << endl;

11. 运算符重载必须有个类类型的参数,所以下面的会被识别成指针的比较
cout << ("hello world" == "hello world") << endl;


2.浅拷贝

浅拷贝:也称位拷贝, 编译器只是将对象中的值拷贝过来 。如果对象中管理资源 ,最后就会导致 多个对象共享同一份资源 ,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。或者修改一个对象资源,会影响另外一个对象。
采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享

具体介绍可以看类和对象(中)的拷贝构造函数部分中有介绍:http://t.csdnimg.cn/2xIJH

3.深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
每个String类对象都要用空间来存放字符串,而s2要用s1拷贝构造出来,因此

深拷贝:给每个对象独立分配资源,保证多个对象之间不会因共享资源造成多次释放导致程序崩溃问题

(1)传统版String的写法

cpp 复制代码
class String
{
public:
    // 全缺省的构造
    String(const char* str = "")//C语言规定常量字符串后面自带一个'\0'
    {//单参数构造函数支持隐式类型转换
	    _size = strlen(str);
	    _capacity = _size;
	    _str = new char[_capacity + 1];
    	strcpy(_str, str);
    }
    // 深拷贝传统写法
    //显示拷贝构造,否则其他函数(substr)会默认调用浅拷贝
    String(const String& s)
    {
	    _str = new char[s._capacity + 1];
	    strcpy(_str, s._str);
	    _size = s._size;
	    _capacity = s._capacity;
    }
    //传统写法
    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;
    }
   
	~String()
	{ //delete nullptr 也不会报错,直接返回
		delete[] _str;
		_str = nullptr;
		_size = _capacity = 0;
	}
    //...
private:
    char* _str;
    size_t _size;
    size_t _capacity;
};

(2)现代版String的写法

cpp 复制代码
class String
{
public:
    String(const char* str = "")
    {
        if (nullptr == str)
        {
            assert(false);
            return;
        }
        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }
    //实现交换函数(库里面)
    void swap(String& s)
    {
    	std::swap(_str, s._str);
	    std::swap(_size, s._size);
	    std::swap(_capacity, s._capacity);
    }
    // 深拷贝现代写法 s2(s1)
    String(const String& s)
    {// 不能将随机值交换给tmp,释放时就释放成野指针了,
     // 内置类型在定义时初始化成nullptr
	    String tmp(s._str);
	    swap(tmp);// this 和 tmp交换(调的是命名空间的交换函数)
    }
    
   	//现代版赋值重载=  (s1 = s3)
	//String& operator=(const String& s)
	//{
	//	if (this != &s)
	//	{
	//		String tmp(s._str);
	//		swap(tmp);//tmp指向s3空间,出作用域tmp自动销毁
	//	}
	//	return *this;
	//}
		
	//s1 = s3 现代版赋值重载=(简化版)
	String& operator=(String tmp)//这里直接触发拷贝构造tmp(局部变量)
	{
		swap(tmp);
		return *this;
	}
   	~String()
	{ //delete nullptr 也不会报错,直接返回
		delete[] _str;
		_str = nullptr;
		_size = _capacity = 0;
	}
    //...
private://内置类型未初始化(初始化列表)编译器默认是随机值
	char* _str = nullptr;
	size_t _size = 0;
	size_t _capacity = 0;
}

● 实现现代版拷贝构造:

1. 内置类型未初始化(初始化列表)编译器默认是随机值,调用变现代拷贝构造使用swap函数,不能将随机值交换给tmp,释放局部变量tmp时,就释放成野指针!
2. 内置类型在定义时初始化,指针给缺省值nullptr,其余给缺省值0。
3. 实现s2(s1):

用s1创造局部变量tmp,再将s2与局部变量tmp交换,tmp指向nullptr,s2指向tmp原有资源,tmp出作用域销毁。

● 实现现代版赋值重载=:

类似于拷贝构造用s1创造局部变量tmp,再将s3与局部变量tmp交换,tmp指向s3原有资源,s3指向tmp原有资源,函数返回所需要的s3指向资源,tmp出作用域销毁。

(3)思考(swap)

C++98,算法库里面的swap和我们String类里实现的swap有什么区别?

cpp 复制代码
// swap函数模板,具体实现调用的是实例化版本
template <class T> void swap ( T& a, T& b )
{
  T c(a); a=b; b=c;
}

算法库里面的swap调用了拷贝构造,赋值;总共实现3次深拷贝,多次开空间和拷贝数据,效率低。

cpp 复制代码
void swap(String& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

类里实现的swap只实现了1次深拷贝,内置类型的交换不用开空间,大大提高了效率。

(4)写实拷贝

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数 :用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源**,** 如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有 其他对象在使用该资源。
使用写实拷贝优点:

Func()函数返回值可以不用调用深拷贝,而是调用浅拷贝,让ret指向str的原有空间,出了作用域str销毁,引用计数-1,这种方式提高效率。


vs一般默认是深拷贝,有的的编译器可能用的是写实拷贝。

cpp 复制代码
//如果用写实拷贝,要在每个修改对象的接口中都插入该函数
void copy_on_write()
{
	if (count > 1)
	{
		//深拷贝
	}
}	

4.vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。

(1)vs下string结构的说明

string总共占28个字节 ,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义
string中字符串的存储空间
当字符串长度小于16时,使用内部固定的字符数组来存放
当字符串长度大于等于16时,从堆上开辟空间

cpp 复制代码
union _Bxty
{ // storage for small buffer or pointer to larger one
    value_type _Buf[_BUF_SIZE];
    pointer _Ptr;
    char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;

这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建
好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的
容量
最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。

(2)g++下string的结构

G++下,string是通过写时拷贝实现的,string对象总共占4个字节(32位)/ 8字节(64位),内部只包含了一个 指针,该指针将来指向一块堆空间,内部包含了如下字段:
● 空间总大小
● 字符串有效长度
● 引用计数
● 指向堆空间的指针,用来存储字符串。

cpp 复制代码
struct _Rep_base
{
    size_type     _M_length;
    size_type     _M_capacity;
    _Atomic_word     _M_refcount;
};
相关推荐
小白要加油哈21 分钟前
游戏开发--C#面试题
开发语言·笔记·学习·microsoft·unity·c#·游戏引擎
lb363636363627 分钟前
冒泡选择法(c基础)
c语言
闻缺陷则喜何志丹40 分钟前
【C++ 滑动窗口】2134. 最少交换次数来组合所有的 1 II
c++·算法·leetcode·力扣·交换·组合·最少
gsgbgxp41 分钟前
C++类中的const成员变量和const成员函数
开发语言·c++·算法
皮克斯的进化之路1 小时前
RabbitMQ的死信队列
java·开发语言
丶Darling.1 小时前
Day41 | 动态规划 :完全背包应用 完全平方数&&单词拆分(类比爬楼梯)
算法·动态规划·dp·lambda·记忆化搜索·回溯·c++\
唐僧洗头爱飘柔95271 小时前
(Go语言)初上手Go?本篇文章帮拿捏Go的数据类型!
开发语言·golang·go语言·go数据类型·go开发·go初上手
唐僧洗头爱飘柔95271 小时前
(Go基础)变量与常量?字面量与变量的较量!
开发语言·后端·golang·go·go语言初上手
地平线开发者1 小时前
征程 6 工具链性能分析与优化 2|模型性能优化建议
算法·自动驾驶
Duck Bro1 小时前
数据结构:顺序表(动态顺序表)
c语言·数据结构·c++·学习·算法