Bjarne Stroustrup的FAQ.docx

上传人:b****0 文档编号:12711536 上传时间:2023-04-21 格式:DOCX 页数:24 大小:26.41KB
下载 相关 举报
Bjarne Stroustrup的FAQ.docx_第1页
第1页 / 共24页
Bjarne Stroustrup的FAQ.docx_第2页
第2页 / 共24页
Bjarne Stroustrup的FAQ.docx_第3页
第3页 / 共24页
Bjarne Stroustrup的FAQ.docx_第4页
第4页 / 共24页
Bjarne Stroustrup的FAQ.docx_第5页
第5页 / 共24页
点击查看更多>>
下载资源
资源描述

Bjarne Stroustrup的FAQ.docx

《Bjarne Stroustrup的FAQ.docx》由会员分享,可在线阅读,更多相关《Bjarne Stroustrup的FAQ.docx(24页珍藏版)》请在冰豆网上搜索。

Bjarne Stroustrup的FAQ.docx

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()

{

vectorv;

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()

{

vectorv;

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)的情况:

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

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 初中教育 > 英语

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1