2.string模拟实现

2.1string模拟实现的注意事项

错误警示1

在 string.h 中定义了一个非内联的全局函数 test_string1()

这里容易犯一个错误,这里注意,类里面的成员函数定义默认是内联函数,不会放在符号表,不会冲突。而在类外面定义全局函数或者全局变量,在Test.cpp和string.cpp都包含了string.h文件,于是test_string1()函数就会各自在两个文件中有一份,在链接时,有两份test_string1()于是就引发冲突的问题。

这里有三种解决方案

  1. 函数声明成static静态static 用于修饰全局函数或变量时,会改变其链接属性。它将符号的链接属性从外部链接改为内部链接,从而该函数/变量的名字仅在当前编译单元(即当前源文件) 内可见。每个包含了该头文件的源文件都会获得一份属于自己的、独一无二的 test_string1() 副本
  2. 设计成inline内联函数(在 C++17 之前,inline 的主要功能是给编译器一个“建议”,建议将函数调用处用函数体替换,以优化性能。但更重要的是,它赋予了函数豁免ODR约束的能力。一个被定义为 inline 的函数或变量(C++17起),允许多个编译单元中存在其定义。链接器会确保所有这些相同的定义最终只保留一份)
  3. 只在头文件中声明,在一个源文件中定义()

前两者本质上就是只在当前文件可见,不进入符号表

错误警示2

构造函数中使用 _str = nullptr 初始化然后在 string.cpp 中打印空对象会导致程序崩溃

 C++ 的 std::cout 和 C 的字符串函数(如 strlenprintf 等)都要求传入的 char* 是一个有效的以空字符结尾的字符串,或者是 nullptr(但需要特殊处理)。nullptr 表示"空指针",不是一个有效的字符串地址。cout 会尝试访问 nullptr 指向的内存来读取字符,这是非法内存访问,导致段错误。

正确做法:应该将空字符串初始化为一个有效的空字符串

错误警示3

wihle循环中整型提升的问题

我们在模拟实现insert函数时,在测试头部插入,也就是insert(,'&')时,程序会崩。这里原因就是在挪动数据时,当end=0时,将[0]挪动到[1]位置时,end--变成-1,原本我们思考的是end>=0时循环继续,end=-1时跳出循环,但是调试时循环没有结束,这里的原因是一个操作符两边的操作数类型不同时,编译器会进行整型提升,一般会从范围小的提升至范围大的类型,由于pos是size_t类型,比较时(循环条件)编译器会将int提升至size_t类型,从而将-1的补码(全是1)直接当作size_t类型,解析出来就是整型的最大值(4,294,967,295)

修正方案1:改变循环条件和方向(推荐)

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; // 从末尾的下一个位置开始(包括'\0')
    // 循环条件:当 end 的位置大于目标位置 pos 时,就一直往前挪
    while (end > pos)
    {
        _str[end] = _str[end - 1];
        end--;
    }
    _str[pos] = ch;
    _size++;
}

修正方案2:强制转换(不推荐,但要知道)

在比较时,将无符号数强制转换为有符号数,但这可能会在 pos 很大时引入新的问题。

    while (end >= (int)pos) // 强制将pos转为int,使比较在有符号数之间进行
    //后续代码不变

错误警示4(重要)

substr中构造子字符串的缺少深拷贝构造函数问题

默认拷贝构造函数:编译器生成的是“浅拷贝”或“按成员拷贝”,它只是简单地复制指针 _str 的值。这导致两个 string 对象(sub 和 suffix)的 _str 成员指向同一块堆内存。当其中一个对象(函数内的局部对象 sub)先被销毁时,它会释放那块共享的内存。另一个对象(如 suffix)的 _str 指针就变成了野指针。后续任何对该对象的访问(如 cout << suffix.c_str())或它自己的析构函数被调用时,都会导致未定义行为,通常是程序崩溃。

注意在不同平台下的不同模式会进行优化(前面章节有非常详细介绍),可能会“合三为一”(非常激进的优化)就是将临时对象和sub对象优化,直接将suffix充当sub对象,也就是整个过程中压根没有拷贝构造,程序也就正常运行了,并不是程序将bug修复了,而是没有触发bug。

解决方案:自己实现深拷贝构造函数

//s2(s1);
string(const string& s)
{
	_str=new char[s._capacity+1];//这里+1是多分配一个空间,用来存放\0
	strcpy(_str,s._str);//拷贝数据
	_size = s._size;
	_capacity = s._capacity;
}

2.2iterator

iterator

iterator都是typedef来的只不过底层可能是指针有可能不是指针,模拟的是指针的行为。

这里可以使用原生指针来做迭代器,因为底层结构是数组。如果如后续所要介绍的链表就不是原生指针来实现迭代器,而是自定义类型,后续会详细了解。

迭代器其实就是封装的一种体现,迭代器通过提供一个统一的、简单的接口来遍历各种不同的数据结构,从而将遍历操作与数据结构的底层实现细节分离开来。这种“隐藏实现细节,暴露统一接口”的行为,正是封装的核心思想。

封装的特点:

  • 隐藏内部实现细节:将对象的状态(数据)和行为(方法)包装在一起,并尽可能隐藏内部的复杂实现。

  • 暴露公开的接口:只提供一个清晰、简洁的接口与外部交互。

  • 目的:降低系统的耦合度,提高代码的模块化、可维护性和安全性。使用者不需要知道内部是如何工作的,只需要知道如何使用接口即可。

2.3传统拷贝和现代拷贝

传统拷贝和现代拷贝

简单来说传统拷贝和现代拷贝的最主要的区别就是,谁负责管理拷贝过程中的资源,效率上没有区别。

  • 传统拷贝程序员手动管理。你需要自己写代码new出新内存,再把数据复制过去,费时费力且容易出错。

  • 现代拷贝编译器或标准库工具自动管理

拷贝构造函数

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)
//{
//	_str=new char[s._capacity+1];//这里+1是多分配一个空间,用来存放\0
//	strcpy(_str,s._str);
//	_size = s._size;
//	_capacity = s._capacity;
//}
//现代版本
string(const string& s)
{
	string tmp(s._str);//用s1的_str构造tmp
	swap(tmp);//等价于 this->swap(tmp);
}

注意:

  1. 这里s就是s1,this指向s2
  2. 如果我们没有在成员变量声明的地方给缺省值,对于内置类型不一定会初始化,如果把随机值交换给tmp,当tmp这个局部对象出作用域后,会调用析构进行释放资源,这时候如果_str未初始化就释放野指针了。

如果在swap函数中没有使用std::swap而直接使用swap,编译器会报参数不匹配的错误,这是因为我们希望调用算法库里面的swap,而不是我们写的这个swap。


赋值也是同理

//s2=s1;//赋值运算符重载
//在此函数中:this 指针指向 s2,参数 s 是 s1 的引用
//传统方法
//string& operator=(const string& s)
//{
//	if (this != &s)//防止自己给自己赋值
//	{
//		delete[] _str;//为了防止空间不够,或者空间过大浪费,重新分配s2空间
//		_str = new char[s._capacity + 1];
//		strcpy(_str, s._str);
//		_size = s._size;
//		_capacity = s._capacity;
//	}
//	return *this;// 返回s2的引用
//}
//现代方法
string& operator=(const string& s)
{
	if (this != &s)//防止自己给自己赋值
	{
		string tmp(s._str);// 1. 利用s1的字符串构造一个临时副本tmp
		swap(tmp);         // 2. 交换s2(this)和tmp的内容。
                            //现在s2拥有了tmp新分配的资源,tmp则拿到了s2的旧资源。
	}
	return *this;
    // 3. 函数结束,局部对象tmp析构,自动释放了s2原来的旧资源。
}

与上面不同的是,s2是已经存在的对象,用s1(s)的_str构造tmp后,用swap交换,s2(this)的旧资源就给了tmp,然后新构造的tmp的新资源换给了s2(this)。由于tmp是局部对象,于是tmp出作用域后调用析构函数就清理了s2的旧资源

//一步到位s1=s2// 调用赋值运算符
string& operator=(string tmp)// 传值!tmp 是 s2 的副本
{
	swap(tmp); // this->swap(tmp); 交换当前对象(s1)和tmp的资源

    return *this;
}// tmp 析构,释放掉 s1 原来的旧资源

这里没有使用引用传参,而是传值传参,传值传参会调用拷贝构造,s2传给tmp对象,交换后,依然是走上面分析的流程。


这里有个问题,之前我们自己实现的swap和算法库里面的swap有什么区别
int main()
{
    string s1("hello world");
    string s2("xxxxxxxxxxxxxx");
    std::swap(s1,s2);
    s1.swap(s2);
    return 0;
}

两个swap都可以实现交换,但是这里的差别就是深拷贝的问题,在这个例子中,算法库里面会有三次深拷贝(效率很低):

  1. 用a(s1)深拷贝一个c(tmp)(一次拷贝构造)
  2. 再把b(s2)赋值给a(s1)(一次拷贝赋值,深拷贝)
  3. 再将c(tmp)赋值给b(s2)(一次拷贝赋值,深拷贝)

当然为了防止这一情况出现,库里面还提供了全局的swap函数

之前有提到过,当函数模板和全局函数同时存在时,会优先使用现成的全局函数,也就是说不会调用深拷贝的版本。

2.4string模拟实现

string.h

#include<iostream>
#include<assert.h>
using namespace std;
namespace name
{
	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;
		}

		//短小频繁的函数,直接在类内定义(默认是inline)
		//string()
		//	: _str(new char[1] {'\0'})//这里不能写成_str = nullptr
		//	, _size(0)
		//	, _capacity(0)
		//{}
		string(const char* str="")//常量字符串默认有一个\0,如果给"\0"就有两个\0
		{//这里不建议用初始化列表,存在初始化顺序问题,推荐函数体内初始化
			_str = new char[strlen(str)+1];//这里+1是多分配一个空间,用来存放\0
			_size= strlen(str);
			_capacity = _size;//capacity不包含\0
			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)
		//{
		//	_str=new char[s._capacity+1];//这里+1是多分配一个空间,用来存放\0
		//	strcpy(_str,s._str);
		//	_size = s._size;
		//	_capacity = s._capacity;
		//}
		//现代版本
		string(const string& s)
		{
			string tmp(s._str);//用s1的_str构造tmp
			swap(tmp); //等价于 this->swap(tmp);
		}

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

		void clear()
		{
			_str[0]='\0';
			_size = 0;//不动capacity
		}

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

		//s2=s1;//赋值运算符重载
		//在此函数中:this 指针指向 s2,参数 s 是 s1 的引用
		//传统方法
		//string& operator=(const string& s)
		//{
		//	if (this != &s)//防止自己给自己赋值
		//	{
		//		delete[] _str;//为了防止空间不够,或者空间过大浪费,重新分配s2空间
		//		_str = new char[s._capacity + 1];
		//		strcpy(_str, s._str);
		//		_size = s._size;
		//		_capacity = s._capacity;
		//	}
		//	return *this;// 返回s2的引用
		//}
		//现代方法
		string& operator=(const string& s)
		{
			if (this != &s)//防止自己给自己赋值
			{
				string tmp(s._str);// 1. 利用s1的字符串构造一个临时副本tmp
				swap(tmp);         // 2. 交换s2(this)和tmp的内容。
				//现在s2拥有了tmp新分配的资源,tmp则拿到了s2的旧资源。
			}
			return *this;
			// 3. 函数结束,局部对象tmp析构,自动释放了s2原来的旧资源。
		}
		////一步到位s1=s2// 调用赋值运算符
		//string& operator=(string tmp)// 传值!tmp 是 s2 的副本
		//{
		//	swap(tmp); // this->swap(tmp); 交换当前对象(s1)和tmp的资源

		//	return *this;
		//}// tmp 析构,释放掉 s1 原来的旧资源

		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,const char* str);
		void insert(size_t pos,char ch);

		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;
		//char _buff[16];
		size_t _size=0;
		size_t _capacity=0;
		static const size_t npos;
		//static const size_t npos=-1;//ok,但是不推荐,可以理解为编译器特殊处理
		//static const double N=1.2;//error
	};

	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 test_string1()//Test.cpp和string.cpp都包含string.h头文件,编译器会报错
	//{
	//	string s1("hello");
	//	string s2;
	//}

	void test_string1();//声明
	void test_string2();//声明
	void test_string3();//声明
	void test_string4();//声明
	void test_string5();//声明
	void test_string6();//声明
	void test_string7();//声明
}

String.cpp

#include"string.h"
namespace name
{
	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';//字符串最后要加'\0'
	}
	
	void string::append(const char* str)
	{
		size_t len= strlen(str);
		if (_size + len > _capacity)
		{
			//大于2倍,需要多少就申请多少,否则申请2倍
			reserve(len+_size>2*_capacity?len+_size: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, const char* str)
	{
		assert(pos<=_size);
		size_t len = strlen(str);

		if (str == nullptr) return;  // 检查空指针
		if (len == 0) return;        // 空字符串不需要操作

		if (len + _size > _capacity)//\0都单独开了,这里不用管
		{
			reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
		}
		//挪数据
		int end = _size;
		while (end >= (int)pos)
		{
			_str[end+len]= _str[end];
			end--;
		}
		memmove(_str+pos,str,len);
		_size += len;
	}

	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)//为了防止当end=0时,end--变成-1,却被整型提升成size_t,所以pos要强制转换成int
		{
			_str[end + 1] = _str[end];
			end--;
		}
		_str[pos] = ch;
		_size++;
	}
	
	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 (ch == _str[i])
				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;
		return ptr-_str;//两个指针相减,就是下标
	}

	string string::substr(size_t pos, size_t len)//传值返回
	{
		assert(pos<_size);
		//更新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);
	}
	bool operator<(const string& s1, const string& s2)
	{
		//int strcmp(const char* str1, const char* str2);
		//Compares the C string str1 to the C string str2.
		//This function starts comparing the first character of each string. If they are equal to each other, 
		//it continues with the following pairs until the characters differ or until a terminating null-character is reached.
		return strcmp(s1.c_str(), s2.c_str()) < 0;
	}
	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return s1<s2 || s1 == s2;
	}
	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 = 512;
		char buff[N];
		int i = 0;

		char ch;
		//in >> ch;//读到空格或者换行了,但是会被认为分隔符,直接被忽略,也就不能结束循环
		in.get(ch);
		while (ch != '\n' && ch != ' ')
		{
			buff[i++] = ch;//性能优化,减少扩容次数
			if (i==N-1)//满了
			{
				buff[i]='\0';
				s+=buff;
				i = 0;
			}

			//s += ch;
			//in >> ch;
			ch=in.get();
		}

		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
}

Test.cpp

#include"string.h"

namespace name
{
	//测试
	void test_string1()
	{
		string s1;
		string s2("hello world");
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;

		for (size_t i = 0; i < s2.size(); i++)
		{
			s2[i] += 2;
		}
		cout << s2.c_str() << endl;

		for (auto& ch : s2)//范围for底层就是迭代器
		{
			ch -= 2;
		}
		cout << s2.c_str() << endl;

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

	void test_string2()
	{
		string s1("hello world");
		s1 += 'd';
		s1 += '!';
		cout << s1.c_str() << endl;

		s1 += "1234567890";
		cout << s1.c_str() << endl;

		s1.insert(5,'#');
		cout << s1.c_str() << endl;

		s1.insert(0, '#');
		cout << s1.c_str() << endl;

		string s2("hello world");
		s2.insert(1, "1234567890");
		cout << "s2:" << s2.c_str() << endl;
	}

	void test_string3()
	{
		string s1("hello world");
		s1.erase(5);
		cout << s1.c_str() << endl;

		string s2("hello world");
		s2.erase(5,100);
		cout << s2.c_str() << endl;

		string s3("hello world");
		s3.erase(5, 2);
		cout << s3.c_str() << endl;
	}

	void test_string4()
	{
		string s("test.cpp.zip");
		size_t pos = s.find('.'); 
		string suffix = s.substr(pos);
		cout << suffix.c_str()<< endl;

		/*string copy(s);//要自己写拷贝构造函数,否则会调用系统默认的浅拷贝构造函数,于是两个指针指向同一块空间,析构的时候会出错
		cout<<copy.c_str()<<endl;*/

		s = suffix;//默认生成的赋值也是浅拷贝,
		cout << s.c_str() << endl;
		cout << suffix.c_str() << endl;

		s = s;
		cout << "self" << s.c_str() << endl;
	}

	void test_string5()
	{
		string s1("hello world");
		string s2("hello world");
		cout << (s1 == s2) << endl;
		cout << (s1 != s2) << endl;
		cout << (s1 > s2) << endl;
		cout << (s1 < s2) << endl;
		cout << (s1 >= s2) << endl;
		cout << (s1 <= s2) << endl;
		cout << (s1 == "hello world") << endl;//隐式类型转换,先调用构造函数,再调用运算符重载
		cout << ("hello world" == "hello world") << endl;//不会隐式类型转换,因为运算符重载必需要有一个参数是类类型
	}

	void test_string6()
	{
		string s1("hello world");
		cout<< s1.c_str()<<endl;
		string s2(s1);
		/*cin >> s1;
		cout << s1.c_str() << endl;
		cin >> s2;*/
		cout << s2.c_str() << endl;
	}

	void test_string7()
	{
		string s1("hello world");
		string s2("xxxxxxxxxxx");

		//std::swap(s1,s2);
		s1.swap(s2);
		cout << s1 << endl;
		cout << s2 << endl;
	}
}

int main()
{
	//name::test_string1();
	//name::test_string2();
	//name::test_string3();
	//name::test_string4();
	//name::test_string6();
	name::test_string7();
	return 0;
}

 

 

更多推荐