《Effective C++》第二版笔记
一、改变旧有的 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;
}
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_){}
使用 Test(User& user_){user = user_;}
除了上面演示的,很多时候 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;
}
先初始化的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++的了解
略