<Effective C++>

****************************************
转自他人笔记
***************************************

绝对经典贴子,甚至比原书更实用
可以先读这个贴子,再去读Hou sir的翻译

正文
第一次读《Effective C++》的时候,感到受益匪浅,后来又读了一次,顺手涂鸦,写了一些自己的理解。今天翻出来看看,觉得可以给C++阵营中的同道看看,如有误导,呵呵,先谢过了。
1. 尽量以const和inline取代#define
你还需要预处理器吗?给他一个超长的假期吧!你的力量将#define丢进博物馆吧。
看看:
#define max(a,b) ((a)>(b)?(a):(b))
你会感叹到:”多好的一个宏啊!”可是恶梦来了:
int a=5,b=0;
max(++a,b);
a被累进了两次。专家的意见:不要再使用宏了!
可是我的经验是:define还是很好用,目前还是不可能完全抛弃它。

2. 尽量以<iostream>取代<stdio.h>
printf和scanf是如此熟悉的函数啊,我想你也是如此。^_^。
可是,他们却都不是类型安全(Type-safe)的,也都不具有可扩充性。
看看cin和cout吧,你可以来重载<<和>>。
呵呵,我想当你经历了这种转变的镇痛后,你会获得新生的。
不过,这种控制台下的东东,我想也不大用的着,^_^,可是你需要的是学会改变!正所谓是:与时俱进!
<iostream>而不是<iostream.h>,我偷懒了?呵呵,当然少打了两个字,何乐而不为呢?当然,真实的原因并非如此,^_^!这个问题留给你吧。

3. 尽量以new和delete取代malloc和free
原因:malloc和free对constructors和destructors一无所知!!!
我觉得这条没有什么多说的!new和delete比起malloc和free是如此好用,何乐而不为呢?
而且我的建议是:总是使用new和delete,让malloc和free见鬼去吧!至少我是这么做的。

4. 尽量使用C++风格的注释形式
C的注释形式/*...*/和C++的注释形式//...我都在使用,但是要注意的是,/*...*/不允许嵌套。
有一点需要注意的是,有些“古董级”的编译器看不懂如下的注释:
#define LIGHT_SPEED 3e8 //m/sec (in a vacuum)
它会把后面的注释也拿出来当作宏的一部分。
呵呵,当然,正如条款1所说,尽量避免吧。

5. 使用相同形式的new和delete
当我需要在堆上分配内存时,我总是感到异常的紧张。因为我实在是被Memory Leaks搞怕了。
以下给出了一点new和delete的用法提示:
string *stringPtr1=new string;
string *stringPtr2=new string;
...
delete stringPtr1; //删除单一对象
delete [ ] stringPtr2; //删除整个对象数组
更多的参见MSDN。
正确地使用new和delete是一名合格C++程序员的最基本素质。

6. 记得在destructor中以delete对付pointer member
还是动态内存分配的问题,足见此问题的重要性!!!
除了记住条款6外,我想再给出几点其他建议:
1、当声明一个指针,在用new为其分配内存前,一定要将其初始化为空
2、当释放内存后,马上将该指针赋为空
3、为一个指针重新分配内存后,必须要将旧的内存释放掉
4、记住,在任何时候,一个指针”要不指向有效内存,要不就是NULL“
5、删除一个NULL指针是安全的。

7. 为内存不足的状况预做准备
new调用后得不到我们所要的内存,这时它向我们掷出了一把飞刀!“接还是不接?”这已经不是作为一名C++程序的道德问题了,而且应该把它当作是一件性命攸关的事!所以,我们必须接住它!(老大,你认为呢?^_^)
接飞刀手法:
当内存需求无法获得满足时,new会在抛出异常前调用一个错误处理函数。这个函数用set_new_handler()来设。至于具体的招数,参见武功秘笈《MSDN》。
new 在无法满足内存需求的时候会抛出一个std::bad_alloc exception。看一下代码:
class Widget { ......};
Widget *pwl=new Widget;
if(pwl==0) ...
别傻了,这样是接不住飞刀的,你死定了。1993年前你可能有戏,那个时候C++要求operator new在无法满足需求时传回0,但是现在它抛出异常!用try...catch...接吧,这样你可能有救!我的建议是:用set_new_handler(),简单有效!
说一些个人意见:对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致“内存耗尽”。我在Windows 98下用Visual C++编写了测试程序。这个程序会无休止地运行下去,根本不会终止。因为32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98已经累得对键盘、鼠标毫无反应。
我可以得出这么一个结论:对于32位以上的应用程序,“内存耗尽”错误处理程序毫无用处。这下可把Unix和Windows程序员们乐坏了:反正错误处理程序不起作用,我就不写了,
省了很多麻烦。我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。
void main(void)
{
float *p = NULL;
while(TRUE)
{
p = new float[1000000];
cout << “eat memory” << endl;
if(p==NULL)
exit(1);
}
}

8. 撰写operator new和operator delete时应遵行的公约
我觉得这个条款讲的不是太通俗,所以我决定来个“俗”点的:
重载new和delete时必须要做到的,这里的重载包括(参见《Thinking in C++》):
Overloading global new & delete;
Overloading new & delete fro a class;
Overloading new & delete for arrays。
你重载过new & delete吗?如果你的回答是Yes,呵呵,我要对你说“老师,您好!”。因为我没有尝试过在我的工程中用过,原因是,我得到了更多的错误!
不过现在有了这练习上乘内功的口诀,就不怕走火入魔了。
口诀:
1、正确的返回值
2、内存不足,调用错误处理函数
3、不索求任何内存时的调用
4、避免不经意遮掩了“正常”形式的new(见条款9)
至于重载new和delete的具体方法。Email:robin_dy@163.com,呵呵,“不要咂我的头呀!”

9. 避免遮掩了new的正规形式
感觉这个条款看得有点困惑?OK,让我细细道来。
我们都知道,在一个类中“重载”了一个全局函数后,那么我们在类中所调用的都将是类中定义的那个函数版本
当然是不指定明确的作用域的时候,也就是这个时候类中的那个函数掩盖了全局函数(对于new来说,就是那个所谓的“正规形式”)。global scope和class scope的区别,呵呵,晕了?^_^
问题:当重载了new后,我们有时候需要调用“正规形式”的new,这时怎么办?
解决:
1、祭出inline函数,搞定!
class X{
public:
void f();
static void * operator new(size_t size, new_handler p);
static void * operator new(size_t size)
{ return ::operator new(size);}
}
2、为自己重载new添加的额外参数添加默认参数值,一样搞定!
class X{
public:
void f();
static void * operator new(size_t size, new_handler p=0);
}

10. 如果你写了一个operator new,请对应写一个operator delete
如果你想自己写一个内存管理系统(考虑过诸如内存数据库之类的东东吗?我曾想过,很有用的东西,SCADA中就有一个例子!我希望自己有机会能尝试一下,呵呵!),记住一条简单的规则:一个operator new,一个operator delete。
讲一点有关自定义内存管理系统的故事吧!
正规形式的new和delete在大多数情况下足够我们用了,但是在效率第一的情况下,我们需要一个自己的内存分配和释放系统。去除正规形式的new的弹性,来换取我们的效率。手段就是用到了Memory Pool(内存池)的技术。预先分配一个可以容纳若干个对象的大块内存,这样当我们请求一个对象所要的内存时,只要简单地从内存池中取出一块用就是了,用完了还给内存池。最后,释放内存池。
想听听我的建议吗?(画外音:你以为你是NEW B啊?!)
”最初的三年内,还是用正规形式的new和delete来获取和释放你所要的内存吧!但是,可能你会自己来重载new和delete,记住条款10。但是,如果你打算写自己的内存管理系统的话,一定要带上我,我还没有进化到那个程度,需要借你的光芒来指引我前进的方向 ,神啊,给我力量吧!”
再讲点内存分配的“废话”(有感于最近和内存打交道的痛苦经历):
分配内存的三种方式如下:
1、Storage can be allocated before the program begins, in the static storage area.静态存储区域的生存期与程序的生存期同。
2、Storage can be created on the stack whenever a particular execution point is reached.我们在程序中用到的大部分内存是在栈上分配的。在栈上分配内存的好处是简单,效率高,内存会自动释放,但是必须预先知道要分配区域的大小。
3、Storage can be allocated from a pool of memory called the heap.这就是我们熟知的动态内存分配。堆上分配的内存,其生存期由程序员决定,必须手动释放。前面的5-10讲了很多关于动态内存分配的东东,足见其重要性。实际工程中用的太多了!!!

11.如果class内动态配置有内存,请为此class声明一个copy constructor和一个assignment运算符
我想多费点时间来讲清楚这个问题,但是限于我的智慧可能永远也讲不清楚,所以你需要运用自己的智慧来理解它。
首先来讲讲函数调用的栈结构:
当编译器为函数调用生成代码时,首先将参数从右至左压栈,然后是函数返回的地址(Return Address)压栈,同时在函数内部,生成代码来将堆栈指针移动(向上或向下,这要视机器而定),为函数的本地变量提供存储空间。当函数调用完毕,栈指针将移动到函数(Return Address)的位置,这样函数的本地变量出栈。那么函数的返回值(尤其是一个自定义的类型)存放在什么地方?答案是将函数的返回值作为一个参数压栈,直接将返回值的信息拷贝至该参数中。这个答案没有解决所有的问题,但它效率很高。
下面是一个函数调用的例子:
int f(int x,char c);
int g=f(a,b);
看一下它对应的汇编代码:
push b;
push a;
call f();
add sp,4;
mov g,register a;
先是两个参数压栈,然后调用函数,完了将参数出栈,将返回值放在寄存器中(因为int是built-in type),传递给返回值g。这与上面讲的函数调用的过程稍有不同。插一句:前段时间碰到很多次stack overflow的错误,搞死我了。但是当我理解了函数调用背后的故事后,stack overflow的问题终于暂时解决了。
何为copy constructor?
当需要从一个已存在的对象创建另一个对象时,会调用copy constructor。当然,我们也可以阻止这样的行为。忠告中会讲到。
看下面的例子:
class String
{
public:
String(const char *str=NULL);
String(const String &other); //copy constructor
virtual ~String();
String & operator=(const String &other);//assignment operator
private:
char *m_data;
}
如果缺少了copy constructor和assignment operator,当进行复制时会进行bitcopy,也就是按位进行拷贝。试想如果上面的类中没有copy constructor和assignment operator,调用如下语句时的问题:
String a(“hello”);//m_data指向字符串“hello”
String b(“World”);//m_data指向字符串“world”
b=a;
这样,经过bitcopy,a和b中的m_data都指向“hello”,“world”没人管了,Memory Leak!!!而且,当a或b中的一个调用了析构函数后,“hello”所在的内存将被释放,这样另一个中的指针指向了一片非法内存!!!
“If you create a copy constructor, the compiler will not perform a bitcopy when creating a new object from an existing one."
忠告:如果class的成员变量中含有任何指针,请为这个类写copy constructor和assignment operator。但是你如果确信你的class不会执行copy和assignment动作,这时候写copy constructor和assignment operator会觉得有点得不偿失,这时候将copy-construction(No definition)声明为private,将阻止使用值传递方式(pass an object of your class by value)。呵呵,我知道这个时候你的头在游泳了。(Your head may be swimming,^_^。)You need a copy constructor only if you are going to pass an object of your class by value。If that never happens, you don't need a copy constructor.

12. 在constructor中尽量以initialization动作取代assignment动作
const members和reference members只能被初始化,不能够被赋值(assigned)。这个时候,如果在构造函数中要对其初始化时必须用member initialization list。 另外,从效率方面考虑,也建议用以initialization动作取代assignment动作。
例外的情况:
1、static class member不应该在构造函数中初始化。
2、如果有很多built-in type类型的class member要初始化,建议使用assignment,这和initialization在效率上没有什么区别,而且容易维护。
3. initialization list中的members初始化次序应该和其在class内的声明次序相同

13.initialization list中的members初始化次序应该和其在class内的声明次序相同
class members系以它们在class内的声明次序来初始化,和它们在member initialization list中出现的次序完全无关。基类的成员变量永远在继承类成员变量之前被初始化,所以如果运用了继承,你应该在member intialization lists起始处列出base class的初始设定值。结论是:对象被初始化时,如果你希望确实掌握真正发生了什么事,请以class内的members声明次序,将各个memebers列于initialization list中。

14. 总是让base class拥有virtual destructor
技术背景(或者称为原因吧):虚拟函数的目的就是允许derived classes有定制行为。
无条件地把所有的destructors都声明为virtual,和从不把任何destructors声明为virtual一样是不明智的行为。如果你确信你的类不会成为基类,不将destructor声明为virtual没有任何技术上的危害,反而因此你获得了一些内存上的节省。因为如果类中含有virtual函数,将需要一个额外的vptr(虚函数指针,指向虚函数表vtable),另外需要一个额外的虚函数表。
另外你还需要知道的是,destructor的作用和drived classes和base classes中destructor的调用次序?(这个我就不想在这里唠叨了,呵呵。)

15. 令operator=传回“ *this 的 reference”
“Technology can change our lives。”所有真正的C++大师(这种人不是太多)都是我的偶像,怀有这种近乎狂热的个人崇拜(当然对象很重要)的人有很多,如果你也是,我很欣赏!
回到正题:这个条款说的是copy construction的返回值问题。
比较如下的两个copy constructor:
String& String::operator=(const String& rhs)
{
...
return *this;
}
String& String::operator=(const String& rhs)
{
...
return rhs;
}
哦,这两个有区别吗?看起来没有任何的区别!但是实际上有!而且很大。因为第二个版本的copy constructor连编译都通不过!
很有用的一个常识: |
将const对象传给一个函数,而该函数却未声明相应的参数为const,是绝对不合法的。

16.在operator = 中为所有的data members设定内容
分两种情况,基类中和继承类中。在基类中没有什么可说的,至于为什么要这样做,扳着脚趾头也能想出来,^_^。在继承类中有点麻烦,不仅要为继承类中的data members赋值,而且必须为基类中的data members赋值。
Derived& Derived::operator=(const Derived& rhs)
{
if(this == &rhs) return *this;//如果是自身,直接返回
static_cast(*this)=rhs; //技巧之所在:对基类中的成员赋值
y=rhs.y; //为继承类中的成员赋值
return *this; //见条款15
}
另外,Derived的copy constructor必须确保调用Base的copy constructor而不是像下面这样做:
class Derived:public Base
{ public:
Derived(const Derived& rhs):Base(rhs),y(rhs.y) { }
......
}

17. 在operator=中检查是否“自己赋值给自己”
“自己赋值给自己”可能带来某种灾难:assignment运算符在为其左侧对象配置新资源之前,通常必须先将原先配置给左侧对象的所有资源释放掉。然而如果是“自己赋值给自己”,这个资源释放动作可能会造成惨重的灾情,因为旧资源可能在配置新资源的过程中派上用场。
解决“自己赋值给自己”的方法就是,一旦检查到存在这种情况就立即返回。下面就引出了另外一个问题:如何识别两个对象等同?
1、如果两个对象中的data members具有相同的值,就认为这两个对象是同一个。
2、如果两个对象在内存中占有相同的内存空间,就认为这两个对象是同一个。
第二个对象等同的定义比较容易实现,效率也比较高,所以我推荐用第二个。
“一盎司预防重于一盎司黄金!”

18. 努力让接口完满且最小化
”C++,上帝赐予我们的语言。上帝在,她就在!”
完满(complete)很好理解,你必须提供clients所要求实现的所有最基本功能。但是,有些功能可以通过现有功能,由用户来完成,不需要面面俱到。如果接口函数过多,这个类越不易使用。同时,也增加了维护成本。

19. 区分member functions,non-member functions和friend functions三者
member functions和non-member functions的区别是:member functions可以是虚函数,而non-member functions不可以。
在一个类中,只要能够避免friend函数,就应该尽量避免,“因为就像真实世界一样,朋友带来的麻烦常常多于其价值。”:)^_^
1、虚拟函数必须是class members。
2、绝不要让operator>>和operator<<成为members。
3、如果non-member functions需要用到class的non-public members,让它成为class的friend functions。

20. 避免将data members放在公开接口中
好像这条没什么好解释的,照做就是了。 暴露太多的data members显然是不合适的。

21. 尽可能使用const
const最具威力的用途是在函数声明时派上用场。在一个函数声明式中,const可以用来修饰函数的传回值、个别参数,以及整个函数。最后一项只对member functions才成立。
const的语义很好理解,但是我们在实际编程中往往忘了奉行其规则。在能用const的地方尽量加上这个关键字,可以避免很多不必要的潜在错误。

22. 尽量使用pass-by-reference,少用pass-by-value
对象以by value的方式传递,其实际意义是由该对象的copy constructor决定的。这可能会使pass-by-value成为成本很高的动作。
以pass by reference,还可以避免所谓的“切割slicing问题”,又称为“upcasting问题”。详见《Thingking in C++》P629
pass by reference是一件美妙的事情,但会导致某些复杂性。最知名的问题就是aliasing(别名问题),见条款17。某些情况下必须pass by value。references的底层几乎都是指针完成,所以passing by reference通常意味着传递的是指针。如果有个小对象,例如一个int,那么pass by value可能比pass by reference的效率更高一些。

23. 当你必须传回object时,不要尝试传回reference
Albert Einstein:make things as simple as possible, but no simpler。
inline const Rational operator*(const Rational& lhs,const Rational& rhs)
{
return Rational(lhs.n*rhs.n, lhs.d*rhs.d);
}
在这里,传回值是必须的,而且是唯一正确的行为,虽然它需要付出传回值构造和析构的代价,但是那只是得到正确的考虑的事情!
记住:有时候需要在传回地址和传回指针之间进行选择,第一需要考虑的是得到正确的行为,其次才是效率等。

24. 在函数重载(function overloading)和参数缺省化(parameter defaulting)之间,谨慎选择
一般而言,如果你可以选择一个合理的默认值,而且你只需要一个算法,那么最好是使用缺省参数,否则可以使用重载函数。

25. 避免对指针型别和数值型别进行重载
void f(int x);
void f(string *ps);
f(0);
调用f(int)还是f(string *)?
以上的问题可以解决,所用到的技术是一个很晚才加进C++语言的特质:member function templates。具体的细节我不想多讲了。因为按照条款所要求的做就是了,尽量不出现这种模棱两可的问题,我们也就不需要解决它了。
这所谓是:不要自找麻烦。

26. 防卫潜伏的ambiguity状态
class Base1
{
public:
int DoIt();
}
class Base2
{
public:
int DoIt();
}
class Derived:public Base1,public Base2
{
}
Derived d;
d.DoIt();//呵呵,错误!
考虑下面的做法:
class Base1
{
public:
int DoIt();
}
class Base2
{
private:
int DoIt();
}
class Derived:public Base1,public Base2
{
}
Derived d;
d.DoIt();//仍然错误!原因是在C++中“存取限制”不能解除“因多继承而来的members”的模棱两可状态。
除了上面所举的例子外,还有一些模棱两可的情况。你所要做的就是避免这种情况的出现。尽量将这种模棱两可的情况消灭在萌芽状态!

27. 如果不想使用编译器暗自产生的member functions,就应该明白拒绝它
举例来说,如果没有自己来写赋值运算符和copy constructor,那么编译器会为你产生缺省的。(参见条款45)。如果你不想别人使用编译器缺省产生的赋值运算符和copy constructor,就将其声明为private。
但是,注意!我说的是别人(使用你的类的人),因为在我们自己的类中,member functions和friend functions还是可以调用private函数。

28. 尝试切割global namespace(全局命名空间)
每一个大型项目中都有一个global scope,而且是唯一的。
常常有一群人在这个global scope中放置自己的各种符号名称(如常量、全局函数等),这往往会造成命名冲突。 我最烦人动不动就把东西往global scope中扔,随意污染global scope到时候就会带来麻烦。
解决的方法就是使用C++ namespace。namespace这个性质加入C++并不是太久的事。很重要,几乎标准程序库中的每一样东西都栖身于namespace std中。还记得条款2中#include <iostream>的写法吗?原因是如果这样写,我们取得的是隐藏于namespace std内的iostream程序库中的元素,避免了命名冲突。
举个例子:
namespace sdm
{
const double BOOK_VERSION = 2.0;
class Handle { ...}
Handle & gethandle();
}
使用方法:
void f()
{
using namespace sdm;
cout<<BOOK_VERSION;
...
Handle h=getHandle();
}

29. 避免传回内部数据的handles
主要出于变量生存期的考虑,安全第一嘛。

30. 避免写出member functions,传回一个non-const pointer或reference并以之指向较低存取层级的members
class Address
{
...
}
class Person
{
public:
Address& personAddress( ){ return address;};
...
private:
Address address;
...
}
Person scott(...);
Address& addr=scott.personAddress( );//假设addr是个全局对象,这样scott.address不再是private,变成了public。指针也是类似的情况。
如果实在需要传回一个non-const pointer或reference并以之指向较低存取层级的members,可以传回一个pointer或reference,指向一个const object。见条款21

31. 千万不要传回“函数内local对象的reference”或“函数内以new获得的指针所指的对象
简单的常识而已(涉及变量的生存期)。

32. 尽可能延缓变量定义式的出现(即需要时才定义之)
经由default constructor构造起一个对象然后再指派实值给它远比直接在构造的时候就指定好初值效率差。
以具有明显意义之初值将变量初始化还可以附带说明变量的目的。
只要延缓变量的定义,便可以改善程序效率,增加程序清晰度,降低变量意义的说明需求。看来似乎是该对那些“一开始就出现”的变量定义说“拜拜”的时候了。

33. 明智地运用inlining
inline函数背后的整个观念是,“对此函数的每一个调用行动”都以函数代码取代之。这么做会增加目标代码(object code)的大小。即使拥有虚拟内存,inline行为造成的程序代码膨胀现象也会导致病态的换页(paging)行为(也就是所谓的thrashing现象),这样会使程序慢得像蜗牛一样。
换个角度,如果一个inline函数的函数体很小,以“函数体”所产生出的代码,可能比以“函数调用”所产生的代码更小。果真如此,将函数inlining化确实可导致较小的目标代码(object code)和较高的cache命中率(cache hit rate)。
inline意味着“在编译阶段,将调用动作以被调用函数的本体取代之”。
一开始,请不要将任何函数声明为inline,或至少将inlining范围限制在那些实在非常琐屑的函数身上。如:
class Person
{
public:
int age() const { return personAge;}
...
private:
int personAge;
...
}
将inline函数运用在那些在程序中占重要效率地位的函数,对其inline化,将会提高程序效率。
只要明智地运用,inline函数会是每个C++程序员的无价之宝,但正如前所述,inline并非如此简单直接。

34. 将文件之间的编译依赖关系降至最低
不要在头文件中再#include其他头文件,除非你的头文件不这样就无法编译。你应该尽可能手动声明你所需要的classes,把#include其他必要头文件的责任让给clients。

35. 确定你的public inheritance,模塑出“isa”的关系
需要烙印在心的规则:
以C++完成面向对象程序设计,最重要的一个规则是:public inheritance(公开继承)意味着”是一种(isa)“的关系。

36. 区分接口继承(interface inheritance)和实现继承(implementation inheritance)
class Shape
{
public:
virtual void draw() const =0;
virtual void error(const string& msg);
int objectID() const;
...
}
1、纯虚函数draw()在Shape类中没有实现,只能在其继承类中实现。声明一个纯虚拟函数的目的是为了让derived classes只继承其接口。
2、一般(非纯)虚拟函数error()。一般(非纯)虚拟函数传统上会提供一份实现代码,derived classes可以自行选择要不要加以改写(override)。声明一般(非纯)虚拟函数的目的,是为了让derived classes继承该函数的接口和缺省行为。
3、非虚拟函数objectID()。声明非虚拟函数的目的是为了令derived classes继承函数的接口及其实现。由于非虚拟函数代表的意义是“不变性"凌驾于“变异性”之上,所以我们不应该在subclass中重新定义它。

37. 绝对不要重新定义继承而来的非虚拟函数
绝对不要!!!(参见条款36)

38. 绝对不要重新定义继承而来的缺省参数值
以上问题可以说等价于“绝对不要重新定义继承而来的虚拟函数的缺省参数值”。(重新定义非虚拟函数是个错误!)
虚拟函数系动态绑定,而缺省参数值却是静态绑定。

39. 避免在继承体系中做向下转型(cast down)动作
转型(casts)之于C++程序员,犹如苹果之于夏娃。
消除downcast的最佳方法就是将转型动作以虚拟函数的调用取代,并且让每一个虚拟函数有一个“无任何动作”的缺省实现代码,以便应用在并不想要施行该虚拟函数的任何classes身上。
有些时候我们必须使用转型动作,怎么办?
C++提供了dynamic_cast运算符,提供所谓的“safe downcasting”。如果转型成功,也就是说那个指针的动态型别与其转型目标一致,会传回一个隶属新型别(转型目标)的指针。如果失败,传回null指针。通过它,我们可以在我们的downcast失败时侦测到。

40. 通过layering技术来模塑has-a或is-implemented-in-terms-of的关系
layering的同义语有composition、containment、embedding。
即声明另一个类对象作为成员变量。

41. 区分inheritance(继承)和templates
要旨:
template应该用来产生一群classes,其中对象型别不会影响class的函数行为。
inheritance应该用于一群classes身上,其中对象型别会影响class的函数行为。

42. 明智地运用private inheritance(私有继承)
private inheritance意味着“根据某物实现(implemented-in-terms-of)".
如果你让class D以private方式继承class B,这么做的原因是,你想采用已经撰写于class B内的某些程序代码,而不是因为B和D的对象有任何概念关系存在。所以private inheritance纯粹只是一种实现上的技术,与观念无涉。

43. 明智地运用多继承
多继承是有用而且可理解地,但是,钻石形地继承图必须避免才行。
劝告:请拒绝多继承的诱惑!!!

44. 说出你的意思并了解你所说的每一句话
1、共同的base class意味着共同的特性。
2、public inheritance意味着是一种(isa)。
3、private inheriance意味着“根据某物实现(is-implemented-in-terms-of)”。
4、Layering意味着“有一个(has-a)”或”根据某物实现(is-implemented-in-terms-of)“。
公开继承时:
1、纯虚拟函数意味着:只有其函数接口会被继承。
2、一般(非纯)虚拟函数意味着:函数的接口及缺省实现代码都会被继承。
3、非虚拟函数意味着:此函数的接口和其实现代码都会被继承。

45. 清楚知道C++(编译器)默默为我们完成和调用那些函数
class Empty
{
}
相当于:
class Empty
{
public:
Empty();
Empty(const Empty& rhs);
~Empty();
Empty& operator=(const Empty& rhs);
Empty * operator&();
const Empty * operator&() const;
}
缺省的析构函数并非是虚拟函数,只有当这个class继承自一个base class而该base class拥有一个virtual destructor。

46. 宁愿编译和连接时出错,也不要执行时才错
但是,也不要太过激进。消除所有的执行时期检验动作是不切实际的。任何程序只要接受交谈式输入,就有可能需要检验输入值的有效性。同理,一个实现有数组边界检验功能的class,总是必须在每次进行数组存取动作时,检查数组索引值的有效性。不过尽管如此,将执行时期的检验移到编译期或者连接期来进行,还是一个值得做的目标。

47. 使用non-local static objects之前先确定它已有初值
non-local static objects是指:
1、定义于global或namespace scope
2、在某个class内声明为static
3、在某个file scope(文件范围)内定义为static。
Singleton pattern技术可以“确保non-local static objects在被使用之前先初始化过”。具体实现不想讲了,如果有需要可以参考相关书籍。

48. 不要对编译器的警告信息视如不见
class B{
public:
virtual void f() const;
};
class D:public B{
public:
virtual void f( ) ;
};
有些编译器会给出一个这样的警告信息(我在VC6.0中测试过,VC6.0不会给出任何警告信息)。
warning:D::f() hides virtual B::f()
也许经验不足的程序员对此信息的响应是:“当然是D::f遮蔽了B::f,那不正是它的行为吗?”错!编译器企图告诉我们的是B所声明的f并未在D中被重新声明,它被完全遮蔽住了。
看看我为此写的一个测试程序:
void fun(B *pb)
{
pb->f();
}
int main(int argc, char* argv[])
{
D myd;
fun(&myd);
return 0;
}
按照我们正常的预期,应该是D::f()被调用,但是实际上是B::f()被调用。如果改动D如下:
class D:public B{
public:
virtual void f( ) const;
};
我们将得到正确的预期行为!
你需要在与编译器打交道的过程中“听懂”它的话,了解它对你说的每句话背后的真正含义,这需要时间!想依赖编译器为你找出所有的错误是个天真的“梦想”!有时候它对所说的不是你所希望它说的,有时候它所说的你轻易的pass了,但却给你带来了麻烦,还有的时候的(就如上文中所示)一声不吭。
记住:编译器的基本用途是将C++代码转换为可执行代码。 不要期望它为你做更多!!!

49. 尽量让自己熟悉C++标准程序库
<string.h>是旧式C表头文件
<string>是披上std外衣的C++头文件
<cstring>是披上std外衣的旧式C头文件
标准程序库中的每样东西几乎都是template。
C++标准程序库里面的东东:
1、C标准函数库。它依然存在,我们还是可以使用它。
2、iostreams。
3、strings。
4、Containers(容器)。如vector、list、queue、stack、deque、map、set和bitset等。
5、Algorithms(算法)。for_each、find、count_if、eauql、search、copy、unique、rotate、sort等等。大约有70个。
6、国际化(internationalization支持。主要是facets和locales组件。
7、数值处理。complex等
8、诊断功能。
标准程序库中与containers和algorithms相关的那一部分通常被称为Standard Template Library(STL)。STL还有第三种组件:iterators。iterators是一种像指针一样的对象,允许STL algorithms和containers能够胶合在一起运作。

50. 加强自己对C++的了解
《The Design and Evolution of C++》,"偶像"Stroustrup写的,有人昵称为“D&E”,也是我的下一本必读书。它清楚地将了什么样的性质以什么样的次序被加进C++之中,以及加进它们的理由。也可以得知哪些性质被拒于C++大门之外,以及被拒绝的原因。甚至可以得知dynamic_cast的内部故事!

终于罗里罗嗦地写完了五十个条款,对C++内部故事的渴望占据了我所有的精力。

原文地址:https://www.cnblogs.com/onwalking/p/3643300.html