Effective C++ 第二版 33)内联 34)将编译依赖降至最低

条款33 明智地使用内联

内联函数: 看起来像函数, 运作起来像函数, 比宏macro要好(条款1), 使用时还不需要承担函数调用的开销;

避免函数调用的开销仅仅是一方面; 为了处理那些没有函数调用的代码, 编译器优化程序本身进行了专门的设计; 当内联一个函数时, 编译器可以对函数体执行特定环境下的优化工作; 

程序世界和现实生活一样, 没有免费午餐, 内联函数也不例外; 内联函数的基本思想在于将每个函数调用以代码体来替换; 这样的做法很可能会增加整个目标代码的体积; 在一台内存有限的计算机里, 过分地使用内联所产生的程序会因为体积太大导致可用空间不够; 即使可以使用虚拟内存, 内联造成的代码膨胀也可能会导致不合理的页面调度行为(系统颠簸), 这将使你的程序运行极慢; 过多的内联还会降低指令高速缓存的命中率, 使取指令的速度降低, 因为从主存取指令比缓存慢;

话说回来, 如果内联函数体非常短, 编译器为这个函数体生成的代码会真的比为函数调用生成的代码小许多; 这种情况, 内联这个函数将会确实带来更小的目标代码和更高的缓存命中率;

Note inline指令就像register, 只是对编译器的一种提示, 而不是命令;

只要编译器愿意, 可以随意地忽略掉你的指令, 事实上编译器常常这么做; e.g. 大多数编译器拒绝内联"复杂"的函数(包含循环和递归的函数); 

Note 即使是最简单的虚函数调用, 编译器的内联处理也无能为力(virtual是等到运行时再决定调用哪个函数, inline是在编译期间将调用的函数用代码体代替, 如果编译器不知道哪个函数将被调用, 就无法生成内联调用);

结论: 一个给定的内联函数是否真的被内联取决于所用的编译器的具体实现; 大多数编译器都可以设置诊断级别, 当声明为内联的函数实际上没有被内联时, 编译器会发出警告信息(条款48);

理论上, 假设写了某个函数f声明为inline, 如果编译器决定不对他内联; f将作为一个非内联函数处理, 为f生成代码时就像是一个普通的"外联"函数, 对f的调用也和普通函数一样;

实际上, 这个方案对解决"被外联的内联outlined inline"非常理想, 但它加入C++标准的时间相对较晚, 较早的C++规范(例如ARM--条款50)告诉编译器制造商去实现的是另外不同的行为, 这一旧的行为在现在编译器中还存在;

内联函数的定义实际都是放在头文件中; 这使得多个要编译的单元(源文件)可以包含同一个头文件, 共享头文件内定义的内联函数所带来的好处;

e.g. 例子中的源文件名义常规cpp结尾:

1
2
3
4
5
6
7
8
9
// 文件example.h
inline  void  f() { ... }  // f 的定义
...
// 文件source1.cpp
#include "example.h" // 包含f 的定义
...  // 包含对f 的调用
// 文件source2.cpp
#include "example.h" // 也包含f 的定义
...  // 也调用f

如果现在采用旧的"被外联的内联"规则, 而且假设f没有被编译器内联, 当source1.cpp编译时, 生成的目标文件将包含称为f的函数, 就像f没有被声明为inline一样; 同样地, 当source2.cpp被编译时, 产生的目标文件也包含一个f函数; 当把两个目标文件链接在一起时, 编译器会因为程序中有两个f的定义而报错;

为了防止这个问题, 旧的规则规定, 对于未被内联的内联函数, 编译器把它当成被声明为static那样处理, 使它局限于当前被编译的文件; 对于刚才的例子, 遵循旧规则的编译器处理source1.cpp中的f时, 就像f在source1.cpp中是static的一样, 对source2.cpp也是; 这一策略消除了链接时的错误, 但带来了开销: 每个包含f的定义(以及调用f)的被编译单元都包含自己的f的静态拷贝; 如果f自身定义了局部静态变量, 那么每个f的拷贝都有这局部变量的一份拷贝; 这会让程序员意想不到, 因为一般来说函数中的static意味着只有一份拷贝;

具体实现也会出乎意料; 无论新规则还是旧规则, 如果内联函数没被内联, 每个调用内联函数的地方还是要承担函数调用的开销; 如果是旧规则, 还要忍受代码体积增加, 每个包含(或调用)f的被编译单元都有一份f的代码及其静态变量的拷贝(更糟糕的, 每个f的拷贝以及每个f的静态变量的拷贝往往处于不同的虚拟内存页面, 所以两个对f的不同拷贝进行调用有可能导致多个页面错误)

还有, 编译器即使很想内联一个函数, 却不得不为这个内联函数生成一个函数体; 特别是当程序中要取一个内联函数的地址, 编译器就必须为此生成一个函数体, 否则编译器没法指向一个不存在的函数的指针;

1
2
3
4
5
6
7
inline  void  f() {...}  // 同上
void  (*pf)() = f;  // pf 指向f
int  main()
{
     f();  // 对 f 的内联调用
     pf();  // 通过pf 对f 的非内联调用
}

>这种情况似乎很荒谬: f的调用被内联了, 在旧的规则下, 每个取f地址的被编译单元还是各自生成了此函数的静态拷贝;(新规则下, 不管涉及的被编译单元有多少, 将只生成唯一一个f的外部拷贝);

即使你从来不使用函数指针, 这类"没被内联的内联函数"也会找到你, 因为不只是程序员使用函数指针, 有时编译器也会这么做; 特别是编译器有时会生成构造和析构函数的外部拷贝, 这样可以通过得到那些函数的指针, 方便地构造和析构类的对象数组;(条款M8)

实际上, 随便一个测试就能证明构造和析构函数常常不适合内联, e.g. Derived的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
class  Base {
public :
...
private :
     string bm1, bm2;  // 基类成员1 和2
};
class  Derived:  public  Base {
public :
     Derived() {}  // Derived 的构造函数是空的, ------但,真的是空的吗?
private :
     string dm1, dm2, dm3;  // 派生类成员1-3
};

>这个构造看起来适合内联, 因为它没有代码; 但只是看上去没有代码, 实际上它含有相当多的代码 [编译器生成]

C++就对象创建和销毁时发生的事件有多方面的规定; 条款5和M8介绍了当使用new时, 动态创建的对象怎样自动地被构造函数初始化, 以及使用delete时析构怎样被调用, 条款13说明了创建一个对象时, 对象的每个基类以及对象的每个数据成员会被自动创建; 对象销毁时, 会自动执行相反的过程(析构); C++规定了哪些必须发生, 但没有规定"怎么"发生, 这取决于编译器的实现者; 程序中比如有代码使得他们发生, 特别是由编译器的实现者写的, 在编译期间插入到你的程序中的代码, 必然藏身于某处--有时, 就藏在构造和析构函数里; 所以, 对于看似空的Derived的构造函数, 有些编译器会产生下面的代码:

1
2
3
4
5
6
7
8
9
10
11
// 一个Derived 构造函数的可能的实现
Derived::Derived()
{
// 如果在堆上创建对象,为其分配堆内存;operator new 的介绍参见条款8
     if  (本对象在堆上)
         this  = ::operator  new ( sizeof (Derived));
     Base::Base();  // 初始化 Base 部分
     dm1.string();  // 构造 dm1
     dm2.string();  // 构造 dm2
     dm3.string(); 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
這本書是多年來我對專業程式員所做的C++ 教學課程下的一個自然產物。我發現,大部份學生在一個星期的密集訓練之後,即可適應這個語言的基本架構,但要他們「將這些基礎架構以有效的方式組合運用」,我實在不感樂觀。於是我開始嘗試組織出一些簡短、明確、容易記憶的準則,做為C++ 高實效性程式開發過程之用。那都是經驗豐富的C++ 程式員幾乎總是會奉行或幾乎肯定要避免的一些事情。 我最初的興趣在於整理出一些可被某種「lint-like 程式」施行的規則,最後我甚至領導一個計劃,研究某種可將C++ 原始碼中違反使用者指定條件之處檢驗出來的工具(你可以在Effective C++ 網站上找到此研究的一份概要報告)。不幸的是在我尚未完成其完整原型之前,這個研究計劃便結束了。幸運的是,目前市面上已有這類C++ 檢驗工具(商品),而且不只一個。 雖然我最初的興趣是在研究可被(某種工具)自動實施的程式設計準則,但我很快瞭解到那個研究方向的侷限性。優秀的C++ 程式員所奉行的準則,多數都難以「公式化」;要不就是雖然它們有許多重要的例外情況,卻被程式員盲目地奉行不渝。這使我念頭一轉:某些東西雖然不比電腦程式精準,但仍能比一本泛泛的C++ 教科書更集中火力,更打到重點。這個念頭的結果就是你手上這本書:一本內含50 個有效建議(如何改善你的C++ 程式技術和你的設計思維)的書。 在這本書中,你會發現一些忠告,告訴你應該做些什麼,為什麼如此;告訴你不應該做些什麼,又為什麼如此。基本而言當然whys 比whats 更重要,但檢閱一列列準則,也確實比強記一本或兩本教科書更輕鬆更方便得多。 和大部份的C++ 書籍不同,我的組織方式並非以語言特性做為依據。也就是說我並不在某處集中討論constructors(建構式),在另一處集中討論virtual functions (虛擬函式),又在第三個地方集中討論inheritance(繼承機制)。不,不是這樣,本書的每一個討論主題都剪裁合度地以一個個準則陳列出來。至於我對某特定語言性質的探討,散佈面積可能涵蓋整本書。 這種作法的優點就是比較容易反映出「特意挑選C++ 做為開發工具」的那些軟體系統的複雜度。在那些系統之中,光只瞭解個別語言特性是不夠的。例如,有經驗的C++ 程式員知道,瞭解inline 函式和瞭解virtual destructors,並不一定表示你瞭解inline virtual destructors。身經百戰的開發人員都認知到,理解C++ 各個特性之間的互動關係,才是有效使用這個語言的最重要關鍵。本書組織反映出這一基本事實。 這種作法的缺點是,你恐怕必須前後交叉參考而非只看一個地方,才能發現我所說的某個C++ 架構的全貌。為了將不方便性降至最低,我在書中各處放了許多交叉索引,書後並有一份涵蓋全部範圍的索引。(譯註:為了協助讀者更容易掌握 Effective C++ 和More Effective C++ 二書,我以Effective C++ CD 為本,為兩書的中文版額外加上兩書之間的交叉索引。此乃原書所無。如果文中出現像條款 M5 這樣的參考指示,M 便是代表More Effective C++) 籌劃第二版期間,我改寫此書的雄心一再被恐懼所取代。成千上萬的程式員熱情擁抱Effective C++ 第一版,我不希望破壞吸引他們的任何東西。但是自從我寫了第一版之後,六年過去了,C++ 有了變化,C++ 程式庫有了變化(見條款49),我對C++ 的瞭解也有了變化,乃至於C++ 的用途也有了變化。許許多多的變化。對我而言,重要的是我必須修訂Effective C++ 以反映那些變化。我嘗試一頁一頁地修改內容,但是書籍和軟體十分類似,局部加強是不夠的,唯一的機會就是系統化地重寫。本書就是重寫後的結果:Effective C++ 2.0 版。 熟悉第一版的讀者,可能有興趣知道,書中的每一個條款都經過重新檢驗。然而我相信第一版的結構至今仍是流暢的,所以整本書的結構並沒有改變。50 個條款中,我保留了48 個,其中某些標題稍有變化(附隨的討論內容亦復如此)。退休下來(被取代的)兩個條款是32 和49,不過原條款32 的許多資訊被我移到如今煥然一新的條款1 中。我將條款41 和42 的次序做了對調,因為這樣比較能夠適當呈現它們修訂後的內容。最後,我把上一版繼承體系圖所採用的箭頭方向顛倒過來,以符合目前幾乎已經一致的習慣:從derived classes 指往base classes。我的More Effective C++ 一書也採用相同習慣(本書最後列有該書摘要)。 本書提供的準則,離鉅細糜遺的程度還很遠,但是完成一個好的準則— 一個幾乎可於任何時間應用於任何程式的準則,動手遠比動嘴困難得多。如果你知道其他準則,可以協助撰寫有效的C++ 程式,我非常樂意聽到你告訴我它們的故事。 此外,說不定你會覺得本書的某些條款不適合成為一般性忠告;或許你認為另有比較好的方法來完成書中所說的任務;或許你認為某些條款在技術討論方面不夠清楚,不夠完全,抑或有誤導之嫌。我衷心盼望你也能夠讓我知道你的這些想法。 Donald Knuth(譯註:經典書籍The Art of Computer Programming, Volume I,II,III 的作者)長久以來為挑出其書錯誤的熱心讀者準備有一份小小的報酬。這個故事傳為美談。追求完美的精神令人佩服。看過那麼多倉促上市錯誤壘壘的C++ 書籍後,我更是特別強烈地希望踵隨Knuth 的風範。因此,如果有人挑出本書的任何錯誤並告訴我— 不論是技術、文法、錯別字、或任何其他東西— 我將在本書新刷的時候,把第一位挑出錯誤的讀者大名加到致謝名單中。 請將你的建議、你的見解、你的批評、以及(喔…真糟…)你的臭蟲報告,寄至: Scott Meyers c/o Publisher, Corporate and Professional Publishing Addison Wesley Longman, Inc. 1 Jacob Way Reading, MA 01867 U. S. A. 或者傳送電子郵件到ec++@awl.com。 我維護有本書第一刷以來的修訂記錄,其中包括錯誤更正、文字修潤、以及技術更新。你可以從Effective C++ 網站取得這份記錄。如果你希望擁有這份資料,但無法上網,請寄申請函到上述地址,我會郵寄一份給你。 Scott Douglas Meyers Stafford, Oregon July 1997

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值