5月26日每日学习

string的实现

0.类的private部分

namespace lcj
{
	class string
	{
	public:


	private:
		size_t _capacity; 
		char* _str;
		size_t _size; 
	};
}

0.1:c_str

c_str是啥,能干嘛:

  • 是string中的一个函数
  • 能把一个string转化为c风格的字符串(char) const char* cstr = s.c_str();

可以直接用于 C 字符串函数

——>能使用strlen(string不行)




‼️注意点:

  1. 不是new出来的,不用delete(指针指向 string 内部管理的内存,不应 delete

  2. 修改string 后,之前获取的指针可能失效

    ——>想再次使用应该重新获取




实现

  • 在.h文件中,在namespace lcjclass string
		const char* c_str()const
		{
			return _str;
		}



0.2:npos

原始含义: “until the end of the string” --到字符串的末尾

原始定义static const size_t npos = -1;

实现:

  • 声明在.h文件lcj域中的string类中的private下:static const size_t npos;
  • 定义在.cpp文件lcj域中:const size_t string::npos = -1;

(奇怪的是cpp后面支持了static const类型的整形可以在.h中直接定义)






1.头文件定义函数导致的多文件重复定义问题

是啥:

函数定义只能出现一次。那么,如果我把用来测试的函数 定义 放在 .h文件中,

——> 两个.c文件都有这个头文件,非法

原因:编译链接的过程 :

  1. 预处理:把 #include 的头文件内容原封不动地复制到每个 .cpp 文件里。
  2. 编译:分别把 a.cppb.cpp 编译成 目标文件 a.obj / a.ob.obj / b.o
    此时 a.obj 里记录了“我定义了 sayHello”,b.obj 里也记录了“我定义了 sayHello”。
  3. 链接:把多个目标文件合并成一个可执行文件。
    链接器看到 a.objb.obj 都定义了一个叫 sayHello 的函数,按照 C++ 的单一定义规则(ODR, One Definition Rule),这是非法的,于是报错

解决方法

  1. 使用inline内联:链接器允许程序中有多个相同的 inline 函数定义,只要它们完全相同。链接器会随便选一个,或者全部丢弃,不会报错。
  2. 静态static成员函数:

静态成员函数

唯一区别:没有this指针

限制:

  • 不能访问普通成员变量和成员函数(因为没有this指针)
  • 只能访问静态(static)变量和函数

使用上:

  1. 普通成员函数(唯一使用方法):a1.GetCount(); ( 有this指针)

  2. 静态成员函数(成员变量也可以):

    1. 直接用:A::GetCount();(✅ 推荐,更方便)(成员变量也可以

    2. 先创建类对象再用:

      A a1;

      a1.GetCount(); (没有this指针,但能找到类域)

⬇️静态成员变量,所有同类对对象共享——>用来计数

  1. 最标准的方法:声明和定义分离

MyString.h

#pragma once
#include<iostream>
using namespace std;

namespace lcj
{
	class string
	{
	public: …… 
	private: …… 
	};



	void test_string1();
    
    
}

MyString.cpp

#include"MyString.h"
namespace lcj
{
	void test_string1()
	{
		string s1;
		string s2("hello world");
	}
}

test.cpp

#include"MyString.h"

int main() 
{
    lcj::test_string1();
    return 0;
}





2.构造函数

2.1带参构造函数的坑点

❌看似正确:

		string(const char* s)
			:_size(strlen(s))
			, _str(new char[_size + 1])
		{ 
         }


	private:
		size_t _capacity; 
		char* _str;
		size_t _size; 

‼️错误点:初始化列表的 初始化顺序 是 按 声明顺序不是初始化列表顺序‼️

——>_str(new char[_size + 1])中的:_size随机数

为什么不建议在初始化列表中定义:

  1. 上面说的,_str(new char[_size + 1])中的:_size 可能因为声明顺序的原因,是随机数
  2. 即使改变了声明顺序,但,如果有人改了代码,就完了

正确的方式

——>建议在函数体内直接定义


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

	private:
		size_t _capacity; 
		char* _str;
		size_t _size; 





2.2默认构造函数

❌看似正确:但是,没法打印——打印时候对空指针解引用

		string()
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{

		}

正确写法:

		string()
			:_str(new char[1]{'\0'})//得写成这样,才能像数组一样操作
			, _size(0)
			, _capacity(0)
		{

		}





2.3合一 ——上面两个构造函数

		string(const char* s = "")//默认有一个‘\0’
		{
			_size = strlen(s);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, s);//strcpy也会加上‘\0’
		}





3.析构函数

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





4.size

		size_t size()
		{
			return _size;
		}



5.operator[ ]

➡️const修饰类的成员函数的知识在这里⬅️

➡️运算符重载的知识在这里⬅️

  • 作用:能直接通过下标访问string中的字符
  • 返回值:对应字符的引用

两个函数

  1. char& operator[] (size_t pos);
  2. const char& operator[] (size_t pos) const;
  • const char& operator[](size_t pos) const为啥末尾有const
    • 允许 const 对象调用:如果一个 string 对象被声明为 const,那么它只能调用那些被 const 修饰的成员函数。没有末尾 constoperator[] 版本无法被 const 对象使用。
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}


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





6.实现范围for

范围for的底层:通过迭代器(begin和end)遍历数组

而迭代器(iterator)是一种类型typedef出的结果

那我自己typedef一个指针作为迭代器不就行了:

实现迭代器:

  • 迭代器不一定是指针,但在此我们可以通过指针来替代
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

这样就可以使用范围for

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

实现const迭代器

  • 和迭代器是函数重载,(参数不同,一个是普通this指针,一个是const this指针)
		typedef const char* const_iterator;

		const_iterator begin() const 
		{
			return _str;
		}

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





5月27日每日学习

7.尾插:push_back、append和+=

➡️运算符重载的知识⬅️




7.0 .h文件中:

        void reserve(size_t n);///开空间


		void push_back(char ch);
		void append(const char* str);

		string& operator+=(char ch);
		string& operator+=(const char*  str);



7.1 reserve:

‼️注意,开的空间 比容量大1

	void string::reserve(size_t n)///开空间
	{
		if (n > _capacity)
		{
			///开新空间,移动数据,删旧空间,改变_str和_capacity
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}



7.2 push_back

❌push_back 的错误写法

  • ‼️没补充最后的\0

	void string::push_back(char ch)
	{
		///检查容量
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}

		_str[_size] = ch;
		++_size;

	}


正确写法

	void string::push_back(char ch)
	{
		///检查容量
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}

		_str[_size] = ch;
		++_size;

		_str[_size] = '\0';
	}



7.3+=单个字符

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



7.4 append

  1. 判断增加后的容量是否超过
    1. 求长度
    2. 扩容
  2. 通过C语言中的strcat函数进行后插
  3. 改变_size
  • 为什么不修改_str和_capacity❓

——>因为reserve函数中已经修改过了


	void string::append(const char* str)
	{
		size_t len = strlen(str); 

		if (len + _size > _capacity)
		{ 
			reserve(len + _size > _capacity ? _capacity + len : 2 * _capacity);
		}


		strcat(_str, str);
		_size += len;
	}





7.5 +=字符串

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





8.insert

a.插入一个字符:

  1. 断言
  2. 像前面的push_back一样,判断要不要扩容
  3. 不断向后挪动数据

‼️特殊情况:

头插,如果写end>=pos(此时pos是0 )挪动数据,就无法成功,

不论end是int还是size_t,

因为pos是size_t类型的(标准库中就是这样的),二者比较的时候会类型转换,范围大转范围小,end会变成size_t类型,导致永远都>=0

解决方式:

  • 方法一:end标成int类型,且将size_t 类型的pos进行强制类型转换为int
  • 方法二:使用指针不用下标
  • 方法三:(end和pos都是size_t类型),前面说的都是从原本字符串的最后一个位置开始向后挪,方法三是,从最后一个位置的后一个位置,赋值为前一个位置上的字符,
    • 这样,本来10个字符的string下标为0~9,
    • 想要头插,先让s[10]=s[9],然后是s[9]=s[8],……最后是s[1]=s[0]。
    • 就不会遇到需要end>=pos的情况了——end从_size+1开始(\0也会被挪动),直到pos+1,所以这时候条件是end> pos

MyString.h

		void insert(size_t pos, char ch);

MyString.cpp

	void string::insert(size_t pos, char ch)
	{ 
		assert(pos < _size);
		///检查容量
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
1. end从 _size 开始(\0不 动)
		//size_t end = _size;

		//while (end > pos)
		//{
		//	_str[end] = _str[end - 1];
		//	--end;
		//}

		//_str[end] = ch; 
		//++_size; 
		//_str[_size] = '\0';

1. end从 _size + 1 开始(\0 会被挪动)
		size_t end = _size + 1;

		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}

		_str[end] = ch;
		++_size; 

	}



b.插入字符串

MyString.h

		void insert(size_t pos, const char* str);

MyString.cpp


	void string::insert(size_t pos, const char* str)
	{
		assert(pos < _size);


		size_t len = strlen(str); 

		if (len + _size > _capacity)
		{
			size_t newcapacity = len + _size > _capacity ? _capacity + len : 2 * _capacity;
			reserve(newcapacity);
		}

		///如果不按照下面这种,逐个后移,和上面插入单个字逻辑相同,也是可以的
		memmove(_str + pos + len , _str + pos, _size + 1 - pos);
		memmove(_str + pos, str, len);


		_size += len;
	}





9.erase

a.后面的全删

b.部分删除

MyString.h

		void erase(size_t pos, size_t len = npos);

MyString.cpp

	void string::erase(size_t pos, size_t len )
	{
		assert(pos < _size);
		if (len > _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}

		else
		{
			memmove(_str + pos, _str + pos + len, _size - pos - len + 1);
			_size = _size - len;
		}

	}





10.find

‼️注意:缺省参数在声明(.h)和定义(.c)中不能同时出现。建议在声明(.h)中写

MyString.h

		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);

MyString.cpp

	size_t string::find(char ch, size_t pos )
	{
		for (int i = 0; i < _size; i++)
		{
			if (ch == _str[i])
				return i;
		}
		return npos;

	}

	size_t string::find(const char* str, size_t pos )
	{
		char* f = strstr(_str, str);
		if (f == nullptr)
			return npos;
		else
			return f - _str;/////////注意不要写成“str”了
	}





11.拷贝构造函数

如果没有这个函数来实现深拷贝,那函数中的string作为返回值就会出错,下面的substr函数也实现不了

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





12.substr

原型:string substr (size_t pos = 0, size_t len = npos) const;

两种情况:

  1. 如果len大于pos后面的字符个数,那就直接把后面的字符串全返回
  2. 如果len小于pos后面的字符个数,那就部分返回

MyString.h

		string substr(size_t pos = 0, size_t len = npos);

MyString.cpp

	string string::substr(size_t pos , size_t len )
	{
			string ret;
		if (len >= _size - pos)
		{
			for (int i = pos; i < _size; i++)
				ret += _str[i];
		}

		else
		{  
			for (int i = pos; i < pos+len; i++)
				ret += _str[i];
		}

		return ret;
	}





13.赋值运算符重载

‼️注意点:

  1. 因为在赋值之前要delete原内存,所以这时候要防止自己给自己赋值
		string& operator=(const string& s)
		{
			if (&s != this)
			{
				delete[] _str;
				_str = new char[_capacity + 1];
				strcpy(_str, s._str);
				_capacity = s._capacity;
				_size = s._size;
			}

			return *this;
		}





14. 比较

MyString.h

		bool operator>(const string& s);
		bool operator==(const string& s);
		bool operator<(const string& s);
		bool operator>=(const string& s);
		bool operator<= (const string & s);
		bool operator!=(const string& s);

MyString.cpp

	bool string::operator>(const string& s)
	{
		return strcmp(_str, s._str) > 0;
	}
	bool string::operator==(const string& s)
	{
		return strcmp(_str, s._str) == 0;

	}
	bool string::operator<(const string& s)
	{
		return (!((*this) == s)) && (!((*this) > s));
	}
	bool string::operator>=(const string& s)
	{
		return !operator<(s);
	}
	bool string::operator<= (const string& s)
	{
		return !operator>(s);
	}
	bool string::operator!=(const string& s)
	{
		return !operator==(s);
	}

更多推荐