我的C++实践(17):代理类技术
2016-07-29 00:00
501 查看
代理类其实就是代理模式的应用。Proxy模式为其他对象提供一种代理以控制这个对象的访问。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层,这个访问层也叫代理。Proxy模式是最常见的模式,在我们生活中处处可见。
1、实现二维数组。 C++中数组各个维的大小必须在编译期确定。要在运行期确定数组大小,我们可以开发一个数组类来代替内建的数组,这样就可以在运行期指定数组的大小。例如对二维数组,开发一个Array2D<T>模板。对二维数组元素的访问用arr[4][6]的形式,但是类对象没有operator[][]这样的重载运算符,因此怎样才能使之与内建数组的行为一致呢?如果在Array2D<T>中直接存储二维数组,我们只有operator[]运算符,它只带一个索引参数,因此不能访问二维数组中的元素。但是一维数组可以通过operator[]直接访问数组的元素,而二维数组实际上是一个一维数组,其中每个元素又是一个一维数组。比如arr[4][6]实际上是(arr[4])[6],先取出arr的第4个元素arr[4],这个元素是一维数组,然后在arr[4]这个元素中取出第6个元素。可见,访问Array2D<T>中的元素分两步,我们可以先开发一个一维数组类Array1D<T>,它的operator[]可以直接访问元素,然后在Array2D<T>中存储一个Array1D<T>数组(而不是原始的二维数组数据),Array2D<T>的operator[]只是返回一个一维数组对象,这是第一步访问,第二步访问则直接代理给了这个一维数组对象,通过它的operator[]最终得到数组中的元素值。如下:
解释:
(1)Array1D<T>是一维数组模板,它直接存储了一个指向数组的指针data,因此在拷贝和赋值时都要进行深拷贝(对data指向的数据进行拷贝),赋值时还要检查数组长度是否一致,若不一致,则不能赋值,抛出异常。由于要创建T类型的数组,因此T必须要有默认构造函数。当然我们可以用容器比如vector来存放数据,而不用data数组,这样就可以不要求T必须有默认构造函数。Array1D<T>的operator[]直接返回数组中元素的引用。
(2)Array2D<T>是二维数组模板,它并没有存储二维数组数据,而存储了一个由一维数组对象组成的数组data。注意因为Array1D<T>没有默认的构造函数,它只有一个单参数的构造函数,因此不能直接用new Array1D<T>[len2]来初始化data。当要创建数组但没有默认构造函数时,我们可以用operator new[]来为数组分配原始的内存,然后用placement new表达式调用显式的构造函数来初始化各个元素的内存。对拷贝构造也类似,只不过调用的是拷贝构造函数。这里创建数组并没有用new操作符,因此在析构时不能用delete操作符(用delete是未定义行为,在Linux下出现segmentation fault错误),必须对数组的各个元素显式地调用析构函数来销毁对象,然后调用operator delete[]来释放整个数组内存。
(3)Array2D<T>的operator[]只是返回下标处的一维数组对象的引用,相当于arr[2],这样对数组元素的访问被代理给了这个一维数组对象arr[2],用它的operator[]最终可以获取到元素的值,即arr[2][3],从测试代码中我们可以看出这个结果。可见,通过代理类Array1D<T>,我们最终实现了与内建行为一致的元素访问语法。这种思想可以推广到多维数组上去。
2、区分operator[]的读操作和写操作。 对于前面“引用计数实现”中介绍的String类,我们通过共享开关实现了一定程度的写时拷贝,但并不完美,它导致有时在读的时候也进行了拷贝。这主要是由于共享开关并不能让operator[]区分读操作和写操作,opeartor[]函数里只是返回下标处字符的引用,然后我们才对这个字符进行读(作右值)或写(作左值)操作,如cout<<s2[2]是读操作,s2[2]='x'是写操作。可见读或写操作并不是在operator[]里面完成的,operator[]内部并不能区分是读还是写。通过代理类技术,我们可以将读或写的判断推迟到operator[]返回之后。修改operator[],让它返回一个字符的代理类对象(而不是字符本身),然后看看这个代理对象是被读(比如赋值其他对象),还是被写(比如被赋值),是写则需要写时拷贝。
解释:
(1)CharProxy类是字符的代理类,它记录了字符的下标和字符属于哪个String对象,用它来控制对字符的访问。String的const版本的operator[]返回const的CharProxy对象,这样我们对这个字符代理对象只能进行读操作。由于CharProxy构造函数的参数为String&,而operator[]中的*this是const的,因此要用const_cast去掉const属性。String的非const版本的operator[]返回非const的CharProxy对象(而不引用,因此不能通过operator[]来修改String的内容),这样我们对这个字符代理对象可读可写。现在,s2[2],s2[3]等操作得到的是CharProxy对象,而不是原始的字符。
(2)现在来看对s2[2]的操作,它是一个CharProxy对象,而不是原始的字符,当进行读操作cout<<s2[2]时,直接用CharProxy中的转型运算符operator char,转型为底部字符,无需拷贝,并且是const的,不能修改String中的这个字符,因此读操作区分出来了,没有拷贝。当进行写操作s2[2]='x',调用CharProxy的第二个赋值运算符,把char型字符赋给CharProxy对象,这需要写时拷贝。对另一种写操作形式s2[2]=s2[3],则调用第一个赋值运算符,也进行了写时拷贝。赋值运算符的左边是从String的operator[]返回的CharProxy对象,作为左值肯定是写操作,因此写操作也区分出来了。可见,通过代理对象,我们区分出了String的operator[]是读操作还是写操作,并且在写操作时进行写时拷贝。注意代理类CharProxy要访问String的私有成员value,因此要声明为友元类。
(3)使用代理类技术并不是没有缺点的。比如原来的s2[2]返回的是直接的字符,现在返回的是CharProxy对象。当然通过一次用户自定义的转型,s2[2]可以当作char型字符来用,但我们不能对s2[2]实施类似于char型的其他运算。比如不能把&s2[2]赋给char*型指针,因为CharProxy没有重载operator&,&s2[2]的结果是CharProxy*型指针。同理还有+=、++、<<=等很多运算,以及需要char&型作为函数参数而不能把s2[2]传过去等,你要使用这些操作,就必须在CharProxy中重载各个运算符。总之,代理对象不可能与它所代理的字符有完全相同的行为,而如果不使用代理对象的话,我们就不存在这样的问题。
1、实现二维数组。 C++中数组各个维的大小必须在编译期确定。要在运行期确定数组大小,我们可以开发一个数组类来代替内建的数组,这样就可以在运行期指定数组的大小。例如对二维数组,开发一个Array2D<T>模板。对二维数组元素的访问用arr[4][6]的形式,但是类对象没有operator[][]这样的重载运算符,因此怎样才能使之与内建数组的行为一致呢?如果在Array2D<T>中直接存储二维数组,我们只有operator[]运算符,它只带一个索引参数,因此不能访问二维数组中的元素。但是一维数组可以通过operator[]直接访问数组的元素,而二维数组实际上是一个一维数组,其中每个元素又是一个一维数组。比如arr[4][6]实际上是(arr[4])[6],先取出arr的第4个元素arr[4],这个元素是一维数组,然后在arr[4]这个元素中取出第6个元素。可见,访问Array2D<T>中的元素分两步,我们可以先开发一个一维数组类Array1D<T>,它的operator[]可以直接访问元素,然后在Array2D<T>中存储一个Array1D<T>数组(而不是原始的二维数组数据),Array2D<T>的operator[]只是返回一个一维数组对象,这是第一步访问,第二步访问则直接代理给了这个一维数组对象,通过它的operator[]最终得到数组中的元素值。如下:
//Array2D.hpp:二维数组类 #ifndef ARRAY_2D_HPP #define ARRAY_2D_HPP #include <cstddef> template<typename T> class Array1D{ //一维数组模板 private: class NotEqualLength{ }; //数组长度不相等时的异常类 std::size_t length; //一维数组长度 T* data; void copy(Array1D<T> const& rhs){ for(std::size_t i=0;i<length;++i) data[i]=rhs.data[i]; } public: Array1D(std::size_t len):length(len),data(new T[len]){ //T必须要有默认构造函数 } Array1D(Array1D<T> const& rhs):length(rhs.length),data(new T[rhs.length]){ copy(rhs); //深拷贝 } Array1D<T>& operator=(Array1D<T> const& rhs){ if(this==&rhs) return *this; if(length!=rhs.length) throw NotEqualLength(); //数组长度不相等,不能赋值 else copy(rhs); //深拷贝 return *this; } ~Array1D(){ delete[] data; } T const& operator[](std::size_t index) const{ //const版本 return data[index]; //直接返回数组中的元素 } T& operator[](std::size_t index){ //非const版本 return data[index]; } std::size_t getLength() const{ //返回数组第1维的大小 return length; } std::size_t getElemSum() const{ //返回数组中元素总个数 return length; } }; template<typename T> class Array2D{ //二维数组模板 private: class NotEqualLength{ }; std::size_t length2,length1; //数组各个维的大小 Array1D<T>* data; public: Array2D(std::size_t len2,std::size_t len1) :length2(len2),length1(len1),data(0){ //为Array1D<T>数组分配原始内存 void* raw=::operator new[](length2*sizeof(Array1D<T>)); data=static_cast<Array1D<T>*>(raw); //用placement new调用构造函数初始化各个元素的内存 for(std::size_t i=0;i<length2;++i) new(data+i) Array1D<T>(length1); } Array2D(Array2D<T> const& rhs) :length2(rhs.length2),length1(rhs.length1),data(0){ //拷贝构造:深拷贝 //为Array1D<T>数组分配原始的内存 void* raw=::operator new[](length2*sizeof(Array1D<T>)); data=static_cast<Array1D<T>*>(raw); //用placement new调用拷贝构造函数来初始化各个元素的内存 for(std::size_t i=0;i<length2;++i) new(data+i) Array1D<T>(rhs.data[i]); } Array2D<T>& operator=(Array2D<T> const& rhs){ //赋值运算符:要深拷贝 if(this==&rhs) return *this; //如果有一维不相等,则数组不能赋值,抛出异常 if((length2!=rhs.length2)||(length1!=rhs.length1)) throw NotEqualLength(); else{ //否则进行深拷贝 for(std::size_t i=0;i<length2;++i) data[i]=rhs.data[i]; } return *this; } ~Array2D(){ //没有用new来创建data数组,就不能直接用delete[]来删除data for(std::size_t i=0;i<length2;++i) data[i].~Array1D<T>(); //显式调用析构函数销毁各个对象 ::operator delete[](static_cast<void*>(data)); //释放内存 } Array1D<T> const& operator[](std::size_t index) const{ //const版本 return data[index]; //返回索引处的一维数组对象 } Array1D<T>& operator[](std::size_t index){ //非const版本 return data[index]; //返回索引处的一维数组对象 } std::size_t getLength2() const{ //返回数组第2维的大小 return length2; } std::size_t getLength1() const{ //返回数组第1维的大小 return length1; } long getElemSum() const{ //返回数组中的元素总个数 return length1*length2; } }; #endif
//Array2Dtest.cpp:对二组数组的测试 #include <cstddef> #include <iostream> #include "Array2D.hpp" int main(){ std::size_t a1=4; std::size_t a2=5; Array1D<int> myarr(a1); //数组的各个维数大小在运行期确定 std::cout<<"myarr's length: "<<myarr.getLength()<<std::endl; //输出一维数组长度 for(std::size_t i=0;i<myarr.getLength();++i) myarr[i]=i; std::cout<<"myarr[2]: "<<myarr[2]<<std::endl; //输出myarr[2]=2 std::cout<<"myarr's elem-numbers: "<<myarr.getElemSum()<<std::endl; //输出元素总个数 Array1D<int> yourarr(myarr); //测试拷贝构造函数 std::cout<<"yourarr[2]: "<<yourarr[2]<<std::endl; Array1D<int> herarr(4); herarr=yourarr; //测试赋值操作符 std::cout<<"herarr[2]: "<<herarr[2]<<std::endl; Array2D<int> arr(a1,a2); std::cout<<"arr's length2: "<<arr.getLength2()<<std::endl; //输出二维数组各个维的长度 std::cout<<"arr's length1: "<<arr.getLength1()<<std::endl; for(std::size_t i=0;i<arr.getLength2();++i) for(std::size_t j=0;j<arr.getLength1();++j) arr[i][j]=i+j; //下标访问与内置数组一样 std::cout<<"arr[2][3]: "<<arr[2][3]<<std::endl; //输出arr[2][3]=5 std::cout<<"arr's elem-numbers: "<<arr.getElemSum()<<std::endl; //输出元素总个数 Array2D<int> arr2(arr); //测试拷贝构造函数 std::cout<<"arr2[2][3]: "<<arr2[2][3]<<std::endl; Array2D<int> arr3(4,5); arr3=arr2; //测试赋值操作符 std::cout<<"arr3[2][3]: "<<arr3[2][3]<<std::endl; Array2D<int> arr4(3,5); arr4=arr3; //抛出异常 return 0; }
解释:
(1)Array1D<T>是一维数组模板,它直接存储了一个指向数组的指针data,因此在拷贝和赋值时都要进行深拷贝(对data指向的数据进行拷贝),赋值时还要检查数组长度是否一致,若不一致,则不能赋值,抛出异常。由于要创建T类型的数组,因此T必须要有默认构造函数。当然我们可以用容器比如vector来存放数据,而不用data数组,这样就可以不要求T必须有默认构造函数。Array1D<T>的operator[]直接返回数组中元素的引用。
(2)Array2D<T>是二维数组模板,它并没有存储二维数组数据,而存储了一个由一维数组对象组成的数组data。注意因为Array1D<T>没有默认的构造函数,它只有一个单参数的构造函数,因此不能直接用new Array1D<T>[len2]来初始化data。当要创建数组但没有默认构造函数时,我们可以用operator new[]来为数组分配原始的内存,然后用placement new表达式调用显式的构造函数来初始化各个元素的内存。对拷贝构造也类似,只不过调用的是拷贝构造函数。这里创建数组并没有用new操作符,因此在析构时不能用delete操作符(用delete是未定义行为,在Linux下出现segmentation fault错误),必须对数组的各个元素显式地调用析构函数来销毁对象,然后调用operator delete[]来释放整个数组内存。
(3)Array2D<T>的operator[]只是返回下标处的一维数组对象的引用,相当于arr[2],这样对数组元素的访问被代理给了这个一维数组对象arr[2],用它的operator[]最终可以获取到元素的值,即arr[2][3],从测试代码中我们可以看出这个结果。可见,通过代理类Array1D<T>,我们最终实现了与内建行为一致的元素访问语法。这种思想可以推广到多维数组上去。
2、区分operator[]的读操作和写操作。 对于前面“引用计数实现”中介绍的String类,我们通过共享开关实现了一定程度的写时拷贝,但并不完美,它导致有时在读的时候也进行了拷贝。这主要是由于共享开关并不能让operator[]区分读操作和写操作,opeartor[]函数里只是返回下标处字符的引用,然后我们才对这个字符进行读(作右值)或写(作左值)操作,如cout<<s2[2]是读操作,s2[2]='x'是写操作。可见读或写操作并不是在operator[]里面完成的,operator[]内部并不能区分是读还是写。通过代理类技术,我们可以将读或写的判断推迟到operator[]返回之后。修改operator[],让它返回一个字符的代理类对象(而不是字符本身),然后看看这个代理对象是被读(比如赋值其他对象),还是被写(比如被赋值),是写则需要写时拷贝。
//string2.hpp:字符串类,使用代理模式来区分读操作和写操作 #ifndef STRING_HPP #define STRING_HPP #include <iostream> #include <cstring> #include "rcobject.hpp" #include "rcptr.hpp" class String{ private: //表示字符串内容的内嵌类,实现了引用计数功能 //这个值对象必须在堆上创建 struct StringValue : public RCObject{ char *data; void init(char const* initValue){ data=new char[strlen(initValue)+1]; strcpy(data,initValue); //对字符串进行拷贝 } StringValue(char const *initValue){ //值对象的构造 init(initValue); } StringValue(StringValue const& rhs){ //值对象的拷贝 init(rhs.data); } ~StringValue(){ delete[] data; } }; RCPtr<StringValue> value; //String对象的内容,用智能指针RCPtr封装它 public: //字符的代理类 class CharProxy{ private: String& theString; //代理字符所从属的String对象 int charIndex; //真正字符在String中的下标 public: CharProxy(String& str,int index):theString(str),charIndex(index){ } CharProxy& operator=(CharProxy const& rhs){ //代理对象之间的写操作:需要写时拷贝 if(this==&rhs) return *this; if(theString.value->isShared()) //若已经被共享,则写时需要拷贝 theString.value=new StringValue(theString.value->data); theString.value->data[charIndex]= rhs.theString.value->data[rhs.charIndex]; //写入操作 return *this; } CharProxy& operator=(char c){ //原始字符到代理对象的写操作:写时拷贝 if(theString.value->isShared()) theString.value=new StringValue(theString.value->data); theString.value->data[charIndex]=c; //写入操作 return *this; } operator char() const{ //对代理对象的读操作:直接转型为底部字符,无需拷贝 return theString.value->data[charIndex]; } }; String(char const* initValue="") :value(new StringValue(initValue)){ //构造函数 } CharProxy const operator[](int index) const{ //const版本:对返回的代理对象只能进行读操作 //因为返回的对象是const的 //对要获取的字符创建一个代理对象返回 return CharProxy(const_cast<String&>(*this),index); } CharProxy operator[](int index){ //非const版本:对返回的代理对象可读可写 return CharProxy(*this,index); } friend class CharProxy; //要访问String的私有成员value friend std::ostream& operator<<(std::ostream&,String const&); }; inline std::ostream& operator<<(std::ostream& os,String const& str){ os<<(str.value)->data; return os; } #endif
//stringtest.cpp:对区分了读操作还是写操作的String类的测试 #include <iostream> #include "string2.hpp" using namespace std; int main(){ String s1("abcd"); cout<<s1<<endl; String s2("efgh"); s2=s1; //s2和s1共享"abcd" cout<<s2<<endl; const String s3(s1); //s1,s2,s3共享“abcd" cout<<s3<<endl; cout<<s3[2]<<endl; //读操作,s3[2]直接转型为底部字符,无拷贝动作 cout<<s2[2]<<endl; //读操作,s2[2]直接转型为底部字符,无拷贝动作 s2[2]='x'; //写操作,会进行写时拷贝 s2[1]=s2[2]; //写操作,会进行写时拷贝 cout<<s2<<endl; //输出修改后的值 return 0; }
解释:
(1)CharProxy类是字符的代理类,它记录了字符的下标和字符属于哪个String对象,用它来控制对字符的访问。String的const版本的operator[]返回const的CharProxy对象,这样我们对这个字符代理对象只能进行读操作。由于CharProxy构造函数的参数为String&,而operator[]中的*this是const的,因此要用const_cast去掉const属性。String的非const版本的operator[]返回非const的CharProxy对象(而不引用,因此不能通过operator[]来修改String的内容),这样我们对这个字符代理对象可读可写。现在,s2[2],s2[3]等操作得到的是CharProxy对象,而不是原始的字符。
(2)现在来看对s2[2]的操作,它是一个CharProxy对象,而不是原始的字符,当进行读操作cout<<s2[2]时,直接用CharProxy中的转型运算符operator char,转型为底部字符,无需拷贝,并且是const的,不能修改String中的这个字符,因此读操作区分出来了,没有拷贝。当进行写操作s2[2]='x',调用CharProxy的第二个赋值运算符,把char型字符赋给CharProxy对象,这需要写时拷贝。对另一种写操作形式s2[2]=s2[3],则调用第一个赋值运算符,也进行了写时拷贝。赋值运算符的左边是从String的operator[]返回的CharProxy对象,作为左值肯定是写操作,因此写操作也区分出来了。可见,通过代理对象,我们区分出了String的operator[]是读操作还是写操作,并且在写操作时进行写时拷贝。注意代理类CharProxy要访问String的私有成员value,因此要声明为友元类。
(3)使用代理类技术并不是没有缺点的。比如原来的s2[2]返回的是直接的字符,现在返回的是CharProxy对象。当然通过一次用户自定义的转型,s2[2]可以当作char型字符来用,但我们不能对s2[2]实施类似于char型的其他运算。比如不能把&s2[2]赋给char*型指针,因为CharProxy没有重载operator&,&s2[2]的结果是CharProxy*型指针。同理还有+=、++、<<=等很多运算,以及需要char&型作为函数参数而不能把s2[2]传过去等,你要使用这些操作,就必须在CharProxy中重载各个运算符。总之,代理对象不可能与它所代理的字符有完全相同的行为,而如果不使用代理对象的话,我们就不存在这样的问题。
相关文章推荐
- C++ Primer学习系列(7):标准库名字和头文件/算法简介/再谈IO库
- C++ Primer学习系列(6):模板与泛型编程/用于大型程序的工具/特殊工具与技术
- 我的C++实践(5):类型萃取技术
- C语言中将0到1000的浮点数用强制指针类型转换的方式生成一幅图像
- C++代码文件名标准化处理工具
- C++代码文件名标准化处理工具
- 解决ActiveMQ中,Java与C++交互中文乱码问题
- C++代码统计工具
- 解决ActiveMQ中,Java与C++交互中文乱码问题
- 进阶项目5-个税计算器之码数分离
- 双缓冲绘图
- Leetcode 216. Combination Sum III (Medium) (cpp)
- 每日进步之leetcode第1题C++实现代码
- C语言编写CAPLdll
- C++实现动态数组
- C++类成员函数作为线程回调函数
- C++ 类的静态成员详细讲解
- 每日进步之leetcode15题C++实现代码
- 苹果和虫子2
- C语言中的数据类型