《Effective C++》第二版笔记

时间:2022-08-29 21:56:27

一、改变旧有的 C 习惯 (Shifting from C to C++)

1、尽量以编译器取代预处理器

  预处理器 #define 通常不被视为语言本身的一部分,如 #define PI 3.1415926,则符号名称 PI 可能没有机会被编译器看见,它可能在编译之前就被预处理器替换了,结果导致名称 PI 没有进入符号表中。则在编译中获取错误信息时,获取到的只有 3.1415926 这样一个常量,而如果 PI 是定义于一个头文件中,而此头文件又是别人写的,就更要花很多时间去追查它了。以编译器代替预处理器的好处是方便调试。

取代的方法是使用: const double PI = 3.1415926;

  定义一个类常量的方法,有以下几种,注意 #define 是没有作用域概念的(即使像下面那样在类的内部声明也是如此),在类中定义枚举的技巧也是常用的方法之一。

class Test { public: static const int pi = 3; static std::string str; const static std::string const_str; #define STR "hello"
    enum { ONE = 1}; }; std::string Test::str = "Hello"; const std::string Test::const_str = "const_Hello"; int main() { std::cout<<Test::pi<<std::endl; std::cout<<Test::str<<std::endl; std::cout<<Test::const_str<<std::endl; std::cout<<STR<<std::endl; std::cout<<Test::ONE<<std::endl; }

  使用 enum 时,如果不需要使用枚举名就可以省略,但如果需要使用,就需要显式声明。这里插入介绍一下关于 enum 的用法:

typedef class Test { public: typedef enum Num{ONE = 1} eNum; void print1(enum Num i){std::cout<<"enum Num i\n";} void print2(eNum i){std::cout<<"eNum i\n";} } cTest; int main() { std::cout<<Test::ONE<<std::endl; class Test t1; cTest t2; t1.print1(Test::ONE); t2.print2(Test::ONE); }

typedef enum 的意义,是为枚举名定义一个别名,方便定义枚举变量,之前定义枚举变量需要使用 enum Num i; 这样的语句,而现在直接使用 Num i; 这样的语句就可以,class 也有类似用法。不过该用法,现在来说应该已经没有什么意义了~~了解即可。

  另一个常用 #define 指令的常见例子是以它来实现宏,如: #define max(a,b) ((a)>(b)?(a):(b)) 它的优势是在调用函数时,不会带来调用函数所需的成本,但是由于在预处理期直接简单替换,可能会引发如括号带来的经典问题。

更好的办法是使用模板函数代替: template<class T> inline const T& max(const T& a,const T&b) {return a > b ? a : b;}

  但预处理器 (preprocessor) 仍有其存在的意思,比如 #include 和 #ifdef/#ifndef 在编译控制过程中的作用。另外,作为代码生成器而言,宏也有一定的意义。

 

2、尽量以 <iostream> 取代 <stdio.h>

  在某些情况下,或许 <stdio.h> 更高效,但是它们都不具有型别安全的性质,也都不可扩充。而 <iostream> 可以重载 operator>> 和 operator<< 来处理各个型别的对象。此外,使用 <iostream> 更简单,不需要记忆如 scanf 的某些格式化规则。如:

 

#include <iostream>

struct Pet
{
    Pet(int _id):id(_id){};
    friend std::ostream& operator<<(std::ostream& os, Pet& pet);
private:
    int id;
};

std::ostream& operator<<(std::ostream& os, Pet& pet)
{
    os<<"petid:"<<pet.id;
    return os;
}

int main()
{
    Pet pet(2);
    std::cout<<pet<<std::endl;
}

 

 

 

 

3、尽量以 new 和 delete 取代 malloc 和 free

  malloc 和 free 带来的问题很简单:它们对 constructors(构造函数) 和 destructors(析构函数)一无所知。而 new/delete 会隐式调用 constructors/destructors。

  string *stringArray1 = static_cast<string*>(malloc(10*sizeof(string)));    //很难初始化数组中的对象

  string *stringArray2 = new string[10];  //指向由10个构造妥当的string对象所构成的数组

  对应的:

  free(stringArray1);  //不会调用析构函数

  delete [] stringArray2;  //注意 [] ,在delete 施加于 stringArray2 身上,在内存被释放之前,数组中的每一个对象的destructor都会被调用一遍。

  另外,请注意 new/delete 和 malloc/free 不可混用。

 

4、尽量使用 C++ 风格的注释形式

  C 风格注释: /*  */             C++ 风格注释: //

  C++风格注释的好处是可以避免注释嵌套带来的问题。

 

二、内存管理

 5、使用相同形式的 new 和 delete

  即被删除的指针,所指的是单一对象还是对象数组?delete 永远没有办法知道答案,除非你告诉它。如果你使用 delete 时未加中括号, delete 便假设删除对象是单一对象,否则便假设删除对象是个数组。如果使用错误,会提示结果未定义。

  string *stringPtr1 = new string;  //delete stringPtr1;

  string *stringPtr2 = new string[100];  //delete [] stringPtr2;

在内置的类型中,比如 int,char,double 等 delete 和 delete[] 是没有区别的,但是如果是自定义的结构或类,如果是对象数组,使用 delete[] 可以依次调用所有对象的析构函数,但如果使用 delete ,则只会调用第一个对象的析构函数,且会提示结果未定义。

附:在每次 new[] 的时候,数组内部的元素字址是连续的,但不同的 new[] 或 new 操作之间不是连续的,猜想应该存储些某些信息,所以在使用 delete[] 的时候,无需带上数组长度就能判断此前 new[] 了多少长度的数组。

这就跟使用 malloc 和 free 的区别一样,它们俩也不会调用析构函数。类似的问题,在多态中,基类指针指向子类对象, delete 基类指针时,如果基类没有 virtual 修饰的析构函数,则子类的析构函数也不会被调用,同样可能产生内存泄漏。

 

6、记得在 destructor 中以 delete 对付 pointer members

  为了避免 memory leak (内存泄漏),在每一个 constructors 中将指针成员初始化;在 assignment 运算符中将指针原有的内存删除,重新配置一块;在 destructor 中删除这个指针。删除一个 null 指针是安全的。

构造函数是按照成员声明的顺序来初始化非 static 成员的,析构函数是按照成员声明的逆顺序来撤销每个非 static 成员的。

 

7、为内存不足的状况预做准备

#include <iostream>

using namespace std; void outofmem() //内存不足时调用的函数,可以在这里释放一些mem
{ cout<<"OutOfMem..."<<endl;    //abort(); //非正常中止程序
}
int main() { set_new_handler(outofmem); //如果mem不足,会不断调用该函数,直到足够 //set_new_handler(NULL); //卸除new-handler try { int *pArray = new int[1000000000L]; }catch(std::bad_alloc&) { cout<<"throw std::bad_alloc exception"<<endl; } }

  当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的 new_handler。为了指定这个“用以处理内存不足”的函数,客户必须调用 set_new_handler, 注意:当 operator new 无法满足内存申请时,它会不断调用 new-handler 函数,直到找到足够内存。一旦没有安装任何的 new_handler,operator new 才会在内存分配不成功时抛出 bad_alloc 异常。

 

8、撰写 operator new 和 operator delete 时应遵行的公约

  new 操作会一再尝试配置内存,并在每次失败后调用错误处理函数,这是因为它假想错误处理函数或许能够做某些有益的动作(例如释放某些内存)。只有当指向错误处理函数的指针是 NULL(默认处理函数指针也是NULL) 时, new 操作才会抛出一个 exception。此外,C++ standard 要求,即使用户要求的是 0bytes 内存, new 操作也应该传回一个合法指针。

 

9、避免遮掩了 new 的正规形式

#include <iostream>

class User { public: void * operator new(size_t size) { std::cout<<"size: "<<size<<std::endl; } void * operator new(size_t size,std::string str) { std::cout<<"size: "<<size <<"\nname: " << str<< std::endl; } int id; }; int main() { User* user = new User; User* user1 = new ("JIM")User; }

 《Effective C++》第二版笔记

 

10、如果你写了一个 operator new ,请对应写一个 operator delete

为什么要撰写自己的一份 operator new 或 operator delete 呢?答案多半是因为效率。所以,一般情况下并不推荐你重写这两个函数。

 

三、构造函数、析构函数和 赋值运算符

11、如果 class 内动态配置有内存,请为此 class 声明一个 copy constructor 和一个 assignment 运算符

  复制构造函数和赋值运算符重载都是默认就有的,但却是浅复制,如果有引用类型,则指向原来对象的引用。这样当一个对象析构时,另一个对象的引用类型成员会出现内存泄漏问题。

 

12、在 constructor 中尽量以 initialization 动作取代 assignment 动作

#include <iostream> #include <string>

using namespace std; class User { public: User(){cout<<"constructor"<<endl;} User(User& user) { userid = user.userid; name = user.name; cout<<"copy constructor"<<endl; } User& operator=(const User& user) { userid = user.userid; name = user.name; cout<<"assignment"<<endl; return *this; } int userid; string name; }; class Test { public: Test(){} Test(User& user_):user(user_){} // Test(User& user_){user = user_;}
 User user; }; int main() { User obj1,obj2; cout<<"\nTest 'User obj(obj2);' :"<<endl; User obj(obj2); //copy constructor
    cout<<"\nTest 'User obj3 = obj2;' :"<<endl; User obj3 = obj2;    //copy constructor
    cout<<"\nTest 'obj1 = obj 2;' :"<<endl; obj1 = obj2;    //assignment
 cout<<"\nTest 'Test test(obj1);' :"<<endl; Test test(obj1); }

使用 Test(User& user_):user(user_){}
《Effective C++》第二版笔记

使用 Test(User& user_){user = user_;}

《Effective C++》第二版笔记

除了上面演示的,很多时候 initialization list 初始化要比 assignment 的效率高之外,const members 和 reference members 只能用 initialization list 来初始化,而不能赋值(assignment)。

注意:static class members绝不应该在一个class's constructor中被初始化,static members在每一个程序执行时,只应该被初始化一次。

static 成员数据可以使用 const 来修饰,表示类的静态常量。但 static 成员函数不能使用 const 来修改,因为 static  成员函数是类的组成部分,而不是任何对象的组成部分。同时,staitc 成员函数也不能被声明为虚函数,而且 static 成员函数定义里也不能使用 this 指针。

 

13、initialization 中的 members 初始化次序应该和其在 class 内的声明次序相同

 C++类中的成员是以它们在 class 中声明的次序来初始化,而跟它们在 member initialization members 中的次序无关。所以最好使两者保持一致,否则如果需要以一个成员来初始化另一个成员时,就有可能会发生意想不到的错误。

#include <iostream> #include <string>

using namespace std; class Test { public: Test():c(3),a(c),b(c){} int a,b,c; }; int main() { Test test; cout<<"a: "<<test.a<<"\nb: "<<test.b<<"\nc: "<<test.c<<endl; }

《Effective C++》第二版笔记

先初始化的data member后析构,所以base data member后析构,跟栈中的变量一样先定义的变量后析构一样。如果类继承多个类,那么base data member的初始化顺序由继承的先后顺序决定,先继承的先初始化。

这也可以用来解释类成员的显式初始化(类全体数据成员都为 public 时)的顺序问题。如: struct Data{ int id;string name; };    可以使用  Data obj = {1,"Tom"}; 来初始化,而不可以使用 Data obj = {"Tom",1}; 一样。

 

14、总是让 base class 拥有 virtual destructor

#include <iostream> #include <string>

using namespace std; class A { public: virtual ~A(){cout<<"~A()"<<endl;} }; class B : public A { public: ~B(){cout<<"~B()"<<endl;} }; int main() { A* a = new B; delete a; }

如果 A 的析构函数不是虚析构函数的话,则输出:
 ~A()
如果 A 的析构函数是虚析构函数的话,则输出:
~B()
~A()

在基类指针指向子类对象时,delete 基类指针,如果基类的析构函数不是 vritual 的话,则只会调用基类析构函数;否则会先调用子类析构函数,再调用基类析构函数。比如当子类中有指针类型成员需要在析构函数中清理的时候,也会导致内存泄漏。或者在子类中有引用计数的实现时,也会发生计数错误(引用计数会在析构函数中将引用计数 -1)。

在继承关系中,只有 virtual 函数来决定的是否存在多态。若基类中的方法是virtual的,而子类没有覆写则直接继承过来;如果子类覆写了,则子类的虚拟表中存的就是子类方法的地址。若基类的方法不是 virtual 的,即使子类有一个一样的方法,也不存在多态行为,基类指针只会调用基类的方法。同样的若基类的析构函数不是 virtual 的,那么在delete 这个指针的时候就会出现不可知的情况,子类的析构函数不会被调用,所以当你决定让一个类成为基类,那么就让它的 destructor 为 virtual。

但是也不需要让每一个类的 destructor 成为 virtual ,因为含有 virtual 方法的类都有一个指向 virtual table 的指针,会让对象变大,如果对象本来就不太的话可能会出现成本翻倍的情况。只有当 class 中含有至少一个虚拟方法时才让它的析构函数成为虚拟的,因为至少含有一个虚拟方法,才有被继承的意义。

 

15、令 operator= 传回 " *this 的 reference "

有人可能认为传回 void 也是可以的(事实也确实如此),但是如果这样,就无法进行链式写法,如: string a = b = c = "Hello";  标准库里的 string, vector 等的实现都遵循了这一原则。

 

16、在 operator= 中为所有的 data members 设定(赋值)内容

这个不需要多说。

 

17、在 operator= 中检查是否 "自己赋值给自己"

一个理由是为了效率。另一个更重要的理由,是为了正确性。如果类中有指针成员,在 operator= 中,会需要先 delete 掉动态分配的内存,然后再配置新的内存。如果是自己赋值给自己的话,问题可想而知。

 

四、类与函数之设计和声明

18、努力让接口完满且最小化

  这是一种设计规范方面的吧,不说了。

 

19、区分 member functions, non-member functions 和 friend functions 三者

  member function可以是虚函数而non-member function不可以(废话),如果一个方法不需要访问类的私有成员,就不应该成为这个类的 friend function(还是废话)。另外像 operator<< 及 operator>> 这样的方法,应该设计成 friend function。

 

20、避免将 data members 放在公开接口中

  这个不废话了,面向对象原则。 

 

21、尽可能使用 const

const 修饰方法的返回值:如果返回的是指针,则该指针的内容不能被修改,且只能被赋予同样是 const 的同类指针;如果返回值不是指针,则无意义。

const 修饰方法:表示在这个方法中不能修改data member,但可以改变 mutable 修饰的 data member 。const 修饰成员方法时,在函数声明和定义中,都不能省略此关键字。

const 修饰参数:表示这个参数在这个方法中不能修改

是否存在 const 也可以实现方法的重载,const 对象只能调用对应的 const 方法,非 const 对象可以调用非 const 方法,当没有非 const 方法时,才会调用非 const 方法。

成员函数具有一个附加的隐含形参,指向该类对象的一个指针,为 this ,解引用 *this 就可以得到当前对象。在普通的非 const 成员函数中,this类型是一个const指针,可以改变this指向的值,但不可以改变this所保存的地址。在const成员函数中,this的类型是一个指向const类类型的对象的const指针,两者皆不能改变。这也就是const成员函数不能改变其成员的原因。

在一个成员函数中,是否有 const ,可能会产生四种重载函数。分别是没有const、const返回值、const函数、const参数。

在const参数重载中,只有形如: void fun(const int *pi);算一种重载。   void fun(int* const pi);  与 void fun(int *pi) 是等同的。因为 int* const pi 没有实用意义,pi 是否指向其它地址,并没有什么影响。

 

22、尽量使用 pass-by-reference ,少用 pass-by-value

效率问题,pass-by-value 需要重新配置内存,而 pass-by-reference 是指向同一处指针。另外,有些时候只能用 pass-by-reference ,比如说想要操作原来的变量。

 

23、当你必须传回 object 时,不要尝试传回 reference

 我觉得跟第31条意思差不多:千万不要传回 "函数内 local 对象的 reference" 或 "函数内以 new 获得的指针所指的对象"。因为它已经失效了,会引起错误。

 

24、在函数重载和参数缺省化之间,谨慎抉择

选择哪个取决于两个问题:一是是否有适当的值可以用来当作缺省参数;二是希望使用多少个算法。

如果有合适的缺省参数,且只需要一个算法,那最好使用缺省参数法,否则你可以使用重载函数。 

 

25、避免对指针型别和数值型别进行重载

void fun(int i);

void fun(int* pi);

在使用 fun(0); 时只会调用 int 型的重载,想调用指针的重载,需要使用 fun((int*)0); 调用 fun(NULL) 则会编译错误,提示有歧义。

c++11 中已经可以使用 nullptr 来区分 0 和 空指针,即 fun(nullptr)。

 

26、防卫潜伏的 ambiguity(模棱两可)状态

void fun(int i) { cout<<i<<endl; } void fun(char c) { cout<<c<<endl; } int main() { fun(97.3); }

上面的代码会编译出错,提示有歧义。

可以使用 f(static_cast<int>(97.3));   或  f(static_cast<char>(97.3));

class B; class A { public: A(B&);  //A对象可以由B对象构造出来
}; class B { public: operator A() const;     //重载类型转换操作符,A对象可以由B对象转换来
}; void fun(A&); int main() { B b; fun(b); //这时编译器就不知道调用上面哪种方法把b转换成A对象了,因为两种方法都是对的。
}

关于类型重载操作符 operator typename() const 的用法。

 

27、如果不想使用编译器暗自产生的 member functions,就应该明白拒绝它

如果想禁止复制对象,可以在 copy constructor 和 assignment operator 前加上 private 修饰,并且只声明而不定义。(只做前者,友元函数和成员函数仍然是可以复制的)。

其它类似的还有构造函数等。c++0x 标准中可以使用 delete ,更为明确。

 

28、尝试切割 global namespace (全局命名空间)

比如说一些方法和常量,如果放在全局文件里,可能会造成命名冲突,所以请使用命名空间来避免这种情况。

如: namespace myns{   const double PI = 3.14;   }

使用时使用 myns::PI 就行了。当然也可以先引入命名空间,或 using myns::PI;

另外也可以使用结构体(或类)来实现同样的功能:

struct ss{ static const PI; };      const double ss::PI = 3.14;

明确调用全局命名空间的方法,是使用两个冒号开头,如  ::max(3,5);

 

五、类与函数之实现

29、避免传回内部数据的 handles

 

30、避免写出 member functions ,传回一个 non-const pointer 或 reference 并以之指向较低存取层级的 members

 

31、千万不要传回 "函数内 local 对象的 reference" 或 "函数内以 new 获得的指针所指的对象"

不要传回函数内的 local 对象的指针或引用,这很好理解,因为该指针或引用在离开作用域后被析构了,导致未定义行为。虽然使用 new 在堆中分配内存来存储,然后赋值于此并返回能解决这个问题,但是函数的使用者却必须要记得使用 delete 行为,否则会造成内存泄漏。而你不应该去指望函数使用者能百分之百的进行该安全操作,所以尽量避免。

 

32、尽可能延缓变量定义式的出现

不只是应该延迟变量的定义,甚至应该尝试延缓到能够给予它初值为止。这样可以避免构造(和析构)非必要的对象,还可以避免无意义的 default constructions。“直接在构造的时候就指定好初值”,远比 "经 default constructor 构造起一个对象,然后再赋值" 效率要高得多。

 

33、明智地运用 inlining

1、直接用代码替换,减少函数调用成本
2、坏处:造成代码膨胀现象,可能会导致病态的换页现象
3、是否内联,是由编译器决定的,而不是由程序员。大部分编译器会拒绝将复杂的(内有循环或递归调用)函数inline,也会拒绝虚函数的 inline 请求,因为 virtual 是动态行为,而 inline 是静态行为,本身就是矛盾的。查看是否内联,可以通过汇编代码查看调用函数是否使用 CALL 指令,或者使用 nm 查看目标文件是否存在函数名符号。
4、构造函数和析构函数最好不要inline,即使inline,编译器也会产生出 out-of-line 副本,以方便获取函数指针。这一点不清楚。

 

34、将文件之间的编译依赖关系降至最低

 

 

六、继承关系与面向对象设计

35、确定你的 public inheritance,模塑出 "isa" 的关系

public inheritance 都是 is-a 的关系,而 protect 和 private 则不是。 

 

36、区分接口继承和实现继承

纯虚函数的作用:(非实用函数)

1、强制子类实现其纯虚方法。
2、禁止生产基类对象。

非纯虚函数的作用:(部分实用函数:提供默认的通用实现,只有在子类需要特化的时候才重载)

让派生类继承其接口和和默认的行为。

非虚函数的作用:(实用函数:直接使用即可,不过子类也可以选择覆盖,但如果要必要覆盖的话,应该选择上面的方案,见条款 37)

让派生类继承其接口和实现。

 

37、绝对不要重新定义继承而来的非虚函数

VTABLE中,编译器放置了这个类中,或者它的基类中所有已经声明为 virtual 的函数的地址。如果在这个派生类中没有对基类中声明为 virtual 的函数进行重新定义,编译器就使用基类的这个虚函数的地址。

非虚方法是静态绑定,虚拟方法是动态绑定。

所以如果在子类中重定义继承而来的非虚拟函数,则该函数只能被对象的静态类型调用。如果基类对象指向子类型,调用该方法,也只会调用基类的该方法,而不会调用动态类型的子类方法。

子类对象应该都是基类对象,是 is-a 的关系,但是如果子类重定义基类的非虚函数,那这一条也不成立了,这也属于设计问题。

 

38、绝对不要重新定义继承而来的缺省参数值

缺省参数值是静态绑定,如果在子类中重载一个带有缺省参数值的函数,且改变基类中的缺省参数值设定的话,会导致最终调用子类的函数,但会使用基类的缺省参数值。这肯定是我们不想看到的。如:

#include <iostream>

using namespace std; class Father { public: virtual void fun(int age = 50) { cout<<"Father's age is "<<age<<endl; } }; class Son : public Father { public: void fun(int age = 20) { cout<<"Son's age is "<<age<<endl; } }; int main() { Father *f = new Son; f->fun();     //Son's age is 50
}

C++为什么不把缺省参数值改为动态绑定呢?答案是出于效率方面的考虑。 

 

39、避免在继承体系中做向下转型动作

将基类对象强制赋给子类对象,称为向下转型。 

 

40、通过 layering 技术来模塑 has-a 或 is-implemented-in-terms-of 的关系

通过在类中包含另一个类的对象,通过包裹该对象的行为来实现自己的接口,是一种 has-a 关系,但会产生编译依赖问题。

 

41、区分 inheritance 和 templates

 

42、明智地运用 private inheritance

1、如果是私有继承,编译器不会隐式的将子类对象转化成基类对象

2、私有继承,基类所有函数在子类都变成私有属性

3、私有继承意味着根据某物实现,与 layering 相比,当 protected members 和虚函数牵扯进来会有很大的优越性。

4、私有继承,子类仅仅是使用了父类中的代码,他们没有任何概念上的关系。

 

43、明智地运用多继承

1、多继承会产生模棱两可,子类调用方法如果两个父类都有,则必须指明使用的是哪个父类

2、多继承会产生钻石型继承体现,为了使得祖先类只有一份,请在两个父类继承祖先的时候采用虚继承(而这在设计祖先类的时候一般是无法预料到的)

3、可以通过public继承方式继承接口,private继承方式继承实现,来完成目的

 

44、说出你的意思并了解你所说的每一句话

略,只是上面几条的总结。 

 

七、杂项讨论

45、清楚知道C++编译器默默为我们完成和调用了哪些函数

 一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符。另外,如果你没有声明任何构造函数,它也将为你声明一个缺省构造函数。所有这些函数都是公有的。

如果写了一个空的类,如:

class Test{};

其意义相当于:

class Test { public: Test(); //default constructor
    Test(const Test& test);             //copy constructor
    ~Test();                            //destructor
    Test& operator=(const Test& test);  //assignment operator
    Test* operator&();                  //address-of operator
    const Test* operator&() const;      //const address-of operator
};

 

46、宁愿编译和连接出错,也不要执行时才出错

 

47、使用 non-local static object 之前先确定它已有初值

非局部静态对象指的是这样的对象: 
1、定义在全局或名字空间范围内(例如:theFileSystem和tempDir),
2、在一个类中被声明为static,或,
3、在一个文件范围被定义为static。 

你绝对无法控制不同被编译单元中非局部静态对象的初始化顺序。 

如果你不强求一定要访问 "非局部静态对象",而愿意访问具有和非局部静态对象 "相似行为" 的对象(不存在初始化问题),难题就消失了。取而代之的是一个很容易解决的问题,甚至称不上是一个问题。 

这种技术 —— 有时称为 "单一模式"(译注:即Singleton pattern,参见 "Design Patterns" 一书)---- 本身很简单。首先,把每个非局部静态对象转移到函数中,声明它为static。其次,让函数返回这个对象的引用。这样,用户将通过函数调用来指明对象。换句话说,用函数内部的static对象取代了非局部静态对象。

虽然关于 "非局部" 静态对象什么时候被初始化,C++几乎没有做过说明;但对于函数中的静态对象(即,"局部" 静态对象)什么时候被初始化,C++却明确指出:它们在函数调用过程中初次碰到对象的定义时被初始化。所以,如果你不对非局部静态对象直接访问,而用返回局部静态对象引用的函数调用来代替,就能保证从函数得到的引用指向的是被初始化了的对象。这样做的另一个好处是,如果这个模拟非局部静态对象的函数从没有被调用,也就永远不会带来对象构造和销毁的开销;而对于非局部静态对象来说就没有这样的好事。 

 

48、不要对编译器的警告信息视如不见

 略

 

49、尽量让自己熟悉C++标准程序库

 略

 

50、加强自己对C++的了解

 略