【C++初阶】string类的模拟构建

前面学习了string类的常见接口,这里我们来模拟实现一下,由于有一些接口现在的的知识还没有掌握,我们实现了一下接口

1.string模拟实现代码总览

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
using namespace std;
namespace gjy//为了与标准库区分,这里用string,放在自己的命名空间里
{
	class string
	{
	public:
		using iterator = char*;//类型别名,起一个别名表示迭代器,(其实就是指针)
		using const_iterator = const char*;
		//-------------一、默认成员函数-------------
		string(const char* str = "")//为什么是这个参数,
			:_size(strlen(str))
			, _capacity(_size)
			, _str(new char[_capacity + 1])
		{
			assert(str!=nullptr);
			//_size = strlen(str);//不包含\0
			//_capacity = _size;//先设置成size的大小,后面再扩容
			//_str = new char[strlen(str) + 1];//new返回对象的类型,指向新开建的的空间,+1是为存放\0
			strcpy(_str, str);
		}
		//拷贝构造
		string(const string& s)
			:_size(s._size),
			_capacity(s._capacity),
			_str(new char[_capacity+1])
		{
			strcpy(_str, s._str);
		}
		////析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
		}
		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;
		}
		//-------------二、容量操作接口-------------
		void checkcapacity()
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
		}
		size_t size()const
		{
			return _size;
		}
		size_t capacity()const
		{
			return _capacity;
		}
		size_t length()const
		{
			return _size;
		}
		void reserve(size_t n)//小于等于size的时候不做处理(肯定也就小于capacity)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];//选择 strncpy 而非 strcpy,核心是为了控制拷贝长度,防止缓冲区溢出,让字符串操作更安全、更可控。
				strncpy(tmp, _str,_size+1);//strcpy不检查目标空间是否足够,会造成溢出
				delete[] _str;
				_capacity = n;
				_str = tmp;
			}
		}
		void resize(size_t n,char ch='\0')//将有效字符的个数改为n个,多余的用字符c填充
		{
			if (n < _size)
			{
				_size = n;
				*(_str + _size) = '\0';//直接提前结束就可以了,\0下标是_size
			}
			else
			{
				if (n > _capacity)
				{
					reserve(n);
				}
				for (int i = 0;i <(n - _size);i++)
				{
					_str[_size + i] = ch;/// 使用参数 ch 而非硬编码 'c'
				}
				_size = n;
				_str[n] = '\0';
			}
		}
		bool empty()const
		{
			return(!(bool)_size);
		}
		void clear()
		{
			_size = 0;
			_str[_size] = '\0';//清空有效字符,保留空间
		}
		//-------------三、访问元素和遍历操作,返回引用减少拷贝-------------
		char& operator[](size_t pos)//引用返回减少拷贝
		{
			assert(pos < _size);
			return *(_str + pos);
		}
		const char& operator[](size_t pos)const//[]处理越界是断言,at是抛出异常
		{
			assert(pos < _size);
			return *(_str + pos);
		}
		char& at(size_t pos) {
			if (pos >= _size) {  // 检查下标是否越界
				throw std::out_of_range("string::at: pos out of range");
			}
			return _str[pos];
		}
		const char& at(size_t pos) const {
			if (pos >= _size) {  // 检查下标是否越界
				throw std::out_of_range("string::at: pos out of range");
			}
			return _str[pos];
		}
		//迭代器
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return (_str + _size);
		}
		const_iterator begin()const//写成cbegin编译器识别不出来
		{
			return _str;
		}
		const_iterator end()const//不能写成cend,编译器识别不出来
		{
			return _str+_size;
		}
		//rend和rbegin直接反过来
		//范围for:基于迭代器实现的,有了迭代器自己就有了,不用手动实现
		//back和front
		char& back()
		{
			assert(_size > 0);  // 空字符串禁止调用
			return _str[_size - 1];
		}
		char& front()
		{
			assert(_size > 0);  // 空字符串禁止调用
			return _str[0];
		}
		//-------------四、字符串修改操作-------------
		void push_back(char n)
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size++] = n;
			_str[_size] = '\0';
		}
		void append(const char* str)
		{
			size_t len_str = strlen(str);
			size_t new_size = _size + len_str;  // 单独计算原字符串长度
			if (new_size > _capacity)
			{
				reserve(new_size);
			}
			strncpy(_str + _size, str,len_str);// 只拷贝 len_str 个字符
			_size = new_size;
			_str[_size] = '\0';// 手动补结束符(strncpy 可能不补)
		}
		string& operator+=(const char ch)
		{
			push_back(ch);//成员函数一样有this指针
			return *this;
		}
		string& operator+=(const char* _s)
		{
			append(_s);
			return *this;
		}
		string& insert(size_t pos, const char ch)
		{
			assert(pos <=_size);
			checkcapacity();
			for (size_t i = _size;i > pos;i--)
			{
				_str[i] = _str[i - 1];
			}
			_str[pos] = ch;
			_size++;
			_str[_size] = '\0';
			return *this;
		}
		string& insert(size_t pos, const char* str)
		{
			assert(pos <=_size);
			assert(str != nullptr);
			size_t len = strlen(str);//计算插入的长度
			if (len == 0)
			{
				return *this;
			}
			// 检查容量,不够则扩容(至少扩到能容纳插入后的总长度)
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			// 关键:从后往前挪动原有字符,腾出 len 长度
			for (size_t i = _size; i >= pos; --i) {
				_str[i + len] = _str[i];
			}
			//拷贝插入的字符串到pos位置
			strncpy(_str + pos, str, len);//拷贝len歌字符,不含str自带的\0
			//更新大小和结束符
			_size += len;
			_str[_size] = '\0';
			return *this;
		}
		string& erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
			size_t n = _size - pos;
			if (n <=len)
			{
				_size = pos;
				_str[_size] = '\0';
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}
		//------------五、运算符重载-------------
		bool operator>(const string& s)const
		{
			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);
		}
		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);
		}
		//输入输出重载
		friend std::ostream& operator<<(std::ostream& out, const string& s);
		friend std::istream& operator>>(std::istream& in, string& s);
		friend std::istream& ::getline(std::istream& in, string& s);
		//------------六、字符串操作------------
		//返回c风格的字符串
		const char* c_str()const
		{
			return _str;
		}
		//交换函数
		void swap(string& s)//直接用库里的swap函数,仅交换成员变量,无内存分配/释放,高效且安全
		{
			// 交换所有成员变量(指针、大小、容量)
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		//查找
		size_t find(const char ch, size_t pos = 0)
		{
			assert(pos < _size);
			for (size_t i = pos;i < _size;i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}
			return npos;
		}
		//正向查找第一个匹配的字符串
		size_t find(const char* str, size_t pos = 0)
		{
			assert(pos < _size);
			const char* ret = strstr(_str + pos, str);//返回类型是char*类型的指针,子串首次出现的起始位置
			if (ret)
			{
				return ret - _str;//返回子字符串第一个字符的下标,两个指针相减代表相隔个数(p2-p1/sizeof(char))
			}
			else
			{
				return npos;
			}
		}
		string substr(size_t pos, size_t len = npos)
		{
			size_t leftlen = _size - pos;
			if (len > leftlen)
				len = leftlen;
			string tmp;
			tmp.reserve(len);
			for (size_t i = 0;i>(std::istream& in, gjy::string& s)
	{
		s.clear();
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}
	//读取一行含有空格的字符串
	std::istream& getline(std::istream& in, string& s)
	{
		s.clear(); //清空字符串
		char ch = in.get(); //读取一个字符
		while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
		{
			s += ch; //将读取到的字符尾插到字符串后面
			ch = in.get(); //继续读取字符
		}
		return in;
	}
}
const size_t gjy::string::npos = -1;//静态成员类外定义

2.默认成员函数

2.1.构造函数

//构造函数
string(const char* str = "")//为什么是这个参数,''单引号是字符常量不包含\0
		:_size(strlen(str))
		, _capacity(_size)
		, _str(new char[_capacity + 1])//多一个空间来存放\0
	{
		assert(str!=nullptr);
		//_size = strlen(str);//不包含\0
		//_capacity = _size;//先设置成size的大小,后面再扩容
		//_str = new char[strlen(str) + 1];//new返回对象的类型,指向新开建的的空间,+1是为存放\0
		strcpy(_str, str);
	}


2.2.拷贝构造函数

在显式定义拷贝构造函数之前先聊一聊深浅拷贝的问题,如果我们没有显式实现拷贝构造编译器默认生成的类的拷贝构造就是浅拷贝只是值进行拷贝,没有对重新分配资源,这样会导致很多问题:在程序结束过后对资源进行两次析构,此外前面一个对象析构过后再访问拷贝产生的对象还会造成野指针的访问。

2.2.1.浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致 多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该 资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

2.2.2.深拷贝

        拷贝产生的对象和原对象拥有独立的空间,重新为新对象分配资源,修改不会影响彼此,一般分配了资源的对象都需要在拷贝构造中实现深拷贝(也就是需要手动实现析构函数的对象),一般拷贝构造都是使用的深拷贝。

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

方法一:传统实现

//拷贝构造
string(const string& s)
	:_size(s._size),
	_capacity(s._capacity),
	_str(new char[_capacity+1])//多的一个空间存放'\0'
{
	strcpy(_str, s._str);
}

首先给新对象分配源对象capacity+1大小的空间大小,再将成员变量拷贝给新对象,可以使用C语言的strcpy函数进行字符串内容的拷贝。

方法二:现代写法

//现代写法
 string(const String& s)
 : _str(nullptr),
   _size(0)
   _capacity(0)
 {
 string strTmp(s._str);//这里是构造函数,不是拷贝构造
 swap(_str, strTmp._str);
 }

现代写法的原理就是先用咱们写的构造函数用s的内容进行构造,这里在函数中构造的临时变量出了作用域就会销毁(这样完成了代码的复用),后面再使用swap函数进行两个对象的值的交换,相当于这个临时变量虽然没了但是他分配的资源却被我们的新对象使用了,新对象“借壳上市”,这里的swap函数是我们类里面的不是标准库里面的swap函数(那个模板函数实例化,还要分配空间代价很大,有自己的swap就不会用库里的),我们后面实现

这里swap函数调用了std库里的swap函数,不用我们在考虑深浅拷贝的问题


2.3.赋值运算符重载函数

与拷贝构造函数类似,实现赋值运算符重载也需要考虑深浅拷贝的问题,以下是深拷贝实现的两种形式:

2.3.1.传统写法

赋值运算符重载的传统实现与拷贝构造函数的传统实现基本一致,但有以下关键区别:

  1. 在为新字符串分配空间前,需要先释放左值对象原有的_str空间
  2. 操作前需检查是否为自赋值情况,若发现自赋值则直接返回无需处理
//赋值运算符重载
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;//返回左值(支持连续赋值)
	}

2.3.2.现代写法

赋值运算符重载函数的现代实现方式与拷贝构造函数的现代写法类似。两者的区别在于:拷贝构造函数通过构造临时对象并与原对象交换来实现,而赋值运算符重载函数则采用值传递接收右值参数,利用编译器自动调用拷贝构造函数生成对象副本,再将该副本与左值对象进行交换。

//现代写法1
string& operator=(string s) //编译器接收右值的时候自动调用拷贝构造函数
{
	swap(s); //交换这两个对象
	return *this; //返回左值(支持连续赋值)
}

        传值传参,编译器调用拷贝构造函数产生临时对象,出作用域过后就自动销毁,swap函数就是前面的swap函数, 但这种写法无法避免自己给自己赋值,就算是自己给自己赋值这些操作也会进行,虽然操作之后对象中_str指向的字符串的内容不变,但是字符串存储的地址发生了改变,为了避免这种操作我们可以采用下面这种写法:

//现代写法2
string& operator=(const string& s)
{
    if(*this != &s)//防止自己赋值自己
    {
        string tmp(s);  //利用拷贝构造出临时对象tmp
        swap(tmp);  //交换这两个对象
    }
    return *this;//返回左值
}


2.4.析构函数

        由于string类的成员_str指向堆内存空间,而对象销毁时这些堆空间不会自动释放,因此我们需要显式编写析构函数。在析构函数中使用delete操作来手动释放_str指向的堆内存,从而避免内存泄漏问题。

	////析构函数
	~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
	}

注意先delete再置空就行

3.容量操作

3.1.size() capacity() length()接口不用过多说明


3.2.reserve()

void reserve(size_t n)//小于等于size的时候不做处理(肯定也就小于capacity)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];//选择 strncpy 而非 strcpy,核心是为了控制拷贝长度,防止缓冲区溢出,让字符串操作更安全、更可控。多开一个存放\0
		strncpy(tmp, _str,_size+1);//strcpy不检查目标空间是否足够,会造成溢出
		delete[] _str;
		_capacity = n;
		_str = tmp;
	}
}

我们先来回忆一下reserve(n)函数的作用,为对象预留空间

  • 仅在 n 大于当前容量时,才会增加容量(至少到 n)1.5或者2倍;
  • 若 n 小于等于当前容量,容量不变,不会执行缩容操作。

注意:

        代码中使用strncpy而非strcpy来拷贝对象的C字符串,是为了确保即使字符串中包含有效字符'\0'也能完整拷贝(strcpy会在遇到第一个'\0'时终止拷贝)。


3.3.resize()

void resize(size_t n,char ch='\0')//将有效字符的个数改为n个,多余的用字符c填充
{
	if (n < _size)
	{
		_size = n;
		*(_str + _size) = '\0';//直接提前结束就可以了,\0下标是_size
	}
	else
	{
		if (n > _capacity)
		{
			reserve(n);
		}
		for (int i = 0;i <(n - _size);i++)
		{
			_str[_size + i] = ch;/// 使用参数 ch 而非硬编码 'c'
		}
		_size = n;
		_str[n] = '\0';
	}
}

        resize接口和reserve不同会改变对象的size

  • 当n大于当前的size时,将size扩展到n,扩展字符填充字符ch
  • 当n小于size时,将size减到n个


3.4.empty()

bool empty()const
{
	return(!(bool)_size);
}


3.5.clear()

void clear()
{
	_size = 0;
	_str[_size] = '\0';//清空有效字符,保留空间
}

  clear接口将size置为0,但是保留capacity空间


4.遍历和访问元素操作

4.1.operator []接口

char& operator[](size_t pos)//引用返回减少拷贝
{
	assert(pos < _size);
	return *(_str + pos);
}

        返回char&类型减少一次拷贝,同时支持对返回对象进行修改操作,返回它本身

我们来谈谈内置类型和自定义类型的传值返回

        如果返回的是基本数据类型(如 charintdouble 等),传值返回时会产生该类型的拷贝(但因为基本类型本身占用内存小,拷贝开销通常可忽略)。

        而如果返回的是自定义类型(如类对象),传值返回会触发对象的拷贝构造,产生较大开销;但对于基本类型,传值返回的拷贝开销非常小,所以代码中用 char& 引用返回,主要是为了支持对返回值的修改操作(比如 s[0] = 'a'; 这种场景),同时也顺带减少了基本类型的拷贝开销(虽然这部分开销本就很小)。

        基本类型传值返回会产生拷贝,但开销极小;自定义类型传值返回会产生对象拷贝构造,开销较大。代码中返回 char& 更关键的作用是支持修改,其次才是减少拷贝。

4.2.const operator[]

const char& operator[](size_t pos)const//[]处理越界是断言,at是抛出异常
{
	assert(pos < _size);
	return *(_str + pos);
}

4.3.at和const at

char& at(size_t pos) {
	if (pos >= _size) {  // 检查下标是否越界
		throw std::out_of_range("string::at: pos out of range");
	}
	return _str[pos];
}

原理和operator[]类似,只是处理越界问题的方式不一样,at是抛出异常,需要捕获,而[]是直接断言

const char& at(size_t pos) const {
	if (pos >= _size) {  // 检查下标是否越界
		throw std::out_of_range("string::at: pos out of range");
	}
	return _str[pos];
}

4.4.back() front()接口不用过多说明

//back和front
char& back()
{
	assert(_size > 0);  // 空字符串禁止调用
	return _str[_size - 1];
}
char& front()
{
	assert(_size > 0);  // 空字符串禁止调用
	return _str[0];
}


4.5.迭代器部分

        由于反向迭代器的实现太过于复杂,这里我们就只先实现正向的四个迭代器

预先操作,给类型取一个别名,方便返回值变量是iterator我们能看懂

using iterator = char*;//类型别名,起一个别名表示迭代器,(其实就是指针)
using const_iterator = const char*;

也可以这样写:

typedef char* iterator;
typedef const char* const_iterator;

begin() end()  begin()const  end()const

iterator begin()
{
	return _str;
}
iterator end()
{
	return (_str + _size);
}
const_iterator begin()const//写成cbegin编译器识别不出来
{
	return _str;
}
const_iterator end()const//不能写成cend,编译器识别不出来
{
	return _str+_size;
}

4.6.迭代器

        实际上,范围for循环的原理并不复杂。在代码编译阶段,编译器会自动将其转换为迭代器形式。也就是说,范围for循环的实现依赖于迭代器支持。既然我们已经实现了string类的迭代器,自然就能使用范围for循环来遍历string了。


5.修改操作

5.1.push_back()

push_back函数用于在字符串末尾追加字符。其实现逻辑如下:

  1. 首先检查当前容量是否足够,若不足则调用reserve进行扩容
  2. 完成字符追加后,必须在新增字符后手动添加字符串结束符'\0'
  3. 该操作确保字符串始终以'\0'结尾,避免打印时出现非法访问问题
	void push_back(char n)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size++] = n;
		_str[_size] = '\0';
	}

push_back功能也可通过复用后续实现的insert函数或者append来完成。

5.2.append()

        append函数的作用是在当前字符串的后面尾插一个字符串,尾插前需要判断当前字符串的空间能否容纳下尾插后的字符串,若不能,则需要先进行增容,然后再将待尾插的字符串尾插到对象的后方,因为待尾插的字符串后方自身带有’\0’,所以我们无需再在后方设置’\0’。

void append(const char* str)
{
	size_t len_str = strlen(str);
	size_t new_size = _size + len_str;  // 单独计算原字符串长度,不包含\0
	if (new_size > _capacity)
	{
		reserve(new_size);
	}
	strncpy(_str + _size, str,len_str);// 只拷贝 len_str 个字符
	_size = new_size;
	_str[_size] = '\0';// 手动补结束符(strncpy 可能不补)
}

也可以使用strcpy函数直接拷贝str到后面,建议还是手动添加一个 '\0' 谨慎起见

//同上
    strcpy(_str+_size,str);//从原来的\0处开始拷贝
    _size=new_size;
    _str[_size]='\0';
}


5.3.operator+=

 +=运算符重载允许字符串与字符、字符串与字符串之间直接进行尾插操作。当处理字符串与字符的尾插时,可直接调用push_back函数实现

单字符直接复用push_back函数

string& operator+=(const char ch)
{
	push_back(ch);//成员函数一样有this指针
	return *this;
}

+=运算符实现字符串与字符串之间的尾插直接调用append函数即可。

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


5.4.insert()

 insert函数用于在字符串的任意位置插入字符或字符串。当插入字符时,首先需要检查pos参数是否合法,若非法则操作终止。接着判断当前字符串容量是否足够容纳插入后的结果,若不足则需调用reserve进行扩容。插入过程相对简单:先将pos位置及其后的所有字符向后移动一位,腾出空间后再将目标字符插入指定位置。

分为插入一个字符和字符串的情况,所考虑的扩容方案也不同

插入单字符时:

string& insert(size_t pos, const char ch)
{
	assert(pos <=_size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	for (size_t i = _size;i > pos;i--)
	{
		_str[i] = _str[i - 1];
	}
	_str[pos] = ch;
	_size++;
	_str[_size] = '\0';
	return *this;
}

插入字符串时:

这时候不能只是*2倍扩容,可能是一个很长的字符串,这时候就直接先计算插入后新串的长度,再进行扩容,reserve(_size+len)实际开了_size+len+1个空间,多出来的存放了\0,这个\0是原串的,从_str[_size]开始往_str [_size+len]后挪,再--i,一直到pos位置。为了谨慎可以再手动添加\0

string& insert(size_t pos, const char* str)
{
	assert(pos <=_size);
	assert(str != nullptr);
	size_t len = strlen(str);//计算插入的长度
	if (len == 0)
	{
		return *this;
	}
	// 检查容量,不够则扩容(至少扩到能容纳插入后的总长度)
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	// 关键:从后往前挪动原有字符,腾出 len 长度
	for (size_t i = _size; i >= pos; --i) {
		_str[i + len] = _str[i];
	}
	//拷贝插入的字符串到pos位置
	strncpy(_str + pos, str, len);//拷贝len歌字符,不含str自带的\0
	//更新大小和结束符
	_size += len;
	_str[_size] = '\0';
	return *this;
}

5.5.erase()

 erase函数用于删除字符串中从指定位置开始的n个字符,使用前需先验证pos参数的有效性。删除操作分为两种情况处理:

1、删除pos位置及后续所有字符: 只需在pos位置置入'\0',并更新对象的size属性即可。

2、当需要删除 pos 位置及其之后的部分有效字符时,可采用以下方法:将后方需要保留的有效字符前移覆盖待删除部分。由于原字符串末尾已存在终止符 '\0',无需额外添加。

string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	size_t n = _size - pos;
	if (n <=len)//剩余长度小于要删除的长度
	{
		_size = pos;
		_str[_size] = '\0';
	}
	else//剩余长度大于要删除的长度,直接把后面的往前面挪,覆盖掉原来要删除的部分
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
	return *this;
}

6.运算符重载

6.1.>运算符重载

复用strcmp函数,比较两个字符串的ascll码值

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


6.2.==运算符重载

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


6.3.其他四个

我们只需重载其中的两个,剩下的四个关系运算符可以通过复用已经重载好了的两个关系运算符来实现。

bool operator>=(const string& s)const
{
	return (*this > s) || (*this == s);
}
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);
}

7.字符串操作

7.1.c_str()返回c风格的字符串 

//返回c风格的字符串
const char* c_str()const
{
	return _str;
}


7.2.swap()函数

        swap函数用于交换两个对象的数据,直接调用库里的swap模板函数将对象的各个成员变量进行交换即可。但我们若是想在这里调用库里的swap模板函数,需要在swap函数之前加上“std::”(作用域限定符),告诉编译器优先在std命名空间范围寻找swap函数,否则编译器编译时会认为你调用的是正在实现的swap函数(有现成的就用现成的,因为用库里的会实例化函数,函数内部也会开辟资源效率低,可以看上面的图片)。

//交换函数
void swap(string& s)//直接用库里的swap函数,仅交换成员变量,无内存分配/释放,高效且安全
{
	// 交换所有成员变量(指针、大小、容量)
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}


7.3.find()查找函数

1.正向查找第一个匹配的字符

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

首先判断pos的合法性,然后通过遍历的方法进行查找,如果找到了,则直接返回,否则返回npos(size_t类型的-1,补码保存会变成全1,这时候是最大的无符号整型)

2.正向查找第一个匹配的字符串

//正向查找第一个匹配的字符串
size_t find(const char* str, size_t pos = 0)
{
	assert(pos < _size);
	const char* ret = strstr(_str + pos, str);//返回类型是char*类型的指针,子串首次出现的起始位置
	if (ret)
	{
		return ret - _str;//返回子字符串第一个字符的下标,两个指针相减代表相隔个数(p2-p1/sizeof(char))
	}
	else
	{
		return npos;
	}
}

 首先也是先判断所给pos的合法性,然后我们可以通过调用strstr函数进行查找。strstr函数若是找到了目标字符串会返回子串的起始位置,若是没有找到会返回一个空指针。若是找到了目标字符串,我们可以通过计算目标字符串的起始位置和对象C字符串的起始位置的差值,进而得到目标字符串起始位置的下标。


7.4.substr()子串返回

string substr(size_t pos, size_t len = npos)
{
	size_t leftlen = _size - pos;
	if (len > leftlen)
		len = leftlen;//防止越界访问
	string tmp;
	tmp.reserve(len);
	for (size_t i = 0;i

8.<< 和>>运算符重载 和getline函数

注意:为什么定义成友元函数

一、为什么要定义成友元函数      

  C++ 中,运算符重载的本质是函数调用。对于成员函数重载,左操作数必须是当前类的对象(因为成员函数隐含 this 指针作为第一个参数)。

而输入输出运算符的使用形式是:

cout << s;  // 等价于 operator<<(cout, s)
cin >> s;   // 等价于 operator>>(cin, s)
  • 左操作数是 ostream 或 istream 对象(如 coutcin),右操作数是自定义的 string 对象。
  • 若将 operator<< 或 operator>> 定义为 string 类的成员函数,则左操作数必须是 string 对象,语法会变成 s << cout,这与实际使用习惯完全矛盾。

因此,必须将其定义为全局函数(非成员函数),才能让左操作数是流对象(cout/cin)。但全局函数无法直接访问 string 类的私有成员(如 _str_size),因此需要通过友元声明赋予其访问权限。

二、为什么要在类外定义?

友元函数的声明在类内(用于授予访问权限),但定义必须在类外,原因是:

  1. 友元函数不是类的成员函数,不依赖类的实例,不能在类内定义(类内定义的函数默认是成员函数)。
  2. 保持类的封装性:类内只声明接口(友元关系),类外实现具体逻辑,避免类定义过于臃肿。
//输入输出重载
friend std::ostream& operator<<(std::ostream& out, const string& s);
friend std::istream& operator>>(std::istream& in, string& s);
friend std::istream& getline(std::istream& in, string& s);
//类里声明

1.<<运算符重载

std::ostream& operator<<(std::ostream& out, const gjy::string& s)
{
	//使用范围for遍历字符串,并输出
	for (auto e : s)
	{
		out << e;
	}
	return out;//支持连续输出
}

2.>>运算符重载

std::istream& operator>>(std::istream& in, gjy::string& s)
{
	s.clear();
	char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}


3.getline函数

	//读取一行含有空格的字符串
	std::istream& getline(std::istream& in, string& s)
	{
		s.clear(); //清空字符串
		char ch = in.get(); //读取一个字符
		while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
		{
			s += ch; //将读取到的字符尾插到字符串后面
			ch = in.get(); //继续读取字符
		}
		return in;
	}
}

注意:


拷贝构造和赋值重载现代写法swap(复用临时对象拷贝构造和析构)看课件
交换函数这一块:

1. 使用库里面的swap会降低效率(实际没有使用)

2.临时对象创建,相当于借用了一个壳,后面自己销毁


为什么输入输出运算符重载要使用友元函数,而不定义到类里面

posted @ 2025-12-05 11:31  gccbuaa  阅读(0)  评论(0)    收藏  举报