在在著名的more effective C++ 中就有一条出名的条款:Never treat arrays polymorhically ,意思是不要用多态去处理数组,那么这是为什么呢?就请往下看几个简单的例子(代码写的很烂,示例用)
class A{ //classA中只有一个四字节的int型
public :
int i = 0;
friend ostream& operator<<(ostream& os , A & a){
os << a.i << endl;
return os;
}
};
class B : public A{ //classB中什么都没有
};
void f(A[], int); //该函数以A的形式输出A中的int
void f(B[], int); //该函数以B的形式输出A中的int
int main()
{
A * a = new B[10];
cout << "size of A:" << sizeof(A) << " size of B:" << sizeof(B) << endl;
f(a , 10);
f((static_cast<B*> (a)) , 10);<span style="white-space:pre"> </span>//强制转型为B*
}
void f(A a[] , int n)
{
for(int i = 0 ; i < n ; i++){
cout << a[i];
}
cout << endl;
}
void f(B a[] , int n)
{
for(int i = 0 ; i < n ; i++){
cout << a[i];
}
}
这段代码的输出是:
size of A:4 size of B:4
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
很好,这完全符合我们的预期,A的大小就是包含的int型的大小,而B的大小则是与A相同,i的值全都初始化0,这也是对的,但当我们稍微改一点点就出问题了:
class A{ //classA中只有一个四字节的int型
public :
int i = 0;
friend ostream& operator<<(ostream& os , A & a){
os << a.i << endl;
return os;
}
};
class B : public A{ //classB中也有一个int型
int j = 1;
};<span style="white-space:pre"> </span>//其他不变
还是执行一样的函数,输出结果:
size of A:4 size of B:8
0
1
0
1
0
1
0
1
0
1
0
0
0
0
0
0
0
0
0
0
问题出来了,当B中比A包含了更多数据时,以A数组去显示就出现了意外,到底是问什么呢,这次我们换个手段,把地址打印出来:
void f(A a[] , int n)
{
for(int i = 0 ; i < n ; i++){
cout << "address: " << &a[i] << " " << a[i];
}
cout << endl;
}
void f(B a[] , int n)
{
for(int i = 0 ; i < n ; i++){
cout << "address: " << &a[i] << " " << a[i];
}
}
输出为:
size of A:4 size of B:8
address: 0x555720 0
address: 0x555724 1
address: 0x555728 0
address: 0x55572c 1
address: 0x555730 0
address: 0x555734 1
address: 0x555738 0
address: 0x55573c 1
address: 0x555740 0
address: 0x555744 1
address: 0x555720 0
address: 0x555728 0
address: 0x555730 0
address: 0x555738 0
address: 0x555740 0
address: 0x555748 0
address: 0x555750 0
address: 0x555758 0
address: 0x555760 0
address: 0x555768 0
现在就能够很清楚的看到问题,A的size是4byte,B的size是8byte,当函数以为自己接收到的是一个A时,它就以A数组去处理(a[ i ]相当于*( a + i ))而A的size是4,所以每次计算地址都只加4,导致进入了本来是B中j的地盘,所以打印出了同一个对象的int j ,当参数是B*时就没有这个问题,因为B的size是8,所以每次地址偏移的值是正确的。
可能有些人想用virtual来解决这个关于继承的问题,那么我们来看看到底虚函数能不能拯救这个情况:
class A{ //只改变classA
public :
int i = 0;
friend ostream& operator<<(ostream& os , A & a){
os << a.i << endl;
return os;
}
virtual ~A(){}<span style="white-space:pre"> </span>//增加了虚析构函数
};<span style="white-space:pre"> </span>//该类实现动态绑定???
相同主函数输出结果:
size of A:16 size of B:16
address: 0x58a3e8 0
address: 0x58a3f8 0
address: 0x58a408 0
address: 0x58a418 0
address: 0x58a428 0
address: 0x58a438 0
address: 0x58a448 0
address: 0x58a458 0
address: 0x58a468 0
address: 0x58a478 0
address: 0x58a3e8 0
address: 0x58a3f8 0
address: 0x58a408 0
address: 0x58a418 0
address: 0x58a428 0
address: 0x58a438 0
address: 0x58a448 0
address: 0x58a458 0
address: 0x58a468 0
address: 0x58a478 0
你也许会很满意的看着这个结果,用了虚函数后实现了动态绑定。但真的是这样吗?这样真的不会有错了吗?下面我做一个小小的修改:
class B : public A{ //现在classB中有两个int
int j = 1 , k = 2;
};
相同主函数输出结果:
size of A:16 size of B:24
address: 0x33a3e8 0
address: 0x33a3f8 4939312
address: 0x33a408 2
address: 0x33a418 0
address: 0x33a428 4939312
address: 0x33a438 2
address: 0x33a448 0
address: 0x33a458 4939312
address: 0x33a468 2
address: 0x33a478 0
address: 0x33a3e8 0
address: 0x33a400 0
address: 0x33a418 0
address: 0x33a430 0
address: 0x33a448 0
address: 0x33a460 0
address: 0x33a478 0
address: 0x33a490 0
address: 0x33a4a8 0
address: 0x33a4c0 0
问题出现了!当用Aclass去处理Bclass时地址偏移再次出现问题,B的实际大小为24byte,而函数中却根据A的大小16byte去偏移。这么说我们解决的问题就变成了怎么保持A和B的size一样了。但这在一般的情况下根本不可能也不科学,毕竟继承下来的类谁规定不能有更多的成员变量。这也只能告诉我们一个真理,多态与数组就是不能好好的相处,我们应该要着眼的不是在这里解决这样无聊的问题。
那万一我们一定要这么做怎么办,万一就是要让数组呈现多态怎么办,我们可以换一个角度想,我们刚刚之所以出问题是因为我们在偏移指针的时候使用的是基类的size,那么我可以这样做,用一个全是基类指针组成的数组,再让每个基类指针指向基类或派生类,这样在偏移时只会偏移一个指针的大小,但却能表现出对象多态性,比较简单的是用一个vector(或者list)封装起来:
void f(vector<A*> & va);
int main()
{
vector<A*> va{new B(), new B() , new B() , new B() , new B() , new B() , new B()};
f(va);
}
void f(vector<A*> & va)
{
for(int i = 0 ; i < va.size() ; i++){
cout << "address: " << va[i] << " " << *va[i];
}
}
输出结果:
address: 0x335720 0
address: 0x335760 0
address: 0x335740 0
address: 0x335c20 0
address: 0x335c70 0
address: 0x335c90 0
address: 0x335c40 0
这下它们全表现正常了,这个方法的坏处是在用完后要自己一个个回收数据。其实如果拆掉vector的包装,原始的指针数组应该是这样的:
void f(A** , int);
int main()
{
A *a[7];
for(int i = 0 ; i < 7 ; i++){
a[i] = new B();
}
f(a , 7);
}
void f(A** pa, int n)
{
for(int i = 0 ; i < n ; i++){
cout << "address: " << pa[i] << " " << *pa[i];
}
}
输出省略(与上例相似)
其实就是一个全是指针的数组,每个指针指向一个具体的对象,这样的一层间接关系就避开了数组与多态错综复杂的关系,发挥了面向对象的功力。
(以上代码有很多不完整的地方,比如没有delete对象,只是作为示例学习用)