前引:上一篇文章小编已经整理出了String的常用接口,梳理了各个接口的功能、参数,如何使用等各种实例。本篇文章将带大家看看String这些接口的实践使用,探索这些接口的实用性,是如何增加代码效率的。在本篇文章的末尾,还奉上了部分底层的模拟实现,String类的使用是有趣的,下面我们来从实践中感受String类带给我们的快捷、效率!
目录
string类的模拟实现
下面我们来实现String的底层,解读String的原理:
std库里面的String有它的专属空间,也就是C++库
下面我们来命名自己的空间,同时String是一个类,我们需要实现:自定义空间+一个类
namespace Space
{
class string
{
};
}
构造初始化
观察库里面的 string 初始化特点:
可以看到它的变量有三个:size、capacity、字符空间
下面我们来自己实现它的构造初始化:
初始化size、capacity、空间、存储
//自定义构造
string(const char* allocator)
:_size(strlen(allocator))
,_capacity(2*_size+1)
{
try
{
_allocator = new char[_capacity];
}
catch (const exception& _allocator)
{
cout << "空间开辟失败" << endl;
}
//存储
strcpy(_allocator, allocator);
}
效果展示:
可以看到是没有问题的,但是我们平常是可能按下面的方式初始化的:
string S;
所以我们还得再写一个无参默认构造,如下:
注意:不能是全缺省的,否则参数相同,编译器无法区分
//默认构造
string()
:_size(0)
, _capacity(10)
{
try
{
_allocator = new char[_capacity];
}
catch (const exception& _allocator)
{
cout << "空间开辟失败" << endl;
}
}
以上我们的构造初始化就写好了,可以随时应对各种初始化情况,总代码如下:
namespace Space
{
class string
{
public:
//默认构造
string()
:_size(0)
, _capacity(10)
{
try
{
_allocator = new char[_capacity];
}
catch (const exception& _allocator)
{
cout << "空间开辟失败" << endl;
}
}
//自定义构造
string(const char* allocator)
:_size(strlen(allocator))
,_capacity(2*_size+1)
{
try
{
_allocator = new char[_capacity];
}
catch (const exception& _allocator)
{
cout << "空间开辟失败" << endl;
}
//存储
strcpy(_allocator, allocator);
}
private:
size_t _size;
size_t _capacity;
char* _allocator;
};
}
析构函数
这个函数很简单,释放空间,改变 size、capacity这些就可以了,如下:
//析构
~string()
{
delete[]_allocator;
_size = 0;
_capacity = 0;
cout << "释放成功" << endl;
}
流提取
在上面我们已经实现了读取函数,但是追求方便,且两者有很大区别,比如:
cout << S1.Read() << endl;
cout << S1 << endl;
ostream& operator<<(ostream& out, const string& _stl) //没有找命名空间里面
{
for (auto ac : _stl)
{
cout << ac;
}
return out;
}
注意:不能在成员函数中实现,因为this指针会抢占第一个操作符位置,所以我们放在外面实现
区别:
C的字符数组,以\0为终止算长度
String不看\0,以size为终止长度,例如:
这样看虽然没有什么区别,但是如果添加上\0就有很大的变化了
可以看到流提取是不受\0影响的,所以我们需要注意这个点,字符的打印在流提取不受\0影响
为什么流提取的实现需要以ostream&作为返回值
(1)允许多次连续提取
(2)避免流对象的开销,规定直接传引用
strcpy与memcpy的区别
特性 | strcpy | memcpy |
---|---|---|
参数类型 | char* |
void* |
终止条件 | 遇到\0 停止 |
按指定字节数完成复制 |
长度控制 | 自动计算 | 显式指定 |
数据安全 | 高风险 | 可控风险 |
适用场景 | 纯字符串操作 | 任意内存数据复制 |
所以对于字符串我们需要根据形式区分二者的拷贝,否则会出很大的问题
流插入
在模拟流插入时我们同样要注意this指针的问题,因此需要在成员函数外面定义
我们看下面的问题:
注意(1):所以我们需要在输入之前清理之前原本的字符,然后重置_size,效果如下:
注意(2):我们每次调用+=,都会开辟空间,效率可以优化,先存进数组里面,再最后统一拷 贝
istream& operator>>(istream& in, string& _stl)
{
//清理缓冲区
_stl.clear();
//输入元素
char c = in.get();
//临时数组
int i = 0;
char buff[128] = "\0";
//直到c结束
while (c != '\n' && c != '\0')
{
//先存入buff数组
buff[i++] = c;
//如果临时数组满了,就给_stl
if (i == 127)
{
memcpy(_stl._allocator, buff, i);
//重置数组
i = 0;
}
//_stl += c;
//注意get会自动向后移动
c = in.get();
}
//如果i没有重置,说明没有发生存满
if (i != 127)
{
for (int j = 0; j < i; j++)
{
//如果满了就扩容
if (_stl._size == _stl._capacity)
{
_stl.reserve(2 * _stl._size);
_stl._capacity = 2 * _stl._size;
}
//转移到对象里面
_stl._allocator[_stl._size++] = buff[j];
}
}
return in;
}
效果展示:
大小比较
我们拿 > 举例:大小比较我们一般采用的是运算符重载,里面根据当前字符的ASCII值比较
//大小比较
bool operator>(const string& S)const
{
size_t p1 = 0, p2 = 0;
//比较不同长度
while (p1<_size && p2<S._size)
{
if (_allocator[p1] == S._allocator[p2])
{
p1++;
p2++;
}
else
{
if (_allocator[p1] > S._allocator[p2])
{
return true;
}
else
return false;
}
}
//此时前面的字符都相等,但是没有比较完
if (p1 < _size)
{
return true;
}
else
return false;
return false;
}
效果展示:
拷贝构造
原理我就不说了,咱们直接实现:
//拷贝构造
string(const string& S)
{
_allocator = new char[S._capacity];
_size = S._size;
_capacity = S._capacity;
//数据拷贝
memcpy(_allocator, S._allocator, S._capacity);
}
赋值运算符重载
将一个对象的内容赋给另一个对象,因为前面我们已经有了一定的了解,我们下面换一种玩法:
注意:如果没有写拷贝构造等,属于浅拷贝,那么多次释放同一个空间会出问题
我们知道swap可以交换任意形式的变量,所以我们先来实现一个可以交换对象的swap:
思路:先根据赋值对象A拷贝构造一个临时对象B,然后把临时对象B的数据给*this
void swap(const string& S)
{
//先开辟空间,注意S出了swap函数会销毁
string tmp(S);
//目的:创建一个临时变量,把临时变量的空间、大小等信息转给S
std:: swap(_allocator, tmp._allocator);
std:: swap(_size, tmp._size);
std:: swap(_capacity, tmp._capacity);
}
下面我们直接调用这个swap函数就OK了:
string& operator=(string& S)
{
(*this).swap(S);
return *this;
}
效果展示:
最后我们再梳理以下思路:
现在有两个对象:A和B,我们的目标是A=B
进入swap函数先以B为模板调用拷贝构造出C,此时C是临时对象,内容与B完全一致
然后将C的内容交换给A,C虽然会释放,但是它在堆上开的内容只会main函数调用析构才会释放
这里我们先完成构造、析构,由于排版问题,下一篇我们来完成它的功能结尾!
【雾非雾】期待与你的下次相遇!