Bjarne Stroustrup的FAQ文档格式.docx
- 文档编号:2959582
- 上传时间:2023-05-01
- 格式:DOCX
- 页数:24
- 大小:26.41KB
Bjarne Stroustrup的FAQ文档格式.docx
《Bjarne Stroustrup的FAQ文档格式.docx》由会员分享,可在线阅读,更多相关《Bjarne Stroustrup的FAQ文档格式.docx(24页珍藏版)》请在冰点文库上搜索。
#include<
iostream>
vector>
algorithm>
usingnamespacestd;
intmain()
{
vector<
double>
v;
doubled;
while(cin>
>
d)v.push_back(d);
//读入元素
if(!
cin.eof()){//检查输入是否出错
cerr<
<
"
formaterror\n"
;
return1;
//返回一个错误
}
cout<
read"
<
v.size()<
elements\n"
reverse(v.begin(),v.end());
elementsinreverseorder:
\n"
for(inti=0;
i<
v.size();
++i)cout<
v[i]<
'
\n'
return0;
//成功返回
对这段程序的观察:
这是一段标准的ISOC++程序,使用了标准库(standardlibrary)。
标准库工具在命名空间std中声明,封装在没有.h后缀的头文件中。
如果你要在Windows下编译它,你需要将它编译成一个“控制台程序”(consoleapplication)。
记得将源文件加上.cpp后缀,否则编译器可能会以为它是一段C代码而不是C++。
是的,main()函数返回一个int值。
读到一个标准的向量(vector)中,可以避免在随意确定大小的缓冲中溢出的错误。
读到一个数组(array)中,而不产生“简单错误”(sillyerror),这已经超出了一个新手的能力——如果你做到了,那你已经不是一个新手了。
如果你对此表示怀疑,我建议你阅读我的文章“将标准C++作为一种新的语言来学习”("
LearningStandardC++asaNewLanguage"
),你可以在本人著作列表(mypublications
list)中下载到它。
!
cin.eof()是对流的格式的检查。
事实上,它检查循环是否终结于发现一个end-of-file(如果不是这样,那么意味着输入没有按照给定的格式)。
更多的说明,请参见你的C++教科书中的“流状态”(stream
state)部分。
vector知道它自己的大小,因此我不需要计算元素的数量。
这段程序没有包含显式的内存管理。
Vector维护一个内存中的栈,以存放它的元素。
当一个vector需要更多的内存时,它会分配一些;
当它不再生存时,它会释放内存。
于是,使用者不需要再关心vector中元素的内存分配和释放问题。
程序在遇到输入一个“end-of-file”时结束。
如果你在UNIX平台下运行它,“end-of-file”等于键盘上的Ctrl+D。
如果你在Windows平台下,那么由于一个BUG它无法辨别“end-of-file”字符,你可能倾向于使用下面这个稍稍复杂些的版本,它使用一个词“end”来表示输入已经结束。
string>
//读入一个元素
cin.eof()){//检查输入是否失败
cin.clear();
//清除错误状态
strings;
cin>
s;
//查找结束字符
if(s!
="
end"
){
//返回错误
更多的关于使用标准库将事情简化的例子,请参见《C++程序设计语言》中的“漫游标准库”("
TouroftheStandardLibrary"
)一章。
为什么编译要花这么长的时间?
你的编译器可能有问题。
也许它太老了,也许你安装它的时候出了错,也许你用的计算机已经是个古董。
在诸如此类的问题上,我无法帮助你。
但是,这也是很可能的:
你要编译的程序设计得非常糟糕,以至于编译器不得不检查数以百计的头文件和数万行代码。
理论上来说,这是可以避免的。
如果这是你购买的库的设计问题,你对它无计可施(除了换一个更好的库),但你可以将你自己的代码组织得更好一些,以求得将修改代码后的重新编译工作降到最少。
这样的设计会更好,更有可维护性,因为它们展示了更好的概念上的分离。
看看这个典型的面向对象的程序例子:
classShape{
public:
//interfacetousersofShapes
virtualvoiddraw()const;
virtualvoidrotate(intdegrees);
//...
protected:
//commondata(forimplementersofShapes)
Pointcenter;
Colorcol;
};
classCircle:
publicShape{
voiddraw()const;
voidrotate(int){}
intradius;
classTriangle:
voidrotate(int);
Pointa,b,c;
设计思想是,用户通过Shape的public接口来操纵它们,而派生类(例如Circle和Triangle)的实现部分则共享由protected成员表现的那部分实现(implementation)。
这不是一件容易的事情:
确定哪些实现部分是对所有的派生类都有用的,并将之共享出来。
因此,与public接口相比,protected成员往往要做多得多的改动。
举例来说,虽然理论上“中心”(center)对所有的
图形都是一个有效的概念,但当你要维护一个三角形的“中心”的时候,是一件非常麻烦的事情——对于三角形,当且仅当它确实被需要的时候,计算这个中心才是有意义的。
protected成员很可能要依赖于实现部分的细节,而Shape的用户(译注:
user此处译为用户,指使用Shape类的代码,下同)却不见得必须依赖它们。
举例来说,很多(大多数?
)使用Shape的代码在逻辑上是与“颜色”无关的,但是由于Shape中“颜色”这个定义的存在,却可能需要一堆复杂的头文件,来结合操作系统的颜色概念。
当protected部分发生了改变时,使用Shape的代码必须重新编译——即使只有派生类的实现部分才能够访问protected成员。
于是,基类中的“实现相关的信息”(informationhelpfultoimplementers)对用户来说变成了象接口一样敏感的东西,它的存在导致了实现部分的不稳定,用户代码的无谓的重编译(当实现部分发生
改变时),以及将头文件无节制地包含进用户代码中(因为“实现相关的信息”需要它们)。
有时这被称为“脆弱的基类问题”(brittlebaseclassproblem)。
一个很明显的解决方案就是,忽略基类中那些象接口一样被使用的“实现相关的信息”。
换句话说,使用接口,纯粹的接口。
也就是说,用抽象基类的方式来表示接口:
virtualvoiddraw()const=0;
virtualvoidrotate(intdegrees)=0;
virtualPointcenter()const=0;
//nodata
Pointcenter()const{returncenter;
Pointcent;
Pointcenter()const;
现在,用户代码与派生类的实现部分的变化之间的关系被隔离了。
我曾经见过这种技术使得编译的时间减少了几个数量级。
但是,如果确实存在着对所有派生类(或仅仅对某些派生类)都有用的公共信息时怎么办呢?
可以简单把这些信息封装成类,然后从它派生出实现部分的类:
structCommon{
publicShape,protectedCommon{
为什么一个空类的大小不为0?
要清楚,两个不同的对象的地址也是不同的。
基于同样的理由,new总是返回指向不同对象的指针。
看看:
classEmpty{};
voidf()
Emptya,b;
if(&
a==&
b)cout<
impossible:
reporterrortocompilersupplier"
Empty*p1=newEmpty;
Empty*p2=newEmpty;
if(p1==p2)cout<
}
有一条有趣的规则:
一个空的基类并不一定有分隔字节。
structX:
Empty{
inta;
voidf(X*p)
void*p1=p;
void*p2=&
p->
a;
nice:
goodoptimizer"
这种优化是允许的,可以被广泛使用。
它允许程序员使用空类以表现一些简单的概念。
现在有些编译器提供这种“空基类优化”(emptybaseclassoptimization)。
我必须在类声明处赋予数据吗?
不必须。
如果一个接口不需要数据时,无须在作为接口定义的类中赋予数据。
代之以在派生类中给出它们。
参见“为什么编译要花这么长的时间?
”。
有时候,你必须在一个类中赋予数据。
考虑一下复合类(classcomplex)的情况:
template<
classScalar>
classcomplex{
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();
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:
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这样的类的信息被完全分离开来了。
为什么重载在继承类中不工作?
这个问题(非常常见)往往出现于这样的例子中:
classB{
intf(inti){cout<
f(int):
returni+1;
classD:
publicB{
doublef(doubled){cout<
f(double):
returnd+1.3;
D*pd=newD;
pd->
f
(2)<
f(2.3)<
它输出的结果是:
f(double):
3.3
3.6
而不是象有些人猜想的那样:
f(int):
3
换句话说,在B和D之间并没有发生重载的解析。
编译器在D的区域内寻找,找到了一个函数doublef(double),并执行了它。
它永远不会涉及(被封装的)B的区域。
在C++中,没有跨越区域的重载——
对于这条规则,继承类也不例外。
更多的细节,参见《C++语言的设计和演变》和《C++程序设计语言》。
但是,如果我需要在基类和继承类之间建立一组重载的f()函数呢?
很简单,使用using声明:
usingB:
:
f;
//makeeveryffromBavailable
进行这个修改之后,输出结果将是:
这样,在B的f()和D的f()之间,重载确实实现了,并且选择了一个最合适的f()进行调用。
我能够在构造函数中调用一个虚拟函数吗?
可以,但是要小心。
它可能不象你期望的那样工作。
在构造函数中,虚拟调用机制不起作用,因为继承类的重载还没有发生。
对象先从基类被创建,“基类先于继承类(basebeforederived)”。
看看这个:
B(conststring&
ss){cout<
Bconstructor\n"
f(ss);
virtualvoidf(conststring&
){cout<
B:
f\n"
D(conststring&
ss):
B(ss){cout<
Dconstructor\n"
voidf(conststring&
D:
f
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Bjarne Stroustrup的FAQ Stroustrup FAQ