return0;//成功返回
}
更多的关于使用标准库将事情简化的例子,请参见《C++程序设计语言》中的“漫游标准库”("TouroftheStandardLibrary")一章。
为什么编译要花这么长的时间?
你的编译器可能有问题。
也许它太老了,也许你安装它的时候出了错,也许你用的计算机已经是个古董。
在诸如此类的问题上,我无法帮助你。
但是,这也是很可能的:
你要编译的程序设计得非常糟糕,以至于编译器不得不检查数以百计的头文件和数万行代码。
理论上来说,这是可以避免的。
如果这是你购买的库的设计问题,你对它无计可施(除了换一个更好的库),但你可以将你自己的代码组织得更好一些,以求得将修改代码后的重新编译工作降到最少。
这样的设计会更好,更有可维护性,因为它们展示了更好的概念上的分离。
看看这个典型的面向对象的程序例子:
classShape{
public:
//interfacetousersofShapes
virtualvoiddraw()const;
virtualvoidrotate(intdegrees);
//...
protected:
//commondata(forimplementersofShapes)
Pointcenter;
Colorcol;
//...
};
classCircle:
publicShape{
public:
voiddraw()const;
voidrotate(int){}
//...
protected:
intradius;
//...
};
classTriangle:
publicShape{
public:
voiddraw()const;
voidrotate(int);
//...
protected:
Pointa,b,c;
//...
};
设计思想是,用户通过Shape的public接口来操纵它们,而派生类(例如Circle和Triangle)的实现部分则共享由protected成员表现的那部分实现(implementation)。
这不是一件容易的事情:
确定哪些实现部分是对所有的派生类都有用的,并将之共享出来。
因此,与public接口相比,protected成员往往要做多得多的改动。
举例来说,虽然理论上“中心”(center)对所有的
图形都是一个有效的概念,但当你要维护一个三角形的“中心”的时候,是一件非常麻烦的事情——对于三角形,当且仅当它确实被需要的时候,计算这个中心才是有意义的。
protected成员很可能要依赖于实现部分的细节,而Shape的用户(译注:
user此处译为用户,指使用Shape类的代码,下同)却不见得必须依赖它们。
举例来说,很多(大多数?
)使用Shape的代码在逻辑上是与“颜色”无关的,但是由于Shape中“颜色”这个定义的存在,却可能需要一堆复杂的头文件,来结合操作系统的颜色概念。
当protected部分发生了改变时,使用Shape的代码必须重新编译——即使只有派生类的实现部分才能够访问protected成员。
于是,基类中的“实现相关的信息”(informationhelpfultoimplementers)对用户来说变成了象接口一样敏感的东西,它的存在导致了实现部分的不稳定,用户代码的无谓的重编译(当实现部分发生
改变时),以及将头文件无节制地包含进用户代码中(因为“实现相关的信息”需要它们)。
有时这被称为“脆弱的基类问题”(brittlebaseclassproblem)。
一个很明显的解决方案就是,忽略基类中那些象接口一样被使用的“实现相关的信息”。
换句话说,使用接口,纯粹的接口。
也就是说,用抽象基类的方式来表示接口:
classShape{
public:
//interfacetousersofShapes
virtualvoiddraw()const=0;
virtualvoidrotate(intdegrees)=0;
virtualPointcenter()const=0;
//...
//nodata
};
classCircle:
publicShape{
public:
voiddraw()const;
voidrotate(int){}
Pointcenter()const{returncenter;}
//...
protected:
Pointcent;
Colorcol;
intradius;
//...
};
classTriangle:
publicShape{
public:
voiddraw()const;
voidrotate(int);
Pointcenter()const;
//...
protected:
Colorcol;
Pointa,b,c;
//...
};
现在,用户代码与派生类的实现部分的变化之间的关系被隔离了。
我曾经见过这种技术使得编译的时间减少了几个数量级。
但是,如果确实存在着对所有派生类(或仅仅对某些派生类)都有用的公共信息时怎么办呢?
可以简单把这些信息封装成类,然后从它派生出实现部分的类:
classShape{
public:
//interfacetousersofShapes
virtualvoiddraw()const=0;
virtualvoidrotate(intdegrees)=0;
virtualPointcenter()const=0;
//...
//nodata
};
structCommon{
Colorcol;
//...
};
classCircle:
publicShape,protectedCommon{
public:
voiddraw()const;
voidrotate(int){}
Pointcenter()const{returncenter;}
//...
protected:
Pointcent;
intradius;
};
classTriangle:
publicShape,protectedCommon{
public:
voiddraw()const;
voidrotate(int);
Pointcenter()const;
//...
protected:
Pointa,b,c;
};
为什么一个空类的大小不为0?
要清楚,两个不同的对象的地址也是不同的。
基于同样的理由,new总是返回指向不同对象的指针。
看看:
classEmpty{};
voidf()
{
Emptya,b;
if(&a==&b)cout<<"impossible:
reporterrortocompilersupplier";
Empty*p1=newEmpty;
Empty*p2=newEmpty;
if(p1==p2)cout<<"impossible:
reporterrortocompilersupplier";
}
有一条有趣的规则:
一个空的基类并不一定有分隔字节。
structX:
Empty{
inta;
//...
};
voidf(X*p)
{
void*p1=p;
void*p2=&p->a;
if(p1==p2)cout<<"nice:
goodoptimizer";
}
这种优化是允许的,可以被广泛使用。
它允许程序员使用空类以表现一些简单的概念。
现在有些编译器提供这种“空基类优化”(emptybaseclassoptimization)。
我必须在类声明处赋予数据吗?
不必须。
如果一个接口不需要数据时,无须在作为接口定义的类中赋予数据。
代之以在派生类中给出它们。
参见“为什么编译要花这么长的时间?
”。
有时候,你必须在一个类中赋予数据。
考虑一下复合类(classcomplex)的情况:
templateclasscomplex{
public:
complex():
re(0),im(0){}
complex(Scalarr):
re(r),im(0){}
complex(Scalarr,Scalari):
re(r),im(i){}
//...
complex&operator+=(constcomplex&a)
{re+=a.re;im+=a.im;return*this;}
//...
private:
Scalarre,im;
};
设计这种类型的目的是将它当做一个内建(built-in)类型一样被使用。
在声明处赋值是必须的,以保证如下可能:
建立真正的本地对象(genuinelylocalobjects)(比如那些在栈中而不是在堆中分配
的对象),或者使某些简单操作被适当地inline化。
对于那些支持内建的复合类型的语言来说,要获得它们提供的效率,真正的本地对象和inline化都是必要的。
为什么成员函数默认不是virtual的?
因为很多类并不是被设计作为基类的。
例如复合类。
而且,一个包含虚拟函数的类的对象,要占用更多的空间以实现虚拟函数调用机制——往往是每个对象占
用一个字(word)。
这个额外的字是非常可观的,而且在涉及和其它语言的数据的兼容性时,可能导致麻烦
(例如C或Fortran语言)。
要了解更多的设计原理,请参见《C++语言的设计和演变》(TheDesignandEvolutionofC++)。
为什么析构函数默认不是virtual的?
因为很多类并不是被设计作为基类的。
只有类在行为上是它的派生类的接口时(这些派生类往往在堆中分配,通过指针或引用来访问),虚拟函数才有意义。
那么什么时候才应该将析构函数定义为虚拟呢?
当类至少拥有一个虚拟函数时。
拥有虚拟函数意味着一个
类是派生类的接口,在这种情况下,一个派生类的对象可能通过一个基类指针来销毁。
例如:
classBase{
//...
virtual~Base();
};
classDerived:
publicBase{
//...
~Derived();
};
voidf()
{
Base*p=newDerived;
deletep;//虚拟析构函数保证~Derived函数被调用
}
如果基类的析构函数不是虚拟的,那么派生类的析构函数将不会被调用——这可能产生糟糕的结果,例如派生类的资源不会被释放。
为什么不能有虚拟构造函数?
虚拟调用是一种能够在给定信息不完全(givenpartialinformation)的情况下工作的机制。
特别地,虚拟允许我们调用某个函数,对于这个函数,仅仅知道它的接口,而不知道具体的对象类型。
但是要建立一个对象,你必须拥有完全的信息。
特别地,你需要知道要建立的对象的具体类型。
因此,对构造函数的调用不可能是虚拟的。
当要求建立一个对象时,一种间接的技术常常被当作“虚拟构造函数”来使用。
有关例子,请参见《C++程序设计语言》第三版15.6.2.节。
下面这个例子展示一种机制:
如何使用一个抽象类来建立一个适当类型的对象。
structF{//对象建立函数的接口
virtualA*make_an_A()const=0;
virtualB*make_a_B()const=0;
};
voiduser(constF&fac)
{
A*p=fac.make_an_A();//将A作为合适的类型
B*q=fac.make_a_B();//将B作为合适的类型
//...
}
structFX:
F{
A*make_an_A()const{returnnewAX();}//AX是A的派生
B*make_a_B()const{returnnewBX();}//AX是B的派生
};
structFY:
F{
A*make_an_A()const{returnnewAY();}//AY是A的派生
B*make_a_B()const{returnnewBY();}//BY是B的派生
};
intmain()
{
user(FX());//此用户建立AX与BX
user(FY());//此用户建立AY与BY
//...
}
这是所谓的“工厂模式”(thefactorypattern)的一个变形。
关键在于,user函数与AX或AY这样的类的信息被完全分离开来了。
为什么重载在继承类中不工作?
这个问题(非常常见)往往出现于这样的例子中:
#include
usingnamespacestd;
classB{
public:
intf(inti){cout<<"f(int):
";returni+1;}
//...
};
classD:
publicB{
public:
doublef(doubled){cout<<"f(double):
";returnd+1.3;}
//...
};
intmain()
{
D*pd=newD;
cout<f
(2)<<'\n';
cout<f(2.3)<<'\n';
}
它输出的结果是:
f(double):
3.3
f(double):
3.6
而不是象有些人猜想的那样:
f(int):
3
f(double):
3.6
换句话说,在B和D之间并没有发生重载的解析。
编译器在D的区域内寻找,找到了一个函数doublef(double),并执行了它。
它永远不会涉及(被封装的)B的区域。
在C++中,没有跨越区域的重载——
对于这条规则,继承类也不例外。
更多的细节,参见《C++语言的设计和演变》和《C++程序设计语言》。
但是,如果我需要在基类和继承类之间建立一组重载的f()函数呢?
很简单,使用using声明:
classD:
publicB{
public:
usingB:
:
f;//makeeveryffromBavailable
doublef(doubled){cout<<"f(double):
";returnd+1.3;}
//...
};
进行这个修改之后,输出结果将是:
f(int):
3
f(double):
3.6
这样,在B的f()和D的f()之间,重载确实实现了,并且选择了一个最合适的f()进行调用。
我能够在构造函数中调用一个虚拟函数吗?
可以,但是要小心。
它可能不象你期望的那样工作。
在构造函数中,虚拟调用机制不起作用,因为继承类的重载还没有发生。
对象先从基类被创建,“基类先于继承类(basebeforederived)”。
看看这个:
#include
#include
usingnamespacestd;
classB{
public:
B(conststring&ss){cout<<"Bconstructor\n";f(ss);}
virtualvoidf(conststring&){cout<<"B:
:
f\n";}
};
classD:
publicB{
public:
D(conststring&ss):
B(ss){cout<<"Dconstructor\n";}
voidf(conststring&ss){cout<<"D:
:
f