Bjarne Stroustrup的FAQ.docx
- 文档编号:12711536
- 上传时间:2023-04-21
- 格式:DOCX
- 页数:24
- 大小:26.41KB
Bjarne Stroustrup的FAQ.docx
《Bjarne Stroustrup的FAQ.docx》由会员分享,可在线阅读,更多相关《Bjarne Stroustrup的FAQ.docx(24页珍藏版)》请在冰豆网上搜索。
BjarneStroustrup的FAQ
BjarneStroustrup的FAQ:
C++的风格与技巧
翻译:
左轻侯
(译注:
本文的翻译相当艰苦。
BjarneStroustrup不愧是创立C++语言的一代大师,不但思想博大精
深,而且在遣词造句上,也非常精微深奥。
有很多地方,译者反复斟酌,都不能取得理想的效果,只能尽
力而为。
Html格式的文档见译者主页:
如果你对这个翻译稿有任何意见和建议,请发信给译者:
onekey@。
原文的地址为:
(BjarneStroustrup博士,1950年出生于丹麦,先后毕业于丹麦阿鲁斯大学和英国剑挢大学,AT&T大规模程序设计研究部门负责人,AT&T贝尔实验室和ACM成员。
1979年,B.S开始开发一种语言,当时称为"CwithClass",后来演化为C++。
1998年,ANSI/ISOC++标准建立,同年,B.S推出其经典著作TheC++ProgrammingLanguage的第三版。
)
这是一些人们经常向我问起的有关C++的风格与技巧的问题。
如果你能提出更好的问题,或者对这些答案有所建议,请务必发Email给我(bs@)。
请记住,我不能把全部的时间都花在更新我的主页上面。
更多的问题请参见我的generalFAQ。
关于术语和概念,请参见我的C++术语表(C++glossary.)。
请注意,这仅仅是一个常见问题与解答的列表。
它不能代替一本优秀教科书中那些经过精心挑选的范例与解释。
它也不能象一本参考手册或语言标准那样,提供详细和准确的说明。
有关C++的设计的问题,请参见《C++语言的设计和演变》(TheDesignandEvolutionofC++)。
关于C++语言与标准库的使用,请参见《C++程序设计语言》(TheC++ProgrammingLanguage)。
特别是在一个学期的开始,我常常收到许多关于编写一个非常简单的程序的询问。
这个问题最典型的解决办法是,将它反复读上几遍,做某些事情,然后写出答案。
下面是一个这样做的例子:
#include
#include
#include
usingnamespacestd;
intmain()
{
vector
doubled;
while(cin>>d)v.push_back(d);//读入元素
if(!
cin.eof()){//检查输入是否出错
cerr<<"formaterror\n";
return1;//返回一个错误
}
cout<<"read"< reverse(v.begin(),v.end()); cout<<"elementsinreverseorder: \n"; for(inti=0;i 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”来表示输入已经结束。 #include #include #include #include usingnamespacestd; intmain() { vector doubled; while(cin>>d)v.push_back(d);//读入一个元素 if(! cin.eof()){//检查输入是否失败 cin.clear();//清除错误状态 strings; cin>>s;//查找结束字符 if(s! ="end"){ cerr<<"formaterror\n"; return1;//返回错误 } } cout<<"read"< reverse(v.begin(),v.end()); cout<<"elementsinreverseorder: \n"; for(inti=0;i 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)的情况: template 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< (2)<<'\n'; cout< } 它输出的结果是: 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
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Bjarne Stroustrup的FAQ Stroustrup FAQ