在BASIC、 COBOL等 中,你可以创二维、三维乃至 n 维数组。但在 C++中呢?只是有时可以,而且也只是某种程度上的。
这是合法的:
int data[10][20]; // 2D array: 10 by 20
而相同的结构如果使用变量作维的大小时,这是不可以的:
void processInput(int dim1, int dim2)
{
int data[dim1][dim2]; // error! array dimensions
... // must be known during
}
甚至,在堆分配时都是不合法的:
int *data = new int[dim1][dim2]; // error!
多维数组在 C++中的有用程度和其它语言相同,所以找到一个象样的支持方法是很重要的。常用方法是 C++中的标准方法:用一个类来实现我们所需要的而 C++语言中并没有提供的东西。因此,我们可以定义一个类模板来实现二维数组:
template<typename T>
class Array2D{
public:
Array2D(int dim1, int dim2);
...
};
现在我们可以定义我们所需要的数组了:
Array2D<int> data(10, 20);
Array2D<float> *data = new Array2D<float>(10, 20);
void processInput(int dim1, int dim2)
Array2D<int> data(dim1, dim2);
}
然而,使用这些 array 对象并不直接了当。根据 C 和 C++中的语法习惯,我们应该能够使用[]来索引数组:
cout << data[3][6];
但我们在 Array2D 类中应该怎样申明下标操作以使得我们可以这么做?
我们最初的冲动可能是申明一个 operator[][]函数:
template<class T>
class Array2D {
public:
// declarations that won't compile T& operator[][](int index1, int index2);
const T& operator[][](int index1, int index2) const;
...
};
然而,没有operator[][]这样的东西,编译期不会通过。
如果你能容忍奇怪的语法,你可以使用()来索引数组。这时,只需要重载operator():
template<class T>
class Array2D {
public:
T& operator()(int index1, int index2);
const T& operator()(int index1, int index2) const;
...
用户于是这么使用数组:
cout << data(3, 6);
这很容易实现,并很容易推广到任意多维的数组。缺点是你的 Array2D 对象看起来和内嵌数组一点都不象。实际上,上面访问元素(3,6)的操作看起来相函数调用。
如果你拒绝让访问数组行为看起来象是从 FORTRAN 流窜过来的,你将再次会到使用[]
上来。虽然没有 operator[][],但写出下面这样的代码是合法的:
int data[10][20];
...
cout << data[3][6]; // fine
上面说明了什么?
这说明变量data不是真正的二维数组,它是一个10元素的一维数组。其中每一个元素又都是一个20元素的数组,所以表达式data[3][6]
实际上是(data[3])[6]
,也就是 data的第四个元素这个数组的第 7 个元素。简而言之,第一个[]返回的是一个数组,第二个[]从这个返回的数组中再去取一个元素。
我们可以通过重载Array2D类的operator[]来玩同样的把戏:Array2D的operator[]
返回一个新类Array1D的对象,再重载Array1D的operator[]来返回所需要的二维数组中的元素:
template<typename T>
class Array2D{
public:
class Array1D{
public:
T& operator[](int index);
const T& operator[](int index) const;
...
};
Array1D operator(int index);
const Array1D operator[](int index)const;
};
现在,它合法了:
Array2D<float> data(10, 20);
...
cout << data[3][6]; // fine
这里,data[3]返回一个 Array1d 对象,在这个对象上的 operator[]操作返回二维数组中(3,6)位置上的浮点数。
Array2D 的用户并不需要知道 Array1D 类的存在。这个背后的“一维数组”对象从概念上来说,并不是为 Array2D 类的用户而存在的。其用户编程时就象他们在使用真正的二维数组一样。对于 Array2D 类的用户这样做是没有意义的:为了满足 C++的反复无常,这些对象必须在语法上兼容于其中的元素是另一个一维数组的一个一维数组。
每个Array1D对象扮演的是一个一维数组,而这个一维数组没有在使用Array2D的程序中出现。扮演其他对象的对象被称为代理类。在这个例子里,Array1D 是一个代理类,它扮演的是一个再概念上不存在的一维数组
String s1, s2;
...
cout << s1[5]; // read s1
s2[5] = 'x'; // write s2
s1[3] = s2[8]; // write s1, read s2
注意,operator[]可以在两种不同的情况下调用:读一个字符或写一个字符。读是个右值操作;写是个左值操作。通常,将一个对象做左值使用意味着它可能被修改,做右值用意味着它不能够被修改。
我们想区分将 operator[]用作左值还是右值,因为,对于有引用计数的数据结构,读操作的代价可以远小于写操作的代价,引用计数对象的写操作将导致
整个数据结构的拷贝,而读不需要,只要简单地返回一个值。
解决方法:也许不可能在operator内部区分左值还是右值,但是我们可以区别对待读操作还是写操作-----将判断读/写行为推迟到我们知道operator[]的结果被怎么使用之后的话。因此,我们所需要的是有一个方法将读或写的判断推迟到
operator[]返回之后。
代理类可以让我们得到我们所需要的时机,因为我们可以修改operator[]让它返回一个proxy对象而不是字符本身。我们可以等着看这个proxy怎么被使用。如果是读它,我们可以断定 operator[]的调用是读。如果它被写,我们必须将 operator[]的调用处理为写。
我们来看代码,但是首先要理解我们使用的 proxy 类。在 proxy 类上只能做三件事:
这里是一个被带引用计数的 string类用作 proxy类以区分 operator[]是作左值还是右值使用的例子:
class String {
public:
class CharProxy {
public:
CharProxy(String& str, int index); // creation
CharProxy& operator=(const CharProxy& rhs); // lvalue
CharProxy& operator=(char c);
operator char() const; // rvalue
private:
String& theString;
int charIndex;
};
const CharProxy operator[](int index) const; // for const Strings
CharProxy operator[](int index); // for non-const Strings
...
friend class CharProxy;
private:
RCPtr<StringValue> value;
};
String s1, s2; // reference-counted strings
// using proxies
...
cout << s1[5]; // still legal, still works
s2[5] = 'x'; // also legal, also works
s1[3] = s2[8]; // of course it's legal, of course it works
看下为什么能够工作。
cout << s1[5];
表达式s1[5]返回的是CharProxy对象。没有为这样的对象定义输出流操作,所以编译器努力的寻找一个隐式类型转换以使得operator<<调用成功。它们找到一个:在CharProxy类内部声明了一个隐式转换到char的操作。于是自动调用这个转换操作,结果就是CharProxy类扮演的字符被打印输出了。这个CharProxy到char的转换是所有代理对象作为右值使用时发生的典型行为。
作左值时的处理就不一样了。再看:
s2[5] = 'x';
和前面一样,表达式 s2[5]返回的是一个 CharProxy 对象,但这次它是赋值操作的目标。由于赋值的目标是 CharProxy 类,所以调用的是 CharProxy 类中的赋值操作。这至关重要,因为在 CharProxy 的赋值操作中,我们知道被赋值的 CharProxy 对象是作左值使用的。因此,我们知道 proxy 类扮演的字符是作左值使用的,必须执行一些必要的操作以实现字符的左值操作。
同理,语句
s1[3] = s2[8];
调用作用于两个 CharProxy 对象间的赋值操作,在此操作内部,我们知道左边一个是作左值,右边一个作右值。
从而,String的operator[]函数的代码如下:
const String::CharProxy String::operator[](int index) const
{
return CharProxy(const_cast<String&>(*this), index);
}
String::CharProxy String::operator[](int index)
{
return CharProxy(*this, index);
}
每个函数都创建和返回一个proxy对象来替代字符。根本没有对那个字符作任何操作;我们将它推迟直到我们知道是读操作还是写操作。
注意,operator[]的const版本返回一个const的proxy对象。因为CharProxy::operator=是个非 const的成员函数,这样的 proxy对象不能作赋值的目标使用。因此,不管是从 operator[]的 const 版本返回的 proxy 对象,还是它所扮演的字符都不能作左值使用。很方便啊,它正好是我们想要的 const 版本的 operator[]的行为。
同样要注意在 const 的 operator[]返回而创建 CharProxy 对象时,对*this 使用的const_cast。这使得它满足了 CharProxy 类的构造函数的需要,它的构造函
数只接受一个非 const 的 String 类。类型转换通常是领人不安的,但在此处,operator[]返回的 CharProxy 对象自己是 const 的,所以不用担心 String 内部的字符可能被通过 proxy类被修改。
通过operator[]返回的proxy对象记录了它属于哪个string对象以及所扮演的字符的下标:
String::CharProxy::CharProxy(String& str, int index)
: theString(str), charIndex(index) {}
将 proxy 对象作右值使用时很简单--只需返回它所扮演的字符就可以了:
String::CharProxy::operator char() const
{
return theString.value->data[charIndex];
}
回头再看 CahrProxy 的赋值操作的实现,这是我们必须处理 proxy 对象所扮演的字符作赋值的目标(即左值)使用的地方。我们可以将 CharProxy 的赋值操作实现如下:
String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs)
{
// if the string is sharing a value with other String objects,
// break off a separate copy of the value for this string only
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
// now make the assignment: assign the value of the char
// represented by rhs to the char represented by *this
theString.value->data[charIndex] =
rhs.theString.value->data[rhs.charIndex];
return *this;
}
String::CharProxy& String::CharProxy::operator=(char c)
{
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
theString.value->data[charIndex] = c;
return *this;
}
如果 String::operator[]返回一个 CharProxy 而不是 char &,下面的代码将
不能编译:
String s1 = "Hello";
char *p = &s1[1]; // error!
表达式 s1[1]返回一个 CharProxy,于是“=”的右边是一个 CharProxy*
。没有从CharProxy *
到 char *的转换函数,所以 p 的初始化过程编译失败了。通常,取 proxy 对象地址的操作与取实际对象地址的操作得到的指针,其类型是不同的。
要消除这个不同,你需要重载 CharProxy 类的取地址运算:
class String {
public:
class CharProxy {
public:
...
char * operator&();
const char * operator&() const;
...
};
...
};
这些函数很容易实现。const 版本返回其扮演的字符的 const 型的指针:
const char * String::CharProxy::operator&() const
{
return &(theString.value->data[charIndex]);
}
非 const 版本有多一些操作,因为它返回的指针指项的字符可以被修改:
char * String::CharProxy::operator&()
{
// make sure the character to which this function returns
// a pointer isn't shared by any other String objects
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
// we don't know how long the pointer this function
// returns will be kept by clients, so the StringValue
// object can never be shared
theString.value->markUnshareable();
return &(theString.value->data[charIndex]);
}
char 和代理它的 CharProxy 的第二个不同之处出现带引用计数的数组模板中如果我们想用 proxy 类来区分其 operator[]作左值还是右值时:
template<class T> // reference-counted array
class Array { // using proxies
public:
class Proxy {
public:
Proxy(Array<T>& array, int index);
Proxy& operator=(const T& rhs);
operator T() const;
...
};
const Proxy operator[](int index) const;
Proxy operator[](int index);
...
};
看一下这个数组可能被怎样使用:
Array<int> intArray;
...
intArray[5] = 22; // fine
intArray[5] += 5; // error!
++intArray[5]; // error!
如我们所料,当 operator[]作最简单的赋值操作的目标时,是成功的,但当它出现operator+=和 operator++的左侧时,失败了。因为 operator[]返回一个 proxy 对象,而它没有 operator+=和 operator++操作。同样的情况存在于其它需要左值的操作中,包括operator*=、operator<<=、operator–等等。如果你想让这些操作你作用在 operator[]上,必须为 Arrar< T>::Proxy 类定义所有这些函数。这是一个极大量的工作,你可能不愿意去做的。不幸的是,你要么去做这些工作,要么没有这些操作,不能两全。
一个类似的问题必须面对:通过 proxy 对象调用实际对象的成员函数。想避开它是不可能的。例如,假设我们用带引用计数的数组处理有理数。我们将定义一个 Rational 类,然后使用前面看到的 Array 模板:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
...
};
Array<Rational> array;
这是我们所期望的使用方式,但我们很失望:
cout << array[4].numerator(); // error!
int denom = array[22].denominator(); // error!
现在,不同之处很清楚了;operator[]返回一个 proxy 对象而不是实际的 Rational 对象。但成员函数 numerator()和 denominator()只存在于 Rational 对象上,而不是其 proxy对象。因此,你的编译器发出了抱怨。要使得 proxy 对象的行为和它们所扮演的对象一致,你必须重载可作用于实际对象的每一个函数。
void swap(char& a, char& b); // swaps the value of a and
b
String s = "+C+"; // oops, should be "C++"
swap(s[0], s[1]); // this should fix the problem, but it won't compile
String::operator[]返回一个 CharProxy 对象,但 swap()函数要求它所参数是 char &类型。一个 CharProxy 对象可以隐式地转换为一个 char,但没有转换为 char &的转换函数。
最后一种proxy对象不能无缝替换实际对象的情况是隐式类型转换。当proxy对象隐式转换为它所扮演的实际对象时,一个用户自定义的转换函数被调用了。比如,一个CharProxy对象可以转换为它扮演的char,通过调用operator char()函数。编译器在调用函数而将参数转换为此函数所要的类型时,只调用一个用户自定义的转换函数。于是,很可能在函数调用时,传实际对象是成功的而传 proxy 对象是失败的。例如,我们有一个 TVStation 类和一个函数 watchTV():
class TVStation {
public:
TVStation(int channel);
...
};
void watchTV(const TVStation& station, float hoursToWatch);
借助于 int 到 TVStation 的隐式类型转换,我们可以这么做:
watchTV(10, 2.5);
然而,当使用那个用 proxy 类区分 operator[]作左右值的带引用计数的数组模板时,我们就不能这么做了:
Array<int> intArray;
intArray[4] = 10;
watchTV(intArray[4], 2.5);
由于问题发生在隐式类型转换上,它很难解决。实际上,更好的设计应该是申明它的构造函数为 explicit,以使得第一次的调用 watchTV()的行为都编译失败。关