目录
构造函数的实现
拷贝构造
赋值重载
const问题
迭代器打印
范围for打印
运算符重载
reserve模拟
插入数据
push_back
append
先贴出一段错误代码:
#include
#include
namespace zzl//避免与库冲突
{
class string
{
public:
string()
:_str(nullptr)
,_size(0)
,_capacity(0)
{}
string(const char* str)
:_str(str)//_str要为const类型
, _size(strlen(_str))
, _capacity(strlen(_str))
{}
private:
const char* _str;
size_t _size;
size_t _capacity;
};
}
输出c风格的字符串:
const char* c_str()//外部不加const,指针可以改变
{
return _str;//返回常量字符串
}
这个错误涉及对空指针解引用,也就是说,在我们通过c_str得到字符串内容的过程中,涉及对空指针解引用引发了这样的错误。
当我们使用cout输出一个指针变量时,默认情况下会输出该指针变量所指向的内存地址,而不是指针本身的值。
如果我们想输出指针的地址,可以将其强制转为(void*)使其打印地址。
除此之外,它还存在一些问题,我们再增加两个成员函数
char& operator[](size_t pos)/支持改变返回值a[i]++
{
assert(pos < size)//\0不存有效数据
return _str[pos];
}
size_t size()//模拟size()
{
return _size;
}
在定义_str时,为了成功初始化也加上了const,但这样的问题就是无法改变其值。
我们定义的构造函数不能扩容这是问题之二。
对单参构造函数的改动:
string(const char* str)
:_size(strlen(_str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
这里我只初始化了_size,目的是避免依赖带来的初始化顺序不匹配问题,没有多次用strlenO(n)提高了效率,接着扩容(不要忘了\0!),并复制内容。而我们的const成员变量现在也可以去掉啦。
对无参构造的改动:
string()
:_str(new char[1])//匹配析构
,_size(0)
,_capacity(0)
{
_str[0] = '\0';
}
~string()
{
delete[] _str;
_capacity = _size = 0;
}
为了析构与扩容匹配,这里用[ ]。因为无参不存储有效数据,所以我们用\0初始化
成功输出:
根据我们学过的缺省知识,构造函数还可以再进行简化
如果我们不传参,默认传空串
string(const char* str = "\0")//""也可以
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
可以看到逻辑是没有错的。为空串时,capacity和size都为0
注意这里不要把值定为nullptr,后面strlen会解引用出错,也不要传递单个字符(strlen用来计算字符串)
成功实现了浅拷贝!
两大问题:1.析构两次程序崩溃 2.指针指向同一块空间,有篡改的风险
实现拷贝构造:
string(const string& s)
:_size(s._size)
,_capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
成功实现深拷贝!
与拷贝构造稍微有点不同的是,赋值重载是对两个已经初始化后的操作数的运算,这就存在一个问题,两个操作数数据元素不同的问题,可能一方的数据远大于另一方,一方数据远小于另一方,当然也不排除两者数据差不多的情况。如果是前两种情况,会造成空间的大量浪费,所以我们可以使用提前释放空间的方式进行赋值。
string& operator=(const string& s)
{
delete[] _str;
_str = new char[s._capacity + 1];
_size = s._size;
_capacity = s._capacity;//释放后更改的capacity
strcpy(_str, s._str);
return *this;
}
这段代码还存在一个问题:
所以我们加上判断:
if (this != &s)
{}
return *this;
注意不要用 ==判断,因为我们没有写对应的重载,用地址判断十分巧妙(引用的特性)。
开辟空间失败怎么办?可以抛异常的方式解决,抛异常意味着空间已经被delete,这样的后果是原空间不能被正常使用了。如果想避免这种情况也可以通过中间指针的方式解决。
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;//指向堆区
_size = s._size;
_capacity = s._capacity;
}
运行结果:
现在我们给定一个函数,让它打印string的值,观察现象
在函数体中调用[ ]重载发生了典型的权限放大问题,我们在学习库里的string时发现[ ]重载有两种类型,const和非const,这样做的好处就是支持一些情况下可读,可写。
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
注意这里两个const的用法:第一个const与引用构成常量引用返回,返回值_str[pos]作为一块堆上的空间(不在堆上申请也存在),在返回其别名后就不能修改了,而第二个const修饰this指针,与可写[]构成重载,且不能在体内改变其指向对象的值。
编译器会根据所传递的参数选择最适合的对象。
这提醒我们:在写一些可写类型的成员函数时,要注意是否需要提供它的仅读版本,这点很重要!
类内:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str+size();
}
因为是一块连续的空间,所以我们可以暂且用指针的方式模拟迭代器。
类外:
zzl::string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << ' ';
++it;
}
cout << endl;
}
需要添加 string::表明使用的是string内重定义的char*
和内置类型不同,我们使用auto输出自定义类型的数据时,它所调用的底层是调用我们自己写的迭代器。
for (auto& ch : s)
{
cout << ch << ' ';
}
类似define的直接替换,如果我将begin换成start它就会报错。
如果我把他放进我们刚才写的Print函数内又会出现一个新的问题——
权限放大
我们观察这段代码更容易理解:
void Print(const string& s)
{
string::iterator it = s2.begin();
}
char* = const char*,明显的权限放大,所以迭代器也支持const版本进行readonly。
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + size();
}
运行结果:
注意这里的const修饰的是指针指向的内容,所以指针是可以++或--进行迭代的。
思考一下,string的比较是比较二者的size大小还是capacity?
答案都不是。它们甚至连\0也不放过 ~_~
正确答案是比较ascii码值,我们可以用c中的strcmp函数复用,省区我们写的功夫。
bool operator==(const string& s)const
{
return !strcmp(_str, s._str);
//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;
return *this > s || s == *this;
}
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);
}
希望大家在复用strcmp完后不要跟我一样比较错对象,后面的比较是两个string对象的比较而不是两个str指针的比较。同时注意加上const,以应付不时之需。
运行结果:
void reserve(size_t n)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
推荐使用中间变量的方法扩容,扩容失败不至于原数据丢失。
void push_back(char ch)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);//复用
}
_str[_size] = ch;//\0处插入
_size++;
_str[_size] = '\0';
}
这里采用2倍扩策略,注意当用这种方式尾插时需要手动添加\0,以免无界出现乱码
这里还有一个隐藏很深的问题,如果一开始是无参的string传递的capacity为0就会造成capacity*2=0的情况,以至于我们添加的\0造成数组越界。为了避免这种情况,有两种解决方式
在构造时判断
_capacity = _size==0? 3:_size;
在传参时判断
reserve(_capacity == 0 ? 3: _capacity * 2);
//reserve(_capacity*2+1);
void append(const char* s)
{
int len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, s);
_size += len;
}
由于不知道字符串大小,就开辟相应字符串长度的大小即可,由于我们在reserve里为\0预留了空间,所以这里不再加1。
这里不推荐用strcat遍历数组查找\0的方式追加,我们知道要追加的位置直接用strcpy更便捷。
直接复用我们这里实现的两个尾插接口:
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;
}
今天就先讲到这,下次我们对string模拟进行一个收尾。