赞
踩
本文将要对STL容器string进行模拟实现,将要实现string常用构造函数,析构函数,拷贝构造函数以及常用增删查改接口,介绍如何通过函数复用以达到简化代码,如何通过写实拷贝提高程序效率,通过模拟实现达到加深对string的理解,提高自身编程技巧的效果。
注:本文在读者拥有string相关知识储备的基础下更易于理解,可跳转链接阅读博主的另一篇文章掌握如何使用string后再来阅读
private:
char* _str=nullptr;//字符串
size_t _capacity=0;//容量
size_t _size=0;//有效字符个数
static const size_t npos;
//static const size_t npos=-1 vs下int类型支持给static和const同时修饰的变量用缺省值
默认构造函数(无参构造函数)是构造一个空的字符串。
string()
:_str(new char[1])
, _capacity(0)
, _size(0)
{
_str[0] = '\0';
}
像上面这样写如何?可以,但是可以更简洁一些。
像下面这样写更加简洁并且好处在于C-string构造函数可以同时担任默认构造函数与C-string构造函数的角色,这一点利用了语言的语法规则,如果我们显示定义构造函数后编译器则不会再生成默认构造函数
string(const char* str="")//对象实例化不传递参数则默认用空字符串拷贝构造
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
注:实现string(const char* str="")后不可以再显示实现默认构造函数,否则会造成调用歧义。
在调用方面,如果我们想要实例化一个空字符串的string对象要以以下方式调用
string s1;
绝对不可以用以下的方式调用,错误示例如下:
在这种情况下编译器会把它识别为函数声明
string s1();
拷贝构造函数是用一个类对象实例化另一个类对象
string(const string& str)
{
_size = str._size;
_capacity = str._capacity;
_str = str._str;
}
如果以以上方式编写后进行调用会发生什么
在调用之前我们先实现一个方便我们观察运行时现象的析构函数
清理对象占用内存资源
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
//cout<<~string()<<endl;此语句仅为观察使用
}
运行程序
监视窗口
RUN:运行错误
以上为典型浅拷贝引起的delete内存释放错误,浅拷贝使得两个对象共享同一块内存资源,在内存释放时对同一内存空间进行多次释放引起错误。
默认拷贝构造函数同样是浅拷贝,仅对对象的成员变量的值进行拷贝。
什么是浅拷贝?
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。其实我们可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。什么是深拷贝
深拷贝是指在进行对象拷贝时,不仅复制对象本身的成员变量,还复制对象所指向的动态分配的资源(例如堆内存)到新的对象中。这意味着拷贝后的对象和原对象拥有独立的资源副本,彼此之间不会相互影响。
当对象中含有动态分配的资源,如指针指向的内存块,或者其他动态分配的资源(文件句柄、网络连接等),进行深拷贝是非常重要的,以避免多个对象共享同一块资源导致释放重复、悬挂指针(悬挂指针:指的是一个指针变量指向了曾经被分配的内存地址,但该内存已经被释放或者回收了。在这种情况下,指针仍然指向原来的内存地址,但那个地址现在可能已经被操作系统重新分配给了其他程序或变量,或者已经被标记为不可用。)等问题。
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供
拷贝构造正确编写方法
string(const string& str)
{
_str = new char[str._capacity + 1];
strcpy(_str, str._str);
_size = str._size;
_capacity = str._capacity;
}
通过函数复用进行优化
void swap(string& str)//交换对象内容 { std::swap(_size, str._size); std::swap(_capacity, str._capacity); std::swap(_str, str._str); } string(const string& str) :_str(nullptr)//_str用nullptr初始化是为了确保与之交换的指针不会成为野指针,导致不必要错误 ,_size(0) ,_capacity(0) { string temp(str._str); swap(temp); //this->swap(temp);编译器为每个非静态的成员函数配备一个this指针 //这个this指针可以显示使用,也可以不显示使用 }
这样设计的原理是调用C-string构造函数后再交换两个对象的内容,temp对象出作用域之后自动销毁。
这种方法虽然简化了代码但存在着一定问题,像_size 与_capacity的大小可能与被拷贝对象的值不相同,但问题可忽略。
关于为什么不使用标准库的swap函数进行数据交换
在C++之前swap是以下形式实现
它首先会拷贝构造一个对象,然后再进行赋值拷贝,这种实现方法十分低效
C++11之后新增的运行移动语义使得其实现更高效,如果使用C++之后的swap,它的效率与我们模拟实现的方法的效率相比更高效,它省去了一切资源的开辟。
此内容需要大篇幅讲解才能理清逻辑,因本文重点不在此,故不进行详细讲解。
string& operator =(const string& s)
{
if (this != &s)//如果“自己”给“自己”赋值则直接跳过
{
char* temp = new char[s._capacity + 1];
strcpy(temp, s._str);
delete[] _str;
_str = temp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
优化
string& operator=(const string& s)
{
if (this != &s)
{
string temp(s);
swap(temp);
}
return *this;
}
再优化
string& operator=(string s)
{
swap(s);
return *this;
}
通过值拷贝的方式传递参数,直接生成一个临时对象,再交换对象内容,达到简化代码的效果。
根据Windows平台下的reserve规则
n > _capacity时对对象的容量进行扩容
n<=_capacity时不对对象的容量进行修改
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
开辟新空间,转移数据,释放原空间,修改成员变量
根据Windows平台下的reserve规则
n>_size
首先判断n > _capacity是否成立,成立则首先进行扩容操作
再将[_size,n)区间内容填入指定字符ch
n<=_size时对对象的有效字符进行缩减,将有效字符缩减至指定个数
void resize(size_t n, char ch = '\0')
{
if (n > _size)
{
reserve(n);
for (int i = _size; i < n; ++i)
{
_str[i] = ch;
}
}
_str[n] = '\0';//必须给_str[n]赋值'\0',以做字符串结束标志
_size = n;
}
返回字符串有效字符长度
size_t size() const
{
return _size;
}
返回空间总大小
size_t capacity() const
{
return _capacity;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
清空字符串内容只需要修改字符串结束标志,下次再对字符串进行操作会覆盖式写入内容。
交换两个对象内容
void swap(string& str)
{
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
std::swap(_str, str._str);
}
在指定位置插入一个字符
string& insert(size_t pos, char ch) { assert(pos <= _size); // 判断是否需要扩容 if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } size_t end = _size + 1; while (end > pos)//将插入位置 pos 之后的字符依次向后移动一个位置。 { _str[end] = _str[end - 1]; --end; } _str[pos] = ch;//将字符 ch 插入到指定的插入位置 pos ++_size;//插入字符后,将字符串的实际大小 _size 增加 1 return *this; }
在指定位置插入一个字符串
string& insert(size_t pos, const char* str) { assert(pos <= _size); size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } // 挪动数据 size_t end = _size + len; while (end >= pos + len)//将插入位置 pos 之后的字符依次向后移动 len 个位置,为新字符串的插入留出空间 { _str[end] = _str[end - len]; --end; } strncpy(_str + pos, str, len);//将字符串拷贝到指定位置 _size += len;//插入字符后,将字符串的实际大小 _size 增加 len return *this; }
在字符串后追加一个字符串
void append(const char* str)
{
size_t len = strlen(str);
// 判读是否需要扩容
if (_size + len > _capacity)
{
reserve(_size+len);
}
strcpy(_str + _size, str);
_size += len;
}
通过复用insert实现
void append(const char* str)
{
insert(_size, str);
}
字符串尾插一个字符
void push_back(char ch)
{
// 判读是否需要扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';//作为字符串必须要字符串结束标志
}
通过复用insert实现
void push_back(char ch)
{
insert(_size, ch);
}
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
如果len 等于缺省值 npos则将pos及以后字符全部删除
如果删除字符个数超过pos位置后字符总和则同样将pos及以后字符全部删除
如果以上两种情况不成立则直接将从位置 pos + len 开始的字符复制到位置 pos,覆盖掉要删除的字符。
+= 运算符在字符串的操作中通常被用作连接(拼接)操作。
连接一个字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
连接一个字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
返回pos位置的字符,可修改pos位置字符
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
返回pos位置的字符,不可修改pos位置字符
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
注意断言,下标位置不能等于_size,_size指向最后一个字符的下一个也就是字符0,但是实际库中的string的字符0我们是可以访问到的,这里主要表示有效数据的访问。
返回C格式字符串
const char* c_str() const
{
return _str;
}
获取一个子字符串
string substr(size_t pos, size_t len = npos) const { assert(pos < _size); string str; if (len == npos || pos + len >= _size) { str.reserve(_size-pos); str._size = _size - pos; strcpy(str._str, _str + pos); } else { str.reserve(len); str._size = len; strncpy(str._str,_str + pos,len); str._str[_size] = '\0'; } return str; }
如果len长度超过pos位置后字符总和或者为缺省值则将pos及以后字符全部复制,反之则复制其区间到目标字符串str,然后将字符串末尾添加字符串结束标志’\0’.
优化
string substr(size_t pos, size_t len = npos) const { assert(pos < _size); size_t end = pos+len; if (len == npos || pos + len >= _size) { end = _size; } string str; str.reserve(end - pos); for (size_t i = pos; i < end; ++i) { str += _str[i]; } return str; }
确定被拷贝字符串的末尾位置,然后将被拷贝字符串的字符依次连接到目标字符串中。
查找一个字符查找成功返回其下标,查找一个字符串查找成功返回其字符串的起始下标,查找失败均返回npos值
size_t find(const char ch, size_t pos = 0) { assert(pos < _size); while (pos < _size) { if (_str[pos] == ch) { return pos; } ++pos; } return npos; } size_t find(const char* str, size_t pos = 0) { const char* ptr = strstr(_str + pos, str); if (ptr == nullptr) { return npos; } else { return ptr - _str; } }
strstr 是一个库函数,用于在一个字符串中查找另一个字符串的首次出现位置
char *strstr(const char *haystack, const char *needle);
haystack 是要在其中进行搜索的字符串,也被称为主字符串。
needle 是要在 haystack 中查找的子字符串。
如果 needle 在 haystack 中找到,则 strstr 返回指向 haystack 中 needle 首次出现位置的指针。如果未找到 needle,则返回 NULL。
流插入运算符<<重载要定义为全局函数,因为定义为全局函数,因此在其实现中不能直接访问类的私有成员,而需要通过类的公有接口进行访问,或者将其定义为类的友元函数进而访问其私有成员。
std::ostream& operator<<(std::ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
std::istream& operator>>(std::istream& in, string& s)
{
s.clear();
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
使用istream的成员函数get,每次读入一个字符
以上设计方法存在频繁扩容的问题,如果我们频繁输入就会频繁进行扩容操作,频繁进行函数调用会降低效率,因此我们可以创建一个”输入缓冲区buff“当缓冲区填满则将缓冲区内容刷新出去,当输入结束再将未刷新的缓冲区进行刷新,该策略在语言层面和操作系统层面有着广泛应用。
std::istream& operator>>(std::istream& in, string& s) { s.clear(); char buff[128] = { '\0' }; size_t i = 0; char ch = in.get(); while (ch != ' ' && ch != '\n') { if (i == 127) { s += buff; i = 0; } buff[i++] = ch; ch = in.get(); } if (i >= 0) { buff[i] = '\0'; s += buff; } return in; }
#pragma once #include<Cassert> namespace zyc { class string { //friend std::ostream& operator<<(std::ostream& out, const zyc::string& s);//设置为友元函数直接访问类的私有成员变量 typedef char* iterator; typedef const char* const_iterator; public: string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str); } void swap(string& str) { std::swap(_size, str._size); std::swap(_capacity, str._capacity); std::swap(_str, str._str); } string(const string& str) :_str(nullptr) , _size(0) , _capacity(0) { string temp(str._str); swap(temp); } string& operator=(string s) { swap(s); return *this; } ~string() { delete[] _str; _str = nullptr; _size = 0; _capacity = 0; } void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } void resize(size_t n, char ch = '\0') { if (n > _size) { reserve(n); for (int i = _size; _size < n; i++) { _str[i] = ch; } } _str[n] = '\0'; _size = n; } string& insert(size_t pos, char ch) { assert(pos <= _size); // 满了就扩容 if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } size_t end = _size + 1; while (end > pos) { _str[end] = _str[end - 1]; --end; } _str[pos] = ch; ++_size; return *this; } string& insert(size_t pos, const char* str) { assert(pos <= _size); size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } // 挪动数据 size_t end = _size + len; while (end >= pos + len) { _str[end] = _str[end - len]; --end; } strncpy(_str + pos, str, len); _size += len; return *this; } void append(const char* str) { size_t len = strlen(str); // 满了就扩容 if (_size + len > _capacity) { reserve(_size + len); } strcpy(_str + _size, str); _size += len; } /*void append(const char* str) { insert(_size, str); }*/ void push_back(char ch) { // 满了就扩容 if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } _str[_size] = ch; ++_size; _str[_size] = '\0'; } void erase(size_t pos, size_t len = npos) { assert(pos < _size); if (len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } } /*string substr(size_t pos, size_t len = npos) const { assert(pos < _size); string str; if (len == npos || pos + len >= _size) { str.reserve(_size-pos); str._size = _size - pos; strcpy(str._str, _str + pos); } else { str.reserve(len); str._size = len; strncpy(str._str,_str + pos,len); str._str[_size] = '\0'; } return str; }*/ string substr(size_t pos, size_t len = npos) const { assert(pos < _size); size_t end = pos+len; if (len == npos || pos + len >= _size) { end = _size; } string str; str.reserve(end - pos); for (size_t i = pos; i < end; ++i) { str += _str[i]; } return str; } string& operator+=(char ch) { push_back(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; } const char* c_str() const { return _str; } size_t size() const { return _size; } size_t capacity() const { return _capacity; } size_t find(const char ch, size_t pos = 0) { assert(pos < _size); while (pos < _size) { if (_str[pos] == ch) { return pos; } ++pos; } return npos; } size_t find(const char* str, size_t pos = 0) { const char* ptr = strstr(_str + pos, str); if (ptr == nullptr) { return npos; } else { return ptr - _str; } } const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } void clear() { _str[0] = '\0'; _size = 0; } private: char* _str;//字符串 size_t _capacity;//容量 size_t _size;//有效字符个数 static const size_t npos = -1; //vs下int类型支持给static和const同时修饰的变量用缺省值 }; std::ostream& operator<<(std::ostream& out, const string& s) { for (size_t i = 0; i < s.size(); ++i) { out << s[i]; } return out; } /*std::istream& operator>>(std::istream& in, string& s) { s.clear(); char ch = in.get(); while (ch != ' ' && ch != '\n') { s += ch; ch = in.get(); } return in; }*/ std::istream& operator>>(std::istream& in, string& s) { s.clear(); char buff[128] = { '\0' }; size_t i = 0; char ch = in.get(); while (ch != ' ' && ch != '\n') { if (i == 127) { s += buff; i = 0; } buff[i++] = ch; ch = in.get(); } if (i >= 0) { buff[i] = '\0'; s += buff; } return in; } }
写时拷贝(Copy-on-Write,简称COW)是一种计算机程序设计领域的优化策略,用于延迟复制资源的实际发生,直到真正需要修改资源时。在写时拷贝的场景中,多个引用(或“视图”)最初指向同一份资源。当某个引用尝试修改资源时,系统才会创建该资源的一个副本,并让修改的引用指向这个新的副本,而其他的引用仍然指向原始的资源。
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的,既然是引用计数,必然有一个变量类是某些类所共有的。当第一个类构造时,string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时,这个计数会减一,直到最后一个类析构时,此时的引用计数变为1或0,这时对对象的空间进行释放。
关于这个引用计数怎么设计,我在这里介绍两种设计思路:
1:在类内部设置一个指针指向一片开辟的内存空间。
2:在string成员变量_str指向的堆内存空间中多开辟一个整形的空间进行计数(这里博主采用第二种设计思路)。
当我们知道引用计数的设计思路后,就要考虑什么时候应该进行写时拷贝
写时拷贝发生在对string对象进行修改操做时,比如insert,push_back,append,erease等成员函数+=,[],=赋值操作,以及析构操作时发生写时拷贝。
对于写时拷贝为主要实现了以下三个私有成员函数,方便发生写时拷贝时进行调用。
//获得引用计数
int& GetRefCount()
{
return *((int*)(_str - 4));
}
//写时拷贝
void Sub(size_t n)
{
char* tmp = new char[n + 5];
tmp += 4;
size_t len = _size;
strcpy(tmp, _str);
Release();
_str = tmp;
GetRefCount() = 1;
_capacity = n;
_size=len;
}
申请新空间,释放旧空间,在交换前前对引用计数–,交换后将新空间的引用计数置为1.
//检查是否需要对空间进行释放
void Release()
{
if (--GetRefCount() == 0)
{
delete[](_str - 4);
}
}
#pragma once #include<Cassert> #include<iostream> namespace zyc { class string { typedef char* iterator; typedef const char* const_iterator; public: string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 5]; _str += 4; GetRefCount()=1; strcpy(_str, str); } void swap(string& str) { std::swap(_size, str._size); std::swap(_capacity, str._capacity); std::swap(_str, str._str); } string(const string& str) :_str(str._str) ,_size(str._size) ,_capacity(str._capacity) { ++GetRefCount(); } string& operator=(const string& s) { _str = s._str; _size = s._size; _capacity = s._capacity; ++GetRefCount(); return *this; } ~string() { Release(); } void reserve(size_t n) { if (n > _capacity) { Sub(n); } } void resize(size_t n, char ch = '\0') { if (n > _size) { reserve(n); for (int i = _size; _size < n; i++) { _str[i] = ch; } } _str[n] = '\0'; _size = n; } string& insert(size_t pos, char ch) { assert(pos <= _size); Sub(_capacity); // 满了就扩容 if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } size_t end = _size + 1; while (end > pos) { _str[end] = _str[end - 1]; --end; } _str[pos] = ch; ++_size; return *this; } string& insert(size_t pos, const char* str) { assert(pos <= _size); Sub(_capacity); size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } // 挪动数据 size_t end = _size + len; while (end >= pos + len) { _str[end] = _str[end - len]; --end; } strncpy(_str + pos, str, len); _size += len; return *this; } void append(const char* str) { insert(_size, str); } void push_back(char ch) { insert(_size, ch); } void erase(size_t pos, size_t len = npos) { assert(pos < _size); Sub(_capacity); if (len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } } string& operator+=(char ch) { push_back(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; } string substr(size_t pos, size_t len = npos) const { assert(pos < _size); size_t end = pos + len; if (len == npos || pos + len >= _size) { end = _size; } string str; str.reserve(end - pos); for (size_t i = pos; i < end; ++i) { str += _str[i]; } return str; } const char* c_str() const { return _str; } size_t size() const { return _size; } size_t capacity() const { return _capacity; } size_t find(const char ch, size_t pos = 0) { assert(pos < _size); while (pos < _size) { if (_str[pos] == ch) { return pos; } ++pos; } return npos; } size_t find(const char* str, size_t pos = 0) { const char* ptr = strstr(_str + pos, str); if (ptr == nullptr) { return npos; } else { return ptr - _str; } } const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } char& operator[](size_t pos) { assert(pos < _size); Sub(_size); return _str[pos]; } void clear() { Sub(_capacity); _str[0] = '\0'; _size = 0; } int count() { return GetRefCount(); } private: void Release() { if (--GetRefCount() == 0) { delete[](_str - 4); } } int& GetRefCount() { return *((int*)(_str - 4)); } void Sub(size_t n) { char* tmp = new char[n + 5]; tmp += 4; size_t len = _size; strcpy(tmp, _str); Release(); _str = tmp; GetRefCount() = 1; _capacity = n; _size = len; } char* _str;//字符串 size_t _capacity;//容量 size_t _size;//有效字符个数 static const size_t npos = -1; //vs下int类型支持给static和const同时修饰的变量用缺省值 }; std::ostream& operator<<(std::ostream& out, const string& s) { for (size_t i = 0; i < s.size(); ++i) { out << s[i]; } return out; } std::istream& operator>>(std::istream& in, string& s) { s.clear(); char buff[128] = { '\0' }; size_t i = 0; char ch = in.get(); while (ch != ' ' && ch != '\n') { if (i == 127) { s += buff; i = 0; } buff[i++] = ch; ch = in.get(); } if (i >= 0) { buff[i] = '\0'; s += buff; } return in; } }
注意:我们实现的写时拷贝存并不成熟,像以下修改策略,发生修改操作时,这种操作根本就不会被我们所发现。
string str1 = "hello";
char& ref = str1[0];
string str2 = str1;
ref = 'y';
并且库的string也存在着缺陷,详情我在这里推荐一篇文章,推荐大家阅读
C++的STD::STRING的“读时也拷贝”技术!
本章到此结束,感谢您的阅读!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。