学习笔记之Effective C++ 2nd Edition
Effective C++ 2nd Edition
50 Spefical Ways to Improve Your Programs and Designs by Scott Meyers @1999
初学C++时必读书,其中有丰富生动的程序示例,50条大师总结的经验方法规则。
-------------目录--------------
Ch1改变旧有的C习惯(Shifting from C to C++)
条款1:尽量以 const 和 inline 取代 #define
Prefer const and inline to #define.
条款2:尽量用<iostream>而不用<stdio.h>
Prefer <iostream> to <stdio.h>.
条款3:尽量以 new 和 delete 取代 malloc() 和 free()
Prefer new and delete to malloc and free.
条款4:尽量使用 C++ 风格的注释形式
Prefer C++-style comments.
Ch2内存管理(Memory Management)
条款5:使用相同形式的 new 和 delete
Use the same form in corresponding uses of new and delete
条款6:记得在 destructor 中以delete 对付 pointer members
Use delete on pointer members in destructor
条款7:为内存不足的状况预做准备
Be prepared for out-of-memory conditions.
条款8:撰写 operator new 和 operator delete 时应遵行公约
Adhere to convention when writing operator new and operator delete.
条款9:避免遮掩了 new 的正规形式
Avoid hiding the "normal" form of new.
条款10:如果你写了一个operator new,请对应也写一个operator delete
Write operator delete if you write operator new.
Ch3 构造函数、析构函数和赋值运算符 Constructors, Destructors, and Assignment Operators
条款11:如果classes内动态配置有内存,请为此 class 宣告一个 copy constructor 和一个 assignment 运算符 Declare a copy constructor and an assignment operatorfor classes with dynamically allocated memory.
条款12:在 constructor 中尽量以 initialization 取代 assignment
Prefer initialization to assignment in constructors.
条款13:Initialization list 中的 members 初始化排列次序应该和其在 class 内的声明次序相同
List members in an initialization list in the orderin which they are declared.
条款14:总是让 base class 拥有 virtual destructor
Make sure base classes have virtual destructors.条款15:令 operator= 传回"*this 的 reference"Have operator= return a reference to *this.
条款16:在 operator= 中为所有的 data members 赋予内容
Assign to all data members in operator=.
条款17:在 operator= 中检查是否"自己赋值给自己"
Check for assignment to self in operator=.
Ch4类与函数之设计和声明 Classes and Functions: Design and Declaration
条款18:努力让接口完满且最小化
Strive for class interfaces that are complete and minimal.
条款19:区分member functions, non-member functions和 friend functions三者 Differentiate among member functions, non-member functions,and friend functions.
条款20:避免将 data members 放在公开接口中
Avoid data members in the public interface.
条款21:尽可能使用 const
Use const whenever possible.
条款22:尽量使用 pass-by-reference(传址),少用 pass-by-value(传值)
Prefer pass-by-reference to pass-by-value.
条款23:当你必须传回object 时,不要尝试传回 reference
Don’t try to return a reference when you must return an object.
条款24:在函数重载(function overloading)和参数缺省化(parameter defaulting)之间,谨慎抉择
Choose carefully between function overloadingand parameter defaulting.
条款25:避免对指针型别和数值型别进行重载
Avoid overloading on a pointer and a numerical type.
条款26:防卫潜伏的 ambiguity(模棱两可)状态
Guard against potential ambiguity.
条款27:如果不想使用编译器暗自产生的member functions,就应该明白拒绝它
Explicitly disallow use of implicitly generated member functionsyou don’t want.
条款28:尝试切割 global namespace(全域命名空间)
Partition the global namespace.
Ch5类与函数之实现 Classes and Functions: Implementation
条款29:避免传回内部数据的 handles
Avoid returning "handles" to internal data.
条款30:避免写出member function,传回一个 non-const pointer 或reference 并以之指向较低存取层级的 members
Avoid member functions that return non-const pointers orreferences to members less accessible than themselves.
条款31:千万不要传回"函数内 local 对象的 reference",或是"函数内以 new 获得的指针所指的对象"
Never return a reference to a local object or toa dereferenced pointer initialized by new within the function.
条款32:尽可能延缓变量定义式的出现
Postpone variable definitions as long as possible.
条款33:明智地运用 inlining
Use inlining judiciously.
条款34:将文件之间的编译依赖关系(compilation dependencies)降至最低
Minimize compilation dependencies between files.
Ch6继承机制与面向对象设计 Inheritance and Object-Oriented Design
条款35:确定你的 public inheritance 模塑出 "isa" 的关系
Make sure public inheritance models "isa."
条款36:区分"接口继承(interface inheritance)"和"实现继承(implementation inheritance)" Differentiate between inheritance of interface andinheritance of implementation.
条款37:绝对不要重新定义一个继承而来的非虚拟函数
Never redefine an inherited nonvirtual function.
条款38:绝对不要重新定义一个继承而来的缺省参数值
Never redefine an inherited default parameter value.
条款39:避免在继承体系中做 cast down(向下转型)的动作
Avoid casts down the inheritance hierarchy.
条款40:通过 layering(分层技术)来模塑 has-a 或is-implemented-in-terms-of 的关系
Model "has-a" or "is-implemented-in-terms-of" through layering.
条款41:区分 inheritance 和 templates
Differentiate between inheritance and templates.
条款42:明智地运用 private inheritance(私有继承)
Use private inheritance judiciously.
条款43:明智地运用多继承(multiple inheritance,MI)
Use multiple inheritance judiciously.
条款44:说出你的意思并了解你所说的每一句话
Say what you mean; understand what you’re saying.
Ch7杂项讨论(Miscellany)
条款45:知道 C++(编译器)默默为我们完成和调用哪些函数
Know what functions C++ silently writes and calls.
条款46:宁愿编译和连接时出错,也不要执行时才错
Prefer compile-time and link-time errors to runtime errors. Ensure that non-local static objects are initializedbefore they’re used.
条款48:不要对编译器的警告讯息视而不见
Pay attention to compiler warnings.
条款49:尽量让自己熟悉 C++ 标准程序库
Familiarize yourself with the standard library.
条款50:加强自己对 C++ 的了解
Improve your understanding of C++.
-------------------------------
Ch1 从C转向C++
- Heap
corruption detected这种错误很容易出现,一般出现该错误,在debug中都有大致如下的错误信息:
heap corruption detected:
after normal block(#xxx) at 0x xxxxxxxx
crt detected that the application wrote to menory after end of heap buffer
这是典型的内存溢出错误,常在内存的delete处发生,而且一般在debug版本中可能出现,release版本中可能并不报错.
出现这个错误的原因一般都是操作new申请的内存溢出,因为在c++中,如果用new分配一段内存,操作的时候改变了该部分的大小,在delete时就会出错.比如说如下部分:
char* p=new char[5];
strcpy(p,"aaaaa");
delete[] p;
这段代码就会出错,因为申请了一个size为5的内存,但是strcpy过去了一个size为6的字符串,因此破坏了这个指针,运行debug版本的时候就会出现先前的错误,但是在release版本中,溢出一个字节的内存很有可能是没有错误的,然后潜在的隐患是肯定存在的,因此,我们在debug遇到这样的错误时候一定要仔细检查对new出的指针的操作。
Ch2 内存管理
- 条款7预先准备好内存不够的情况需详读。
Ch3 构造函数,析构函数和赋值操作符
- 静态成员的初始值都被缺省设为0。
- 内存泄漏和内存池有一个重要的不同之处。内存泄漏会无限地增长,即使客户循规蹈矩;而内存池的大小决不会超过客户请求内存的最大值。
- 用成员初始化列表还是比在构造函数里赋值要好。这次的原因在于效率。当使用成员初始化列表时,只有一个string成员函数被调用。而在构造函数里赋值时,将有两个被调用。
- 当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候,对类的数据成员用赋值比用初始化更合理。
- 类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系。
- 类的静态成员要在类外定义,缺省初始化为0。
- 当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
Ch4 类和函数:设计与声明
- 假设f是想正确声明的函数,c是和它相关的类:
· 虚函数必须是成员函数。如果f必须是虚函数,就让它成为c的成员函数。
· operator>>和operator<<决不能是成员函数。如果f是operator>>或operator<<,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。
· 只有非成员函数对最左边的参数进行类型转换。如果f需要对最左边的参数进行类型转换,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。
· 其它情况下都声明为成员函数。如果以上情况都不是,让f成为c的成员函数。 - 堆栈溢出就是不顾堆栈中分配的局部数据块大小,向该数据块写入了过多的数据,导致数据越界,结果覆盖了老的堆栈数据。 或者解释为 在长字符串中嵌入一段代码,并将过程的返回地址覆盖为这段代码的地址,这样当过程返回时,程序就转而开始执行这段自编的代码了。
- 这是微软的一个全新概念:应用程序池是将一个或多个应用程序链接到一个或多个工作进程集合的配置。因为应用程序池中的应用程序与其他应用程序被工作进程边界分隔,所以某个应用程序池中的应用程序不会受到其他应用程序池中应用程序所产生的问题的影响。
- 线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文.多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定. 线程的运行中需要使用计算机的内存资源和CPU。
- 定义一个简单的函数returnstudent,它取一个student参数(通过值)然后立即返回它(也通过值)。中间两次传值。
- 引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。因此,如果是一个很小的对象——例如int——传值实际上会比传引用更高效。
- 函数值传递时注意"切割问题":当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。
- 一个函数只能有两种方法创建一个新对象:在堆栈里或在堆上。在堆栈里创建对象时伴随着一个局部变量的定义,基于堆的对象是通过使用new产生的。
- ??:c++允许编译器的设计者采用一些优化措施来提高所生成的代码的性能,所以,在有些场合,operator*的返回值会被安全地除去(见条款m20)。
Ch5 类和函数:实现
- 成员函数和友元函数还是可以调用私有函数,除非——如果你够聪明的话——不去定义(实现)这个函数。这样,当无意间调用了这个函数时,程序在链接时就会报错。 对于const成员函数来说,返回句柄是不明智的,因为它会破坏数据抽象。
- 对于非const成员函数来说,返回句柄会带来麻烦,特别是涉及到临时对象时。句柄就象指针一样,可以是悬浮(dangle)的。所以一定要象避免悬浮的指针那样,尽量避免悬浮的句柄。
- 你不仅要将变量的定义推迟到必须使用它的时候,还要尽量推迟到可以为它提供一个初始化参数为止。这样做,不仅可以避免对不必要的对象进行构造和析构,还可以避免无意义的对缺省构造函数的调用。而且,在对变量进行初始化的场合下,变量本身的用途不言自明,所以在这里定义变量有益于表明变量的含义。
- 一般来说,实际编程时最初的原则是不要内联任何函数,除非函数确实很小很简单。
- 慎重地使用内联,不但给了调试器更多发挥作用的机会,还将内联的作用定位到了正确的位置:它是一个根据需要而使用的优化工具。不要忘了从无数经验得到的这条80-20定律(参见条款M16):一个程序往往花80%的时间来执行程序中20%的代码。这是一条很重要的定律,因为它提醒你,作为程序员的一个很重要的目标,就是找出这20%能够真正提高整个程序性能的代码。你可以选择内联你的函数,或者没必要就不内联,但这些选择只有作用在"正确"的函数上才有意义。
- 一旦找出了程序中那些重要的函数,以及那些内联后可以确实提高程序性能的函数(这些函数本身依赖于所在系统的体系结构),就要毫不犹豫地声明为inline。同时,要注意代码膨胀带来的问题,并监视编译器的警告信息(参见条款48),看看是否有内联函数没有被编译器内联。
- 对于上面那个号称为空的Derived的构造函数,有些编译器会为它产生相当于下面的代码:
// 一个Derived构造函数的可能的实现
Derived::Derived()
{
// 如果在堆上创建对象,为其分配堆内存;
// operator new的介绍参见条款8
if (本对象在堆上)
this = ::operator new(sizeof(Derived));
Base::Base(); // 初始化Base部分
dm1.string(); // 构造dm1
dm2.string(); // 构造dm2
dm3.string(); // 构造dm3
} - Person类仅仅用一个指针来指向某个不确定的实现,这样的类常常被称为句炳类(Handle class)或信封类(Envelope class)。(对于它们所指向的类来说,前一种情况下对应的叫法是主体类(Body class);后一种情况下则叫信件类(Letter class)。)
- 除了句柄类,另一选择是使Person成为一种特殊类型的抽象基类,称为协议类(Protocol class)。根据定义,协议类没有实现;它存在的目的是为派生类确定一个接口(参见条款36)。所以,它一般没有数据成员,没有构造函数;有一个虚析构函数(见条款14),还有一套纯虚函数,用于制定接口。
- 协议类的用户必然要有什么办法来创建新对象。这常常通过调用一个函数来实现,此函数扮演构造函数的角色,而这个构造函数所在的类即那个真正被实例化的隐藏在后的派生类。这种函数叫法挺多(如工厂函数(factory function),虚构造函数(virtual constructor)),但行为却一样:返回一个指针,此指针指向支持协议类接口(见条款M25)的动态分配对象。
- 句柄类和协议类分离了接口和实现,从而降低了文件间编译的依赖性。"但,所有这些把戏会带来多少代价呢?",我知道你在等待罚单的到来。答案是计算机科学领域最常见的一句话:它在运行时会多耗点时间,也会多耗点内存。
- 句柄类的情况下,成员函数必须通过(指向实现的)指针来获得对象数据。这样,每次访问的间接性就多一层。此外,计算每个对象所占用的内存大小时,还应该算上这个指针。还有,指针本身还要被初始化(在句柄类的构造函数内),以使之指向被动态分配的实现对象,所以,还要承担动态内存分配(以及后续的内存释放)所带来的开销 ---- 见条款10。
- 对于协议类,每个函数都是虚函数,所有每次调用函数时必须承担间接跳转的开销(参见条款14和M24)。而且,每个从协议类派生而来的对象必然包含一个虚指针(参见条款14和M24)。这个指针可能会增加对象存储所需要的内存数量(具体取决于:对于对象的虚函数来说,此协议类是不是它们的唯一来源)。
- 最后一点,句柄类和协议类都不大会使用内联函数。使用任何内联函数时都要访问实现细节,而设计句柄类和协议类的初衷正是为了避免这种情况。
Ch6 继承和面向对象设计
- 如果能理解C++各种部件的含义,你将发现自己对面向对象设计的认识大大转变。你将不再停留在为区分C++语言提供的不同部件而苦恼,而是在思考要为你的软件系统做些什么。一旦知道自己想做什么,将它转化为相应的C++部件将是一件很容易的事。
- C++面向对象编程中一条重要的规则是:公有继承意味着 "是一个" 。
- 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
- 为一个纯虚函数提供定义也是可能的。也就是说,你可以为Shape::draw提供实现,C++编译器也不会阻拦,但调用它的唯一方式是通过类名完整地指明是哪个调用:
- 声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现。
- 声明一个除纯虚函数外什么也不包含的类很有用。这样的类叫协议类(Protocol class),它为派生类仅提供函数接口,完全没有实现。
- 声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现。
- 条款35解释了公有继承的含义是
"是一个",条款36说明了为什么
"在一个类中声明一个非虚函数实际上为这个类建立了一种特殊性上的不变性"。如果将这些分析套用到类B、类D和非虚成员函数B::mf,那么,
· 适用于B对象的一切也适用于D对象,因为每个D的对象 "是一个" B的对象。
· B的子类必须同时继承mf的接口和实现,因为mf在B中是非虚函数。 - Never redefine an inherited default parameter value, because default parameter values are statically bound, while virtual functions — the only functions you should be overriding — are dynamically bound.
- 转换之于C++程序员,就象苹果之于夏娃。
- 这种类型的转换 ---- 从一个基类指针到一个派生类指针 ---- 被称为 "向下转换",因为它向下转换了继承的层次结构。在刚看到的例子中,向下转换碰巧可以工作;但正如下面即将看到的,它将给今后的维护人员带来恶梦。
- "向下转换" 可以通过几种方法来消除。最好的方法是将这种转换用虚函数调用来代替,同时,它可能对有些类不适用,所以要使这些类的每个虚函数成为一个空操作。第二个方法是加强类型约束,使得指针的声明类型和你所知道的真的指针类型之间没有出入。为了消除向下转换,无论费多大工夫都是值得的,因为向下转换难看、容易导致错误,而且使得代码难于理解、升级和维护(参见条款M32)。
- "安全的向下转换",它通过C++的dynamic_cast运算符(参见条款M2)来实现。当对一个指针使用dynamic_cast时,先尝试转换,如果成功(即,指针的动态类型(见条款38)和正被转换的类型一致),就返回新类型的合法指针;如果dynamic_cast失败,返回空指针。
- 使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这一过程称为 "分层"(Layering)。
- "分层" 这一术语有很多同义词,它也常被称为:构成(composition),包含(containment)或嵌入(embedding)。
- 当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。
- 当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。
- 这为我们引出了私有继承的含义:私有继承意味着 "用...来实现"。如果使类D私有继承于类B,这样做是因为你想利用类B中已经存在的某些代码,而不是因为类型B的对象和类型D的对象之间有什么概念上的关系。因而,私有继承纯粹是一种实现技术。用条款36引入的术语来说,私有继承意味着只是继承实现,接口会被忽略。如果D私有继承于B,就是说D对象在实现中用到了B对象,仅此而已。私有继承在软件 "设计" 过程中毫无意义,只是在软件 "实现" 时才有用。
- 接口类(interface class)example
- IntStack和CatStack所有成员函数是(隐式)内联函数,这意味着使用这些接口类所带来的开销几乎是零。- 内联函数隐式请求即在类定义的内部定义函数。
- 如果使用分层,就达不到这样的效果。只有继承才能访问保护成员,只有继承才使得虚函数可以重新被定义。(虚函数的存在会引发私有继承的使用,例子参见条款43)因为存在虚函数和保护成员,有时私有继承是表达类之间 "用...来实现" 关系的唯一有效途径。所以,当私有继承是你可以使用的最合适的实现方法时,就要大胆地使用它。同时,广泛意义上来说,分层是应该优先采用的技术,所以只要有可能,就要尽量使用它。
- C++中,关于MI一条不容争辩的事实是,MI的出现就象打开了潘朵拉的盒子,带来了单继承中绝对不会存在的复杂性。其中,最基本的一条是二义性(参见条款26)。如果一个派生类从多个基类继承了一个成员名,所有对这个名字的访问都是二义的;你必须明确地说出你所指的是哪个成员。
- 虚基类的构造函数则不同,它的参数是由继承结构中最底层派生类的成员初始化列表指定的。这就造成,负责初始化虚基类的那个类可能在继承图中和它相距很远;如果有新类增加到继承结构中,执行初始化的类还可能改变。(避免这个问题的一个好办法是:消除对虚基类传递构造函数参数的需要。最简单的做法是避免在这样的类中放入数据成员。这本质上是Java的解决之道:Java中的虚基类(即,"接口")禁止包含数据)。
- 协议类(Protocol class)的存在仅仅是为派生类制定接口;它没有数据成员,没有构造函数,有一个虚析构函数(参见条款14),有一组用来指定接口的纯虚函数。
- 理解不同的面向对象构件在C++中的含义十分重要。这和仅仅知道C++语言的规则有很大的不同。
- 公有继承和 "是一个"
的等价性,以及非虚成员函数和 "特殊性上的不变性"
的等价性,是C++构件如何和设计思想相对应的例子。下面的列表总结了这些对应关系中最重要的几个。
· 共同的基类意味着共同的特性。如果类D1和类D2都把类B声明为基类,D1和D2将从B继承共同的数据成员和/或共同的成员函数。见条款43。
· 公有继承意味着 "是一个"。如果类D公有继承于类B,类型D的每一个对象也是一个类型B的对象,但反过来不成立。见条款35。
· 私有继承意味着 "用...来实现"。如果类D私有继承于类B,类型D的对象只不过是用类型B的对象来实现而已;类型B和类型D的对象之间不存在概念上的关系。见条款42。
· 分层意味着 "有一个" 或 "用...来实现"。如果类A包含一个类型B的数据成员,类型A的对象要么具有一个类型为B的部件,要么在实现中使用了类型B的对象。见条款40。 - 下面的对应关系只适用于公有继承的情况:
· 纯虚函数意味着仅仅继承函数的接口。如果类C声明了一个纯虚函数mf,C的子类必须继承mf的接口,C的具体子类必须为之提供它们自己的实现。见条款36。
· 简单虚函数意味着继承函数的接口加上一个缺省实现。如果类C声明了一个简单(非纯)虚函数mf,C的子类必须继承mf的接口;如果需要的话,还可以继承一个缺省实现。见条款36。
· 非虚函数意味着继承函数的接口加上一个强制实现。如果类C声明了一个非虚函数mf,C的子类必须同时继承mf的接口和实现。实际上,mf定义了C的 "特殊性上的不变性"。见条款36。
Ch7 杂项
- 要想写出高效的软件,就必须知道:编译器在背后为你(给你?)做了些什么,怎样保证非局部的静态对象在被使用前已经被初始化,能从标准库得到些什么,从何处着手深入理解语言底层的设计思想。
- 当C++编译器通过它的时候。如果你没有声明下列函数,体贴的编译器会声明它自己的版本。这些函数是:一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符。另外,如果你没有声明任何构造函数,它也将为你声明一个缺省构造函数。所有这些函数都是公有的。
- 没有运行时检查,程序会更小更快。
- Date的例子将运行时检查用编译时检查来取代。你可能想知道什么时候可以使用链接时检查。实际上,不是经常这么做。C++用链接器来保证所需要的函数只被定义一次(参见条款45,"需要" 一个函数会带来什么)。它还使用链接器来保证静态对象(参见条款47)只被定义一次。你可以用同样的方法使用链接器。例如,条款27说明,对于一个显式声明的函数,如果想有意禁止对它进行定义,链接器检查就很有用。
- 但不要过于强求。想消除所有的运行检查是不切实际的。例如,任何允许交互式输入的程序都要进行输入验证。同样地,某个类中如果包含需要执行上下限检查的数组,每次访问数组时就要对数组下标进行检查。尽管如此,将检查从运行时转移到编译或链接时一直是值得努力的目标,只要实际可行,就要追求这一目标。这样做的奖赏是,程序会更小,更快,更可靠。
- 非局部静态对象指的是这样的对象:
· 定义在全局或名字空间范围内(例如:theFileSystem和tempDir),
· 在一个类中被声明为static,或,
· 在一个文件范围被定义为static。 - 你绝对无法控制不同被编译单元中非局部静态对象的初始化顺序。
- 如果你不强求一定要访问 "非局部静态对象",而愿意访问具有和非局部静态对象 "相似行为" 的对象(不存在初始化问题),难题就消失了。取而代之的是一个很容易解决的问题,甚至称不上是一个问题。
- 这种技术 ---- 有时称为 "单一模式"(译注:即Singleton pattern,参见 "Design Patterns" 一书)---- 本身很简单。首先,把每个非局部静态对象转移到函数中,声明它为static。其次,让函数返回这个对象的引用。这样,用户将通过函数调用来指明对象。换句话说,用函数内部的static对象取代了非局部静态对象。(参见条款M26)
- 虽然关于 "非局部" 静态对象什么时候被初始化,C++几乎没有做过说明;但对于函数中的静态对象(即,"局部" 静态对象)什么时候被初始化,C++却明确指出:它们在函数调用过程中初次碰到对象的定义时被初始化。所以,如果你不对非局部静态对象直接访问,而用返回局部静态对象引用的函数调用来代替,就能保证从函数得到的引用指向的是被初始化了的对象。这样做的另一个好处是,如果这个模拟非局部静态对象的函数从没有被调用,也就永远不会带来对象构造和销毁的开销;而对于非局部静态对象来说就没有这样的好事。
- 编译器的设计者肯定比你更清楚到底发生了什么。
- warning: D::f() hides virtual B::f() - 对于这条警告,很多缺乏经验的程序员会这样自言自语,"D::f当然会隐藏B::f ---- 本来就应该是这样!" 错了。编译器想告诉你的是:声明在B中的f没有在D中重新声明,它被完全隐藏了(参见条款50:为什么这样)。忽视这条编译器警告几乎肯定会导致错误的程序行为。你会不停地调试去找原因,而这个错误实际上早就被编译器发现了。
- 标准C++库中有哪些主要组件:
· 标准C库。
· Iostream。和 "传统" Iostream的实现相比,它已经被模板化了,继承层次结构也做了修改,增强了抛出异常的能力,可以支持string(通过stringstream类)和国际化(通过locales ---- 见下文)。当然,你期望Iostream库所具有的东西几乎全都继续存在。也就是说,它还是支持流缓冲区,格式化标识符,操作子和文件,还有cin,cout,cerr和clog对象。这意味着可以把string和文件当做流,还可以对流的行为进行更广泛的控制,包括缓冲和格式化。
· String。string对象在大多数应用中被用来消除对char*指针的使用。它们支持你所期望的那些操作(例如,字符串连接,通过operator[]对单个字符进行常量时间级的访问,等等),它们可以转换成char*,以保持和现有代码的兼容性,它们还自动处理内存管理。一些string的实现采用了引用计数(参见条款M29),这会带来比基于char*的字符串更佳的性能(时间和空间上)。
· 容器。不要再写你自己的基本容器类!标准库提供了下列高效的实现:vector(就象动态可扩充的数组),list(双链表),queue, stack,deque,map,set和bitset。唉,竟然没有hash table(虽然很多制造商作为扩充提供),但多少可以作为补偿的一点是, string是容器。这很重要,因为它意味着对容器所做的任何操作(见下文)对string也适用。 - 什么?你不明白我为什么说标准库的实现很高效?很简单:标准库规定了每个类的接口,而且每条接口规范中的一部分是一套性能保证。
· 算法。标准容器当然好,如果存在易于使用它们的方法就更好。标准库就提供了大量简易的方法(即,预定义函数,官方称为算法(algorithm) ---- 实际上是函数模板),其中的大多数适用于库中所有的容器 ---- 以及内建数组(built-in arrays)!
· 对国际化的支持。
· 对数字处理的支持。
· 诊断支持。标准库支持三种报错方式:C的断言(参见条款7),错误号,例外。为了有助于为例外类型提供某种结构,标准库定义了下面的例外类(exception class)层次结构: - |---domain_error
|----- logic_error<---- |---invalid_argument
| |---length_error
| |---out_of_range
exception<- |
| |--- range_error
|-----runtime_error<--|---underflow_error
|---overflow_error - C++之所以发展到现在这个样子,在于它有自己的设计目标。理解了这些设计目标,就不难弄懂所有这些东西了。C++最首要的目标在于:
· 和C的兼容性。
· 效率。
· 和传统开发工具及环境的兼容性。
· 解决真实问题的可应用性。 - 为什么隐式生成的拷贝构造函数和赋值运算符要象现在这样工作呢,尤其是指针(参见条款11和45)?因为这是C对struct进行拷贝和赋值的方式,和C兼容很重要。为什么析构函数不自动被声明为virtual(参见条款14),为什么实现细节必须出现在类的定义中(参见条款34)呢?因为不这样做就会带来性能上的损失,效率很重要。为什么C++不能检测非局部静态对象之间的初始化依赖关系(参见条款47)呢?因为C++支持单独编译(即,分开编译源模块,然后将多个目标文件链接起来,形成可执行程序),依赖现有的链接器,不和程序数据库打交道。所以,C++编译器几乎不可能知道整个程序的一切情况。最后一点,为什么C++不让程序员从一些繁杂事务如内存管理(参见条款5-10)和低级指针操作中解脱出来呢?因为一些程序员需要这些处理能力,一个真正的程序员的需要至关重要。
- 借助于D&E和ARM,你会对C++的设计和实现获得透彻理解,从而可能参悟到:有时候,看似巴洛克风格的建筑外观之后,是合理严肃的结构设计。(译注:巴洛克风格的建筑极尽富丽堂皇、粉装玉琢,因而结构复杂,甚至有点怪异)将这些理解和C++标准的具体细节结合起来,你就矗立于软件开发的坚实基础之上,从而走向真正有效的C++程序设计之路。