C++ FAQ 11

来源:互联网 发布:手机淘宝添加旺旺客服 编辑:程序博客网 时间:2024/06/10 20:20

摘自 http://helloworld.cc/cpp/cppfaq1.html

C++ FAQ

最近发现C++ FAQ,发现里面有很多东西,自己都不明白,所以想把它仔细的看完,顺便把自己原来不知道或者理解的不够仔细的地方摘抄下来,以后也方便再翻出来看看。

[9.3] 为什么我应该用内联函数?而不是原来清晰的 #define 宏?
[Recently rewrote the sentence on #define being evil (on 7/00). Click here to go to the next FAQ in the "chain" of recent changes.]

因为#define宏是在四处是有害的:罪状#1, 罪状#2, 罪状#3, 和 罪状#4。

和 #define 宏不同的是,内联函数总是对参数只精确地进行一次求值,从而避免了那声名狼藉的宏错误。换句话说,调用内联函数和调用正规函数是等价的,差别仅仅是更快:


 // 返回 i 的绝对值的宏
 #define unsafe(i)  /
         ( (i) >= 0 ? (i) : -(i) )
 
 // 返回 i 的绝对值的内联函数
 inline
 int safe(int i)
 {
   return i >= 0 ? i : -i;
 }
 
 int f();
 
 void userCode(int x)
 {
   int ans;
 
   ans = unsafe(x++);   // 错误!x 被增加两次
   ans = unsafe(f());   // 危险!f()被调用两次
 
   ans = safe(x++);     // 正确! x 被增加一次
   ans = safe(f());     // 正确! f() 被调用一次
 }
和宏不同的,还有内联函数的参数类型被检查,并且被正确地进行必要的转换。

宏是有害的;非万不得已不要用。

[10.5] 当建立一个 Fred 对象数组时,哪个构造函数将被调用?
[Recently changed so it uses new-style headers and the std:: syntax and reworded references to STL (on 7/00). Click here to go to the next FAQ in the "chain" of recent changes.]

Fred 的默认构造函数(以下讨论除外)。

你无法告诉编译器调用不同的构造函数(以下讨论除外)。如果你的Fred类没有默认构造函数,那么试图创建一个Fred对象数组将会导致编译时出错。

 

 class Fred {
 public:
   Fred(int i, int j);
   // ... 假设 Fred 类没有默认构造函数 ...
 };
 
 int main()
 {
   Fred a[10];               // 错误:Fred 类没有默认构造函数
   Fred* p = new Fred[10];   // 错误:Fred 类没有默认构造函数
 }
然而,如果你正在创建一个标准的std::vector<Fred>,而不是 Fred对象数组(既然数组是有害的,那么你可能应该这么做),则在 Fred 类中不需要默认构造函数。因为你能够给std::vector一个用来初始化元素的Fred 对象:

 

 #include <vector>
 
 int main()
 {
   std::vector<Fred> a(10, Fred(5,7));
   // 在std::vector 中的 10 个 Fred对象将使用 Fred(5,7) 来初始化
   // ...
 }
虽然应该使用std::vector而不是数组,但有有应该使用数组的时候,那样的话,有“数组的显式初始化”语法。它看上去是这样的:

 

 class Fred {
 public:
   Fred(int i, int j);
   // ... 假设Fred类没有默认构造函数...
 };
 
 int main()
 {
   Fred a[10] = {
     Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7),
     Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7)
   };
 
   // 10 个 Fred对象将使用 Fred(5,7) 来初始化.
   // ...
 }
当然你不必每个项都做Fred(5,7)—你可以放任何你想要的数字,甚至是参数或其他变量。重点是,这种语法是(a)可行的,但(b)不如std::vector语法漂亮。记住这个:数组是有害的—除非由于编译原因而使用数组,否则应该用std::vector 取代。

[10.6] 构造函数应该用“初始化列表”还是“赋值”?
[Recently rewrote (on 4/01). Click here to go to the next FAQ in the "chain" of recent changes.]

初始化列表。事实上,构造函数应该在初始化列表中初始化所有成员对象。

例如,构造函数用初始化列表Fred::Fred() : x_(whatever) { }来初始化成员对象 x_。这样做最普通的好处是提高性能。如,whatever表达式和成员变量 x_ 相同,whatever表达式的结果直接由内部的x_来构造——编译器不会产生对象的两个拷贝。即使类型不同,使用初始化列表时编译器通常也能够做得比使用赋值更好。

建立构造函数的另一种(错误的)方法是通过赋值,如:Fred::Fred() { x_ = whatever; }。在这种情况下,whatever表达式导致一个分离的,临时的对象被建立,并且该临时对象被传递给x_对象的赋值操作。然后该临时对象会在 ;处被析构。这样是效率低下的。

这好像还不是太坏,但这里还有一个在构造函数中使用赋值的效率低下之源:成员对象会被以默认构造函数完整的构造,例如,可能分配一些缺省数量的内存或打开一些缺省的文件。但如果 whatever表达式和/或赋值操作导致对象关闭那个文件和/或释放那块内存,这些工作是做无用功(举例来说,如默认构造函数没有分配一个足够大的内存池或它打开了错误的文件)。

结论:其他条件相等的情况下,使用初始化列表的代码会快于使用赋值的代码。

注意:如果x_的类型是诸如int或者char* 或者float之类的内建类型,那么性能是没有区别的。但即使在这些情况下,我个人的偏好是为了对称,仍然使用初始化列表而不是赋值来设置这些数据成员。

还有一个没有提到,对const的成员,必须用初始化,因为它不能被赋值。例如:

class test
{
public:
    test(int b):a(b){}
    //test(int c){a=c;} error
    const int a;
};

[10.7] 可以在构造函数中使用 this 指针吗?
[Recently rewrote because of a suggestion from Perry Rapp (on 4/01). Click here to go to the next FAQ in the "chain" of recent changes.]

某些人认为不应该在构造函数中使用this指针,因为这时this对象还没有完全形成。然后,只要你小心,是可以在构造函数(在函数体甚至在初始化列表中)使用this的。

以下是始终可行的:构造函数的函数体(或构造函数所调用的函数)能可靠地访问基类中声明的数据成员和/或构造函数所属类声明的数据成员。这是因为所有这些数据成员被保证在构造函数函数体开始执行时已经被完整的建立。

以下是始终不可行的:构造函数的函数体(或构造函数所调用的函数)不能向下调用被派生类重定义的虚函数。如果你的目的是得到派生类重定义的函数,那么你将无功而返。注意,无论你如何调用虚成员函数:显式使用this指针(如,this->method()),隐式的使用this指针(如,method()),或甚至在this对象上调用其他函数来调用该虚成员函数,你都不会得到派生类的重写函数。这是底线:即使调用者正在构建一个派生类的对象,在基类的构造函数执行期间,对象还不是一个派生类的对象。

以下是有时可行的:如果传递 this 对象的任何一个数据成员给另一个数据成员的初始化程序,你必须确保该数据成员已经被初始化。好消息是你能使用一些不依赖于你所使用的编译器的显著的语言规则,来确定那个数据成员是否已经(或者还没有)被初始化。坏消息是你必须知道这些语言规则(例如,基类子对象首先被初始化(如果有多重和/或虚继承,则查询这个次序!),然后类中定义的数据成员根据在类中声明的次序被初始化)。如果你不知道这些规则,则不要从this对象传递任何数据成员(不论是否显式的使用了this关键字)给任何其他数据成员的初始化程序!如果你知道这些规则,则需要小心。

[10.11] 什么是“static initialization order fiasco”?
你的项目的微妙杀手。

static initialization order fiasco是对C++的一个非常微妙的并且常见的误解。不幸的是,错误发生在main()开始之前,很难检测到。

简而言之,假设你有存在于不同的源文件x.cpp 和y.cpp的两个静态对象x 和 y。再假定y对象的构造函数会调用x对象的某些方法。

就是这些。就这么简单。

结局是你完蛋不完蛋的机会是50%-50%。如果碰巧x.cpp的编辑单元先被初始化,这很好。但如果y.cpp的编辑单元先被初始化,然后y的构造函数比x的构造函数先运行。也就是说,y的构造函数会调用x对象的方法,而x对象还没有被构造。

我听说有些人受雇于麦当劳,享受他们的切碎肉的新工作去了。

如果你觉得不用工作,在卧室的一角玩俄罗斯方块是令人兴奋的,你可以到此为止。相反,如果你想通过用一种系统的方法防止灾难,来提高自己继续工作而存活的机会,你可能想阅读下一个 FAQ。

注意:static initialization order fiasco不作用于内建的/固有的类型,象int 或 char*。例如,如果创建一个static float对象,不会有静态初始化次序的问题。静态初始化次序真正会崩溃的时机只有在你的static或全局对象有构造函数时。

[ Top | Bottom | Previous section | Next section ]


--------------------------------------------------------------------------------

 

[10.12] 如何防止“static initialization order fiasco”?
使用“首次使用时构造(construct on first use)”法,意思就是简单地将静态对象包裹于函数内部。

例如,假设你有两个类,Fred 和 Barney。有一个称为x的全局Fred对象,和一个称为y的全局Barney对象。Barney的构造函数调用了x对象的goBowling()方法。 x.cpp文件定义了x对象:

 

 // File x.cpp
 #include "Fred.hpp"
 Fred x;
y.cpp文件定义了y对象:

 

 // File y.cpp
 #include "Barney.hpp"
 Barney y;
Barney构造函数的全部看起来可能是象这样的:

 

 // File Barney.cpp
 #include "Barney.hpp"
 
 Barney::Barney()
 {
   // ...
   x.goBowling();
   // ...
 }
正如以上所描述的,由于它们位于不同的源文件,那么 y 在 x 之前构造而发生灾难的机率是50%。

这个问题有许多解决方案,但一个非常简便的方案就是用一个返回Fred对象引用的全局函数x(),来取代全局的Fred对象 x。

 

 // File x.cpp
 
 #include "Fred.hpp"
 
 Fred& x()
 {
   static Fred* ans = new Fred();
   return *ans;
 }
由于静态局部对象只在控制流第一次越过它们的声明时构造,因此以上的new Fred()语句只会执行一次:x()被第一次调用时。每个后续的调用将返回同一个Fred对象(ans指向的那个)。然后你所要做的就是将 x 改成 x():

 

 // File Barney.cpp
 #include "Barney.hpp"
 
 Barney::Barney()
 {
   // ...
   x().goBowling();
   // ...
 }
由于该全局的Fred对象在首次使用时被构造,因此被称为首次使用时构造法(Construct On First Use Idiom)

这种方法的不利方面是Fred对象不会被析构。C++ FAQ Book有另一种技巧消除这个影响(但面临了“static de-initialization order fiasco”的代价)。

注意:对于内建/固有类型,象int 或 char*,不必这样做。例如,如果创建一个静态的或全局的float对象,不需要将它包裹于函数之中。静态初始化次序真正会崩溃的时机只有在你的static或全局对象有构造函数时。

[ Top | Bottom | Previous section | Next section ]


--------------------------------------------------------------------------------

 

[10.13] 对于静态数据成员,如何防止“static initialization order fiasco”?
使用与描述过的相同的技巧,但这次使用静态成员函数而不是全局函数而已。

假设类 X 有一个static Fred对象:

 

 // File X.hpp
 
 class X {
 public:
   // ...
 
 private:
   static Fred x_;
 };
自然的,该静态成员被分开初始化:

 

 // File X.cpp
 
 #include "X.hpp"
 
 Fred X::x_;
自然的,Fred对象会在 X 的一个或多个方法中被使用:

 

 void X::someMethod()
 {
   x_.goBowling();
 }
但现在“灾难情景”就是如果某人在某处不知何故在Fred对象被构造前调用这个方法。例如,如果某人在静态初始化期间创建一个静态的 X 对象并调用它的someMethod()方法,然后你就受制于编译器是在someMethod()被调用之前或之后构造 X::x_。(ANSI/ISO C++委员会正在设法解决这个问题,但诸多的编译器对处理这些更改一般还没有完成;关注此处将来的更新。)

无论何种结果,将X::x_ 静态数据成员改为静态成员函数总是最简便和安全的:

 

 // File X.hpp
 
 class X {
 public:
   // ...
 
 private:
   static Fred& x();
 };
自然的,该静态成员被分开初始化:

 

 // File X.cpp
 
 #include "X.hpp"
 
 Fred& X::x()
 {
   static Fred* ans = new Fred();
   return *ans;
 }
然后,简单地将 x_ 改为 x():

 

 void X::someMethod()
 {
   x().goBowling();
 }
如果你对性能敏感并且关心每次调用X::someMethod()的额外的函数调用的开销,你可以设置一个static Fred&来取代。正如你所记得的,静态局部对象仅被初始化一次(控制流程首次越过它们的声明处时),因此,将只调用X::x()一次:X::someMethod()首次被调用时:

 

 void X::someMethod()
 {
   static Fred& x = X::x();
   x.goBowling();
 }
注意:对于内建/固有类型,象int 或 char*,不必这样做。例如,如果创建一个静态的或全局的float对象,不需要将它包裹于函数之中。静态初始化次序真正会崩溃的时机只有在你的static或全局对象有构造函数时。

[11.2] 局部对象析构的顺序是什么?
与构造函数反序:先被构造的,被后析构。

以下的例子中,b 的析构函数会被首先执行,然后是 a 的析构函数:

 

 void userCode()
 {
   Fred a;
   Fred b;
   // ...
 }
[ Top | Bottom | Previous section | Next section ]


--------------------------------------------------------------------------------

 

[11.3] 数组中的对象析构顺序是什么?
与构造函数反序:先被构造的,后被析构。

以下的例子中,析构的顺序是a[9], a[8], ..., a[1], a[0]:

 

 void userCode()
 {
   Fred a[10];
   // ...
 }

[11.10] 什么是“定位放置new(placement new)”,为什么要用它 ?
定位放置new(placement new)有很多作用。最简单的用处就是将对象放置在内存中的特殊位置。这是依靠 new表达式部分的指针参数的位置来完成的:

 

 #include <new>        // 必须 #include 这个,才能使用 "placement new"
 #include "Fred.h"     // class Fred 的声明
 
 void someCode()
 {
   char memory[sizeof(Fred)];     // Line #1
   void* place = memory;          // Line #2
 
   Fred* f = new(place) Fred();   // Line #3 (详见以下的“危险”)
   // The pointers f and place will be equal
 
   // ...
 }
Line #1 在内存中创建了一个sizeof(Fred)字节大小的数组,足够放下 Fred 对象。Line #2 创建了一个指向这块内存的首字节的place指针(有经验的 C 程序员会注意到这一步是多余的,这儿只是为了使代码更明显)。Line #3 本质上只是调用了构造函数 Fred::Fred()。Fred构造函数中的this指针将等于place。因此返回的 f 将等于place。

建议:万不得已时才使用“placement new”语法。只有当你真的在意对象在内存中的特定位置时才使用它。例如,你的硬件有一个内存映象的 I/O计时器设备,并且你想放置一个Clock对象在那个内存位置。

危险:你要独自承担这样的责任,传递给“placement new”操作符的指针所指向的内存区域必须足够大,并且可能需要为所创建的对象进行边界调整。编译器和运行时系统都不会进行任何的尝试来检查你做的是否正确。如果 Fred 类需要将边界调整为4字节,而你提供的位置没有进行边界调整的话,你就会亲手制造一个严重的灾难(如果你不明白“边界调整”的意思,那么就不要使用placement new语法)。

你还有析构放置的对象的责任。这通过显式调用析构函数来完成:

 

 void someCode()
 {
   char memory[sizeof(Fred)];
   void* p = memory;
   Fred* f = new(p) Fred();
   // ...
   f->~Fred();   // 显式调用定位放置的对象的析构函数
 }
这是显式调用析构函数的唯一时机。

[12.1] 什么是“自赋值”?
自赋值就是将对象赋值给本身。例如,

 

 #include "Fred.hpp"    // 声明 Fred 类
 
 void userCode(Fred& x)
 {
   x = x;   // 自赋值
 }
很明显,以上代码进行了显式的自赋值。但既然多个指针或引用可以指向相同对象(别名),那么进行了自赋值而自己却不知道的情况也是可能的:

 

 #include "Fred.hpp"    // 声明 Fred 类
 
 void userCode(Fred& x, Fred& y)
 {
   x = y;   // 如果&x == &y就可能是自赋值
 }
 
 int main()
 {
   Fred z;
   userCode(z, z);
 }
[ Top | Bottom | Previous section | Next section ]


--------------------------------------------------------------------------------

 

[12.2] 为什么应该当心“自赋值”?
如果不注意自赋值,将会使你的用户遭受非常微妙的并且一般来说非常严重的bug。例如,如下的类在自赋值的情况下将导致灾难:

 

 class Wilma { };
 
 class Fred {
 public:
   Fred()                : p_(new Wilma())      { }
   Fred(const Fred& f)   : p_(new Wilma(*f.p_)) { }
  ~Fred()                { delete p_; }
   Fred& operator= (const Fred& f)
     {
       // 差劲的代码:没有处理自赋值!
       delete p_;                // Line #1
       p_ = new Wilma(*f.p_);    // Line #2
       return *this;
     }
 private:
   Wilma* p_;
 };
如果有人将 Fred 对象赋给其本身,由于*this和 f 是同一个对象,line #1同时删除了this->p_和f.p_。而 line #2使用了已经不存在的对象*f.p_,这样很可能导致严重的灾难。

作为 Fred 类的作者,你最起码有责任确信在Fred对象上自赋值是无害的。不要假设用户不会在对象上这样做。如果对象由于自赋值而崩溃,那是你的过失。

另外:上述的Fred::operator= (const Fred&)还有第二个问题:如果在执行new Wilma(*f.p_)时,抛出了异常(例如,内存不够的异常或者Wilma的拷贝构造函数中的异常), this->p_将成为悬空指针——它所指向的内存不再是可用的。这可以通过在删除就对象前创建对象来解决。

[ Top | Bottom | Previous section | Next section ]


--------------------------------------------------------------------------------

 

[12.3] 好,好;我会处理自赋值的。但如何做呢?
[Recently reworded the last paragraph (on 7/00). Click here to go to the next FAQ in the "chain" of recent changes.]

在你创建类的每时每刻,都应该当心自赋值。这并不意味着需要为你所有的类都增加额外的代码:只要对象优雅地处理自赋值,而不管是否必须增加额外的代码。

如果不需要为赋值算符增加额外代码,这里有一个简单而有效的技巧:

 

 Fred& Fred::operator= (const Fred& f)
 {
   if (this == &f) return *this;   // 优雅地处理自赋值
 
   // 此处写正常赋值的代码...
 
   return *this;
 }
显式的测试并不总是必要的。例如,如果修正前一个 FAQ中的赋值算符使之处理new抛出的异常和/或Wilma类的拷贝构造函数抛出的异常,可能会写出如下的代码。注意这段代码有(令人高兴的)自动处理自赋值的附带效果:

 

 Fred& Fred::operator= (const Fred& f)
 {
   // 这段代码优雅地(但隐含的)处理自赋值
   Wilma* tmp = new Wilma(*f.p_);   // 如果异常在此处被抛出也没有问题
   delete p_;
   p_ = tmp;
   return *this;
 }
在象这个例子的情况下(自赋值是无害的但是低效),一些程序员想通过增加另外的不必要的测试,如“if (this == &f) return *this;”来改善自赋值时的效率。通常来说,使自赋值情况更高效而使得非自赋值情况更低效的折衷是错误的。例如,为Fred类的赋值算符增加如上的if测试会使得非自赋值情况更低效(一个额外的(而且不必要的)条件分支)。如果自赋值实际上一千次才发生一次,那么 if 将浪费99.9%的时间周期。