C++ 简易string类实现(六)-真正的写时复制
来源:互联网 发布:c程序员面试题库 编辑:程序博客网 时间:2024/06/01 17:26
在C++ 简易string类实现(二)-引用计数中,我们引入了写时复制(copy on write),但因为C++编译期无法告诉我们operator[]是被用于读取或写,出于安全,这里假设对non-const 的operator[]的调用都是写操作,这样虽然能够正常运行,但是诸如下面的代码:
String str1 = "123";std::cout << str1[2]; //读操作str1[1] = 2; //写操作
第2行里,明显是一个读操作,但是为了安全,也会被假设在写操作.这样使用上固然没问题,但是依然不是真正的写时复制(copy on write),那么有没有办法使得编译期针对operator[],可以”智能”地知道,何时是写操作,何时是读操作吗?
下面是C++ 简易string类实现(二)-引用计数中String类的non-const版的operator[]:
char& String::operator[](size_t index_)
返回类型是char&,这里的问题是,对于写操作或者读操作,char&没有有效的办法做区分.在 C++ 简易string类实现(五)-进一步抽象中,我们引入CountHolder的一个思想是,”加上一层间接性”.这里我们同样加入一层间接性,将返回的char类型包装是一个类,我们知道,对于一个类(而不是其成员函数的返回类型),可以很容易知道其调用是写操作还是读操作(利用所调用的成员函数区分).基于此,我们引入proxy class(代理类),令String的operator返回是字符串中字符的proxy,而不返回字符本身.然后我们可以等待,看看这个proxy如何被调用.如果它被读,我们可以(有点过时地)将operator[]的调用动作视为一个读取动作.如果它被写,我们必须将operator[]的调用视为一个写操作.
对于即将使用的proxy,有3件事需呀做:
(1)产生它,本例也就是指定它代表哪一个字符串中的哪一个字符;
(2)以它作为赋值动作(assignment)的目标(接受端),这种情况下,是对它所代表的字符串内的字符做赋值动作.如果这么使用,proxy代表的将是”调用operator[]函数”的那个字符串的左值运用.
(3)以其它方式使用之.如果这么使用,proxy表现的是”调用operator[]函数”的那个字符串的右值运用.
下面是一个带有引用计数特性的String class,其中利用proxy class 来区分operator[]的左值运用(写操作)和右值运用(读操作):
class String{ friend class CharProxy; //访问String私有变量public: String(const char* str_ = ""); class CharProxy { public: CharProxy(String& str_, int index_); //构造 CharProxy& operator=(const CharProxy& rhs_); //左值运用 CharProxy& operator=(char ch_); //左值运用 operator char() const; //右值运用 private: //记录所引用的String对象和当前字符的下标, //在左值运用时,可以修改String对象的值 String& _theString; int _charIndex; }; const CharProxy operator[](int index) const; CharProxy operator[](int index);private: struct StringValue : public RCObject { char* ptr; StringValue(const char* str_); StringValue(const StringValue& rhs_); ~StringValue(); private: void init(const char* str_); }; RCPtr<StringValue> _value;};
这里的String class和C++ 简易string类实现(四)-自动操作引用次数中的那个String class之间唯一的差别就是,此处的两个operator[]都返回CharProxy对象.然而对于用户可以忽略这点(对于某些操作,例如对CharProxy变量做&操作会有问题,下面会说到),犹如operator[]返回字符一样:
String str1 = "123";std::cout << str1[2]; //合法,有效运转str1[1] = 2; //合法,有效运转
考虑这行代码
std::cout << str1[2];
表达式str1[2]产生一个CharProxy对象.此对象不曾定义output操作符,所以编译器赶紧寻找一个它能够实施的隐式类型转换,使得operator<<调用操作能够成功.编译器真的找到了一个:将CharProxy隐式转型为char.此转换函数声明于CharProxy函数内.于是编译器自动调用这个转换操作符,于是CharProxy所表现的字符串字符被打印出去.这是典型的CharProxy-to-char转换,发生在所有”被用来作为右值”的CharProxy对象身上.
左值运用的处理方式又不相同,例如这行代码
str1[1] = 2;
和上例一样,表达式str1[1]导出一个CharProxy对象,但这次这个对象是assignment动作的目标物.会调用哪个assignment操作符呢?由于目标物是个CharProxy,所以被调用的assignment操作符会是CharProxy class所定义的那个.这很重要,因为在CharProxy的assignment操作符内,我们确知”被赋值的CharProxy对象被用来作为一个左值”.我们因此知道proxy所代表的那个”字符串内的字符”将被用来作为一个左值,并因此采取所有必要行动,对该字符实施左值处理.
即使是以下这段代码:
String s1 = "1234";String s2 = "12333";s1[3] = s2[3];
最后一行调用的两个CharProxy对象的assignment操作符,在该操作符中我们知道左端对象被用来作为一个左值,右端对象被用来作为一个右值.
对于String的operator[]
const CharProxy operator[](int index) const;CharProxy operator[](int index);
每个函数都只是产生并返回”被请求字符”的一个proxy.没有任何动作施加于此字符身上:我们延缓此等行为,直到知道该行为是”读取”还是”写”.
虽然Proxy class很适合用来区分operator[]的左值运用和右值运用,但是这项技术并非没有缺点.我们希望proxy object能够无间隙地取代它们所代表的对象,但是这样的想法却很难达成.因为除了赋值(assignment)之外,对象亦有可能在其它情境下被当做左值使用,而在那种情况下的proxies常常会有与真实对象不同的行为.
例如下述代码:
String s1 = "123456";char* p = &s2[1]; //错误,无法通过编译
第二行代码无法通过编译的原因是,&s2[1]返回的是一个CharProxy*类型指针,由于不存在任何函数可以将CharProxy * 转换为 char *,所以上述的p的初始化动作无法通过编译.一般而言,”对proxy取址”所获得的指针类型和”对真实对象地址”所获得的指针类型不同.
为了消除这个难点,我们需要在CharProxy class内将取址(address of)操作符加以重载:
class String{public: ... class CharProxy { public: ... char* operator&(); const char* operator&() const; ... }; ...};
对应的实现为:
char* String::CharProxy::operator&(){ //确定"标记的字符"(本函数返回一个指针指向它)所属的字符串实值不为 //任何其它String对象共享(如果共享,就做一份专属副本出来) if (_theString._value->isShared()) { _theString._value = new StringValue(_theString._value->ptr); } //我们不知道clients会将本函数返回的指针保留多久,所以"目标字符" //所属的字符串实值(一个StringValue对象)绝不可以被共享 _theString._value->markUnshareable(); return &(_theString._value->ptr[_charIndex]);}const char* String::CharProxy::operator&() const{ return &(_theString._value->ptr[_charIndex]);}
此时,
String s1 = "123456";char* p = &s2[1]; //编译通过
问题得以解决,但本质上,CharProxy和char是不同的,我们可以通过重载的方式解决遇到的问题,这样无疑增加了工作量,但现在我们在copy-on-copy上的工作已经做到很好了,这些工作量的增加还是可以接受的,当然可以通过将一些通用的运算符加以封装,得以复用,然后针对具体的类,再增加额外所需的函数.
完整代码如下,
结构图:
class RCObject{public: RCObject(); RCObject(const RCObject& rhs_); //使RCObject成为抽象基类,但该纯虚函数需要提供 //定义,不然会使被继承的类无法在栈上创建(原因可 //查阅如何仅在堆上或栈上分配内存) virtual ~RCObject() = 0; public: void addReference(); void removeReference(); void markUnshareable(); bool isShareable() const; bool isShared() const;private: RCObject& operator=(const RCObject&) = delete;private: int refCount; bool shareable;};template<typename T>class RCPtr{public: RCPtr(T* realPtr = 0); RCPtr(const RCPtr& rhs_); RCPtr& operator=(const RCPtr& rhs_); ~RCPtr();public: T* operator->() const; T& operator*() const;private: void init();private: T* _ptr;};class String{ friend class CharProxy;public: String(const char* str_ = ""); class CharProxy { public: CharProxy(String& str_, int index_); //构造 CharProxy& operator=(const CharProxy& rhs_); //左值运用 CharProxy& operator=(char ch_); //左值运用 operator char() const; //右值运用 char* operator&(); const char* operator&() const; private: //记录所引用的String对象和当前字符的下标, //在左值运用时,可以修改String对象的值 String& _theString; int _charIndex; }; const CharProxy operator[](int index) const; CharProxy operator[](int index);private: struct StringValue : public RCObject { char* ptr; StringValue(const char* str_); StringValue(const StringValue& rhs_); ~StringValue(); private: void init(const char* str_); }; RCPtr<StringValue> _value;};
RCObject::RCObject() :refCount(0), shareable(true) //refCount初始化为0,其值完全有RCPtr控制{}RCObject::RCObject(const RCObject&)//调用无参构造函数,注意:该调用仅能在//初始化成员列表里,如果在函数实现内调用,//那么仅仅是在栈上生成新的对象,而不是完成//该对象的成员初始化 :RCObject(){ std::cout << "RCObject" << std::endl;}RCObject::~RCObject(){ //std::cout << "~RCObject" << std::endl;}void RCObject::addReference(){ ++refCount;}void RCObject::removeReference(){ if (--refCount == 0) { delete this; }}void RCObject::markUnshareable(){ shareable = false;}bool RCObject::isShareable() const{ return shareable;}bool RCObject::isShared() const{ return refCount > 1;}template<typename T>RCPtr<T>::RCPtr(const RCPtr& rhs_) : _ptr(rhs_._ptr){ init();}template<typename T>void RCPtr<T>::init(){ if (_ptr == nullptr) { return; } if (_ptr->isShareable() == false) { //如果其值不可共享,那么就赋值一份 //注意,这里将新对象的赋值行为,由之前的 //String转移到RCPtr,此时T(在原有的String实现中是StringValue) //若要完成深拷贝,T(StringValue)必须重写赋值运算符 _ptr = new T(*_ptr); } //计数器完全由RCPtr控制,即使上个if语句内重新赋值的对象 //其引用计数也由RCPtr控制,故将RCObject的refCount初值赋值0 _ptr->addReference(); }template<typename T>RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs_){ if (_ptr != rhs_._ptr) { if (_ptr != nullptr) { _ptr->removeReference(); } _ptr = rhs_._ptr; init(); } return *this;}template<typename T>RCPtr<T>::~RCPtr(){ if (_ptr != nullptr) { _ptr->removeReference(); }}template<typename T>T* RCPtr<T>::operator->() const{ return _ptr;}template<typename T>T& RCPtr<T>::operator*() const{ return *_ptr;}const String::CharProxy String::operator[](int index_) const{ //为了简洁,这里略去对下标的安全检查 return CharProxy(const_cast<String&>(*this), index_);}String::CharProxy String::operator[](int index_){ //将决定读写的权利交给CharProxy,这里当做读,简单的返回 //CharProxy实例 return CharProxy(*this, index_);}String::CharProxy::CharProxy(String& str_, int index_) : _theString(str_), _charIndex(index_){}String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs_){ if (_theString._value->isShared()) { _theString._value = new String::StringValue(_theString._value->ptr); } //不再需RCObject中的shareable变量及对应的成员函数 _theString._value->ptr[_charIndex] = rhs_._theString._value->ptr[rhs_._charIndex]; return *this;}String::CharProxy& String::CharProxy::operator=(char ch_){ if (_theString._value->isShared()) { _theString._value = new StringValue(_theString._value->ptr); } _theString._value->ptr[_charIndex] = ch_; return *this;}String::CharProxy::operator char() const{ return _theString._value->ptr[_charIndex];}char* String::CharProxy::operator&(){ //确定"标记的字符"(本函数返回一个指针指向它)所属的字符串实值不为 //任何其它String对象共享(如果共享,就做一份专属副本出来) if (_theString._value->isShared()) { _theString._value = new StringValue(_theString._value->ptr); } //我们不知道clients会将本函数返回的指针保留多久,所以"目标字符" //所属的字符串实值(一个StringValue对象)绝不可以被共享 _theString._value->markUnshareable(); return &(_theString._value->ptr[_charIndex]);}const char* String::CharProxy::operator&() const{ return &(_theString._value->ptr[_charIndex]);}void String::StringValue::init(const char* str_){ ptr = new char[strlen(str_) + 1]; strcpy(ptr, str_);}String::StringValue::StringValue(const StringValue& rhs_){ init(rhs_.ptr);}String::StringValue::StringValue(const char* str_){ init(str_);}String::StringValue::~StringValue(){ if (ptr != nullptr) { delete[] ptr; }}
- C++ 简易string类实现(六)-真正的写时复制
- [C++] String类的复制控制的实现
- 关于C++ string类的写时复制
- C++ string 的写时复制
- String类的简易实现(C++语言)
- C++ 简易string类实现(一)
- String COW 写时复制
- 自己写一个简易的string类型
- 【C++】浅析浅拷贝,深拷贝及写时拷贝(copy_on_write),模拟实现String类。
- [linux 0.11]写时复制的实现
- 用C语言写的简易记事本(Linux下)
- 【C语言】String类的写时拷贝
- 写时拷贝的方式实现基本的String类
- 用C写的简易计算器
- C语言写的简易计算器
- C语言写的一个简易计算器
- C++string类部分函数仿写(用C语言实现)
- c++ String 类的简单实现和写时拷贝
- linux 数据库管理
- lintcode刷题(python)——(4)
- Linux运维笔记-文档总结-正向代理和反向代理
- LeetCode 456. 132 Pattern
- SpriteKit游戏如何一步一步重构在场景中增加金币动画(一)
- C++ 简易string类实现(六)-真正的写时复制
- 前端标签的小技巧-----自定义标签属性(灵活使用js/JQ脚本)
- linux学习 Apache
- 学习笔记TF010:softmax分类
- spring boot 中logback多环境配置
- android studio perView 无法预览
- 备考PMP第十一天
- RTSP
- linux学习 CDN加速