模拟实现string类(声明与定义分离)

目录

1.预览头文件

2.在.cpp文件中具体实现string类

2.1 常量npos

2.2 swap函数

2.3 构造函数

2.4 拷贝构造函数  

 2.5 析构函数

2.6 赋值运算符重载

2.7 c_str 函数

2.8 size 函数

2.9 比较大小

2.10 重载 [ ] 运算符

2.11 迭代器的实现

2.12 reserve函数

2.13 resize 函数

2.14 push_back、append、+=

2.15 Insert、Erase

2.16 find 函数

2.17 substr 函数

2.18 重载 << 运算符

 2.19 重载 >> 运算符


1.预览头文件

#pragma once
#include <iostream>
#include <assert.h>

namespace dfq
{
	class string
	{
	public:
        //静态变量
		static const size_t npos;
		//交换两个string对象
		void swap(string& s);
		//显示实现默认成员函数:构造函数、拷贝构造函数、析构函数、赋值重载函数
		string(const char* str = "");
		string(const string& s);
		~string();
		string& operator=(const string& s);
		//string对象代表的字符串的指针、长度
		const char* c_str()const;
		size_t size()const;
		//比较大小
		bool operator<(const string& s)const;
		bool operator==(const string& s)const;
		bool operator<=(const string& s)const;
		bool operator>(const string& s)const;
		bool operator>=(const string& s)const;
		bool operator!=(const string& s)const;
		//[]、迭代器
		char& operator[](size_t pos);
		const char& operator[](size_t pos)const;
		typedef char* iterator;
		iterator begin();
		iterator end();
		typedef const char* const_iterator;
		const_iterator begin()const;
		const_iterator end()const;
		//增删查改
		void reserve(size_t n);
		void resize(size_t n, char ch = '\0');

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

		void Insert(size_t pos, size_t n, 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, size_t len = npos);

		void clear();
	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	};
	std::ostream& operator<<(std::ostream& out, const string& s);
	std::istream& operator>>(std::istream& in, string& s);
}

为了不与标准库中的string类冲突,另外建立一个命名空间以模拟声明一个string类

我们可以将string类的成员变量简记为

一个 _size 变量,记录有效字符的个数,即字符串的长度

一个 _capacity 变量,记录动态开辟的空间大小,字符串的容量

一个 _str 变量,指向动态开辟的空间该空间用于存储字符串的字符数据

2.在.cpp文件中具体实现string类

2.1 常量npos

const size_t dfq::string::npos = -1;

(1)声明npos命名空间dfq 中的 string类 中的一个常量,并定义为 -1

(2)静态变量的作用域是在文件作用域或函数作用域

2.2 swap函数

void dfq::string::swap(dfq::string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

(1)声明swap命名空间dfq 中的 string类 中的一个函数

(2)swap用于交换两个string类对象

交换两个对象就是交换两个对象的成员变量

(3)该函数主要是在 重载=运算符 时用到,届时再讲为什么不能用标准库中的swap函数

2.3 构造函数

dfq::string::string(const char* str)
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	memcpy(_str, str, _size + 1);
}

(1))声明string命名空间dfq 中的 string类 中的一个函数

(2)这里只模拟实现 以字符串创建string对象 的构造函数

并选择在函数体中初始化成员变量

(3)缺省参数不能在声明与定义中同时出现

给定的缺省参数的意思是,可以创建一个是空字符串的string对象

(4)创建string对象时

_size的大小 就是 str 所指字符串的长度

_capacity 最开始与 _size保持一致(这里为了简单,选择保持一致。现实中的有些编译环境会因为对齐等原因导致容量大于有效数据)

开辟的空间大小应该是(容量+1),这个1是为'\0'留着的,

因为容量指的是存储有效数据的空间大小,'\0'不属于有效数据,

打印字符串时,又须注意'\0'的存在

(5)拷贝字符串时,不要忘记'\0'

2.4 拷贝构造函数  

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

(1)声明string命名空间dfq 中的 string类 中的一个函数

(2)拷贝构造函数同样不要忘记'\0'

(3)选择使用memcpy函数进行逐字节地拷贝对应字符

不选择strcpy的原因是,谨防字符串中间有'\0',导致少拷贝了数据

 2.5 析构函数

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

(1)声明~string命名空间dfq 中的 string类 中的一个函数

2.6 赋值运算符重载

dfq::string& dfq::string::operator=(const dfq::string& s)
{
	if (*this != s)
	{
		string temp(s);
		swap(temp);
	}
	return *this;
}

(1)=运算符有返回值,返回的是一个string对象,因为这里是模拟实现的string类

所以这里的string要声明为dfq中的string

(1)声明operator=命名空间dfq 中的 string类 中的一个函数

(1)创建一个temp临时对象,然后*this对象 与temp对象进行交换

当函数调用完毕时,析构函数会销毁*this对象原来所包含的内容

*this对象也已成为s对象的一份拷贝

(1)这里的妙处在于:

并非手动开辟空间,释放空间,一切交给拷贝构造函数和析构函数去做

(1)这里不能用标准库中的swap函数的原因是:

标准库中的swap函数涉及赋值运算符重载

一旦调用标准库中的swap函数由于局部优先原则

swap函数内部会调用我们自己写的赋值运算符重载函数

然后我们自己写的赋值运算符重载函数又去调用标准库中的swap函数

如此递归下去,最终导致栈溢出问题

下面是拷贝构造的示意图

开始的时候,*this对象与temp对象各自占有各自的空间及数据,此时

temp对象是s对象的一份拷贝

我们的目的是让*this对象成为s对象的一份拷贝

交换temp对象与*this对象

此时,*this对象占据原来属于temp对象的空间及数据,成为了s对象的一份拷贝

temp对象则占据原来属于*this对象的空间及数据

赋值运算符重载函数调用完毕,

temp对象会被析构函数销毁

原来属于*this对象的空间及数据就会被销毁

我们的目的,让*this对象成为s对象的一份拷贝也已达到

(1)因为string对象会被分配资源,所以必须显式写一个赋值运算符重载函数,形成深拷贝

这里就涉及到浅拷贝与深拷贝两个概念

特性浅拷贝深拷贝
定义仅复制对象的基本数据类型和指针的地址,不复制指针指向的实际资源。复制对象的基本数据类型,并为动态分配的资源分配新的内存,同时复制其内容。
内存分配共享同一块动态分配的内存。分配新的内存空间,并复制资源内容。
资源独立性两个对象共享同一块资源,修改一个对象会影响另一个对象。两个对象拥有各自独立的资源,互不影响。
析构问题析构时可能导致重复释放同一块内存,引发未定义行为(如程序崩溃)。析构时各自释放自己的内存,不会引发重复释放问题。
安全性较低,存在悬空指针和重复释放的风险。较高,避免了悬空指针和重复释放的问题。
性能较快,因为只需复制指针地址。较慢,因为需要分配新的内存并复制内容。
使用场景适用于不包含动态分配资源的对象,或资源管理由外部负责的对象。适用于包含动态分配资源的对象,需要确保资源独立性和安全性的场景。

2.7 c_str 函数

const char* dfq::string::c_str()const
{
	return _str;
}

返回指向字符串的指针

2.8 size 函数

size_t dfq::string::size()const
{
	return _size;
}

返回指向字符串的长度

2.9 比较大小

bool dfq::string::operator<(const dfq::string& s)const
{
	//先按照短的字符串进行比较,小就是真
	int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
	//前几个相等,就比长度
	//"hello""hello"    false
	//"helloxxx""hello" false
	//"hello""helloxxx" true
	return ret == 0 ? _size < s._size : ret < 0;
}

bool dfq::string::operator==(const dfq::string& s)const
{
	return (_size == s._size)
		&& (memcmp(_str, s._str, _size) == 0);
}

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

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

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

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

(1)比较大小只需要实现两个关系(<、==或者>、==),其余关系进行复用即可

(1) < 关系实现的时候,注意字符串中间可能包含'\0'的情况,

所以使用memcmp进行逐字节的比较

2.10 重载 [ ] 运算符

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

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

(1)重载运算符[]时需要注意的是,

检查pos位置的有效性

普通对象调用时,可读可修改,所以传引用返回

const对象调用时,只可读,所以const对象的引用要用const修饰

2.11 迭代器的实现

dfq::string::iterator dfq::string::begin()
{
	return _str;
}

dfq::string::iterator dfq::string::end()
{
	return _str + _size;
}

dfq::string::const_iterator dfq::string::begin()const
{
	return _str;
}

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

模拟实现string中的迭代器,将其定义为指针相关函数调用就返回相对应的指针

typedef char* iterator;
typedef const char* const_iterator;

2.12 reserve函数

void dfq::string::reserve(size_t n)
{
	if (n > _capacity)
	{
		char* temp = new char[n + 1];
		memcpy(temp, _str, _size + 1);
		delete[] _str;
		_str = temp;
		_capacity = n;
	}
}

一般情况下不会缩容,所以只实现扩容操作

n指的是有效数据个数,'\0'的考虑交给函数内部实现

+1操作是为'\0'准备的

2.13 resize 函数

void dfq::string::resize(size_t n, char ch)
{
	//不管三七二十一,先reserve一下,是否开空间取决于n
	reserve(n);
	//然后从_size开始填入字符,只用当n >= _size 时,才会进入for循环,否则就不会进入
	for (size_t i = _size; i < n; ++i)
	{
		_str[i] = ch;
	}
	_str[n] = '\0';
	_size = n;
}

(1)resize函数可能会开空间可能不会,是否开空间直接交给reserve函数处理

(2)

当n大于等于_size时,进入for循环字符填充开始,并在n位置添上'\0'

当n小于_size时,不进入for循环,直接在n这个位置截断字符串

2.14 push_back、append、+=

void dfq::string::Push_Back(char ch)
{
	//先判断扩容
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size++] = ch;
	_str[_size] = '\0';
}

void dfq::string::append(const char* str)
{
	size_t len = strlen(str);
	//判断扩容
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	memcpy(_str + _size, str, len + 1);
	_size += len;
}

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

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

(1)追加一个字符并扩容时,注意容量的初始值是否为0

(1)追加一个字符串时,注意不要忘记拷贝'\0'

(1)+=就是复用push_back函数与append函数

2.15 Insert、Erase

void dfq::string::Insert(size_t pos, size_t n, char ch)
{
	assert(pos <= _size);
	reserve(_size + n);
	//谨防pos == 0
	//移动
	size_t end = _size;
	while (end >= pos && end != npos)
	{
		_str[end + n] = _str[end];
		--end;
	}
	_size += n;
	//填充
	for (size_t i = pos; i < pos + n; ++i)
	{
		_str[i] = ch;
	}
}

void dfq::string::Erase(size_t pos, size_t len)
{
	assert(pos < _size);
	if (len == npos || pos + len >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		size_t end = pos + len;
		while (end <= _size)
		{
			_str[pos++] = _str[end++];
		}
		_size -= len;
	}
}

(1)在pos位置插入n个字符时,注意

检查pos的有效性

从后往前挪动数据时,若pos为0,end是size_t类型,会发生溢出现象

(2)清除pos位置后的len个字符时,注意判断len的大小,不进行缩容操作

2.16 find 函数

size_t dfq::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 dfq::string::find(const char* str, size_t pos)
{
	assert(pos < _size);
	char* temp = strstr(_str, str);
	if (temp)
	{
		return temp - _str;
	}
	else
	{
		return npos;
	}
}

find函数值得注意的是,找不到时返回的是 npos

2.17 substr 函数

dfq::string dfq::string::substr(size_t pos, size_t len)
{
	assert(pos < _size);
	string temp;
	size_t n = len;
	if (len == npos || len + pos >= _size)
	{
		n = _size - pos;
	}

	for (size_t i = pos; i < pos + n; ++i)
	{
		temp += _str[i];
	}

	return temp;
}

创建一个变量n存储要截取的字符个数

创建一个临时对象存储子串并返回

2.18 重载 << 运算符

std::ostream& dfq::operator<<(std::ostream& out, const dfq::string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

重载流插入运算符时,需要注意的是,

ostream必须加引用&,避免不必要的拷贝开销

且不加const修饰,因为ostream对象需要被改变

s对象被const修饰的原因是防止s对象的内容在函数中被改变

选择范围for逐字符的打印s对象中的内容,谨防字符串中含有'\0'的情况

 2.19 重载 >> 运算符

void dfq::string::clear()
{
	_str[0] = '\0';
	_size = 0;
}

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

	char buff[128];
	int i = 0;
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}

		ch = in.get();
	}

	buff[i] = '\0';
	s += buff;
	return in;
}

重载流提取运算符时,

(1)首先需要清理s对象中的内容

因为标准库所实现的>>运算符只会将当前控制台中的内容存储到string对象中

string对象原有的内容会被清理,例子如下

sh

第一次输入"hello",hello会被存入s1对象当中,打印s1对象时输出"hello"

紧接着第二次输入"dfq",dfq会被存入s1对象当中,但打印s1对象时输出的是"dfq",

说明原来的"hello"已被清理

(1)标准库的 >> 运算符会跳过前导空白字符(如空格、制表符、换行符)

正是这样,我们不能直接 用 in 对象读取字符 (in >> ch),因为,>>运算符遇到空白字符时会自动跳过,while循环就不会终止,这里我们选择使用istream中的get函数,get函数可以从控制台中逐字节的读取字符,这样就能够终止while循环啦

(1)+=的扩容可能太频繁,通过创建一个较大的数组先存储有效数据,数组满了或循环结束时就填充到s对象中去,这样可以有效避免频繁扩容的情况,以此提高效率

Rebuild started: Project: Project *** Using Compiler &#39;V6.22&#39;, folder: &#39;E:\Keil_v5\ARM\ARMCLANG\Bin&#39; Rebuild target &#39;Target 1&#39; assembling startup_stm32f10x_md.s... Start/core_cm3.c(445): error: non-ASM statement in naked function is not supported 445 | uint32_t result=0; | ^ Start/core_cm3.c(442): note: attribute is here 442 | uint32_t __get_PSP(void) __attribute__( ( naked ) ); | ^ Start/core_cm3.c(465): error: parameter references not allowed in naked functions 465 | "BX lr \n\t" : : "r" (topOfProcStack) ); | ^ Start/core_cm3.c(461): note: attribute is here 461 | void __set_PSP(uint32_t topOfProcStack) __attribute__( ( naked ) ); | ^ Start/core_cm3.c(479): error: non-ASM statement in naked function is not supported 479 | uint32_t result=0; | ^ Start/core_cm3.c(476): note: attribute is here 476 | uint32_t __get_MSP(void) __attribute__( ( naked ) ); | ^ Start/core_cm3.c(499): error: parameter references not allowed in naked functions 499 | "BX lr \n\t" : : "r" (topOfMainStack) ); | ^ Start/core_cm3.c(495): note: attribute is here 495 | void __set_MSP(uint32_t topOfMainStack) __attribute__( ( naked ) ); | ^ 4 errors generated. compiling core_cm3.c... compiling misc.c... compiling system_stm32f10x.c... compiling stm32f10x_adc.c... compiling stm32f10x_dac.c... compiling stm32f10x_exti.c... compiling stm32f10x_dbgmcu.c... compiling stm32f10x_dma.c... compiling stm32f10x_crc.c... compiling stm32f10x_cec.c... compiling stm32f10x_bkp.c... compiling stm32f10x_can.c... compiling stm32f10x_flash.c... compiling stm32f10x_pwr.c... compiling stm32f10x_fsmc.c... compiling stm32f10x_
03-31
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值