您的位置:首页 > 编程语言 > C语言/C++

我的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[]最终得到数组中的元素值。如下:

//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中重载各个运算符。总之,代理对象不可能与它所代理的字符有完全相同的行为,而如果不使用代理对象的话,我们就不存在这样的问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: