《C++ string模拟实现深度剖析:内存管理策略与迭代器设计(中卷)

本篇文章是对string底层库函数的具体模拟实现,目的就是为了更好的了解string 的底层 , C++是一个极度追求效率的一个语言,我们需要了解为啥要少用 某个函数,为啥这个函数就坑了程序,导致程序的效率降低。

本篇文章,我们会像上篇一样,从是什么,为什么,怎么用来告诉大家string隐藏的底层逻辑到底是怎么样的,那么直接开始。

目录

一、经典的string类问题

二、模拟实现

2.1构造函数

2.2.c_str函数

2.3.析构函数

2.4.size字符串大小

2.5.operator[ ]符号重载

2.6.迭代器

2.7.push_back函数(含reserve函数)

2.8.append函数

2.9.insert函数

2.10.erase函数

2.11.find函数

2.12.operator+= 函数重载

2.13.substr函数

2.14.拷贝构造(深拷贝)

2.15.resize函数

2.16.流插入

2.17.流提取

2.18.关系运算符号

2.19.赋值操作符重载


一、经典的string类问题

对于自己编写的 string , 为了和库里的 string 进行区分 , 我们可以使用 命名空间域 namespace进行隔离。

string.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
using namespace std;
 
namespace CYH
{
	class string
	{
	public:
        //这里我们一般只留一个构造 - 全缺省的默认构造(最后面会有)
		string();
		~string();
		string(const char* str);
 
		//比较短的代码就放在类里,内联函数,不用建立函数栈帧了,提高效率
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

string.cpp

#include"string.h"
 
namespace CYH
{
	string::string()
		:_str(new char[1] {'\0'})
		, _size(0)
		, _capacity(0)
	{
 
	}
 
	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}
 
	string::string(const char* str)
		//+1 的原因是还有一个 \0 要保存
		:_size(strlen(str))
	{
		_capacity = _size;
		_str = new char[_size + 1];
		strcpy(_str, str);
 
	}
}

问题1:为什么无参构造的初始化列表的 c_str 需要 new char[1] {'\0'} ? 

如果我们按照往常一样, 把_str 置为 nullptr 。当你调用 c_str( ) 的时候 , 编译器认为你需要打印的不是指针,而是字符串 , 字符串的结束符标志位 '\n' 。 初始化如果为空,就会导致编译器进行空指针的解引用,从而导致程序崩溃。

问题2:不是说尽量在初始化列表里面初始化吗 ? 可是在有参构造这里,为什么除了 _size , 其他的都在函数体内进行初始化 ?

平常我们就是按照如下的方法来初始化变量 ,尽可能的在 初始化列表里面初始化 变量 。

但是  strlen 是在运行时运算的 ,时间复杂度为 O(n)  这里用三个 O(n) 。

这里我们一想,那就先计算 _size , 然后其他的都调用_size这样效率就提高了,但是,这样是不对的。这是一个大坑。

我们乍一看,好像效率确实有很大的提升,但是存在一定的安全隐患,比如空间没开够 , 数组越界 , 野指针析构 , 程序就崩溃了。

string::string(const char* str)
		//+1 的原因是还有一个 \0 要保存
		:_size(strlen(str))
	{
		_capacity = _size;
		_str = new char[_size + 1];
		strcpy(_str, str);
 

比如我们这里提到的_size需要加一,表明这里的size此时是0,那么也就会达到我所说的越界的问题。

二、模拟实现

2.1构造函数

     首先实现传递常量字符串实现构造函数。遇见\0停止构造,\0前面为一段字符。

      思路:内置类型可以在初始化列表以及函数体实现初始化,自定义类型在初始化列表初始化。

      易错点:初始化列表一定要按照内置类型的声明顺序进行初始化操作,对常量字符串要在堆上重新创建空间存储,便于后面的操作。

string(const char* str) //权限平移,加上cosnt
{
	_str = new char[strlen(str) + 1];
	//strcpy(_str, str);
    memcpy(_str,s._str._size+1);
	_size = strlen(str);//_size为有效字符数量不包含 \0
	_capacity = _size;//对于capacity来说也是以有效空间(可以存储多少有效字符),开辟空间已经预留\0位置。
}

对无参的默认构造编写

这个上面讲过了,这里直接给出代码

string()
{
	_str = new char[1];
	str[0] = '\0';//开辟一个空间储存 \0
	_size = _capacity = 0;
}

2.2.c_str函数

为普通对象以及常量对象提供的提供对字符串内容的只读访问;前面const是为了防止修改,后面const是为了权限的平移和缩小保证正常传值。

const char* c_str() const;

模拟:

const char* c_str() const // 确保字符串无法修改,返回为const修饰的常量
{
	return _str;
}

2.3.析构函数

思路:释放空间,内置类型置0或者nullptr

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

2.4.size字符串大小

size_t size() const
{
	return _size;
}

2.5.operator[ ]符号重载

思路:字符串本质为有序的数组,为了防止访问越界加上assert限制访问位置,按照访问字符数组的方式,返回对应位置的字符即可。

易错点:如果要在字符串的外部修改字符串 ,需要返回值为引用。

引用返回的好处:返回引用不需要拷贝字符,直接返回内存地址的别名,避免了不必要的内存开销,尤其在处理长字符串时能提升性能。

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

2.6.迭代器

思路:迭代器作为访问方式的一种,而且迭代器可以是指针或者内部类,但是内部类实现太麻烦,因此这里使用typedef 将char* 名为iterator较为简单。end()指向字符串最后有效字符的下一个位置以及begin()函数指向第一个字符的位置。

//迭代器
typedef char* iterator;
iterator begin()
{
	return _str;
}
iterator end()
{
	return _str + _size;
}

2.7.push_back函数(含reserve函数)

思路:检查容量和有效字符个数,小于容量先扩容二倍。然后对有效字符的后一个位置赋值。

void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];// 开辟+1 为了放\0
		//strcpy(tmp, _str);
        memcpy(tmp, _str, _size + 1);
		delete[] _str;
		_str = tmp;
		_capacity = n;
		tmp = nullptr;
	}
}
		
void push_back(const char a)
{
	if (_size == _capacity)
		reserve(_capacity * 2);
	_str[_size] = a;
	_size++;
	_str[_size] = '\0';
}
 

2.8.append函数

思路:检测追加字符串的大小,然后进行扩容检测,在复用push_back尾插字符。

void append(const char* str)
{
	size_t len = strlen(str);
	//检测容量
	if (len + _size > _capacity)
	{
		reserve(len + _size);
	}
	//插入数据 复用push_back 左开右闭 len  位置为\0。
	//size_t i = 0;
	//for (i = 0; i < len ; i++)
	//{
	//	push_back(str[i]);
	//} 
    
		
    memcpy(_str + _size, str, len + 1 );
	_size += len;
}

2.9.insert函数

插入一个字符串

注意:挪动数据时要注意类型转换

void insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t len = strlen(str);
	if (len + _size > _capacity)
	{
		reserve(len + _size);
	}
			
	//挪动数据
	size_t end = _size;
	while (end >= pos && end != npos)
	{
		_str[end + len] = _str[end];
		end--;
	}
	//插入数据
	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = str[i];
	}
		_size += len;
	}

插入多个字符

思路同上,但是

当操作符两边的操作数的类型不一样的时候,会发生类型提升

类型提升核心规则:

小容量类型向大容量类型提升:

char/short 等小整型会先提升为 int 类型
float 会提升为 double 类型

符号处理规则:

有符号和无符号类型混合运算时,会统一提升为无符号类型
但若大容量类型是有符号类型,则可能保留有符号特性

在相同宽度(内存大小相等)下,无符号整型能表示的最大值是有符号整型最大值的约 2 倍,整体范围更大 只能表示非负数

void insert(size_t pos, size_t n, const char a)
{
	assert(pos <= _size);//确保插入在有效字符附近
	//容量
	if (_size + n > _capacity)
	{
		reserve(_size + n);
	}
	//挪动数据
	size_t end = _size;
	while (end >= pos && end != npos)//由于size_t类型 end=0之后再-1会导致end变为-1但是对于
    //size_t来说是size_t的最大值
	//无法正常的进行比较,所以加上end!=npos
	{
		_str[end + n] = _str[end];
		end--;
	}
	//插入数据
	for (size_t i = 0; i < n ; i++) 
	{
		_str[pos + i] = a;
	}
	 _size += n;
 
}

2.10.erase函数

//移除数据 覆盖删除
void erase(size_t pos,size_t n = npos )
{
	assert(pos <= _size);
	size_t i = 0;
	//分为两种情况
	if (pos + n >=_size || n == npos)
	{
		_str[pos] = '\0';
		_size = pos;
				
	}
	else
	{
		for (i = 0; i < _size - pos; i++) 
		{
			 _str[pos + i] = _str[pos + n + i];
		}
			
		     _size -= n;
	}
}

2.11.find函数

//寻找 字符
size_t find(const char ch, size_t pos = 0) 
{
	assert(pos < _size);//寻找ch的位置,\0位置舍去,就没有等号了
	size_t i = 0;	
	while (i < _size)
	{
		if (_str[i] == ch)
			return i;
		i++;
	}
	return npos;
 
	}
//寻找字符串
size_t find(const char* str, size_t pos = 0)
{
    char* tmp = strstr(_str, str);
	if (tmp)
	{
		return tmp - _str;
	}
	else
	{
		return npos;
	}
}

2.12.operator+= 函数重载

//+=操作符重载
void operator+=(const char ch)
{
	push_back(ch);
		
}
void operator+=(const char* str)
{
	append(str);
}

2.13.substr函数

功能:从pos位置开始,取出len长度的字符

思路:npos和len+pos大于_size的情况,需要重置len,然后创建临时tmp字符串,再重新输出 。

//从pos处取出长度为len的字符串
string substr(const size_t pos, size_t len = npos)//函数全部模拟完成后验证此函数
{
	assert(pos < _size);
	if (len == npos || len + pos > _size)
	{
		len = _size - pos;
	}
	size_t i = 0;
	string tmp;
	reserve(len);
	for (i = 0; i < len; i++) 
	{
		tmp += _str[pos + i];
	}
	return tmp;
			
}

这里再看一下深浅拷贝的区别:

2.14.拷贝构造(深拷贝)

//拷贝构造函数
string(const string& s) 
{
	_str = new char[s._capacity + 1];
	//strcpy(_str, s._str);
    memcpy(_str,s._str._size+1);
	_size = s._size;
	_capacity = s._capacity;
}

2.15.resize函数

功能:将有效字符的长度扩展或者缩短为 n,多余的位置若有c则填充c,没有c则填充\0;已经存在字符的位置是不能修改的。

思路:分为三种,n小于_size,n大于_size(细分的n大于capacity,n小于capacity)

易错点:插入字符ch的位置和结束的位置。

void resize(size_t n, const char ch = '\0')
{
	if (n < _size)
	{
		_size = n;
		_str[_size] = '\0';
	}
	else
	{
		reserve(n);
		for (size_t i = _size; i < n; i++)
		{
			_str[i] = ch;
		}
		_size = n;
		_str[_size] = '\0';
	}	
}
 

2.16.流插入

功能:重载<< 让自定义类型string适用于<< 符号

思路:一种为友元函数,但是对于string来说,\0不能代表结束的标识符,因此会出了错;

建议使用第二种,按照_size的大小输出每一个字符。直到_size的位置

易错点:忽略 \0 不是结束操作符的情况,ostream具有防止拷贝的功能,因此<<重载函数拒绝拷贝构造。因此采用引用参数的格式。

第一种

void operator<<(ostream& out, const string& s)
{
	cout << s._str;
}

第二种

// 流插入
ostream& operator<<(ostream& out, const string& s)
{
    for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i];
	}
	return out;
}

2.17.流提取

注意:

1.cin为阻塞型接口,其遇见空格以及换行符号,不会读取,默认为输入间隔符号,换行符同时承担着 “输入间隔标识” 和 “输入结束触发” 的双重角色。因此再流提取需要使用istream的成员函数 get来读取符号,其可以读取任何值,包括空格。(getline)
 

2.每一次的输入,都是新的输入,因此要将原本string中的内容进行覆盖或者清理。
 

3.随着输入的增加,频繁使用s+=符号,会不断的调用reserve,不断的开空间,导致程序的成本增大,浪费空间和时间。因此采用数组的先行存储,再复制到string上
 

4.对于std中的cout 会忽略字符前面的空格,因此需要一个循环过滤掉空格

思路:按照注意来,先过滤空格一个换行符号,创建数组,将输入的数据暂存于数组中,然后等数组满了之后或者输入结束之后,将其复制给string中。

istream& operator>>(istream& in,  string& s)
{
	s.clear();
	char ch = in.get();
	while (ch == ' ' || ch == '\n')
	{
		ch = in.get();//重新读取
	}
	char arr[128] = { '\0' };
	size_t i = 0;
	while( ch != ' '&& ch != '\n')
	{
		arr[i] = ch;
		i++;
		if (i == 127)
		{
			arr[i] = '\0';
			s += arr;
			i = 0;
		}
		ch = in.get();
	}
	if (i != 0)
	{
		arr[i] = '\0';
		s += arr;
	}
 
	return in;
}

2.18.关系运算符号

功能:适用于自定义类型string的比较大小。

思路:使用memcmp函数比较,写出>再写出=就可以复用了。

bool operator<(const string& s)
{
	size_t size = _size;
	if (_size < s._size)
	{
		size = s._size;
	}
	int ret = memcmp(_str, s._str, size);
		
	/*if (ret == 0)
	{
		return _size < s._size ? 1 : 0;
	}
	else
		return ret;*/
	return ret == 0 ? _size < s._size : ret < 0;
}
bool operator==(const string& s)
{
	return _size == s._size&&
		memcmp(_str, s._str, _size) == 0;
}
bool operator<=(const string& s)
{
	return *this < s || *(this) == s;
}
bool operator>(const string& s)
{
	return !(*this <= s);
}
bool operator>=(const string& s)
 
{
	return *this > s || *(this) == s;
}

2.19.赋值操作符重载

void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size,s._size);
	std::swap(_capacity,s._capacity);
}
 
string& operator=(const string& s)
{
	string tmp(s);
	swap(tmp);
	return *this;
}
 
//简写为
 
string& operator=(const string s)
{
	swap(tmp);
	return *this;
}

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值