日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

高质量C++/C编程指南(林锐)

發(fā)布時(shí)間:2024/9/27 c/c++ 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 高质量C++/C编程指南(林锐) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

版本/狀態(tài)

作者

參與者

起止日期

備注

V 0.9

草稿文件

林銳

?

?

2001-7-1至

2001-7-18

林銳起草

V 1.0

正式文件

林銳

?

?

2001-7-18至

2001-7-24

朱洪海審查V 0.9,

林銳修正草稿中的錯(cuò)誤

?

?

?

?

?

?

?

?

?

?

?

?

?

前?言

???? 軟件質(zhì)量是被大多數(shù)程序員掛在嘴上而不是放在心上的東西!

????除了完全外行和真正的編程高手外,初讀本書,你最先的感受將是驚慌:“哇!我以前捏造的C++/C程序怎么會(huì)有那么多的毛病?”

????別難過,作者只不過比你早幾年、多幾次驚慌而已。

????請(qǐng)花一兩個(gè)小時(shí)認(rèn)真閱讀這本百頁經(jīng)書,你將會(huì)獲益匪淺,這是前面N-1個(gè)讀者的建議。

一、編程老手與高手的誤區(qū)

自從計(jì)算機(jī)問世以來,程序設(shè)計(jì)就成了令人羨慕的職業(yè),程序員在受人寵愛之后容易發(fā)展成為毛病特多卻常能自我臭美的群體。

如今在Internet上流傳的“真正”的程序員據(jù)說是這樣的:

(1)????真正的程序員沒有進(jìn)度表,只有討好領(lǐng)導(dǎo)的馬屁精才有進(jìn)度表,真正的程序員會(huì)讓領(lǐng)導(dǎo)提心吊膽。

(2)????真正的程序員不寫使用說明書,用戶應(yīng)當(dāng)自己去猜想程序的功能。

(3)????真正的程序員幾乎不寫代碼的注釋,如果注釋很難寫,它理所當(dāng)然也很難讀。

(4)????真正的程序員不畫流程圖,原始人和文盲才會(huì)干這事。

(5)????真正的程序員不看參考手冊(cè),新手和膽小鬼才會(huì)看。

(6)????真正的程序員不寫文檔也不需要文檔,只有看不懂程序的笨蛋才用文檔。

(7)????真正的程序員認(rèn)為自己比用戶更明白用戶需要什么。

(8)????真正的程序員不接受團(tuán)隊(duì)開發(fā)的理念,除非他自己是頭頭。

(9)????真正的程序員的程序不會(huì)在第一次就正確運(yùn)行,但是他們?cè)敢馐刂鴻C(jī)器進(jìn)行若干個(gè)30小時(shí)的調(diào)試改錯(cuò)。

(10)真正的程序員不會(huì)在上午9:00到下午5:00之間工作,如果你看到他在上午9:00工作,這表明他從昨晚一直干到現(xiàn)在。

……

具備上述特征越多,越顯得水平高,資格老。所以別奇怪,程序員的很多缺點(diǎn)竟然可以被當(dāng)作優(yōu)點(diǎn)來欣賞。就象在武俠小說中,那些獨(dú)來獨(dú)往、不受約束且?guī)c(diǎn)邪氣的高手最令人崇拜。我曾經(jīng)也這樣信奉,并且希望自己成為那樣的“真正”的程序員,結(jié)果沒有得到好下場(chǎng)。

我從讀大學(xué)到博士畢業(yè)十年來一直勤奮好學(xué),累計(jì)編寫了數(shù)十萬行C++/C代碼。有這樣的苦勞和疲勞,我應(yīng)該稱得上是編程老手了吧?

我開發(fā)的軟件都與科研相關(guān)(集成電路CAD和3D圖形學(xué)領(lǐng)域),動(dòng)輒數(shù)萬行程序,技術(shù)復(fù)雜,難度頗高。這些軟件頻頻獲獎(jiǎng),有一個(gè)軟件獲得首屆中國大學(xué)生電腦大賽軟件展示一等獎(jiǎng)。在1995年開發(fā)的一套圖形軟件庫到2000年還有人買。羅列出這些“業(yè)績(jī)”,可以說明我算得上是編程高手了吧?

可惜這種個(gè)人感覺不等于事實(shí)。

讀博期間我曾用一年時(shí)間開發(fā)了一個(gè)近10萬行C++代碼的3D圖形軟件產(chǎn)品,我內(nèi)心得意表面謙虛地向一位真正的軟件高手請(qǐng)教。他雖然從未涉足過3D圖形領(lǐng)域,卻在幾十分鐘內(nèi)指出該軟件多處重大設(shè)計(jì)錯(cuò)誤。讓人感覺那套軟件是用紙糊的華麗衣服,扯一下掉一塊,戳一下破個(gè)洞。我目瞪口呆地意識(shí)到這套軟件毫無實(shí)用價(jià)值,一年的心血白化了,并且害死了自己的軟件公司。

人的頓悟通常發(fā)生在最心痛的時(shí)刻,在沮喪和心痛之后,我作了深刻反省,“面壁”半年,重新溫習(xí)軟件設(shè)計(jì)的基礎(chǔ)知識(shí)。補(bǔ)修“內(nèi)功”之后,又覺得腰板硬了起來。博士畢業(yè)前半年,我曾到微軟中國研究院找工作,接受微軟公司一位資深軟件工程師的面試。他讓我寫函數(shù)strcpy的代碼。

太容易了吧?

錯(cuò)!

這么一個(gè)小不點(diǎn)的函數(shù),他從三個(gè)方面考查:

(1)編程風(fēng)格;

(2)出錯(cuò)處理;

(3)算法復(fù)雜度分析(用于提高性能)。

在大學(xué)里從來沒有人如此嚴(yán)格地考查過我的程序。我化了半個(gè)小時(shí),修改了數(shù)次,他還不盡滿意,讓我回家好好琢磨。我精神抖擻地進(jìn)“考場(chǎng)”,大汗淋漓地出“考場(chǎng)”。這“高手”當(dāng)?shù)靡蔡C囊了。我又好好地反省了一次。

我把反省后的心得體會(huì)寫成文章放在網(wǎng)上傳閱,引起了不少軟件開發(fā)人員的共鳴。我因此有幸和國產(chǎn)大型IT企業(yè)如華為、上海貝爾、中興等公司的同志們廣泛交流。大家認(rèn)為提高質(zhì)量與生產(chǎn)率是軟件工程要解決的核心問題。高質(zhì)量程序設(shè)計(jì)是非常重要的環(huán)節(jié),畢竟軟件是靠編程來實(shí)現(xiàn)的。

我們心目中的老手們和高手們能否編寫出高質(zhì)量的程序來?

不見得都能!

就我的經(jīng)歷與閱歷來看,國內(nèi)大學(xué)的計(jì)算機(jī)教育壓根就沒有灌輸高質(zhì)量程序設(shè)計(jì)的觀念,教師們和學(xué)生們也很少自覺關(guān)心軟件的質(zhì)量。勤奮好學(xué)的程序員長(zhǎng)期在低質(zhì)量的程序堆中滾爬,吃盡苦頭之后才有一些心得體會(huì),長(zhǎng)進(jìn)極慢,我就是一例。

現(xiàn)在國內(nèi)IT企業(yè)擁有學(xué)士、碩士、博士文憑的軟件開發(fā)人員比比皆是,但他們?cè)诮邮艽髮W(xué)教育時(shí)就“先天不足”,豈能一到企業(yè)就突然實(shí)現(xiàn)質(zhì)的飛躍。試問有多少軟件開發(fā)人員對(duì)正確性、健壯性、可靠性、效率、易用性、可讀性(可理解性)、可擴(kuò)展性、可復(fù)用性、兼容性、可移植性等質(zhì)量屬性了如指掌?并且能在實(shí)踐中運(yùn)用自如?。“高質(zhì)量”可不是干活小心點(diǎn)就能實(shí)現(xiàn)的!

我們有充分的理由疑慮:

(1)編程老手可能會(huì)長(zhǎng)期用隱含錯(cuò)誤的方式編程(習(xí)慣成自然),發(fā)現(xiàn)毛病后都不愿相信那是真的!

(2)編程高手可以在某一領(lǐng)域?qū)懗鰳O有水平的代碼,但未必能從全局把握軟件質(zhì)量的方方面面。

???????事實(shí)證明如此。我到上海貝爾工作一年來,陸續(xù)面試或測(cè)試過近百名“新”“老”程序員的編程技能,質(zhì)量合格率大約是10%。很少有人能夠?qū)懗鐾耆腺|(zhì)量要求的if語句,很多程序員對(duì)指針、內(nèi)存管理一知半解,……。

領(lǐng)導(dǎo)們不敢相信這是真的。我做過現(xiàn)場(chǎng)試驗(yàn):有一次部門新進(jìn)14名碩士生,在開歡迎會(huì)之前對(duì)他們進(jìn)行“C++/C編程技能”摸底考試。我問大家試題難不難?所有的人都回答不難。結(jié)果沒有一個(gè)人及格,有半數(shù)人得零分。競(jìng)爭(zhēng)對(duì)手公司的朋友們也做過試驗(yàn),同樣一敗涂地。

真的不是我“心狠手辣”或者要求過高,而是很多軟件開發(fā)人員對(duì)自己的要求不夠高。

要知道華為、上海貝爾、中興等公司的員工素質(zhì)在國內(nèi)IT企業(yè)中是比較前列的,倘若他們的編程質(zhì)量都如此差的話,我們?cè)趺锤移谕行」灸贸龈哔|(zhì)量的軟件呢?連程序都編不好,還談什么振興民族軟件產(chǎn)業(yè),豈不胡扯。

我打算定義編程老手和編程高手,請(qǐng)您別見笑。

定義1:能長(zhǎng)期穩(wěn)定地編寫出高質(zhì)量程序的程序員稱為編程老手。

定義2:能長(zhǎng)期穩(wěn)定地編寫出高難度、高質(zhì)量程序的程序員稱為編程高手。

根據(jù)上述定義,馬上得到第一推論:我既不是高手也算不上是老手。

在寫此書前,我閱讀了不少程序設(shè)計(jì)方面的英文著作,越看越羞慚。因?yàn)榘l(fā)現(xiàn)自己連編程基本技能都未能全面掌握,頂多算是二流水平,還好意思談什么老手和高手。希望和我一樣在國內(nèi)土生土長(zhǎng)的程序員朋友們能夠做到:

(1)知錯(cuò)就改;

(2)經(jīng)常溫故而知新;

(3)堅(jiān)持學(xué)習(xí),天天向上。

二、本書導(dǎo)讀

????首先請(qǐng)做附錄B的C++/C試題(不要看答案),考查自己的編程質(zhì)量究竟如何。然后參照答案嚴(yán)格打分。

(1)如果你只得了幾十分,請(qǐng)不要聲張,也不要太難過。編程質(zhì)量差往往是由于不良習(xí)慣造成的,與人的智力、能力沒有多大關(guān)系,還是有藥可救的。成績(jī)?cè)讲?#xff0c;可以進(jìn)步的空間就越大,中國不就是在落后中趕超發(fā)達(dá)資本主義國家嗎?只要你能下決心改掉不良的編程習(xí)慣,第二次考試就能及格了。

(2)如果你考及格了,表明你的技術(shù)基礎(chǔ)不錯(cuò),希望你能虛心學(xué)習(xí)、不斷進(jìn)步。如果你還沒有找到合適的工作單位,不妨到上海貝爾試一試。

(3)如果你考出85分以上的好成績(jī),你有義務(wù)和資格為你所在的團(tuán)隊(duì)作“C++/C編程”培訓(xùn)。希望你能和我們多多交流、相互促進(jìn)。半年前我曾經(jīng)發(fā)現(xiàn)一顆好苗子,就把他挖到我們小組來。

(4)如果你在沒有任何提示的情況下考了滿分,希望你能收我做你的徒弟。

????編程考試結(jié)束后,請(qǐng)閱讀本書的正文。

????本書第一章至第六章主要論述C++/C編程風(fēng)格。難度不高,但是細(xì)節(jié)比較多。別小看了,提高質(zhì)量就是要從這些點(diǎn)點(diǎn)滴滴做起。世上不存在最好的編程風(fēng)格,一切因需求而定。團(tuán)隊(duì)開發(fā)講究風(fēng)格一致,如果制定了大家認(rèn)可的編程風(fēng)格,那么所有組員都要遵守。如果讀者覺得本書的編程風(fēng)格比較合你的工作,那么就采用它,不要只看不做。人在小時(shí)候說話發(fā)音不準(zhǔn),寫字潦草,如果不改正,總有后悔的時(shí)候。編程也是同樣道理。

????第七章至第十一章是專題論述,技術(shù)難度比較高,看書時(shí)要積極思考。特別是第七章“內(nèi)存管理”,讀了并不表示懂了,懂了并不表示就能正確使用。有一位同事看了第七章后覺得“野指針”寫得不錯(cuò),與我切磋了一把。可是過了兩周,他告訴我,他忙了兩天追查出一個(gè)Bug,想不到又是“野指針”出問題,只好重讀第七章。

光看本書對(duì)提高編程質(zhì)量是有限的,建議大家閱讀本書的參考文獻(xiàn),那些都是經(jīng)典名著。

???????如果你的編程質(zhì)量已經(jīng)過關(guān)了,不要就此滿足。如果你想成為優(yōu)秀的軟件開發(fā)人員,建議你閱讀并按照CMMI規(guī)范做事,讓自己的綜合水平上升一個(gè)臺(tái)階。上海貝爾的員工可以向網(wǎng)絡(luò)應(yīng)用事業(yè)部軟件工程研究小組索取CMMI有關(guān)資料,最好能參加培訓(xùn)。

三、版權(quán)聲明

???????本書的大部分內(nèi)容取材于作者一年前的書籍手稿(尚未出版),現(xiàn)整理匯編成為上海貝爾網(wǎng)絡(luò)應(yīng)用事業(yè)部的一個(gè)規(guī)范化文件,同時(shí)作為培訓(xùn)教材。

???????由于C++/C編程是眾所周知的技術(shù),沒有秘密可言。編程的好經(jīng)驗(yàn)應(yīng)該大家共享,我們自己也是這么學(xué)來的。作者愿意公開本書的電子文檔。

???????版權(quán)聲明如下:

(1)讀者可以任意拷貝、修改本書的內(nèi)容,但不可以篡改作者及所屬單位。

(2)未經(jīng)作者許可,不得出版或大量印發(fā)本書。

(3)如果競(jìng)爭(zhēng)對(duì)手公司的員工得到本書,請(qǐng)勿公開使用,以免發(fā)生糾紛。

???????預(yù)計(jì)到2002年7月,我們將建立切合中國國情的CMMI 3級(jí)解決方案。屆時(shí),包括本書在內(nèi)的約1000頁規(guī)范將嚴(yán)格受控。

???????歡迎讀者對(duì)本書提出批評(píng)建議。

林銳,2001年7月

?

第1章?文件結(jié)構(gòu)

每個(gè)C++/C程序通常分為兩個(gè)文件。一個(gè)文件用于保存程序的聲明(declaration),稱為頭文件。另一個(gè)文件用于保存程序的實(shí)現(xiàn)(implementation),稱為定義(definition)文件。

C++/C程序的頭文件以“.h”為后綴,C程序的定義文件以“.c”為后綴,C++程序的定義文件通常以“.cpp”為后綴(也有一些系統(tǒng)以“.cc”或“.cxx”為后綴)。

1.1?版權(quán)和版本的聲明

版權(quán)和版本的聲明位于頭文件和定義文件的開頭(參見示例1-1),主要內(nèi)容有:

(1)版權(quán)信息。

(2)文件名稱,標(biāo)識(shí)符,摘要。

(3)當(dāng)前版本號(hào),作者/修改者,完成日期。

(4)版本歷史信息。

/*

* Copyright (c) 2001,上海貝爾有限公司網(wǎng)絡(luò)應(yīng)用事業(yè)部

* All rights reserved.

*

*?文件名稱:filename.h

*?文件標(biāo)識(shí):見配置管理計(jì)劃書

*?摘????要:簡(jiǎn)要描述本文件的內(nèi)容

*

*?當(dāng)前版本:1.1

*?作????者:輸入作者(或修改者)名字

*?完成日期:2001年7月20日

*

*?取代版本:1.0

*?原作者?:輸入原作者(或修改者)名字

*?完成日期:2001年5月10日

*/

?

示例1-1?版權(quán)和版本的聲明

1.2?頭文件的結(jié)構(gòu)

頭文件由三部分內(nèi)容組成:

(1)頭文件開頭處的版權(quán)和版本聲明(參見示例1-1)。

(2)預(yù)處理塊。

(3)函數(shù)和類結(jié)構(gòu)聲明等。

假設(shè)頭文件名稱為?graphics.h,頭文件的結(jié)構(gòu)參見示例1-2。

l?????????【規(guī)則1-2-1】為了防止頭文件被重復(fù)引用,應(yīng)當(dāng)用ifndef/define/endif結(jié)構(gòu)產(chǎn)生預(yù)處理塊。

l?????????【規(guī)則1-2-2】用?#include <filename.h>?格式來引用標(biāo)準(zhǔn)庫的頭文件(編譯器將從標(biāo)準(zhǔn)庫目錄開始搜索)。

l?????????【規(guī)則1-2-3】用?#include?“filename.h”?格式來引用非標(biāo)準(zhǔn)庫的頭文件(編譯器將從用戶的工作目錄開始搜索)。

2????????【建議1-2-1】頭文件中只存放“聲明”而不存放“定義”

在C++?語法中,類的成員函數(shù)可以在聲明的同時(shí)被定義,并且自動(dòng)成為內(nèi)聯(lián)函數(shù)。這雖然會(huì)帶來書寫上的方便,但卻造成了風(fēng)格不一致,弊大于利。建議將成員函數(shù)的定義與聲明分開,不論該函數(shù)體有多么小。

2????????【建議1-2-2】不提倡使用全局變量,盡量不要在頭文件中出現(xiàn)象extern int value?這類聲明。

//?版權(quán)和版本聲明見示例1-1,此處省略。

?

#ifndef???GRAPHICS_H?//?防止graphics.h被重復(fù)引用

#define???GRAPHICS_H

?

#include <math.h>?????//?引用標(biāo)準(zhǔn)庫的頭文件

#include?“myheader.h” ?//?引用非標(biāo)準(zhǔn)庫的頭文件

void Function1(…);???//?全局函數(shù)聲明

class Box?????????????//?類結(jié)構(gòu)聲明

{

};

#endif

示例1-2 C++/C頭文件的結(jié)構(gòu)

?

1.3?定義文件的結(jié)構(gòu)

定義文件有三部分內(nèi)容:

(1)???????定義文件開頭處的版權(quán)和版本聲明(參見示例1-1)。

(2)???????對(duì)一些頭文件的引用。

(3)???????程序的實(shí)現(xiàn)體(包括數(shù)據(jù)和代碼)。

假設(shè)定義文件的名稱為?graphics.cpp,定義文件的結(jié)構(gòu)參見示例1-3。

//?版權(quán)和版本聲明見示例1-1,此處省略。

?

#include?“graphics.h”?????//?引用頭文件

?

//?全局函數(shù)的實(shí)現(xiàn)體

void Function1(…)

{

}

?

//?類成員函數(shù)的實(shí)現(xiàn)體

void Box::Draw(…)

{

}

示例1-3 C++/C定義文件的結(jié)構(gòu)

1.4?頭文件的作用

早期的編程語言如Basic、Fortran沒有頭文件的概念,C++/C語言的初學(xué)者雖然會(huì)用使用頭文件,但常常不明其理。這里對(duì)頭文件的作用略作解釋:

(1)通過頭文件來調(diào)用庫功能。在很多場(chǎng)合,源代碼不便(或不準(zhǔn))向用戶公布,只要向用戶提供頭文件和二進(jìn)制的庫即可。用戶只需要按照頭文件中的接口聲明來調(diào)用庫功能,而不必關(guān)心接口怎么實(shí)現(xiàn)的。編譯器會(huì)從庫中提取相應(yīng)的代碼。

(2)頭文件能加強(qiáng)類型安全檢查。如果某個(gè)接口被實(shí)現(xiàn)或被使用時(shí),其方式與頭文件中的聲明不一致,編譯器就會(huì)指出錯(cuò)誤,這一簡(jiǎn)單的規(guī)則能大大減輕程序員調(diào)試、改錯(cuò)的負(fù)擔(dān)。

1.5?目錄結(jié)構(gòu)

如果一個(gè)軟件的頭文件數(shù)目比較多(如超過十個(gè)),通常應(yīng)將頭文件和定義文件分別保存于不同的目錄,以便于維護(hù)。

例如可將頭文件保存于include目錄,將定義文件保存于source目錄(可以是多級(jí)目錄)。

如果某些頭文件是私有的,它不會(huì)被用戶的程序直接引用,則沒有必要公開其“聲明”。為了加強(qiáng)信息隱藏,這些私有的頭文件可以和定義文件存放于同一個(gè)目錄。

?

第2章?程序的版式

???????版式雖然不會(huì)影響程序的功能,但會(huì)影響可讀性。程序的版式追求清晰、美觀,是程序風(fēng)格的重要構(gòu)成因素。

可以把程序的版式比喻為“書法”。好的“書法”可讓人對(duì)程序一目了然,看得興致勃勃。差的程序“書法”如螃蟹爬行,讓人看得索然無味,更令維護(hù)者煩惱有加。請(qǐng)程序員們學(xué)習(xí)程序的“書法”,彌補(bǔ)大學(xué)計(jì)算機(jī)教育的漏洞,實(shí)在很有必要。

2.1?空行

空行起著分隔程序段落的作用。空行得體(不過多也不過少)將使程序的布局更加清晰。空行不會(huì)浪費(fèi)內(nèi)存,雖然打印含有空行的程序是會(huì)多消耗一些紙張,但是值得。所以不要舍不得用空行。

l?????????【規(guī)則2-1-1】在每個(gè)類聲明之后、每個(gè)函數(shù)定義結(jié)束之后都要加空行。參見示例2-1(a)

l?????????【規(guī)則2-1-2】在一個(gè)函數(shù)體內(nèi),邏揖上密切相關(guān)的語句之間不加空行,其它地方應(yīng)加空行分隔。參見示例2-1(b?)

?

//?空行

void Function1(…)

{

?…

}

//?空行

void Function2(…)

{

?…

}

//?空行

void Function3(…)

{

?…

}

?

//?空行

while (condition)

{

?statement1;

?//?空行

?if (condition)

?{

?????statement2;

?}

?else

?{

?????statement3;

?}

//?空行

?statement4;

}?

示例2-1(a)?函數(shù)之間的空行??????????????????示例2-1(b)?函數(shù)內(nèi)部的空行

?

2.2?代碼行

l?????????【規(guī)則2-2-1】一行代碼只做一件事情,如只定義一個(gè)變量,或只寫一條語句。這樣的代碼容易閱讀,并且方便于寫注釋。

l?????????【規(guī)則2-2-2】if、for、while、do等語句自占一行,執(zhí)行語句不得緊跟其后。不論執(zhí)行語句有多少都要加{}。這樣可以防止書寫失誤。

?

示例2-2(a)為風(fēng)格良好的代碼行,示例2-2(b)為風(fēng)格不良的代碼行。

?

int width;????//?寬度

int height;???//?高度

int depth;????//?深度

?

int width, height, depth; //?寬度高度深度

?

x = a + b;

y = c + d;

z = e + f;

X?=?a + b;???y = c + d;?z = e + f;

?

if (width < height)

{

dosomething();

}

if (width < height) dosomething();

for (initialization; condition; update)

{

dosomething();

}

//?空行

other();

?

for (initialization; condition; update)

?????dosomething();

other();

?

?

示例2-2(a)?風(fēng)格良好的代碼行????????????????示例2-2(b)?風(fēng)格不良的代碼行

?

2????????【建議2-2-1】盡可能在定義變量的同時(shí)初始化該變量(就近原則)

如果變量的引用處和其定義處相隔比較遠(yuǎn),變量的初始化很容易被忘記。如果引用了未被初始化的變量,可能會(huì)導(dǎo)致程序錯(cuò)誤。本建議可以減少隱患。例如

int width = 10;?????//?定義并初紿化width

int height = 10;????//?定義并初紿化height

int depth = 10;?????//?定義并初紿化depth

?

2.3?代碼行內(nèi)的空格

l?????????【規(guī)則2-3-1】關(guān)鍵字之后要留空格。象const、virtual、inline、case?等關(guān)鍵字之后至少要留一個(gè)空格,否則無法辨析關(guān)鍵字。象if、for、while等關(guān)鍵字之后應(yīng)留一個(gè)空格再跟左括號(hào)‘(’,以突出關(guān)鍵字。

l?????????【規(guī)則2-3-2】函數(shù)名之后不要留空格,緊跟左括號(hào)‘(’,以與關(guān)鍵字區(qū)別。

l?????????【規(guī)則2-3-3】‘(’向后緊跟,‘)’、‘,’、‘;’向前緊跟,緊跟處不留空格。

l?????????【規(guī)則2-3-4】‘,’之后要留空格,如Function(x, y, z)。如果‘;’不是一行的結(jié)束符號(hào),其后要留空格,如for (initialization; condition; update)。

l?????????【規(guī)則2-3-5】賦值操作符、比較操作符、算術(shù)操作符、邏輯操作符、位域操作符,如“=”、“+=”?“>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后應(yīng)當(dāng)加空格。

l?????????【規(guī)則2-3-6】一元操作符如“!”、“~”、“++”、“--”、“&”(地址運(yùn)算符)等前后不加空格。

l?????????【規(guī)則2-3-7】象“[]”、“.”、“->”這類操作符前后不加空格。

2????????【建議2-3-1】對(duì)于表達(dá)式比較長(zhǎng)的for語句和if語句,為了緊湊起見可以適當(dāng)?shù)厝サ粢恍┛崭?#xff0c;如for (i=0; i<10; i++)和if ((a<=b) && (c<=d))

?

void Func1(int x, int y, int z);??????????//?良好的風(fēng)格

void Func1 (int x,int y,int z);???????????//?不良的風(fēng)格

if (year >= 2000)?????????????????????????//?良好的風(fēng)格

if(year>=2000)????????????????????????????//?不良的風(fēng)格

if ((a>=b) && (c<=d))?????????????????????//?良好的風(fēng)格

if(a>=b&&c<=d)????????????????????????????//?不良的風(fēng)格

for (i=0; i<10; i++)??????????????????????//?良好的風(fēng)格

for(i=0;i<10;i++)?????????????????????????//?不良的風(fēng)格

for (i = 0; I < 10; i ++)?????????????????//?過多的空格

x = a < b ? a : b;????????????????????????//?良好的風(fēng)格

x=a<b?a:b;????????????????????????????????//?不好的風(fēng)格

int *x = &y;??????????????????????????????//?良好的風(fēng)格?

int * x = & y;????????????????????????????//?不良的風(fēng)格?

array[5] = 0;?????????????????????????????//?不要寫成?array [ 5 ] = 0;

a.Function();?????????????????????????????//?不要寫成?a . Function();

b->Function();????????????????????????????//?不要寫成?b -> Function();

?

示例2-3?代碼行內(nèi)的空格

?

2.4?對(duì)齊

l?????????【規(guī)則2-4-1】程序的分界符‘{’和‘}’應(yīng)獨(dú)占一行并且位于同一列,同時(shí)與引用它們的語句左對(duì)齊。

l?????????【規(guī)則2-4-2】{ }之內(nèi)的代碼塊在‘{’右邊數(shù)格處左對(duì)齊。

?

示例2-4(a)為風(fēng)格良好的對(duì)齊,示例2-4(b)為風(fēng)格不良的對(duì)齊。

?

void Function(int x)

{

… //?program?code

}

?

void Function(int x){

… //?program?code

}

?

if (condition)

{

… //?program?code

}

else

{

… //?program?code

}

if (condition){

… //?program?code

}

else {

… //?program?code

}

for (initialization; condition; update)

{

… //?program?code

}

for (initialization; condition; update){

… //?program?code

}

While (condition)

{

… //?program?code

}

while (condition){

… //?program?code

}

如果出現(xiàn)嵌套的{},則使用縮進(jìn)對(duì)齊,如:

?????{

???????…

??????????{

???????????…

??????????}

???????…

}

?

示例2-4(a)?風(fēng)格良好的對(duì)齊? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 示例2-4(b)?風(fēng)格不良的對(duì)齊

?

2.5?長(zhǎng)行拆分

l?????????【規(guī)則2-5-1】代碼行最大長(zhǎng)度宜控制在70至80個(gè)字符以內(nèi)。代碼行不要過長(zhǎng),否則眼睛看不過來,也不便于打印。

l?????????【規(guī)則2-5-2】長(zhǎng)表達(dá)式要在低優(yōu)先級(jí)操作符處拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要進(jìn)行適當(dāng)?shù)目s進(jìn),使排版整齊,語句可讀。

?

if ((very_longer_variable1 >= very_longer_variable12)

&& (very_longer_variable3 <= very_longer_variable14)

&& (very_longer_variable5 <= very_longer_variable16))

{

????dosomething();

}

virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix,

?????????????????????????????????CMatrix rightMatrix);

?

for (very_longer_initialization;

?????very_longer_condition;

?????very_longer_update)

{

????dosomething();

}

示例2-5?長(zhǎng)行的拆分

2.6?修飾符的位置

修飾符?*?和?&?應(yīng)該靠近數(shù)據(jù)類型還是該靠近變量名,是個(gè)有爭(zhēng)議的活題。

若將修飾符?*?靠近數(shù)據(jù)類型,例如:int*?x;?從語義上講此寫法比較直觀,即x是int?類型的指針。

上述寫法的弊端是容易引起誤解,例如:int*?x, y;?此處y容易被誤解為指針變量。雖然將x和y分行定義可以避免誤解,但并不是人人都愿意這樣做。

?

l?????????【規(guī)則2-6-1】應(yīng)當(dāng)將修飾符?*?和?&?緊靠變量名

例如:

char?*name;

int???*x, y;????//?此處y不會(huì)被誤解為指針

2.7?注釋

C語言的注釋符為“/*…*/”。C++語言中,程序塊的注釋常采用“/*…*/”,行注釋一般采用“//…”。注釋通常用于:

(1)版本、版權(quán)聲明;

(2)函數(shù)接口說明;

(3)重要的代碼行或段落提示。

雖然注釋有助于理解代碼,但注意不可過多地使用注釋。參見示例2-6。

?

l?????????【規(guī)則2-7-1】注釋是對(duì)代碼的“提示”,而不是文檔。程序中的注釋不可喧賓奪主,注釋太多了會(huì)讓人眼花繚亂。注釋的花樣要少。

l?????????【規(guī)則2-7-2】如果代碼本來就是清楚的,則不必加注釋。否則多此一舉,令人厭煩。例如

i++;????// i?加?1,多余的注釋

l?????????【規(guī)則2-7-3】邊寫代碼邊注釋,修改代碼同時(shí)修改相應(yīng)的注釋,以保證注釋與代碼的一致性。不再有用的注釋要?jiǎng)h除。

l?????????【規(guī)則2-7-4】注釋應(yīng)當(dāng)準(zhǔn)確、易懂,防止注釋有二義性。錯(cuò)誤的注釋不但無益反而有害。

l?????????【規(guī)則2-7-5】盡量避免在注釋中使用縮寫,特別是不常用縮寫。

l?????????【規(guī)則2-7-6】注釋的位置應(yīng)與被描述的代碼相鄰,可以放在代碼的上方或右方,不可放在下方。

l?????????【規(guī)則2-7-8】當(dāng)代碼比較長(zhǎng),特別是有多重嵌套時(shí),應(yīng)當(dāng)在一些段落的結(jié)束處加注釋,便于閱讀。

?

?

/*

*?函數(shù)介紹:

*?輸入?yún)?shù):

*?輸出參數(shù):

*?返回值?:

*/

void Function(float x, float y, float z)

{

?…

}

?

if (…)

{

?…

?while (…)

?{

} // end of while

} // end of if

示例2-6?程序的注釋

?

2.8?類的版式

類可以將數(shù)據(jù)和函數(shù)封裝在一起,其中函數(shù)表示了類的行為(或稱服務(wù))。類提供關(guān)鍵字public、protected和private,分別用于聲明哪些數(shù)據(jù)和函數(shù)是公有的、受保護(hù)的或者是私有的。這樣可以達(dá)到信息隱藏的目的,即讓類僅僅公開必須要讓外界知道的內(nèi)容,而隱藏其它一切內(nèi)容。我們不可以濫用類的封裝功能,不要把它當(dāng)成火鍋,什么東西都往里扔。

類的版式主要有兩種方式:

(1)將private類型的數(shù)據(jù)寫在前面,而將public類型的函數(shù)寫在后面,如示例8-3(a)。采用這種版式的程序員主張類的設(shè)計(jì)“以數(shù)據(jù)為中心”,重點(diǎn)關(guān)注類的內(nèi)部結(jié)構(gòu)。

(2)將public類型的函數(shù)寫在前面,而將private類型的數(shù)據(jù)寫在后面,如示例8.3(b)采用這種版式的程序員主張類的設(shè)計(jì)“以行為為中心”,重點(diǎn)關(guān)注的是類應(yīng)該提供什么樣的接口(或服務(wù))。

很多C++教課書受到Biarne Stroustrup第一本著作的影響,不知不覺地采用了“以數(shù)據(jù)為中心”的書寫方式,并不見得有多少道理。

我建議讀者采用“以行為為中心”的書寫方式,即首先考慮類應(yīng)該提供什么樣的函數(shù)。這是很多人的經(jīng)驗(yàn)——“這樣做不僅讓自己在設(shè)計(jì)類時(shí)思路清晰,而且方便別人閱讀。因?yàn)橛脩糇铌P(guān)心的是接口,誰愿意先看到一堆私有數(shù)據(jù)成員!”

?

class A

{

?private:

int????i, j;

float?x, y;

????…

?public:

void Func1(void);

void Func2(void);

}

class A

{

?public:

void Func1(void);

void Func2(void);

?private:

int????i, j;

float?x, y;

????…

}

示例8.3(a)?以數(shù)據(jù)為中心版式? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?示例8.3(b)?以行為為中心的版式

?

第3章?命名規(guī)則

比較著名的命名規(guī)則當(dāng)推Microsoft公司的“匈牙利”法,該命名規(guī)則的主要思想是“在變量和函數(shù)名中加入前綴以增進(jìn)人們對(duì)程序的理解”。例如所有的字符變量均以ch為前綴,若是指針變量則追加前綴p。如果一個(gè)變量由ppch開頭,則表明它是指向字符指針的指針。

“匈牙利”法最大的缺點(diǎn)是煩瑣,例如

int????i,?j,?k;?

float?x,?y,?z;

倘若采用“匈牙利”命名規(guī)則,則應(yīng)當(dāng)寫成

int????iI,?iJ,?ik;?//?前綴?i表示int類型

float?fX,?fY,?fZ;?//?前綴?f表示float類型

如此煩瑣的程序會(huì)讓絕大多數(shù)程序員無法忍受。

據(jù)考察,沒有一種命名規(guī)則可以讓所有的程序員贊同,程序設(shè)計(jì)教科書一般都不指定命名規(guī)則。命名規(guī)則對(duì)軟件產(chǎn)品而言并不是“成敗悠關(guān)”的事,我們不要化太多精力試圖發(fā)明世界上最好的命名規(guī)則,而應(yīng)當(dāng)制定一種令大多數(shù)項(xiàng)目成員滿意的命名規(guī)則,并在項(xiàng)目中貫徹實(shí)施。

3.1?共性規(guī)則

???????本節(jié)論述的共性規(guī)則是被大多數(shù)程序員采納的,我們應(yīng)當(dāng)在遵循這些共性規(guī)則的前提下,再擴(kuò)充特定的規(guī)則,如3.2節(jié)。

l?????????【規(guī)則3-1-1】標(biāo)識(shí)符應(yīng)當(dāng)直觀且可以拼讀,可望文知意,不必進(jìn)行“解碼”。

標(biāo)識(shí)符最好采用英文單詞或其組合,便于記憶和閱讀。切忌使用漢語拼音來命名。程序中的英文單詞一般不會(huì)太復(fù)雜,用詞應(yīng)當(dāng)準(zhǔn)確。例如不要把CurrentValue寫成NowValue。

l?????????【規(guī)則3-1-2】標(biāo)識(shí)符的長(zhǎng)度應(yīng)當(dāng)符合“min-length && max-information”原則。

幾十年前老ANSI C規(guī)定名字不準(zhǔn)超過6個(gè)字符,現(xiàn)今的C++/C不再有此限制。一般來說,長(zhǎng)名字能更好地表達(dá)含義,所以函數(shù)名、變量名、類名長(zhǎng)達(dá)十幾個(gè)字符不足為怪。那么名字是否越長(zhǎng)約好?不見得!?例如變量名maxval就比maxValueUntilOverflow好用。單字符的名字也是有用的,常見的如i,j,k,m,n,x,y,z等,它們通常可用作函數(shù)內(nèi)的局部變量。

l?????????【規(guī)則3-1-3】命名規(guī)則盡量與所采用的操作系統(tǒng)或開發(fā)工具的風(fēng)格保持一致。

例如Windows應(yīng)用程序的標(biāo)識(shí)符通常采用“大小寫”混排的方式,如AddChild。而Unix應(yīng)用程序的標(biāo)識(shí)符通常采用“小寫加下劃線”的方式,如add_child。別把這兩類風(fēng)格混在一起用。

l?????????【規(guī)則3-1-4】程序中不要出現(xiàn)僅靠大小寫區(qū)分的相似的標(biāo)識(shí)符。

例如:

int?x,?X;?????//?變量x?與?X?容易混淆

void foo(int x);????//?函數(shù)foo?與FOO容易混淆

void FOO(float x);

l?????????【規(guī)則3-1-5】程序中不要出現(xiàn)標(biāo)識(shí)符完全相同的局部變量和全局變量,盡管兩者的作用域不同而不會(huì)發(fā)生語法錯(cuò)誤,但會(huì)使人誤解。

l?????????【規(guī)則3-1-6】變量的名字應(yīng)當(dāng)使用“名詞”或者“形容詞+名詞”。

例如:

float?value;

float?oldValue;

float?newValue;

l?????????【規(guī)則3-1-7】全局函數(shù)的名字應(yīng)當(dāng)使用“動(dòng)詞”或者“動(dòng)詞+名詞”(動(dòng)賓詞組)。類的成員函數(shù)應(yīng)當(dāng)只使用“動(dòng)詞”,被省略掉的名詞就是對(duì)象本身。

例如:

DrawBox();???????????//?全局函數(shù)

box->Draw();????????//?類的成員函數(shù)

l?????????【規(guī)則3-1-8】用正確的反義詞組命名具有互斥意義的變量或相反動(dòng)作的函數(shù)等。

例如:

int???minValue;

int???maxValue;

int???SetValue(…);

int???GetValue(…);

2????????【建議3-1-1】盡量避免名字中出現(xiàn)數(shù)字編號(hào),如Value1,Value2等,除非邏輯上的確需要編號(hào)。這是為了防止程序員偷懶,不肯為命名動(dòng)腦筋而導(dǎo)致產(chǎn)生無意義的名字(因?yàn)橛脭?shù)字編號(hào)最省事)。

3.2?簡(jiǎn)單的Windows應(yīng)用程序命名規(guī)則

???????作者對(duì)“匈牙利”命名規(guī)則做了合理的簡(jiǎn)化,下述的命名規(guī)則簡(jiǎn)單易用,比較適合于Windows應(yīng)用軟件的開發(fā)。

l?????????【規(guī)則3-2-1】類名和函數(shù)名用大寫字母開頭的單詞組合而成。

例如:

?class Node;?????????????????//?類名

?class LeafNode;?????????????//?類名

?void?Draw(void);???????????//?函數(shù)名

?void?SetValue(int value);?//?函數(shù)名

l?????????【規(guī)則3-2-2】變量和參數(shù)用小寫字母開頭的單詞組合而成。

例如:

????BOOL flag;

????int?drawMode;

l?????????【規(guī)則3-2-3】常量全用大寫的字母,用下劃線分割單詞。

例如:

????const int MAX = 100;

????const int MAX_LENGTH = 100;

l?????????【規(guī)則3-2-4】靜態(tài)變量加前綴s_(表示static)。

例如:

void Init(…)

{

???????static int s_initValue;??????//?靜態(tài)變量

???????…

}

l?????????【規(guī)則3-2-5】如果不得已需要全局變量,則使全局變量加前綴g_(表示global)。

例如:

int g_howManyPeople;???//?全局變量

int g_howMuchMoney;?//?全局變量

l?????????【規(guī)則3-2-6】類的數(shù)據(jù)成員加前綴m_(表示member),這樣可以避免數(shù)據(jù)成員與成員函數(shù)的參數(shù)同名。

例如:

????void Object::SetValue(int width, int height)

????{

????????m_width = width;

? ? ? ?m_height = height;

? ?}

l?????????【規(guī)則3-2-7】為了防止某一軟件庫中的一些標(biāo)識(shí)符和其它軟件庫中的沖突,可以為各種標(biāo)識(shí)符加上能反映軟件性質(zhì)的前綴。例如三維圖形標(biāo)準(zhǔn)OpenGL的所有庫函數(shù)均以gl開頭,所有常量(或宏定義)均以GL開頭。

3.3?簡(jiǎn)單的Unix應(yīng)用程序命名規(guī)則

?

第4章?表達(dá)式和基本語句

讀者可能懷疑:連if、for、while、goto、switch這樣簡(jiǎn)單的東西也要探討編程風(fēng)格,是不是小題大做?

我真的發(fā)覺很多程序員用隱含錯(cuò)誤的方式寫表達(dá)式和基本語句,我自己也犯過類似的錯(cuò)誤。

表達(dá)式和語句都屬于C++/C的短語結(jié)構(gòu)語法。它們看似簡(jiǎn)單,但使用時(shí)隱患比較多。本章歸納了正確使用表達(dá)式和語句的一些規(guī)則與建議。

4.1?運(yùn)算符的優(yōu)先級(jí)

???????C++/C語言的運(yùn)算符有數(shù)十個(gè),運(yùn)算符的優(yōu)先級(jí)與結(jié)合律如表4-1所示。注意一元運(yùn)算符?+?-?*?的優(yōu)先級(jí)高于對(duì)應(yīng)的二元運(yùn)算符。

優(yōu)先級(jí)

運(yùn)算符

結(jié)合律

?

?

?

?

?

?

?

( )?[ ]?->?.

從左至右

!?~?++?--?(類型)?sizeof

+?-?*?&

從右至左

?

*?/?%

從左至右

+?-

從左至右

<<?>>

從左至右

<???<=???>?>=

從左至右

==?!=

從左至右

&

從左至右

^

從左至右

|

從左至右

&&

從左至右

||

從右至左

?:

從右至左

=?+=?-=?*=?/=?%=?&=?^=

|=?<<=?>>=

從左至右

表4-1?運(yùn)算符的優(yōu)先級(jí)與結(jié)合律

l?????????【規(guī)則4-1-1】如果代碼行中的運(yùn)算符比較多,用括號(hào)確定表達(dá)式的操作順序,避免使用默認(rèn)的優(yōu)先級(jí)。

由于將表4-1熟記是比較困難的,為了防止產(chǎn)生歧義并提高可讀性,應(yīng)當(dāng)用括號(hào)確定表達(dá)式的操作順序。例如:

word = (high << 8) | low

if ((a | b) && (a & c))??

4.2?復(fù)合表達(dá)式

如?a = b = c = 0這樣的表達(dá)式稱為復(fù)合表達(dá)式。允許復(fù)合表達(dá)式存在的理由是:(1)書寫簡(jiǎn)潔;(2)可以提高編譯效率。但要防止濫用復(fù)合表達(dá)式。

l?????????【規(guī)則4-2-1】不要編寫太復(fù)雜的復(fù)合表達(dá)式。

例如:

??????i = a >= b && c < d && c + f <= g + h ;?//?復(fù)合表達(dá)式過于復(fù)雜

l?????????【規(guī)則4-2-2】不要有多用途的復(fù)合表達(dá)式。

例如:

d = (a = b + c) + r ;

該表達(dá)式既求a值又求d值。應(yīng)該拆分為兩個(gè)獨(dú)立的語句:

a = b + c;

d = a + r;

l?????????【規(guī)則4-2-3】不要把程序中的復(fù)合表達(dá)式與“真正的數(shù)學(xué)表達(dá)式”混淆。

例如:?

if (a < b < c)??????????// a < b < c是數(shù)學(xué)表達(dá)式而不是程序表達(dá)式

并不表示??????

if ((a<b) && (b<c))

而是成了令人費(fèi)解的

if ( (a<b)<c )

4.3?if?語句

????if語句是C++/C語言中最簡(jiǎn)單、最常用的語句,然而很多程序員用隱含錯(cuò)誤的方式寫if語句。本節(jié)以“與零值比較”為例,展開討論。

4.3.1?布爾變量與零值比較

l?????????【規(guī)則4-3-1】不可將布爾變量直接與TRUE、FALSE或者1、0進(jìn)行比較。

根據(jù)布爾類型的語義,零值為“假”(記為FALSE),任何非零值都是“真”(記為TRUE)。TRUE的值究竟是什么并沒有統(tǒng)一的標(biāo)準(zhǔn)。例如Visual C++?將TRUE定義為1,而Visual Basic則將TRUE定義為-1。

假設(shè)布爾變量名字為flag,它與零值比較的標(biāo)準(zhǔn)if語句如下:

if (flag)???//?表示flag為真

if (!flag)?//?表示flag為假

其它的用法都屬于不良風(fēng)格,例如:

????if (flag == TRUE)??

????if (flag == 1 )????

????if (flag == FALSE) ?

????if (flag == 0)????

?

4.3.2?整型變量與零值比較

l?????????【規(guī)則4-3-2】應(yīng)當(dāng)將整型變量用“==”或“!=”直接與0比較。

????假設(shè)整型變量的名字為value,它與零值比較的標(biāo)準(zhǔn)if語句如下:

if (value == 0) ?

if (value != 0)

不可模仿布爾變量的風(fēng)格而寫成

if (value)?????//?會(huì)讓人誤解?value是布爾變量

if (!value)

?

4.3.3?浮點(diǎn)變量與零值比較

l?????????【規(guī)則4-3-3】不可將浮點(diǎn)變量用“==”或“!=”與任何數(shù)字比較。

????千萬要留意,無論是float還是double類型的變量,都有精度限制。所以一定要避免將浮點(diǎn)變量用“==”或“!=”與數(shù)字比較,應(yīng)該設(shè)法轉(zhuǎn)化成“>=”或“<=”形式。

????假設(shè)浮點(diǎn)變量的名字為x,應(yīng)當(dāng)將??

if (x == 0.0) ?//?隱含錯(cuò)誤的比較

轉(zhuǎn)化為

if ((x>=-EPSINON) && (x<=EPSINON))

其中EPSINON是允許的誤差(即精度)。

?

4.3.4?指針變量與零值比較

l?????????【規(guī)則4-3-4】應(yīng)當(dāng)將指針變量用“==”或“!=”與NULL比較。

????指針變量的零值是“空”(記為NULL)。盡管NULL的值與0相同,但是兩者意義不同。假設(shè)指針變量的名字為p,它與零值比較的標(biāo)準(zhǔn)if語句如下:

????????if (p == NULL)?// p與NULL顯式比較,強(qiáng)調(diào)p是指針變量

????????if (p != NULL)?

不要寫成

????????if (p == 0)?????//?容易讓人誤解p是整型變量

????????if (p != 0)????

????或者

? ? if (p)??????????//?容易讓人誤解p是布爾變量

????if (!p)????????

?

4.3.5?對(duì)if語句的補(bǔ)充說明

有時(shí)候我們可能會(huì)看到?if (NULL == p)?這樣古怪的格式。不是程序?qū)戝e(cuò)了,是程序員為了防止將?if (p == NULL)?誤寫成?if (p = NULL),而有意把p和NULL顛倒。編譯器認(rèn)為?if (p = NULL)?是合法的,但是會(huì)指出?if (NULL = p)是錯(cuò)誤的,因?yàn)镹ULL不能被賦值。

程序中有時(shí)會(huì)遇到if/else/return的組合,應(yīng)該將如下不良風(fēng)格的程序

????if (condition)?

????????return x;

????return y;

改寫為

????if (condition)

????{

????????return x;

????}

????else

????{

return y;

}

或者改寫成更加簡(jiǎn)練的

return (condition ? x : y);

?

4.4?循環(huán)語句的效率

????C++/C循環(huán)語句中,for語句使用頻率最高,while語句其次,do語句很少用。本節(jié)重點(diǎn)論述循環(huán)體的效率。提高循環(huán)體效率的基本辦法是降低循環(huán)體的復(fù)雜性。

l?????????【建議4-4-1】在多重循環(huán)中,如果有可能,應(yīng)當(dāng)將最長(zhǎng)的循環(huán)放在最內(nèi)層,最短的循環(huán)放在最外層,以減少CPU跨切循環(huán)層的次數(shù)。例如示例4-4(b)的效率比示例4-4(a)的高。

?

for (row=0; row<100; row++)

{

for ( col=0; col<5; col++ )

{

sum = sum + a[row][col];

}

}

for (col=0; col<5; col++ )

{

for (row=0; row<100; row++)

{

????sum = sum + a[row][col];

}

}

示例4-4(a)?低效率:長(zhǎng)循環(huán)在最外層???????????示例4-4(b)?高效率:長(zhǎng)循環(huán)在最內(nèi)層

l?????????【建議4-4-2】如果循環(huán)體內(nèi)存在邏輯判斷,并且循環(huán)次數(shù)很大,宜將邏輯判斷移到循環(huán)體的外面。示例4-4(c)的程序比示例4-4(d)多執(zhí)行了N-1次邏輯判斷。并且由于前者老要進(jìn)行邏輯判斷,打斷了循環(huán)“流水線”作業(yè),使得編譯器不能對(duì)循環(huán)進(jìn)行優(yōu)化處理,降低了效率。如果N非常大,最好采用示例4-4(d)的寫法,可以提高效率。如果N非常小,兩者效率差別并不明顯,采用示例4-4(c)的寫法比較好,因?yàn)槌绦蚋雍?jiǎn)潔。

for (i=0; i<N; i++)

{

if (condition)

????DoSomething();

else

????DoOtherthing();

}

if (condition)

{

for (i=0; i<N; i++)

????DoSomething();

}

else

{

????for (i=0; i<N; i++)

????DoOtherthing();

}

表4-4(c)?效率低但程序簡(jiǎn)潔????????????????表4-4(d)?效率高但程序不簡(jiǎn)潔

4.5 for?語句的循環(huán)控制變量

l?????????【規(guī)則4-5-1】不可在for?循環(huán)體內(nèi)修改循環(huán)變量,防止for?循環(huán)失去控制。

l?????????【建議4-5-1】建議for語句的循環(huán)控制變量的取值采用“半開半閉區(qū)間”寫法。

示例4-5(a)中的x值屬于半開半閉區(qū)間“0 =< x < N”,起點(diǎn)到終點(diǎn)的間隔為N,循環(huán)次數(shù)為N。

示例4-5(b)中的x值屬于閉區(qū)間“0 =< x <= N-1”,起點(diǎn)到終點(diǎn)的間隔為N-1,循環(huán)次數(shù)為N。

相比之下,示例4-5(a)的寫法更加直觀,盡管兩者的功能是相同的。

for (int x=0; x<N; x++)

{

}

for (int x=0; x<=N-1; x++)

{

}

示例4-5(a)?循環(huán)變量屬于半開半閉區(qū)間???????????示例4-5(b)?循環(huán)變量屬于閉區(qū)間

4.6 switch語句

????有了if語句為什么還要switch語句?

switch是多分支選擇語句,而if語句只有兩個(gè)分支可供選擇。雖然可以用嵌套的if語句來實(shí)現(xiàn)多分支選擇,但那樣的程序冗長(zhǎng)難讀。這是switch語句存在的理由。

????switch語句的基本格式是:

switch (variable)

{

case value1 : ?…

break;

case value2 : ?…

break;

????…

????default :?…

break;

}

l?????????【規(guī)則4-6-1】每個(gè)case語句的結(jié)尾不要忘了加break,否則將導(dǎo)致多個(gè)分支重疊(除非有意使多個(gè)分支重疊)。

l?????????【規(guī)則4-6-2】不要忘記最后那個(gè)default分支。即使程序真的不需要default處理,也應(yīng)該保留語句???default : break;?這樣做并非多此一舉,而是為了防止別人誤以為你忘了default處理。

4.7 goto語句

????自從提倡結(jié)構(gòu)化設(shè)計(jì)以來,goto就成了有爭(zhēng)議的語句。首先,由于goto語句可以靈活跳轉(zhuǎn),如果不加限制,它的確會(huì)破壞結(jié)構(gòu)化設(shè)計(jì)風(fēng)格。其次,goto語句經(jīng)常帶來錯(cuò)誤或隱患。它可能跳過了某些對(duì)象的構(gòu)造、變量的初始化、重要的計(jì)算等語句,例如:

goto state;

String s1, s2;?//?被goto跳過

int sum = 0;????//?被goto跳過

state:

如果編譯器不能發(fā)覺此類錯(cuò)誤,每用一次goto語句都可能留下隱患。

????很多人建議廢除C++/C的goto語句,以絕后患。但實(shí)事求是地說,錯(cuò)誤是程序員自己造成的,不是goto的過錯(cuò)。goto?語句至少有一處可顯神通,它能從多重循環(huán)體中咻地一下子跳到外面,用不著寫很多次的break語句;?例如

?{ …

??????{ …

??????????{ …

??????????????goto error;

??????????}

??????}

?}

?error:

?…

就象樓房著火了,來不及從樓梯一級(jí)一級(jí)往下走,可從窗口跳出火坑。所以我們主張少用、慎用goto語句,而不是禁用。

?

第5章?常量

????常量是一種標(biāo)識(shí)符,它的值在運(yùn)行期間恒定不變。C語言用?#define來定義常量(稱為宏常量)。C++?語言除了?#define外還可以用const來定義常量(稱為const常量)。

5.1?為什么需要常量

如果不使用常量,直接在程序中填寫數(shù)字或字符串,將會(huì)有什么麻煩?

(1)???????程序的可讀性(可理解性)變差。程序員自己會(huì)忘記那些數(shù)字或字符串是什么意思,用戶則更加不知它們從何處來、表示什么。

(2)???????在程序的很多地方輸入同樣的數(shù)字或字符串,難保不發(fā)生書寫錯(cuò)誤。

(3)???????如果要修改數(shù)字或字符串,則會(huì)在很多地方改動(dòng),既麻煩又容易出錯(cuò)。

?

l?????????【規(guī)則5-1-1】?盡量使用含義直觀的常量來表示那些將在程序中多次出現(xiàn)的數(shù)字或字符串。

例如:

????#define?????????MAX???100???????/* ?C語言的宏常量 ?*/

const int??????MAX = 100;??????// ?C++?語言的const常量

const float?????PI = 3.14159;???// ?C++?語言的const常量

?

5.2 const?與?#define的比較

????C++?語言可以用const來定義常量,也可以用?#define來定義常量。但是前者比后者有更多的優(yōu)點(diǎn):

(1)???????const常量有數(shù)據(jù)類型,而宏常量沒有數(shù)據(jù)類型。編譯器可以對(duì)前者進(jìn)行類型安全檢查。而對(duì)后者只進(jìn)行字符替換,沒有類型安全檢查,并且在字符替換可能會(huì)產(chǎn)生意料不到的錯(cuò)誤(邊際效應(yīng))。

(2)???????有些集成化的調(diào)試工具可以對(duì)const常量進(jìn)行調(diào)試,但是不能對(duì)宏常量進(jìn)行調(diào)試。

l?????????【規(guī)則5-2-1】在C++?程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。

?

5.3?常量定義規(guī)則

l?????????【規(guī)則5-3-1】需要對(duì)外公開的常量放在頭文件中,不需要對(duì)外公開的常量放在定義文件的頭部。為便于管理,可以把不同模塊的常量集中存放在一個(gè)公共的頭文件中。

l?????????【規(guī)則5-3-2】如果某一常量與其它常量密切相關(guān),應(yīng)在定義中包含這種關(guān)系,而不應(yīng)給出一些孤立的值。

例如:

const?float???RADIUS = 100;

const?float???DIAMETER = RADIUS * 2;

?

5.4?類中的常量

有時(shí)我們希望某些常量只在類中有效。由于#define定義的宏常量是全局的,不能達(dá)到目的,于是想當(dāng)然地覺得應(yīng)該用const修飾數(shù)據(jù)成員來實(shí)現(xiàn)。const數(shù)據(jù)成員的確是存在的,但其含義卻不是我們所期望的。const數(shù)據(jù)成員只在某個(gè)對(duì)象生存期內(nèi)是常量,而對(duì)于整個(gè)類而言卻是可變的,因?yàn)轭惪梢詣?chuàng)建多個(gè)對(duì)象,不同的對(duì)象其const數(shù)據(jù)成員的值可以不同。

????不能在類聲明中初始化const數(shù)據(jù)成員。以下用法是錯(cuò)誤的,因?yàn)轭惖膶?duì)象未被創(chuàng)建時(shí),編譯器不知道SIZE的值是什么。

????class A

????{…

????????const int SIZE = 100; ?//?錯(cuò)誤,企圖在類聲明中初始化const數(shù)據(jù)成員

????????int array[SIZE];????????//?錯(cuò)誤,未知的SIZE

????};

?

const數(shù)據(jù)成員的初始化只能在類構(gòu)造函數(shù)的初始化表中進(jìn)行,例如

????class A

????{…

????????A(int size);????????//?構(gòu)造函數(shù)

????????const int SIZE ;???

????};

????A::A(int size) : SIZE(size)?//?構(gòu)造函數(shù)的初始化表

????{

?????…

????}

????A?a(100);?//?對(duì)象?a?的SIZE值為100

????A?b(200);?//?對(duì)象?b?的SIZE值為200

?

????怎樣才能建立在整個(gè)類中都恒定的常量呢?別指望const數(shù)據(jù)成員了,應(yīng)該用類中的枚舉常量來實(shí)現(xiàn)。例如

????class A

????{…

????????enum { SIZE1 = 100, SIZE2 = 200}; //?枚舉常量

????????int array1[SIZE1];?

????????int array2[SIZE2];

????};

????枚舉常量不會(huì)占用對(duì)象的存儲(chǔ)空間,它們?cè)诰幾g時(shí)被全部求值。枚舉常量的缺點(diǎn)是:它的隱含數(shù)據(jù)類型是整數(shù),其最大值有限,且不能表示浮點(diǎn)數(shù)(如PI=3.14159)。

?

第6章?函數(shù)設(shè)計(jì)

函數(shù)是C++/C程序的基本功能單元,其重要性不言而喻。函數(shù)設(shè)計(jì)的細(xì)微缺點(diǎn)很容易導(dǎo)致該函數(shù)被錯(cuò)用,所以光使函數(shù)的功能正確是不夠的。本章重點(diǎn)論述函數(shù)的接口設(shè)計(jì)和內(nèi)部實(shí)現(xiàn)的一些規(guī)則。

函數(shù)接口的兩個(gè)要素是參數(shù)和返回值。C語言中,函數(shù)的參數(shù)和返回值的傳遞方式有兩種:值傳遞(pass by value)和指針傳遞(pass by pointer)。C++?語言中多了引用傳遞(pass by reference)。由于引用傳遞的性質(zhì)象指針傳遞,而使用方式卻象值傳遞,初學(xué)者常常迷惑不解,容易引起混亂,請(qǐng)先閱讀6.6節(jié)“引用與指針的比較”。

6.1?參數(shù)的規(guī)則

l?????????【規(guī)則6-1-1】參數(shù)的書寫要完整,不要貪圖省事只寫參數(shù)的類型而省略參數(shù)名字。如果函數(shù)沒有參數(shù),則用void填充。

例如:

void SetValue(int width, int height);?//?良好的風(fēng)格

void SetValue(int, int);???????????????//?不良的風(fēng)格

float GetValue(void);??????//?良好的風(fēng)格

float GetValue();??????????//?不良的風(fēng)格

l?????????【規(guī)則6-1-2】參數(shù)命名要恰當(dāng),順序要合理。

例如編寫字符串拷貝函數(shù)StringCopy,它有兩個(gè)參數(shù)。如果把參數(shù)名字起為str1和str2,例如

void StringCopy(char *str1, char *str2);

那么我們很難搞清楚究竟是把str1拷貝到str2中,還是剛好倒過來。

可以把參數(shù)名字起得更有意義,如叫strSource和strDestination。這樣從名字上就可以看出應(yīng)該把strSource拷貝到strDestination。

還有一個(gè)問題,這兩個(gè)參數(shù)那一個(gè)該在前那一個(gè)該在后?參數(shù)的順序要遵循程序員的習(xí)慣。一般地,應(yīng)將目的參數(shù)放在前面,源參數(shù)放在后面。

如果將函數(shù)聲明為:

void StringCopy(char *strSource, char *strDestination);

別人在使用時(shí)可能會(huì)不假思索地寫成如下形式:

char str[20];

StringCopy(str, “Hello World”);?//?參數(shù)順序顛倒

l?????????【規(guī)則6-1-3】如果參數(shù)是指針,且僅作輸入用,則應(yīng)在類型前加const,以防止該指針在函數(shù)體內(nèi)被意外修改。

例如:

void StringCopy(char *strDestination,const char *strSource);

l?????????【規(guī)則6-1-4】如果輸入?yún)?shù)以值傳遞的方式傳遞對(duì)象,則宜改用“const &”方式來傳遞,這樣可以省去臨時(shí)對(duì)象的構(gòu)造和析構(gòu)過程,從而提高效率。

2????????【建議6-1-1】避免函數(shù)有太多的參數(shù),參數(shù)個(gè)數(shù)盡量控制在5個(gè)以內(nèi)。如果參數(shù)太多,在使用時(shí)容易將參數(shù)類型或順序搞錯(cuò)。

2????????【建議6-1-2】盡量不要使用類型和數(shù)目不確定的參數(shù)。

C標(biāo)準(zhǔn)庫函數(shù)printf是采用不確定參數(shù)的典型代表,其原型為:

int printf(const chat *format[, argument]…);

這種風(fēng)格的函數(shù)在編譯時(shí)喪失了嚴(yán)格的類型安全檢查。

6.2?返回值的規(guī)則

l?????????【規(guī)則6-2-1】不要省略返回值的類型。

C語言中,凡不加類型說明的函數(shù),一律自動(dòng)按整型處理。這樣做不會(huì)有什么好處,卻容易被誤解為void類型。

C++語言有很嚴(yán)格的類型安全檢查,不允許上述情況發(fā)生。由于C++程序可以調(diào)用C函數(shù),為了避免混亂,規(guī)定任何C++/ C函數(shù)都必須有類型。如果函數(shù)沒有返回值,那么應(yīng)聲明為void類型。

l?????????【規(guī)則6-2-2】函數(shù)名字與返回值類型在語義上不可沖突。

違反這條規(guī)則的典型代表是C標(biāo)準(zhǔn)庫函數(shù)getchar。

例如:

char c;

c = getchar();

if (c == EOF)

按照getchar名字的意思,將變量c聲明為char類型是很自然的事情。但不幸的是getchar的確不是char類型,而是int類型,其原型如下:

????????int getchar(void);

由于c是char類型,取值范圍是[-128,127],如果宏EOF的值在char的取值范圍之外,那么if語句將總是失敗,這種“危險(xiǎn)”人們一般哪里料得到!導(dǎo)致本例錯(cuò)誤的責(zé)任并不在用戶,是函數(shù)getchar誤導(dǎo)了使用者。

l?????????【規(guī)則6-2-3】不要將正常值和錯(cuò)誤標(biāo)志混在一起返回。正常值用輸出參數(shù)獲得,而錯(cuò)誤標(biāo)志用return語句返回。

回顧上例,C標(biāo)準(zhǔn)庫函數(shù)的設(shè)計(jì)者為什么要將getchar聲明為令人迷糊的int類型呢?他會(huì)那么傻嗎?

在正常情況下,getchar的確返回單個(gè)字符。但如果getchar碰到文件結(jié)束標(biāo)志或發(fā)生讀錯(cuò)誤,它必須返回一個(gè)標(biāo)志EOF。為了區(qū)別于正常的字符,只好將EOF定義為負(fù)數(shù)(通常為負(fù)1)。因此函數(shù)getchar就成了int類型。

我們?cè)趯?shí)際工作中,經(jīng)常會(huì)碰到上述令人為難的問題。為了避免出現(xiàn)誤解,我們應(yīng)該將正常值和錯(cuò)誤標(biāo)志分開。即:正常值用輸出參數(shù)獲得,而錯(cuò)誤標(biāo)志用return語句返回。

函數(shù)getchar可以改寫成?BOOL GetChar(char *c);

雖然gechar比GetChar靈活,例如?putchar(getchar());?但是如果getchar用錯(cuò)了,它的靈活性又有什么用呢?

2????????【建議6-2-1】有時(shí)候函數(shù)原本不需要返回值,但為了增加靈活性如支持鏈?zhǔn)奖磉_(dá),可以附加返回值。

例如字符串拷貝函數(shù)strcpy的原型:

char *strcpy(char *strDest,const char *strSrc);

strcpy函數(shù)將strSrc拷貝至輸出參數(shù)strDest中,同時(shí)函數(shù)的返回值又是strDest。這樣做并非多此一舉,可以獲得如下靈活性:

????char str[20];

????int?length = strlen( strcpy(str, “Hello World”) );

2????????【建議6-2-2】如果函數(shù)的返回值是一個(gè)對(duì)象,有些場(chǎng)合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場(chǎng)合只能用“值傳遞”而不能用“引用傳遞”,否則會(huì)出錯(cuò)。

例如:

class String

{…

????//?賦值函數(shù)

????String & operate=(const String &other);

//?相加函數(shù),如果沒有friend修飾則只許有一個(gè)右側(cè)參數(shù)

friend?String???operate+( const String &s1, const String &s2);

private:

????char *m_data;

}

???????String的賦值函數(shù)operate =?的實(shí)現(xiàn)如下:

String & String::operate=(const String &other)

{

????if (this == &other)

????????return *this;

????delete m_data;

????m_data = new char[strlen(other.data)+1];

????strcpy(m_data, other.data);

????return *this;???//?返回的是?*this的引用,無需拷貝過程

}

對(duì)于賦值函數(shù),應(yīng)當(dāng)用“引用傳遞”的方式返回String對(duì)象。如果用“值傳遞”的方式,雖然功能仍然正確,但由于return語句要把?*this拷貝到保存返回值的外部存儲(chǔ)單元之中,增加了不必要的開銷,降低了賦值函數(shù)的效率。例如:

?String a,b,c;

?…

?a = b;??????//?如果用“值傳遞”,將產(chǎn)生一次?*this?拷貝

?a = b = c; //?如果用“值傳遞”,將產(chǎn)生兩次?*this?拷貝

?

???????String的相加函數(shù)operate +?的實(shí)現(xiàn)如下:

String?operate+(const String &s1, const String &s2) ?

{

????String temp;

????delete temp.data;???// temp.data是僅含‘\0’的字符串

????????temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];

????????strcpy(temp.data, s1.data);

????????strcat(temp.data, s2.data);

????????return temp;

????}

對(duì)于相加函數(shù),應(yīng)當(dāng)用“值傳遞”的方式返回String對(duì)象。如果改用“引用傳遞”,那么函數(shù)返回值是一個(gè)指向局部對(duì)象temp的“引用”。由于temp在函數(shù)結(jié)束時(shí)被自動(dòng)銷毀,將導(dǎo)致返回的“引用”無效。例如:

????c = a + b;

此時(shí)?a + b?并不返回期望值,c什么也得不到,流下了隱患。

6.3?函數(shù)內(nèi)部實(shí)現(xiàn)的規(guī)則

不同功能的函數(shù)其內(nèi)部實(shí)現(xiàn)各不相同,看起來似乎無法就“內(nèi)部實(shí)現(xiàn)”達(dá)成一致的觀點(diǎn)。但根據(jù)經(jīng)驗(yàn),我們可以在函數(shù)體的“入口處”和“出口處”從嚴(yán)把關(guān),從而提高函數(shù)的質(zhì)量。

?

l?????????【規(guī)則6-3-1】在函數(shù)體的“入口處”,對(duì)參數(shù)的有效性進(jìn)行檢查。

很多程序錯(cuò)誤是由非法參數(shù)引起的,我們應(yīng)該充分理解并正確使用“斷言”(assert)來防止此類錯(cuò)誤。詳見6.5節(jié)“使用斷言”。

l?????????【規(guī)則6-3-2】在函數(shù)體的“出口處”,對(duì)return語句的正確性和效率進(jìn)行檢查。

????如果函數(shù)有返回值,那么函數(shù)的“出口處”是return語句。我們不要輕視r(shí)eturn語句。如果return語句寫得不好,函數(shù)要么出錯(cuò),要么效率低下。

注意事項(xiàng)如下:

(1)return語句不可返回指向“棧內(nèi)存”的“指針”或者“引用”,因?yàn)樵搩?nèi)存在函數(shù)體結(jié)束時(shí)被自動(dòng)銷毀。例如

????char * Func(void)

????{

????????char str[] =?“hello world”;?// str的內(nèi)存位于棧上

????????…

????????return str;?????//?將導(dǎo)致錯(cuò)誤

????}

(2)要搞清楚返回的究竟是“值”、“指針”還是“引用”。

(3)如果函數(shù)返回值是一個(gè)對(duì)象,要考慮return語句的效率。例如???

??????????????return String(s1 + s2);

這是臨時(shí)對(duì)象的語法,表示“創(chuàng)建一個(gè)臨時(shí)對(duì)象并返回它”。不要以為它與“先創(chuàng)建一個(gè)局部對(duì)象temp并返回它的結(jié)果”是等價(jià)的,如

String temp(s1 + s2);

return temp;

實(shí)質(zhì)不然,上述代碼將發(fā)生三件事。首先,temp對(duì)象被創(chuàng)建,同時(shí)完成初始化;然后拷貝構(gòu)造函數(shù)把temp拷貝到保存返回值的外部存儲(chǔ)單元中;最后,temp在函數(shù)結(jié)束時(shí)被銷毀(調(diào)用析構(gòu)函數(shù))。然而“創(chuàng)建一個(gè)臨時(shí)對(duì)象并返回它”的過程是不同的,編譯器直接把臨時(shí)對(duì)象創(chuàng)建并初始化在外部存儲(chǔ)單元中,省去了拷貝和析構(gòu)的化費(fèi),提高了效率。

類似地,我們不要將?

return int(x + y);?//?創(chuàng)建一個(gè)臨時(shí)變量并返回它

寫成

int temp = x + y;

return temp;

由于內(nèi)部數(shù)據(jù)類型如int,float,double的變量不存在構(gòu)造函數(shù)與析構(gòu)函數(shù),雖然該“臨時(shí)變量的語法”不會(huì)提高多少效率,但是程序更加簡(jiǎn)潔易讀。

6.4?其它建議

2????????【建議6-4-1】函數(shù)的功能要單一,不要設(shè)計(jì)多用途的函數(shù)。

2????????【建議6-4-2】函數(shù)體的規(guī)模要小,盡量控制在50行代碼之內(nèi)。

2????????【建議6-4-3】盡量避免函數(shù)帶有“記憶”功能。相同的輸入應(yīng)當(dāng)產(chǎn)生相同的輸出。

帶有“記憶”功能的函數(shù),其行為可能是不可預(yù)測(cè)的,因?yàn)樗男袨榭赡苋Q于某種“記憶狀態(tài)”。這樣的函數(shù)既不易理解又不利于測(cè)試和維護(hù)。在C/C++語言中,函數(shù)的static局部變量是函數(shù)的“記憶”存儲(chǔ)器。建議盡量少用static局部變量,除非必需。

2????????【建議6-4-4】不僅要檢查輸入?yún)?shù)的有效性,還要檢查通過其它途徑進(jìn)入函數(shù)體內(nèi)的變量的有效性,例如全局變量、文件句柄等。

2????????【建議6-4-5】用于出錯(cuò)處理的返回值一定要清楚,讓使用者不容易忽視或誤解錯(cuò)誤情況。

6.5?使用斷言

程序一般分為Debug版本和Release版本,Debug版本用于內(nèi)部調(diào)試,Release版本發(fā)行給用戶使用。

斷言assert是僅在Debug版本起作用的宏,它用于檢查“不應(yīng)該”發(fā)生的情況。示例6-5是一個(gè)內(nèi)存復(fù)制函數(shù)。在運(yùn)行過程中,如果assert的參數(shù)為假,那么程序就會(huì)中止(一般地還會(huì)出現(xiàn)提示對(duì)話,說明在什么地方引發(fā)了assert)。

?

?????????void?*memcpy(void *pvTo, const void *pvFrom, size_t size)

{

????????assert((pvTo != NULL) && (pvFrom != NULL));?????//?使用斷言

????????byte *pbTo = (byte *) pvTo;?????//?防止改變pvTo的地址

????????byte *pbFrom = (byte *) pvFrom;?//?防止改變pvFrom的地址

????????while(size -- > 0 )

????????????*pbTo ++ = *pbFrom ++ ;

????????return pvTo;

}

示例6-5?復(fù)制不重疊的內(nèi)存塊

?

assert不是一個(gè)倉促拼湊起來的宏。為了不在程序的Debug版本和Release版本引起差別,assert不應(yīng)該產(chǎn)生任何副作用。所以assert不是函數(shù),而是宏。程序員可以把a(bǔ)ssert看成一個(gè)在任何系統(tǒng)狀態(tài)下都可以安全使用的無害測(cè)試手段。如果程序在assert處終止了,并不是說含有該assert的函數(shù)有錯(cuò)誤,而是調(diào)用者出了差錯(cuò),assert可以幫助我們找到發(fā)生錯(cuò)誤的原因。

很少有比跟蹤到程序的斷言,卻不知道該斷言的作用更讓人沮喪的事了。你化了很多時(shí)間,不是為了排除錯(cuò)誤,而只是為了弄清楚這個(gè)錯(cuò)誤到底是什么。有的時(shí)候,程序員偶爾還會(huì)設(shè)計(jì)出有錯(cuò)誤的斷言。所以如果搞不清楚斷言檢查的是什么,就很難判斷錯(cuò)誤是出現(xiàn)在程序中,還是出現(xiàn)在斷言中。幸運(yùn)的是這個(gè)問題很好解決,只要加上清晰的注釋即可。這本是顯而易見的事情,可是很少有程序員這樣做。這好比一個(gè)人在森林里,看到樹上釘著一塊“危險(xiǎn)”的大牌子。但危險(xiǎn)到底是什么?樹要倒?有廢井?有野獸?除非告訴人們“危險(xiǎn)”是什么,否則這個(gè)警告牌難以起到積極有效的作用。難以理解的斷言常常被程序員忽略,甚至被刪除。[Maguire, p8-p30]

?

l?????????【規(guī)則6-5-1】使用斷言捕捉不應(yīng)該發(fā)生的非法情況。不要混淆非法情況與錯(cuò)誤情況之間的區(qū)別,后者是必然存在的并且是一定要作出處理的。

l?????????【規(guī)則6-5-2】在函數(shù)的入口處,使用斷言檢查參數(shù)的有效性(合法性)。

l?????????【建議6-5-1】在編寫函數(shù)時(shí),要進(jìn)行反復(fù)的考查,并且自問:“我打算做哪些假定?”一旦確定了的假定,就要使用斷言對(duì)假定進(jìn)行檢查。

l?????????【建議6-5-2】一般教科書都鼓勵(lì)程序員們進(jìn)行防錯(cuò)設(shè)計(jì),但要記住這種編程風(fēng)格可能會(huì)隱瞞錯(cuò)誤。當(dāng)進(jìn)行防錯(cuò)設(shè)計(jì)時(shí),如果“不可能發(fā)生”的事情的確發(fā)生了,則要使用斷言進(jìn)行報(bào)警。

6.6?引用與指針的比較

引用是C++中的概念,初學(xué)者容易把引用和指針混淆一起。一下程序中,n是m的一個(gè)引用(reference),m是被引用物(referent)。

????int m;

????int &n = m;

n相當(dāng)于m的別名(綽號(hào)),對(duì)n的任何操作就是對(duì)m的操作。例如有人名叫王小毛,他的綽號(hào)是“三毛”。說“三毛”怎么怎么的,其實(shí)就是對(duì)王小毛說三道四。所以n既不是m的拷貝,也不是指向m的指針,其實(shí)n就是m它自己。

引用的一些規(guī)則如下:

(1)引用被創(chuàng)建的同時(shí)必須被初始化(指針則可以在任何時(shí)候被初始化)。

(2)不能有NULL引用,引用必須與合法的存儲(chǔ)單元關(guān)聯(lián)(指針則可以是NULL)。

(3)一旦引用被初始化,就不能改變引用的關(guān)系(指針則可以隨時(shí)改變所指的對(duì)象)。

????以下示例程序中,k被初始化為i的引用。語句k = j并不能將k修改成為j的引用,只是把k的值改變成為6。由于k是i的引用,所以i的值也變成了6。

????int i = 5;

????int j = 6;

????int &k = i;

????k = j;?// k和i的值都變成了6;

????上面的程序看起來象在玩文字游戲,沒有體現(xiàn)出引用的價(jià)值。引用的主要功能是傳遞函數(shù)的參數(shù)和返回值。C++語言中,函數(shù)的參數(shù)和返回值的傳遞方式有三種:值傳遞、指針傳遞和引用傳遞。

????以下是“值傳遞”的示例程序。由于Func1函數(shù)體內(nèi)的x是外部變量n的一份拷貝,改變x的值不會(huì)影響n,?所以n的值仍然是0。

????void Func1(int x)

{

????x = x + 10;

}

int n = 0;

????Func1(n);

????cout << “n = ” << n << endl;?// n = 0

???

以下是“指針傳遞”的示例程序。由于Func2函數(shù)體內(nèi)的x是指向外部變量n的指針,改變?cè)撝羔樀膬?nèi)容將導(dǎo)致n的值改變,所以n的值成為10。

????void Func2(int *x)

{

????(* x) = (* x) + 10;

}

int n = 0;

????Func2(&n);

????cout << “n = ” << n << endl;?????// n = 10

?

????以下是“引用傳遞”的示例程序。由于Func3函數(shù)體內(nèi)的x是外部變量n的引用,x和n是同一個(gè)東西,改變x等于改變n,所以n的值成為10。

????void Func3(int &x)

{

????x = x + 10;

}

int n = 0;

????Func3(n);

????cout << “n = ” << n << endl;?????// n = 10

?

????對(duì)比上述三個(gè)示例程序,會(huì)發(fā)現(xiàn)“引用傳遞”的性質(zhì)象“指針傳遞”,而書寫方式象“值傳遞”。實(shí)際上“引用”可以做的任何事情“指針”也都能夠做,為什么還要“引用”這東西?

答案是“用適當(dāng)?shù)墓ぞ咦銮∪缙浞值墓ぷ鳌薄?/p>

????指針能夠毫無約束地操作內(nèi)存中的如何東西,盡管指針功能強(qiáng)大,但是非常危險(xiǎn)。就象一把刀,它可以用來砍樹、裁紙、修指甲、理發(fā)等等,誰敢這樣用?

如果的確只需要借用一下某個(gè)對(duì)象的“別名”,那么就用“引用”,而不要用“指針”,以免發(fā)生意外。比如說,某人需要一份證明,本來在文件上蓋上公章的印子就行了,如果把取公章的鑰匙交給他,那么他就獲得了不該有的權(quán)利。

?

第7章?內(nèi)存管理

????歡迎進(jìn)入內(nèi)存這片雷區(qū)。偉大的Bill Gates?曾經(jīng)失言:

640K ought to be enough for everybody

—?Bill Gates 1981

程序員們經(jīng)常編寫內(nèi)存管理程序,往往提心吊膽。如果不想觸雷,唯一的解決辦法就是發(fā)現(xiàn)所有潛伏的地雷并且排除它們,躲是躲不了的。本章的內(nèi)容比一般教科書的要深入得多,讀者需細(xì)心閱讀,做到真正地通曉內(nèi)存管理。

7.1內(nèi)存分配方式

內(nèi)存分配方式有三種:

(1)???????從靜態(tài)存儲(chǔ)區(qū)域分配。內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)存在程序的整個(gè)運(yùn)行期間都存在。例如全局變量,static變量。

(2)???????在棧上創(chuàng)建。在執(zhí)行函數(shù)時(shí),函數(shù)內(nèi)局部變量的存儲(chǔ)單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲(chǔ)單元自動(dòng)被釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。

(3)???????從堆上分配,亦稱動(dòng)態(tài)內(nèi)存分配。程序在運(yùn)行的時(shí)候用malloc或new申請(qǐng)任意多少的內(nèi)存,程序員自己負(fù)責(zé)在何時(shí)用free或delete釋放內(nèi)存。動(dòng)態(tài)內(nèi)存的生存期由我們決定,使用非常靈活,但問題也最多。

7.2常見的內(nèi)存錯(cuò)誤及其對(duì)策

???????發(fā)生內(nèi)存錯(cuò)誤是件非常麻煩的事情。編譯器不能自動(dòng)發(fā)現(xiàn)這些錯(cuò)誤,通常是在程序運(yùn)行時(shí)才能捕捉到。而這些錯(cuò)誤大多沒有明顯的癥狀,時(shí)隱時(shí)現(xiàn),增加了改錯(cuò)的難度。有時(shí)用戶怒氣沖沖地把你找來,程序卻沒有發(fā)生任何問題,你一走,錯(cuò)誤又發(fā)作了。

常見的內(nèi)存錯(cuò)誤及其對(duì)策如下:

u???????內(nèi)存分配未成功,卻使用了它。

編程新手常犯這種錯(cuò)誤,因?yàn)樗麄儧]有意識(shí)到內(nèi)存分配會(huì)不成功。常用解決辦法是,在使用內(nèi)存之前檢查指針是否為NULL。如果指針p是函數(shù)的參數(shù),那么在函數(shù)的入口處用assert(p!=NULL)進(jìn)行檢查。如果是用malloc或new來申請(qǐng)內(nèi)存,應(yīng)該用if(p==NULL)?或if(p!=NULL)進(jìn)行防錯(cuò)處理。

?

u???????內(nèi)存分配雖然成功,但是尚未初始化就引用它。

犯這種錯(cuò)誤主要有兩個(gè)起因:一是沒有初始化的觀念;二是誤以為內(nèi)存的缺省初值全為零,導(dǎo)致引用初值錯(cuò)誤(例如數(shù)組)。

內(nèi)存的缺省初值究竟是什么并沒有統(tǒng)一的標(biāo)準(zhǔn),盡管有些時(shí)候?yàn)榱阒?#xff0c;我們寧可信其無不可信其有。所以無論用何種方式創(chuàng)建數(shù)組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。

?

u???????內(nèi)存分配成功并且已經(jīng)初始化,但操作越過了內(nèi)存的邊界。

例如在使用數(shù)組時(shí)經(jīng)常發(fā)生下標(biāo)“多1”或者“少1”的操作。特別是在for循環(huán)語句中,循環(huán)次數(shù)很容易搞錯(cuò),導(dǎo)致數(shù)組操作越界。

?

u???????忘記了釋放內(nèi)存,造成內(nèi)存泄露。

含有這種錯(cuò)誤的函數(shù)每被調(diào)用一次就丟失一塊內(nèi)存。剛開始時(shí)系統(tǒng)的內(nèi)存充足,你看不到錯(cuò)誤。終有一次程序突然死掉,系統(tǒng)出現(xiàn)提示:內(nèi)存耗盡。

動(dòng)態(tài)內(nèi)存的申請(qǐng)與釋放必須配對(duì),程序中malloc與free的使用次數(shù)一定要相同,否則肯定有錯(cuò)誤(new/delete同理)。

?

u???????釋放了內(nèi)存卻繼續(xù)使用它。

有三種情況:

(1)程序中的對(duì)象調(diào)用關(guān)系過于復(fù)雜,實(shí)在難以搞清楚某個(gè)對(duì)象究竟是否已經(jīng)釋放了內(nèi)存,此時(shí)應(yīng)該重新設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu),從根本上解決對(duì)象管理的混亂局面。

(2)函數(shù)的return語句寫錯(cuò)了,注意不要返回指向“棧內(nèi)存”的“指針”或者“引用”,因?yàn)樵搩?nèi)存在函數(shù)體結(jié)束時(shí)被自動(dòng)銷毀。

(3)使用free或delete釋放了內(nèi)存后,沒有將指針設(shè)置為NULL。導(dǎo)致產(chǎn)生“野指針”。

?

l?????????【規(guī)則7-2-1】用malloc或new申請(qǐng)內(nèi)存之后,應(yīng)該立即檢查指針值是否為NULL。防止使用指針值為NULL的內(nèi)存。

l?????????【規(guī)則7-2-2】不要忘記為數(shù)組和動(dòng)態(tài)內(nèi)存賦初值。防止將未被初始化的內(nèi)存作為右值使用。

l?????????【規(guī)則7-2-3】避免數(shù)組或指針的下標(biāo)越界,特別要當(dāng)心發(fā)生“多1”或者“少1”操作。

l?????????【規(guī)則7-2-4】動(dòng)態(tài)內(nèi)存的申請(qǐng)與釋放必須配對(duì),防止內(nèi)存泄漏。

l?????????【規(guī)則7-2-5】用free或delete釋放了內(nèi)存之后,立即將指針設(shè)置為NULL,防止產(chǎn)生“野指針”。

7.3指針與數(shù)組的對(duì)比

???????C++/C程序中,指針和數(shù)組在不少地方可以相互替換著用,讓人產(chǎn)生一種錯(cuò)覺,以為兩者是等價(jià)的。

???????數(shù)組要么在靜態(tài)存儲(chǔ)區(qū)被創(chuàng)建(如全局?jǐn)?shù)組),要么在棧上被創(chuàng)建。數(shù)組名對(duì)應(yīng)著(而不是指向)一塊內(nèi)存,其地址與容量在生命期內(nèi)保持不變,只有數(shù)組的內(nèi)容可以改變。

指針可以隨時(shí)指向任意類型的內(nèi)存塊,它的特征是“可變”,所以我們常用指針來操作動(dòng)態(tài)內(nèi)存。指針遠(yuǎn)比數(shù)組靈活,但也更危險(xiǎn)。

下面以字符串為例比較指針與數(shù)組的特性。

?

7.3.1?修改內(nèi)容

???????示例7-3-1中,字符數(shù)組a的容量是6個(gè)字符,其內(nèi)容為hello\0。a的內(nèi)容可以改變,如a[0]= ‘X’。指針p指向常量字符串“world”(位于靜態(tài)存儲(chǔ)區(qū),內(nèi)容為world\0),常量字符串的內(nèi)容是不可以被修改的。從語法上看,編譯器并不覺得語句p[0]= ‘X’有什么不妥,但是該語句企圖修改常量字符串的內(nèi)容而導(dǎo)致運(yùn)行錯(cuò)誤。

?

char a[] =?“hello”;

a[0] =?‘X’;

cout << a << endl;

char *p =?“world”;?????//?注意p指向常量字符串

p[0] =?‘X’;????????????//?編譯器不能發(fā)現(xiàn)該錯(cuò)誤

cout << p << endl;

示例7-3-1?修改數(shù)組和指針的內(nèi)容

?

7.3.2?內(nèi)容復(fù)制與比較

????不能對(duì)數(shù)組名進(jìn)行直接復(fù)制與比較。示例7-3-2中,若想把數(shù)組a的內(nèi)容復(fù)制給數(shù)組b,不能用語句?b = a?,否則將產(chǎn)生編譯錯(cuò)誤。應(yīng)該用標(biāo)準(zhǔn)庫函數(shù)strcpy進(jìn)行復(fù)制。同理,比較b和a的內(nèi)容是否相同,不能用if(b==a)?來判斷,應(yīng)該用標(biāo)準(zhǔn)庫函數(shù)strcmp進(jìn)行比較。

????語句p = a?并不能把a(bǔ)的內(nèi)容復(fù)制指針p,而是把a(bǔ)的地址賦給了p。要想復(fù)制a的內(nèi)容,可以先用庫函數(shù)malloc為p申請(qǐng)一塊容量為strlen(a)+1個(gè)字符的內(nèi)存,再用strcpy進(jìn)行字符串復(fù)制。同理,語句if(p==a)?比較的不是內(nèi)容而是地址,應(yīng)該用庫函數(shù)strcmp來比較。

?

????//?數(shù)組…

????char a[] = "hello";

????char b[10];

????strcpy(b, a);???????????//?不能用???b = a;

????if(strcmp(b, a) == 0)???//?不能用?if (b == a)

????//?指針…

????int len = strlen(a);

????char *p = (char *)malloc(sizeof(char)*(len+1));

????strcpy(p,a);????????????//?不要用?p = a;

????if(strcmp(p, a) == 0)???//?不要用?if (p == a)

示例7-3-2?數(shù)組和指針的內(nèi)容復(fù)制與比較

?

?

7.3.3?計(jì)算內(nèi)存容量

????用運(yùn)算符sizeof可以計(jì)算出數(shù)組的容量(字節(jié)數(shù))。示例7-3-3(a)中,sizeof(a)的值是12(注意別忘了’\0’)。指針p指向a,但是sizeof(p)的值卻是4。這是因?yàn)閟izeof(p)得到的是一個(gè)指針變量的字節(jié)數(shù),相當(dāng)于sizeof(char*),而不是p所指的內(nèi)存容量。C++/C語言沒有辦法知道指針?biāo)傅膬?nèi)存容量,除非在申請(qǐng)內(nèi)存時(shí)記住它。

注意當(dāng)數(shù)組作為函數(shù)的參數(shù)進(jìn)行傳遞時(shí),該數(shù)組自動(dòng)退化為同類型的指針。示例7-3-3(b)中,不論數(shù)組a的容量是多少,sizeof(a)始終等于sizeof(char *)。

?

????char a[] = "hello world";

????char *p?= a;

????cout<< sizeof(a) << endl;???// 12字節(jié)

????cout<< sizeof(p) << endl;???// 4字節(jié)

示例7-3-3(a)?計(jì)算數(shù)組和指針的內(nèi)存容量

??????

????void Func(char a[100])

????{

????????cout<< sizeof(a) << endl;???// 4字節(jié)而不是100字節(jié)

}

示例7-3-3(b)?數(shù)組退化為指針

7.4指針參數(shù)是如何傳遞內(nèi)存的?

???????如果函數(shù)的參數(shù)是一個(gè)指針,不要指望用該指針去申請(qǐng)動(dòng)態(tài)內(nèi)存。示例7-4-1中,Test函數(shù)的語句GetMemory(str, 200)并沒有使str獲得期望的內(nèi)存,str依舊是NULL,為什么?

?

void GetMemory(char *p, int num)

{

????p = (char *)malloc(sizeof(char) * num);

}

void Test(void)

{

????char *str = NULL;

????GetMemory(str, 100);????// str?仍然為?NULL?

????strcpy(str, "hello");???//?運(yùn)行錯(cuò)誤

}

示例7-4-1?試圖用指針參數(shù)申請(qǐng)動(dòng)態(tài)內(nèi)存

?

毛病出在函數(shù)GetMemory中。編譯器總是要為函數(shù)的每個(gè)參數(shù)制作臨時(shí)副本,指針參數(shù)p的副本是?_p,編譯器使?_p = p。如果函數(shù)體內(nèi)的程序修改了_p的內(nèi)容,就導(dǎo)致參數(shù)p的內(nèi)容作相應(yīng)的修改。這就是指針可以用作輸出參數(shù)的原因。在本例中,_p申請(qǐng)了新的內(nèi)存,只是把_p所指的內(nèi)存地址改變了,但是p絲毫未變。所以函數(shù)GetMemory并不能輸出任何東西。事實(shí)上,每執(zhí)行一次GetMemory就會(huì)泄露一塊內(nèi)存,因?yàn)闆]有用free釋放內(nèi)存。

如果非得要用指針參數(shù)去申請(qǐng)內(nèi)存,那么應(yīng)該改用“指向指針的指針”,見示例7-4-2。

?

void GetMemory2(char **p, int num)

{

????*p = (char *)malloc(sizeof(char) * num);

}

void Test2(void)

{

????char *str = NULL;

????GetMemory2(&str, 100);?//?注意參數(shù)是?&str,而不是str

????strcpy(str, "hello");??

????cout<< str << endl;

????free(str);?

}

示例7-4-2用指向指針的指針申請(qǐng)動(dòng)態(tài)內(nèi)存

?

由于“指向指針的指針”這個(gè)概念不容易理解,我們可以用函數(shù)返回值來傳遞動(dòng)態(tài)內(nèi)存。這種方法更加簡(jiǎn)單,見示例7-4-3。

?

char *GetMemory3(int num)

{

????char *p = (char *)malloc(sizeof(char) * num);

????return p;

}

void Test3(void)

{

????char *str = NULL;

????str = GetMemory3(100);?

????strcpy(str, "hello");

????cout<< str << endl;

????free(str);?

}

示例7-4-3?用函數(shù)返回值來傳遞動(dòng)態(tài)內(nèi)存

?

用函數(shù)返回值來傳遞動(dòng)態(tài)內(nèi)存這種方法雖然好用,但是常常有人把return語句用錯(cuò)了。這里強(qiáng)調(diào)不要用return語句返回指向“棧內(nèi)存”的指針,因?yàn)樵搩?nèi)存在函數(shù)結(jié)束時(shí)自動(dòng)消亡,見示例7-4-4。

?

char *GetString(void)

{

????char p[] = "hello world";

????return p;???//?編譯器將提出警告

}

void Test4(void)

{

char *str = NULL;

str = GetString();?// str?的內(nèi)容是垃圾

cout<< str << endl;

}

示例7-4-4 return語句返回指向“棧內(nèi)存”的指針

?

用調(diào)試器逐步跟蹤Test4,發(fā)現(xiàn)執(zhí)行str = GetString語句后str不再是NULL指針,但是str的內(nèi)容不是“hello world”而是垃圾。

如果把示例7-4-4改寫成示例7-4-5,會(huì)怎么樣?

?

char *GetString2(void)

{

????char *p = "hello world";

????return p;

}

void Test5(void)

{

????char *str = NULL;

????str = GetString2();

????cout<< str << endl;

}

示例7-4-5 return語句返回常量字符串

?

函數(shù)Test5運(yùn)行雖然不會(huì)出錯(cuò),但是函數(shù)GetString2的設(shè)計(jì)概念卻是錯(cuò)誤的。因?yàn)镚etString2內(nèi)的“hello world”是常量字符串,位于靜態(tài)存儲(chǔ)區(qū),它在程序生命期內(nèi)恒定不變。無論什么時(shí)候調(diào)用GetString2,它返回的始終是同一個(gè)“只讀”的內(nèi)存塊。

?

7.5 free和delete把指針怎么啦?

別看free和delete的名字惡狠狠的(尤其是delete),它們只是把指針?biāo)傅膬?nèi)存給釋放掉,但并沒有把指針本身干掉。

用調(diào)試器跟蹤示例7-5,發(fā)現(xiàn)指針p被free以后其地址仍然不變(非NULL),只是該地址對(duì)應(yīng)的內(nèi)存是垃圾,p成了“野指針”。如果此時(shí)不把p設(shè)置為NULL,會(huì)讓人誤以為p是個(gè)合法的指針。

如果程序比較長(zhǎng),我們有時(shí)記不住p所指的內(nèi)存是否已經(jīng)被釋放,在繼續(xù)使用p之前,通常會(huì)用語句if (p != NULL)進(jìn)行防錯(cuò)處理。很遺憾,此時(shí)if語句起不到防錯(cuò)作用,因?yàn)榧幢鉷不是NULL指針,它也不指向合法的內(nèi)存塊。

?

????char *p = (char *) malloc(100);

????strcpy(p,?“hello”);

????free(p);????????// p?所指的內(nèi)存被釋放,但是p所指的地址仍然不變

????…

????if(p != NULL)???//?沒有起到防錯(cuò)作用

????{

???????strcpy(p,?“world”);?//?出錯(cuò)

}

示例7-5?p成為野指針

7.6?動(dòng)態(tài)內(nèi)存會(huì)被自動(dòng)釋放嗎?

???????函數(shù)體內(nèi)的局部變量在函數(shù)結(jié)束時(shí)自動(dòng)消亡。很多人誤以為示例7-6是正確的。理由是p是局部的指針變量,它消亡的時(shí)候會(huì)讓它所指的動(dòng)態(tài)內(nèi)存一起完蛋。這是錯(cuò)覺!

?

????void Func(void)

{

????char *p = (char *) malloc(100);?//?動(dòng)態(tài)內(nèi)存會(huì)自動(dòng)釋放嗎?

}

示例7-6?試圖讓動(dòng)態(tài)內(nèi)存自動(dòng)釋放

?

????我們發(fā)現(xiàn)指針有一些“似是而非”的特征:

(1)指針消亡了,并不表示它所指的內(nèi)存會(huì)被自動(dòng)釋放。

(2)內(nèi)存被釋放了,并不表示指針會(huì)消亡或者成了NULL指針。

這表明釋放內(nèi)存并不是一件可以草率對(duì)待的事。也許有人不服氣,一定要找出可以草率行事的理由:

????如果程序終止了運(yùn)行,一切指針都會(huì)消亡,動(dòng)態(tài)內(nèi)存會(huì)被操作系統(tǒng)回收。既然如此,在程序臨終前,就可以不必釋放內(nèi)存、不必將指針設(shè)置為NULL了。終于可以偷懶而不會(huì)發(fā)生錯(cuò)誤了吧?

????想得美。如果別人把那段程序取出來用到其它地方怎么辦?

7.7?杜絕“野指針”

“野指針”不是NULL指針,是指向“垃圾”內(nèi)存的指針。人們一般不會(huì)錯(cuò)用NULL指針,因?yàn)橛胕f語句很容易判斷。但是“野指針”是很危險(xiǎn)的,if語句對(duì)它不起作用。

“野指針”的成因主要有兩種:

(1)指針變量沒有被初始化。任何指針變量剛被創(chuàng)建時(shí)不會(huì)自動(dòng)成為NULL指針,它的缺省值是隨機(jī)的,它會(huì)亂指一氣。所以,指針變量在創(chuàng)建的同時(shí)應(yīng)當(dāng)被初始化,要么將指針設(shè)置為NULL,要么讓它指向合法的內(nèi)存。例如

????char *p = NULL;

????char *str = (char *) malloc(100);

?

(2)指針p被free或者delete之后,沒有置為NULL,讓人誤以為p是個(gè)合法的指針。參見7.5節(jié)。

?

(3)指針操作超越了變量的作用范圍。這種情況讓人防不勝防,示例程序如下:

????class A

{??

public:

????void Func(void){ cout <<?“Func of class A”?<< endl; }

};

????void Test(void)

{

????A?*p;

????????{

????????????A?a;

????????????p = &a;?//?注意?a?的生命期

}

????????p->Func();??????// p是“野指針”

}

?

函數(shù)Test在執(zhí)行語句p->Func()時(shí),對(duì)象a已經(jīng)消失,而p是指向a的,所以p就成了“野指針”。但奇怪的是我運(yùn)行這個(gè)程序時(shí)居然沒有出錯(cuò),這可能與編譯器有關(guān)。

?

7.8?有了malloc/free為什么還要new/delete??

???????malloc與free是C++/C語言的標(biāo)準(zhǔn)庫函數(shù),new/delete是C++的運(yùn)算符。它們都可用于申請(qǐng)動(dòng)態(tài)內(nèi)存和釋放內(nèi)存。

對(duì)于非內(nèi)部數(shù)據(jù)類型的對(duì)象而言,光用maloc/free無法滿足動(dòng)態(tài)對(duì)象的要求。對(duì)象在創(chuàng)建的同時(shí)要自動(dòng)執(zhí)行構(gòu)造函數(shù),對(duì)象在消亡之前要自動(dòng)執(zhí)行析構(gòu)函數(shù)。由于malloc/free是庫函數(shù)而不是運(yùn)算符,不在編譯器控制權(quán)限之內(nèi),不能夠把執(zhí)行構(gòu)造函數(shù)和析構(gòu)函數(shù)的任務(wù)強(qiáng)加于malloc/free。

???????因此C++語言需要一個(gè)能完成動(dòng)態(tài)內(nèi)存分配和初始化工作的運(yùn)算符new,以及一個(gè)能完成清理與釋放內(nèi)存工作的運(yùn)算符delete。注意new/delete不是庫函數(shù)。

我們先看一看malloc/free和new/delete如何實(shí)現(xiàn)對(duì)象的動(dòng)態(tài)內(nèi)存管理,見示例7-8。

?

class Obj

{

public :

????????Obj(void){ cout <<?“Initialization”?<< endl; }

~Obj(void){ cout <<?“Destroy”?<< endl; }

void????Initialize(void){ cout <<?“Initialization”?<< endl; }

void????Destroy(void){ cout <<?“Destroy”?<< endl; }

};

void UseMallocFree(void)

{

????Obj?*a = (obj *)malloc(sizeof(obj));???//?申請(qǐng)動(dòng)態(tài)內(nèi)存

????a->Initialize();????????????????????????//?初始化

????//…

????a->Destroy();???//?清除工作

????free(a);????????//?釋放內(nèi)存

}

void UseNewDelete(void)

{

????Obj?*a = new Obj;?//?申請(qǐng)動(dòng)態(tài)內(nèi)存并且初始化

????//…

????delete a;???????????//?清除并且釋放內(nèi)存

}

示例7-8?用malloc/free和new/delete如何實(shí)現(xiàn)對(duì)象的動(dòng)態(tài)內(nèi)存管理

?

類Obj的函數(shù)Initialize模擬了構(gòu)造函數(shù)的功能,函數(shù)Destroy模擬了析構(gòu)函數(shù)的功能。函數(shù)UseMallocFree中,由于malloc/free不能執(zhí)行構(gòu)造函數(shù)與析構(gòu)函數(shù),必須調(diào)用成員函數(shù)Initialize和Destroy來完成初始化與清除工作。函數(shù)UseNewDelete則簡(jiǎn)單得多。

所以我們不要企圖用malloc/free來完成動(dòng)態(tài)對(duì)象的內(nèi)存管理,應(yīng)該用new/delete。由于內(nèi)部數(shù)據(jù)類型的“對(duì)象”沒有構(gòu)造與析構(gòu)的過程,對(duì)它們而言malloc/free和new/delete是等價(jià)的。

????既然new/delete的功能完全覆蓋了malloc/free,為什么C++不把malloc/free淘汰出局呢?這是因?yàn)镃++程序經(jīng)常要調(diào)用C函數(shù),而C程序只能用malloc/free管理動(dòng)態(tài)內(nèi)存。

如果用free釋放“new創(chuàng)建的動(dòng)態(tài)對(duì)象”,那么該對(duì)象因無法執(zhí)行析構(gòu)函數(shù)而可能導(dǎo)致程序出錯(cuò)。如果用delete釋放“malloc申請(qǐng)的動(dòng)態(tài)內(nèi)存”,理論上講程序不會(huì)出錯(cuò),但是該程序的可讀性很差。所以new/delete必須配對(duì)使用,malloc/free也一樣。

7.9?內(nèi)存耗盡怎么辦?

???????如果在申請(qǐng)動(dòng)態(tài)內(nèi)存時(shí)找不到足夠大的內(nèi)存塊,malloc和new將返回NULL指針,宣告內(nèi)存申請(qǐng)失敗。通常有三種方式處理“內(nèi)存耗盡”問題。

(1)判斷指針是否為NULL,如果是則馬上用return語句終止本函數(shù)。例如:

void Func(void)

{

A?*a = new A;

if(a == NULL)

{

????return;

????}

}

?

(2)判斷指針是否為NULL,如果是則馬上用exit(1)終止整個(gè)程序的運(yùn)行。例如:

void Func(void)

{

A?*a = new A;

if(a == NULL)

{

????cout <<?“Memory Exhausted”?<< endl;

????exit(1);

}

????…

}

?

(3)為new和malloc設(shè)置異常處理函數(shù)。例如Visual C++可以用_set_new_hander函數(shù)為new設(shè)置用戶自己定義的異常處理函數(shù),也可以讓malloc享用與new相同的異常處理函數(shù)。詳細(xì)內(nèi)容請(qǐng)參考C++使用手冊(cè)。

?

???????上述(1)(2)方式使用最普遍。如果一個(gè)函數(shù)內(nèi)有多處需要申請(qǐng)動(dòng)態(tài)內(nèi)存,那么方式(1)就顯得力不從心(釋放內(nèi)存很麻煩),應(yīng)該用方式(2)來處理。

很多人不忍心用exit(1),問:“不編寫出錯(cuò)處理程序,讓操作系統(tǒng)自己解決行不行?”

???????不行。如果發(fā)生“內(nèi)存耗盡”這樣的事情,一般說來應(yīng)用程序已經(jīng)無藥可救。如果不用exit(1)?把壞程序殺死,它可能會(huì)害死操作系統(tǒng)。道理如同:如果不把歹徒擊斃,歹徒在老死之前會(huì)犯下更多的罪。

?

???????有一個(gè)很重要的現(xiàn)象要告訴大家。對(duì)于32位以上的應(yīng)用程序而言,無論怎樣使用malloc與new,幾乎不可能導(dǎo)致“內(nèi)存耗盡”。我在Windows 98下用Visual C++編寫了測(cè)試程序,見示例7-9。這個(gè)程序會(huì)無休止地運(yùn)行下去,根本不會(huì)終止。因?yàn)?2位操作系統(tǒng)支持“虛存”,內(nèi)存用完了,自動(dòng)用硬盤空間頂替。我只聽到硬盤嘎吱嘎吱地響,Window 98已經(jīng)累得對(duì)鍵盤、鼠標(biāo)毫無反應(yīng)。

我可以得出這么一個(gè)結(jié)論:對(duì)于32位以上的應(yīng)用程序,“內(nèi)存耗盡”錯(cuò)誤處理程序毫無用處。這下可把Unix和Windows程序員們樂壞了:反正錯(cuò)誤處理程序不起作用,我就不寫了,省了很多麻煩。

我不想誤導(dǎo)讀者,必須強(qiáng)調(diào):不加錯(cuò)誤處理將導(dǎo)致程序的質(zhì)量很差,千萬不可因小失大。

?

void main(void)

{

????float *p = NULL;

????while(TRUE)

????{

????????p = new float[1000000];

????????cout <<?“eat memory”?<< endl;

????????if(p==NULL)

????????????exit(1);

????}

}

示例7-9試圖耗盡操作系統(tǒng)的內(nèi)存

7.10 malloc/free?的使用要點(diǎn)

????函數(shù)malloc的原型如下:

????????void * malloc(size_t size);

????用malloc申請(qǐng)一塊長(zhǎng)度為length的整數(shù)類型的內(nèi)存,程序如下:

????????int?*p = (int *) malloc(sizeof(int) * length);

我們應(yīng)當(dāng)把注意力集中在兩個(gè)要素上:“類型轉(zhuǎn)換”和“sizeof”。

u???????malloc返回值的類型是void *,所以在調(diào)用malloc時(shí)要顯式地進(jìn)行類型轉(zhuǎn)換,將void *?轉(zhuǎn)換成所需要的指針類型。

u???????malloc函數(shù)本身并不識(shí)別要申請(qǐng)的內(nèi)存是什么類型,它只關(guān)心內(nèi)存的總字節(jié)數(shù)。我們通常記不住int, float等數(shù)據(jù)類型的變量的確切字節(jié)數(shù)。例如int變量在16位系統(tǒng)下是2個(gè)字節(jié),在32位下是4個(gè)字節(jié);而float變量在16位系統(tǒng)下是4個(gè)字節(jié),在32位下也是4個(gè)字節(jié)。最好用以下程序作一次測(cè)試:

cout << sizeof(char) << endl;

cout << sizeof(int) << endl;

cout << sizeof(unsigned int) << endl;

cout << sizeof(long) << endl;

cout << sizeof(unsigned long) << endl;

cout << sizeof(float) << endl;

cout << sizeof(double) << endl;

????cout << sizeof(void *) << endl;

???

????在malloc的“()”中使用sizeof運(yùn)算符是良好的風(fēng)格,但要當(dāng)心有時(shí)我們會(huì)昏了頭,寫出?p = malloc(sizeof(p))這樣的程序來。

?

u???????函數(shù)free的原型如下:

void free( void * memblock );

????為什么free函數(shù)不象malloc函數(shù)那樣復(fù)雜呢?這是因?yàn)橹羔榩的類型以及它所指的內(nèi)存的容量事先都是知道的,語句free(p)能正確地釋放內(nèi)存。如果p是NULL指針,那么free對(duì)p無論操作多少次都不會(huì)出問題。如果p不是NULL指針,那么free對(duì)p連續(xù)操作兩次就會(huì)導(dǎo)致程序運(yùn)行錯(cuò)誤。

7.11 new/delete?的使用要點(diǎn)

???????運(yùn)算符new使用起來要比函數(shù)malloc簡(jiǎn)單得多,例如:

int?*p1 = (int *)malloc(sizeof(int) * length);

int?*p2 = new int[length];

這是因?yàn)閚ew內(nèi)置了sizeof、類型轉(zhuǎn)換和類型安全檢查功能。對(duì)于非內(nèi)部數(shù)據(jù)類型的對(duì)象而言,new在創(chuàng)建動(dòng)態(tài)對(duì)象的同時(shí)完成了初始化工作。如果對(duì)象有多個(gè)構(gòu)造函數(shù),那么new的語句也可以有多種形式。例如

class Obj

{

public :

????Obj(void);??????//?無參數(shù)的構(gòu)造函數(shù)

????Obj(int x);?????//?帶一個(gè)參數(shù)的構(gòu)造函數(shù)

}

void Test(void)

{

????Obj?*a = new Obj;

????Obj?*b = new Obj(1);???//?初值為1

????…

????delete a;

????delete b;

}

如果用new創(chuàng)建對(duì)象數(shù)組,那么只能使用對(duì)象的無參數(shù)構(gòu)造函數(shù)。例如

????Obj?*objects = new Obj[100];???//?創(chuàng)建100個(gè)動(dòng)態(tài)對(duì)象

不能寫成

????Obj?*objects = new Obj[100](1);//?創(chuàng)建100個(gè)動(dòng)態(tài)對(duì)象的同時(shí)賦初值1

在用delete釋放對(duì)象數(shù)組時(shí),留意不要丟了符號(hào)‘[]’。例如

????delete []objects;???//?正確的用法

delete objects;?//?錯(cuò)誤的用法

后者相當(dāng)于delete objects[0],漏掉了另外99個(gè)對(duì)象。

7.12?一些心得體會(huì)

我認(rèn)識(shí)不少技術(shù)不錯(cuò)的C++/C程序員,很少有人能拍拍胸脯說通曉指針與內(nèi)存管理(包括我自己)。我最初學(xué)習(xí)C語言時(shí)特別怕指針,導(dǎo)致我開發(fā)第一個(gè)應(yīng)用軟件(約1萬行C代碼)時(shí)沒有使用一個(gè)指針,全用數(shù)組來頂替指針,實(shí)在蠢笨得過分。躲避指針不是辦法,后來我改寫了這個(gè)軟件,代碼量縮小到原先的一半。

我的經(jīng)驗(yàn)教訓(xùn)是:

(1)越是怕指針,就越要使用指針。不會(huì)正確使用指針,肯定算不上是合格的程序員。

(2)必須養(yǎng)成“使用調(diào)試器逐步跟蹤程序”的習(xí)慣,只有這樣才能發(fā)現(xiàn)問題的本質(zhì)。

?

第8章?C++函數(shù)的高級(jí)特性

對(duì)比于C語言的函數(shù),C++增加了重載(overloaded)、內(nèi)聯(lián)(inline)、const和virtual四種新機(jī)制。其中重載和內(nèi)聯(lián)機(jī)制既可用于全局函數(shù)也可用于類的成員函數(shù),const與virtual機(jī)制僅用于類的成員函數(shù)。

???????重載和內(nèi)聯(lián)肯定有其好處才會(huì)被C++語言采納,但是不可以當(dāng)成免費(fèi)的午餐而濫用。本章將探究重載和內(nèi)聯(lián)的優(yōu)點(diǎn)與局限性,說明什么情況下應(yīng)該采用、不該采用以及要警惕錯(cuò)用。

8.1?函數(shù)重載的概念

8.1.1?重載的起源

????自然語言中,一個(gè)詞可以有許多不同的含義,即該詞被重載了。人們可以通過上下文來判斷該詞到底是哪種含義。“詞的重載”可以使語言更加簡(jiǎn)練。例如“吃飯”的含義十分廣泛,人們沒有必要每次非得說清楚具體吃什么不可。別迂腐得象孔已己,說茴香豆的茴字有四種寫法。

????在C++程序中,可以將語義、功能相似的幾個(gè)函數(shù)用同一個(gè)名字表示,即函數(shù)重載。這樣便于記憶,提高了函數(shù)的易用性,這是C++語言采用重載機(jī)制的一個(gè)理由。例如示例8-1-1中的函數(shù)EatBeef,EatFish,EatChicken可以用同一個(gè)函數(shù)名Eat表示,用不同類型的參數(shù)加以區(qū)別。

?

?

void EatBeef(…);???????//?可以改為?????void Eat(Beef …);

void EatFish(…);???????//?可以改為?????void Eat(Fish …);

void EatChicken(…);????//?可以改為?????void Eat(Chicken …);

?

示例8-1-1?重載函數(shù)Eat

?

????C++語言采用重載機(jī)制的另一個(gè)理由是:類的構(gòu)造函數(shù)需要重載機(jī)制。因?yàn)镃++規(guī)定構(gòu)造函數(shù)與類同名(請(qǐng)參見第9章),構(gòu)造函數(shù)只能有一個(gè)名字。如果想用幾種不同的方法創(chuàng)建對(duì)象該怎么辦?別無選擇,只能用重載機(jī)制來實(shí)現(xiàn)。所以類可以有多個(gè)同名的構(gòu)造函數(shù)。

?

8.1.2?重載是如何實(shí)現(xiàn)的?

????幾個(gè)同名的重載函數(shù)仍然是不同的函數(shù),它們是如何區(qū)分的呢?我們自然想到函數(shù)接口的兩個(gè)要素:參數(shù)與返回值。

如果同名函數(shù)的參數(shù)不同(包括類型、順序不同),那么容易區(qū)別出它們是不同的函數(shù)。

如果同名函數(shù)僅僅是返回值類型不同,有時(shí)可以區(qū)分,有時(shí)卻不能。例如:

void Function(void);

int?Function (void);

上述兩個(gè)函數(shù),第一個(gè)沒有返回值,第二個(gè)的返回值是int類型。如果這樣調(diào)用函數(shù):

????int?x = Function ();

則可以判斷出Function是第二個(gè)函數(shù)。問題是在C++/C程序中,我們可以忽略函數(shù)的返回值。在這種情況下,編譯器和程序員都不知道哪個(gè)Function函數(shù)被調(diào)用。

????所以只能靠參數(shù)而不能靠返回值類型的不同來區(qū)分重載函數(shù)。編譯器根據(jù)參數(shù)為每個(gè)重載函數(shù)產(chǎn)生不同的內(nèi)部標(biāo)識(shí)符。例如編譯器為示例8-1-1中的三個(gè)Eat函數(shù)產(chǎn)生象_eat_beef、_eat_fish、_eat_chicken之類的內(nèi)部標(biāo)識(shí)符(不同的編譯器可能產(chǎn)生不同風(fēng)格的內(nèi)部標(biāo)識(shí)符)。

?

如果C++程序要調(diào)用已經(jīng)被編譯后的C函數(shù),該怎么辦?

假設(shè)某個(gè)C函數(shù)的聲明如下:

void foo(int x, int y);

該函數(shù)被C編譯器編譯后在庫中的名字為_foo,而C++編譯器則會(huì)產(chǎn)生像_foo_int_int之類的名字用來支持函數(shù)重載和類型安全連接。由于編譯后的名字不同,C++程序不能直接調(diào)用C函數(shù)。C++提供了一個(gè)C連接交換指定符號(hào)extern“C”來解決這個(gè)問題。例如:

extern “C”

{

???void foo(int x, int y);

???… //?其它函數(shù)

}

或者寫成

extern “C”

{

???#include “myheader.h”

???… //?其它C頭文件

}

這就告訴C++編譯譯器,函數(shù)foo是個(gè)C連接,應(yīng)該到庫中找名字_foo而不是找_foo_int_int。C++編譯器開發(fā)商已經(jīng)對(duì)C標(biāo)準(zhǔn)庫的頭文件作了extern“C”處理,所以我們可以用#include?直接引用這些頭文件。

?

????注意并不是兩個(gè)函數(shù)的名字相同就能構(gòu)成重載。全局函數(shù)和類的成員函數(shù)同名不算重載,因?yàn)楹瘮?shù)的作用域不同。例如:

????void Print(…);?????//?全局函數(shù)

????class A

????{…

????????void Print(…);?//?成員函數(shù)

????}

????不論兩個(gè)Print函數(shù)的參數(shù)是否不同,如果類的某個(gè)成員函數(shù)要調(diào)用全局函數(shù)Print,為了與成員函數(shù)Print區(qū)別,全局函數(shù)被調(diào)用時(shí)應(yīng)加‘::’標(biāo)志。如

????::Print(…);????//?表示Print是全局函數(shù)而非成員函數(shù)

?

8.1.3?當(dāng)心隱式類型轉(zhuǎn)換導(dǎo)致重載函數(shù)產(chǎn)生二義性

????示例8-1-3中,第一個(gè)output函數(shù)的參數(shù)是int類型,第二個(gè)output函數(shù)的參數(shù)是float類型。由于數(shù)字本身沒有類型,將數(shù)字當(dāng)作參數(shù)時(shí)將自動(dòng)進(jìn)行類型轉(zhuǎn)換(稱為隱式類型轉(zhuǎn)換)。語句output(0.5)將產(chǎn)生編譯錯(cuò)誤,因?yàn)榫幾g器不知道該將0.5轉(zhuǎn)換成int還是float類型的參數(shù)。隱式類型轉(zhuǎn)換在很多地方可以簡(jiǎn)化程序的書寫,但是也可能留下隱患。

?

# include <iostream.h>

void output( int x);????//?函數(shù)聲明

void output( float x);?//?函數(shù)聲明

?

void output( int x)

{

????cout << " output int " << x << endl ;

}

?

void output( float x)

{

????cout << " output float " << x << endl ;

}

?

void main(void)

{

????int???x = 1;

????float y = 1.0;

????output(x);??????????// output int 1

????output(y);??????????// output float 1

????output(1);??????????// output int 1

//?output(0.5);????????// error! ambiguous call,?因?yàn)樽詣?dòng)類型轉(zhuǎn)換

????output(int(0.5));???// output int 0

????output(float(0.5));?// output float 0.5

}

示例8-1-3?隱式類型轉(zhuǎn)換導(dǎo)致重載函數(shù)產(chǎn)生二義性

?

8.2?成員函數(shù)的重載、覆蓋與隱藏

????成員函數(shù)的重載、覆蓋(override)與隱藏很容易混淆,C++程序員必須要搞清楚概念,否則錯(cuò)誤將防不勝防。

?

8.2.1?重載與覆蓋

????成員函數(shù)被重載的特征:

(1)相同的范圍(在同一個(gè)類中);

(2)函數(shù)名字相同;

(3)參數(shù)不同;

(4)virtual關(guān)鍵字可有可無。

????覆蓋是指派生類函數(shù)覆蓋基類函數(shù),特征是:

(1)不同的范圍(分別位于派生類與基類);

(2)函數(shù)名字相同;

(3)參數(shù)相同;

(4)基類函數(shù)必須有virtual關(guān)鍵字。

????示例8-2-1中,函數(shù)Base::f(int)與Base::f(float)相互重載,而Base::g(void)被Derived::g(void)覆蓋。

?

#include <iostream.h>

????class Base

{

public:

?????????????void f(int x){ cout << "Base::f(int) " << x << endl; }

void f(float x){ cout << "Base::f(float) " << x << endl; }

?????virtual void g(void){ cout << "Base::g(void)" << endl;}

};

?

????class Derived : public Base

{

public:

?????virtual void g(void){ cout << "Derived::g(void)" << endl;}

};

?

????void main(void)

????{

?????Derived?d;

?????Base *pb = &d;

?????pb->f(42);????????// Base::f(int) 42

?????pb->f(3.14f);?????// Base::f(float) 3.14

?????pb->g();??????????// Derived::g(void)

}

示例8-2-1成員函數(shù)的重載和覆蓋

???

8.2.2?令人迷惑的隱藏規(guī)則

????本來僅僅區(qū)別重載與覆蓋并不算困難,但是C++的隱藏規(guī)則使問題復(fù)雜性陡然增加。這里“隱藏”是指派生類的函數(shù)屏蔽了與其同名的基類函數(shù),規(guī)則如下:

(1)如果派生類的函數(shù)與基類的函數(shù)同名,但是參數(shù)不同。此時(shí),不論有無virtual關(guān)鍵字,基類的函數(shù)將被隱藏(注意別與重載混淆)。

(2)如果派生類的函數(shù)與基類的函數(shù)同名,并且參數(shù)也相同,但是基類函數(shù)沒有virtual關(guān)鍵字。此時(shí),基類的函數(shù)被隱藏(注意別與覆蓋混淆)。

????示例程序8-2-2(a)中:

(1)函數(shù)Derived::f(float)覆蓋了Base::f(float)。

(2)函數(shù)Derived::g(int)隱藏了Base::g(float),而不是重載。

(3)函數(shù)Derived::h(float)隱藏了Base::h(float),而不是覆蓋。

?

#include <iostream.h>

????class Base

{

public:

????virtual?void f(float x){ cout << "Base::f(float) " << x << endl; }

void g(float x){ cout << "Base::g(float) " << x << endl; }

????????????void h(float x){ cout << "Base::h(float) " << x << endl; }

};

????class Derived : public Base

{

public:

????virtual?void f(float x){ cout << "Derived::f(float) " << x << endl; }

void g(int x){ cout << "Derived::g(int) " << x << endl; }

????????????void h(float x){ cout << "Derived::h(float) " << x << endl; }

};

示例8-2-2(a)成員函數(shù)的重載、覆蓋和隱藏

?

????據(jù)作者考察,很多C++程序員沒有意識(shí)到有“隱藏”這回事。由于認(rèn)識(shí)不夠深刻,“隱藏”的發(fā)生可謂神出鬼沒,常常產(chǎn)生令人迷惑的結(jié)果。

示例8-2-2(b)中,bp和dp指向同一地址,按理說運(yùn)行結(jié)果應(yīng)該是相同的,可事實(shí)并非這樣。

?

void main(void)

{

Derived?d;

Base *pb = &d;

Derived *pd = &d;

// Good : behavior depends solely on type of the object

pb->f(3.14f);?// Derived::f(float) 3.14

pd->f(3.14f);?// Derived::f(float) 3.14

?

// Bad : behavior depends on type of the pointer

pb->g(3.14f);?// Base::g(float) 3.14

pd->g(3.14f);?// Derived::g(int) 3????????(surprise!)

?

// Bad : behavior depends on type of the pointer

pb->h(3.14f);?// Base::h(float) 3.14??????(surprise!)

pd->h(3.14f);?// Derived::h(float) 3.14

}

示例8-2-2(b) 重載、覆蓋和隱藏的比較

8.2.3?擺脫隱藏

????隱藏規(guī)則引起了不少麻煩。示例8-2-3程序中,語句pd->f(10)的本意是想調(diào)用函數(shù)Base::f(int),但是Base::f(int)不幸被Derived::f(char *)隱藏了。由于數(shù)字10不能被隱式地轉(zhuǎn)化為字符串,所以在編譯時(shí)出錯(cuò)。

?

class Base

{

public:

void f(int x);

};

class Derived : public Base

{

public:

void f(char *str);

};

void Test(void)

{

Derived *pd = new Derived;

pd->f(10);????// error

}

示例8-2-3?由于隱藏而導(dǎo)致錯(cuò)誤

?

????從示例8-2-3看來,隱藏規(guī)則似乎很愚蠢。但是隱藏規(guī)則至少有兩個(gè)存在的理由:

u???????寫語句pd->f(10)的人可能真的想調(diào)用Derived::f(char *)函數(shù),只是他誤將參數(shù)寫錯(cuò)了。有了隱藏規(guī)則,編譯器就可以明確指出錯(cuò)誤,這未必不是好事。否則,編譯器會(huì)靜悄悄地將錯(cuò)就錯(cuò),程序員將很難發(fā)現(xiàn)這個(gè)錯(cuò)誤,流下禍根。

u???????假如類Derived有多個(gè)基類(多重繼承),有時(shí)搞不清楚哪些基類定義了函數(shù)f。如果沒有隱藏規(guī)則,那么pd->f(10)可能會(huì)調(diào)用一個(gè)出乎意料的基類函數(shù)f。盡管隱藏規(guī)則看起來不怎么有道理,但它的確能消滅這些意外。

?

示例8-2-3中,如果語句pd->f(10)一定要調(diào)用函數(shù)Base::f(int),那么將類Derived修改為如下即可。

class Derived : public Base

{

public:

void f(char *str);

void f(int x) { Base::f(x); }

};

8.3?參數(shù)的缺省值

有一些參數(shù)的值在每次函數(shù)調(diào)用時(shí)都相同,書寫這樣的語句會(huì)使人厭煩。C++語言采用參數(shù)的缺省值使書寫變得簡(jiǎn)潔(在編譯時(shí),缺省值由編譯器自動(dòng)插入)。

????參數(shù)缺省值的使用規(guī)則:

l?????????【規(guī)則8-3-1】參數(shù)缺省值只能出現(xiàn)在函數(shù)的聲明中,而不能出現(xiàn)在定義體中。

例如:

????void Foo(int x=0, int y=0);?//?正確,缺省值出現(xiàn)在函數(shù)的聲明中

?

????void Foo(int x=0, int y=0)?????//?錯(cuò)誤,缺省值出現(xiàn)在函數(shù)的定義體中

????{

????}

為什么會(huì)這樣?我想是有兩個(gè)原因:一是函數(shù)的實(shí)現(xiàn)(定義)本來就與參數(shù)是否有缺省值無關(guān),所以沒有必要讓缺省值出現(xiàn)在函數(shù)的定義體中。二是參數(shù)的缺省值可能會(huì)改動(dòng),顯然修改函數(shù)的聲明比修改函數(shù)的定義要方便。

?

l?????????【規(guī)則8-3-2】如果函數(shù)有多個(gè)參數(shù),參數(shù)只能從后向前挨個(gè)兒缺省,否則將導(dǎo)致函數(shù)調(diào)用語句怪模怪樣。

正確的示例如下:

void Foo(int x, int y=0, int z=0);

錯(cuò)誤的示例如下:

void Foo(int x=0, int y, int z=0);?

?

要注意,使用參數(shù)的缺省值并沒有賦予函數(shù)新的功能,僅僅是使書寫變得簡(jiǎn)潔一些。它可能會(huì)提高函數(shù)的易用性,但是也可能會(huì)降低函數(shù)的可理解性。所以我們只能適當(dāng)?shù)厥褂脜?shù)的缺省值,要防止使用不當(dāng)產(chǎn)生負(fù)面效果。示例8-3-2中,不合理地使用參數(shù)的缺省值將導(dǎo)致重載函數(shù)output產(chǎn)生二義性。

?

#include <iostream.h>

void output( int x);

void output( int x, float y=0.0);

?

void output( int x)

{

????cout << " output int " << x << endl ;

}

?

void output( int x, float y)

{

????cout << " output int " << x << " and float " << y << endl ;

}

?

void main(void)

{

????int x=1;

????float y=0.5;

//?output(x);??????????// error! ambiguous call

????output(x,y);????????// output int 1 and float 0.5

}

?

示例8-3-2?參數(shù)的缺省值將導(dǎo)致重載函數(shù)產(chǎn)生二義性

8.4?運(yùn)算符重載

8.4.1?概念

????在C++語言中,可以用關(guān)鍵字operator加上運(yùn)算符來表示函數(shù),叫做運(yùn)算符重載。例如兩個(gè)復(fù)數(shù)相加函數(shù):

????Complex Add(const Complex &a, const Complex &b);

可以用運(yùn)算符重載來表示:

????Complex operator +(const Complex &a, const Complex &b);

????運(yùn)算符與普通函數(shù)在調(diào)用時(shí)的不同之處是:對(duì)于普通函數(shù),參數(shù)出現(xiàn)在圓括號(hào)內(nèi);而對(duì)于運(yùn)算符,參數(shù)出現(xiàn)在其左、右側(cè)。例如

????Complex a, b, c;

????…

????c = Add(a, b);?//?用普通函數(shù)

????c = a + b;??????//?用運(yùn)算符?+

????如果運(yùn)算符被重載為全局函數(shù),那么只有一個(gè)參數(shù)的運(yùn)算符叫做一元運(yùn)算符,有兩個(gè)參數(shù)的運(yùn)算符叫做二元運(yùn)算符。

????如果運(yùn)算符被重載為類的成員函數(shù),那么一元運(yùn)算符沒有參數(shù),二元運(yùn)算符只有一個(gè)右側(cè)參數(shù),因?yàn)閷?duì)象自己成了左側(cè)參數(shù)。

????從語法上講,運(yùn)算符既可以定義為全局函數(shù),也可以定義為成員函數(shù)。文獻(xiàn)[Murray , p44-p47]對(duì)此問題作了較多的闡述,并總結(jié)了表8-4-1的規(guī)則。

?

運(yùn)算符

規(guī)則

所有的一元運(yùn)算符

建議重載為成員函數(shù)

= () [] ->

只能重載為成員函數(shù)

+= -= /= *= &= |= ~= %= >>= <<=

建議重載為成員函數(shù)

所有其它運(yùn)算符

建議重載為全局函數(shù)

表8-4-1?運(yùn)算符的重載規(guī)則

?

由于C++語言支持函數(shù)重載,才能將運(yùn)算符當(dāng)成函數(shù)來用,C語言就不行。我們要以平常心來對(duì)待運(yùn)算符重載:

(1)不要過分擔(dān)心自己不會(huì)用,它的本質(zhì)仍然是程序員們熟悉的函數(shù)。

(2)不要過分熱心地使用,如果它不能使代碼變得更加易讀易寫,那就別用,否則會(huì)自找麻煩。

?

8.4.2?不能被重載的運(yùn)算符

????在C++運(yùn)算符集合中,有一些運(yùn)算符是不允許被重載的。這種限制是出于安全方面的考慮,可防止錯(cuò)誤和混亂。

(1)不能改變C++內(nèi)部數(shù)據(jù)類型(如int,float等)的運(yùn)算符。

(2)不能重載‘.’,因?yàn)椤?’在類中對(duì)任何成員都有意義,已經(jīng)成為標(biāo)準(zhǔn)用法。

(3)不能重載目前C++運(yùn)算符集合中沒有的符號(hào),如#,@,$等。原因有兩點(diǎn),一是難以理解,二是難以確定優(yōu)先級(jí)。

(4)對(duì)已經(jīng)存在的運(yùn)算符進(jìn)行重載時(shí),不能改變優(yōu)先級(jí)規(guī)則,否則將引起混亂。

8.5?函數(shù)內(nèi)聯(lián)

8.5.1?用內(nèi)聯(lián)取代宏代碼

????C++?語言支持函數(shù)內(nèi)聯(lián),其目的是為了提高函數(shù)的執(zhí)行效率(速度)。

????在C程序中,可以用宏代碼提高執(zhí)行效率。宏代碼本身不是函數(shù),但使用起來象函數(shù)。預(yù)處理器用復(fù)制宏代碼的方式代替函數(shù)調(diào)用,省去了參數(shù)壓棧、生成匯編語言的CALL調(diào)用、返回參數(shù)、執(zhí)行return等過程,從而提高了速度。使用宏代碼最大的缺點(diǎn)是容易出錯(cuò),預(yù)處理器在復(fù)制宏代碼時(shí)常常產(chǎn)生意想不到的邊際效應(yīng)。例如

????#define MAX(a, b)???????(a) > (b) ? (a) : (b)

語句

result = MAX(i, j) + 2 ;

將被預(yù)處理器解釋為

????result = (i) > (j) ? (i) : (j) + 2 ;

由于運(yùn)算符‘+’比運(yùn)算符‘:’的優(yōu)先級(jí)高,所以上述語句并不等價(jià)于期望的

????result = ( (i) > (j) ? (i) : (j) ) + 2 ;

如果把宏代碼改寫為

????#define MAX(a, b)???????( (a) > (b) ? (a) : (b) )

則可以解決由優(yōu)先級(jí)引起的錯(cuò)誤。但是即使使用修改后的宏代碼也不是萬無一失的,例如語句?

result = MAX(i++, j);

將被預(yù)處理器解釋為

????result = (i++) > (j) ? (i++) : (j);

????對(duì)于C++?而言,使用宏代碼還有另一種缺點(diǎn):無法操作類的私有數(shù)據(jù)成員。

?

讓我們看看C++?的“函數(shù)內(nèi)聯(lián)”是如何工作的。對(duì)于任何內(nèi)聯(lián)函數(shù),編譯器在符號(hào)表里放入函數(shù)的聲明(包括名字、參數(shù)類型、返回值類型)。如果編譯器沒有發(fā)現(xiàn)內(nèi)聯(lián)函數(shù)存在錯(cuò)誤,那么該函數(shù)的代碼也被放入符號(hào)表里。在調(diào)用一個(gè)內(nèi)聯(lián)函數(shù)時(shí),編譯器首先檢查調(diào)用是否正確(進(jìn)行類型安全檢查,或者進(jìn)行自動(dòng)類型轉(zhuǎn)換,當(dāng)然對(duì)所有的函數(shù)都一樣)。如果正確,內(nèi)聯(lián)函數(shù)的代碼就會(huì)直接替換函數(shù)調(diào)用,于是省去了函數(shù)調(diào)用的開銷。這個(gè)過程與預(yù)處理有顯著的不同,因?yàn)轭A(yù)處理器不能進(jìn)行類型安全檢查,或者進(jìn)行自動(dòng)類型轉(zhuǎn)換。假如內(nèi)聯(lián)函數(shù)是成員函數(shù),對(duì)象的地址(this)會(huì)被放在合適的地方,這也是預(yù)處理器辦不到的。

C++?語言的函數(shù)內(nèi)聯(lián)機(jī)制既具備宏代碼的效率,又增加了安全性,而且可以自由操作類的數(shù)據(jù)成員。所以在C++?程序中,應(yīng)該用內(nèi)聯(lián)函數(shù)取代所有宏代碼,“斷言assert”恐怕是唯一的例外。assert是僅在Debug版本起作用的宏,它用于檢查“不應(yīng)該”發(fā)生的情況。為了不在程序的Debug版本和Release版本引起差別,assert不應(yīng)該產(chǎn)生任何副作用。如果assert是函數(shù),由于函數(shù)調(diào)用會(huì)引起內(nèi)存、代碼的變動(dòng),那么將導(dǎo)致Debug版本與Release版本存在差異。所以assert不是函數(shù),而是宏。(參見6.5節(jié)“使用斷言”)

?

8.5.2?內(nèi)聯(lián)函數(shù)的編程風(fēng)格

????關(guān)鍵字inline必須與函數(shù)定義體放在一起才能使函數(shù)成為內(nèi)聯(lián),僅將inline放在函數(shù)聲明前面不起任何作用。如下風(fēng)格的函數(shù)Foo不能成為內(nèi)聯(lián)函數(shù):

????inline void Foo(int x, int y); // inline僅與函數(shù)聲明放在一起

????void Foo(int x, int y)

????{

????????…

????}

而如下風(fēng)格的函數(shù)Foo則成為內(nèi)聯(lián)函數(shù):

????void Foo(int x, int y);????

????inline void Foo(int x, int y)???// inline與函數(shù)定義體放在一起

????{

????????…

????}

????所以說,inline是一種“用于實(shí)現(xiàn)的關(guān)鍵字”,而不是一種“用于聲明的關(guān)鍵字”。一般地,用戶可以閱讀函數(shù)的聲明,但是看不到函數(shù)的定義。盡管在大多數(shù)教科書中內(nèi)聯(lián)函數(shù)的聲明、定義體前面都加了inline關(guān)鍵字,但我認(rèn)為inline不應(yīng)該出現(xiàn)在函數(shù)的聲明中。這個(gè)細(xì)節(jié)雖然不會(huì)影響函數(shù)的功能,但是體現(xiàn)了高質(zhì)量C++/C程序設(shè)計(jì)風(fēng)格的一個(gè)基本原則:聲明與定義不可混為一談,用戶沒有必要、也不應(yīng)該知道函數(shù)是否需要內(nèi)聯(lián)。

????定義在類聲明之中的成員函數(shù)將自動(dòng)地成為內(nèi)聯(lián)函數(shù),例如

????class A

????{

public:

????????void Foo(int x, int y) { … } ?//?自動(dòng)地成為內(nèi)聯(lián)函數(shù)

????}

將成員函數(shù)的定義體放在類聲明之中雖然能帶來書寫上的方便,但不是一種良好的編程風(fēng)格,上例應(yīng)該改成:

????//?頭文件

class A

????{

public:

????????void Foo(int x, int y);

????}

????//?定義文件

????inline void A::Foo(int x, int y)

{

}

?

8.5.3?慎用內(nèi)聯(lián)

????內(nèi)聯(lián)能提高函數(shù)的執(zhí)行效率,為什么不把所有的函數(shù)都定義成內(nèi)聯(lián)函數(shù)?

????如果所有的函數(shù)都是內(nèi)聯(lián)函數(shù),還用得著“內(nèi)聯(lián)”這個(gè)關(guān)鍵字嗎?

????內(nèi)聯(lián)是以代碼膨脹(復(fù)制)為代價(jià),僅僅省去了函數(shù)調(diào)用的開銷,從而提高函數(shù)的執(zhí)行效率。如果執(zhí)行函數(shù)體內(nèi)代碼的時(shí)間,相比于函數(shù)調(diào)用的開銷較大,那么效率的收獲會(huì)很少。另一方面,每一處內(nèi)聯(lián)函數(shù)的調(diào)用都要復(fù)制代碼,將使程序的總代碼量增大,消耗更多的內(nèi)存空間。以下情況不宜使用內(nèi)聯(lián):

(1)如果函數(shù)體內(nèi)的代碼比較長(zhǎng),使用內(nèi)聯(lián)將導(dǎo)致內(nèi)存消耗代價(jià)較高。

(2)如果函數(shù)體內(nèi)出現(xiàn)循環(huán),那么執(zhí)行函數(shù)體內(nèi)代碼的時(shí)間要比函數(shù)調(diào)用的開銷大。

????類的構(gòu)造函數(shù)和析構(gòu)函數(shù)容易讓人誤解成使用內(nèi)聯(lián)更有效。要當(dāng)心構(gòu)造函數(shù)和析構(gòu)函數(shù)可能會(huì)隱藏一些行為,如“偷偷地”執(zhí)行了基類或成員對(duì)象的構(gòu)造函數(shù)和析構(gòu)函數(shù)。所以不要隨便地將構(gòu)造函數(shù)和析構(gòu)函數(shù)的定義體放在類聲明中。

一個(gè)好的編譯器將會(huì)根據(jù)函數(shù)的定義體,自動(dòng)地取消不值得的內(nèi)聯(lián)(這進(jìn)一步說明了inline不應(yīng)該出現(xiàn)在函數(shù)的聲明中)。

8.6?一些心得體會(huì)

????C++?語言中的重載、內(nèi)聯(lián)、缺省參數(shù)、隱式轉(zhuǎn)換等機(jī)制展現(xiàn)了很多優(yōu)點(diǎn),但是這些優(yōu)點(diǎn)的背后都隱藏著一些隱患。正如人們的飲食,少食和暴食都不可取,應(yīng)當(dāng)恰到好處。我們要辨證地看待C++的新機(jī)制,應(yīng)該恰如其分地使用它們。雖然這會(huì)使我們編程時(shí)多費(fèi)一些心思,少了一些痛快,但這才是編程的藝術(shù)。

?

第9章?類的構(gòu)造函數(shù)、析構(gòu)函數(shù)與賦值函數(shù)

構(gòu)造函數(shù)、析構(gòu)函數(shù)與賦值函數(shù)是每個(gè)類最基本的函數(shù)。它們太普通以致讓人容易麻痹大意,其實(shí)這些貌似簡(jiǎn)單的函數(shù)就象沒有頂蓋的下水道那樣危險(xiǎn)。

???????每個(gè)類只有一個(gè)析構(gòu)函數(shù)和一個(gè)賦值函數(shù),但可以有多個(gè)構(gòu)造函數(shù)(包含一個(gè)拷貝構(gòu)造函數(shù),其它的稱為普通構(gòu)造函數(shù))。對(duì)于任意一個(gè)類A,如果不想編寫上述函數(shù),C++編譯器將自動(dòng)為A產(chǎn)生四個(gè)缺省的函數(shù),如

????A(void);????????????????????//?缺省的無參數(shù)構(gòu)造函數(shù)

????A(const A &a);??????????????//?缺省的拷貝構(gòu)造函數(shù)

????~A(void);???????????????????//?缺省的析構(gòu)函數(shù)

????A & operate =(const A &a);?//?缺省的賦值函數(shù)

?

這不禁讓人疑惑,既然能自動(dòng)生成函數(shù),為什么還要程序員編寫?

原因如下:

(1)如果使用“缺省的無參數(shù)構(gòu)造函數(shù)”和“缺省的析構(gòu)函數(shù)”,等于放棄了自主“初始化”和“清除”的機(jī)會(huì),C++發(fā)明人Stroustrup的好心好意白費(fèi)了。

(2)“缺省的拷貝構(gòu)造函數(shù)”和“缺省的賦值函數(shù)”均采用“位拷貝”而非“值拷貝”的方式來實(shí)現(xiàn),倘若類中含有指針變量,這兩個(gè)函數(shù)注定將出錯(cuò)。

??????

對(duì)于那些沒有吃夠苦頭的C++程序員,如果他說編寫構(gòu)造函數(shù)、析構(gòu)函數(shù)與賦值函數(shù)很容易,可以不用動(dòng)腦筋,表明他的認(rèn)識(shí)還比較膚淺,水平有待于提高。

本章以類String的設(shè)計(jì)與實(shí)現(xiàn)為例,深入闡述被很多教科書忽視了的道理。String的結(jié)構(gòu)如下:

????class String

????{

?????public:

????????String(const char *str = NULL);?//?普通構(gòu)造函數(shù)

????????String(const String &other);????//?拷貝構(gòu)造函數(shù)

????????~ String(void);?????????????????//?析構(gòu)函數(shù)

????????String & operate =(const String &other);????//?賦值函數(shù)

?????private:

????????char??*m_data;????????????????//?用于保存字符串

????};

9.1?構(gòu)造函數(shù)與析構(gòu)函數(shù)的起源

???????作為比C更先進(jìn)的語言,C++提供了更好的機(jī)制來增強(qiáng)程序的安全性。C++編譯器具有嚴(yán)格的類型安全檢查功能,它幾乎能找出程序中所有的語法問題,這的確幫了程序員的大忙。但是程序通過了編譯檢查并不表示錯(cuò)誤已經(jīng)不存在了,在“錯(cuò)誤”的大家庭里,“語法錯(cuò)誤”的地位只能算是小弟弟。級(jí)別高的錯(cuò)誤通常隱藏得很深,就象狡猾的罪犯,想逮住他可不容易。

???????根據(jù)經(jīng)驗(yàn),不少難以察覺的程序錯(cuò)誤是由于變量沒有被正確初始化或清除造成的,而初始化和清除工作很容易被人遺忘。Stroustrup在設(shè)計(jì)C++語言時(shí)充分考慮了這個(gè)問題并很好地予以解決:把對(duì)象的初始化工作放在構(gòu)造函數(shù)中,把清除工作放在析構(gòu)函數(shù)中。當(dāng)對(duì)象被創(chuàng)建時(shí),構(gòu)造函數(shù)被自動(dòng)執(zhí)行。當(dāng)對(duì)象消亡時(shí),析構(gòu)函數(shù)被自動(dòng)執(zhí)行。這下就不用擔(dān)心忘了對(duì)象的初始化和清除工作。

???????構(gòu)造函數(shù)與析構(gòu)函數(shù)的名字不能隨便起,必須讓編譯器認(rèn)得出才可以被自動(dòng)執(zhí)行。Stroustrup的命名方法既簡(jiǎn)單又合理:讓構(gòu)造函數(shù)、析構(gòu)函數(shù)與類同名,由于析構(gòu)函數(shù)的目的與構(gòu)造函數(shù)的相反,就加前綴‘~’以示區(qū)別。

除了名字外,構(gòu)造函數(shù)與析構(gòu)函數(shù)的另一個(gè)特別之處是沒有返回值類型,這與返回值類型為void的函數(shù)不同。構(gòu)造函數(shù)與析構(gòu)函數(shù)的使命非常明確,就象出生與死亡,光溜溜地來光溜溜地去。如果它們有返回值類型,那么編譯器將不知所措。為了防止節(jié)外生枝,干脆規(guī)定沒有返回值類型。(以上典故參考了文獻(xiàn)[Eekel, p55-p56])

9.2?構(gòu)造函數(shù)的初始化表

???????構(gòu)造函數(shù)有個(gè)特殊的初始化方式叫“初始化表達(dá)式表”(簡(jiǎn)稱初始化表)。初始化表位于函數(shù)參數(shù)表之后,卻在函數(shù)體?{}?之前。這說明該表里的初始化工作發(fā)生在函數(shù)體內(nèi)的任何代碼被執(zhí)行之前。

???????構(gòu)造函數(shù)初始化表的使用規(guī)則:

u???????如果類存在繼承關(guān)系,派生類必須在其初始化表里調(diào)用基類的構(gòu)造函數(shù)。

例如

????class A

????{…

????????A(int x);???????// A的構(gòu)造函數(shù)

};?

????class B : public A

????{…

????????B(int x, int y);// B的構(gòu)造函數(shù)

????};

????B::B(int x, int y)

?????: A(x)?????????????//?在初始化表里調(diào)用A的構(gòu)造函數(shù)

????{

?????…

}??

u???????類的const常量只能在初始化表里被初始化,因?yàn)樗荒茉诤瘮?shù)體內(nèi)用賦值的方式來初始化(參見5.4節(jié))。

u???????類的數(shù)據(jù)成員的初始化可以采用初始化表或函數(shù)體內(nèi)賦值兩種方式,這兩種方式的效率不完全相同。

????非內(nèi)部數(shù)據(jù)類型的成員對(duì)象應(yīng)當(dāng)采用第一種方式初始化,以獲取更高的效率。例如

????class A

{…

????A(void);????????????????//?無參數(shù)構(gòu)造函數(shù)

????A(const A &other);??????//?拷貝構(gòu)造函數(shù)

????A & operate =( const A &other);?//?賦值函數(shù)

};

?

????class B

????{

?????public:

????????B(const A &a);?// B的構(gòu)造函數(shù)

?????private:?

????????A?m_a;?????????//?成員對(duì)象

};

?

示例9-2(a)中,類B的構(gòu)造函數(shù)在其初始化表里調(diào)用了類A的拷貝構(gòu)造函數(shù),從而將成員對(duì)象m_a初始化。

示例9-2 (b)中,類B的構(gòu)造函數(shù)在函數(shù)體內(nèi)用賦值的方式將成員對(duì)象m_a初始化。我們看到的只是一條賦值語句,但實(shí)際上B的構(gòu)造函數(shù)干了兩件事:先暗地里創(chuàng)建m_a對(duì)象(調(diào)用了A的無參數(shù)構(gòu)造函數(shù)),再調(diào)用類A的賦值函數(shù),將參數(shù)a賦給m_a。

?

B::B(const A &a)

?: m_a(a)??????????

{

???…

}

B::B(const A &a)

{

m_a = a;

}

?示例9-2(a)?成員對(duì)象在初始化表中被初始化??????示例9-2(b)?成員對(duì)象在函數(shù)體內(nèi)被初始化

?

對(duì)于內(nèi)部數(shù)據(jù)類型的數(shù)據(jù)成員而言,兩種初始化方式的效率幾乎沒有區(qū)別,但后者的程序版式似乎更清晰些。若類F的聲明如下:

class F

{

?public:

????F(int x, int y);????????//?構(gòu)造函數(shù)

?private:

????int m_x, m_y;

????int m_i, m_j;

}

示例9-2(c)中F的構(gòu)造函數(shù)采用了第一種初始化方式,示例9-2(d)中F的構(gòu)造函數(shù)采用了第二種初始化方式。

?

F::F(int x, int y)

?: m_x(x), m_y(y)??????????

{

???m_i = 0;

???m_j = 0;

}

F::F(int x, int y)

{

???m_x = x;

???m_y = y;

???m_i = 0;

???m_j = 0;

}

?示例9-2(c)?數(shù)據(jù)成員在初始化表中被初始化?????示例9-2(d)?數(shù)據(jù)成員在函數(shù)體內(nèi)被初始化

9.3?構(gòu)造和析構(gòu)的次序

???????構(gòu)造從類層次的最根處開始,在每一層中,首先調(diào)用基類的構(gòu)造函數(shù),然后調(diào)用成員對(duì)象的構(gòu)造函數(shù)。析構(gòu)則嚴(yán)格按照與構(gòu)造相反的次序執(zhí)行,該次序是唯一的,否則編譯器將無法自動(dòng)執(zhí)行析構(gòu)過程。

一個(gè)有趣的現(xiàn)象是,成員對(duì)象初始化的次序完全不受它們?cè)诔跏蓟碇写涡虻挠绊?#xff0c;只由成員對(duì)象在類中聲明的次序決定。這是因?yàn)轭惖穆暶魇俏ㄒ坏?#xff0c;而類的構(gòu)造函數(shù)可以有多個(gè),因此會(huì)有多個(gè)不同次序的初始化表。如果成員對(duì)象按照初始化表的次序進(jìn)行構(gòu)造,這將導(dǎo)致析構(gòu)函數(shù)無法得到唯一的逆序。[Eckel, p260-261]

9.4?示例:類String的構(gòu)造函數(shù)與析構(gòu)函數(shù)

???????// String的普通構(gòu)造函數(shù)

???????String::String(const char *str)

{

????if(str==NULL)

????{

????????m_data = new char[1];

????????*m_data =?‘\0’;

????}??

????else

????{

????????int length = strlen(str);

????????m_data = new char[length+1];

????????strcpy(m_data, str);

????}

}??

?

// String的析構(gòu)函數(shù)

???????String::~String(void)

{

????delete [] m_data;??

//?由于m_data是內(nèi)部數(shù)據(jù)類型,也可以寫成?delete m_data;

???????}

9.5?不要輕視拷貝構(gòu)造函數(shù)與賦值函數(shù)

???????由于并非所有的對(duì)象都會(huì)使用拷貝構(gòu)造函數(shù)和賦值函數(shù),程序員可能對(duì)這兩個(gè)函數(shù)有些輕視。請(qǐng)先記住以下的警告,在閱讀正文時(shí)就會(huì)多心:

u???????本章開頭講過,如果不主動(dòng)編寫拷貝構(gòu)造函數(shù)和賦值函數(shù),編譯器將以“位拷貝”的方式自動(dòng)生成缺省的函數(shù)。倘若類中含有指針變量,那么這兩個(gè)缺省的函數(shù)就隱含了錯(cuò)誤。以類String的兩個(gè)對(duì)象a,b為例,假設(shè)a.m_data的內(nèi)容為“hello”,b.m_data的內(nèi)容為“world”。

現(xiàn)將a賦給b,缺省賦值函數(shù)的“位拷貝”意味著執(zhí)行b.m_data?=?a.m_data。這將造成三個(gè)錯(cuò)誤:一是b.m_data原有的內(nèi)存沒被釋放,造成內(nèi)存泄露;二是b.m_data和a.m_data指向同一塊內(nèi)存,a或b任何一方變動(dòng)都會(huì)影響另一方;三是在對(duì)象被析構(gòu)時(shí),m_data被釋放了兩次。

?

u???????拷貝構(gòu)造函數(shù)和賦值函數(shù)非常容易混淆,常導(dǎo)致錯(cuò)寫、錯(cuò)用。拷貝構(gòu)造函數(shù)是在對(duì)象被創(chuàng)建時(shí)調(diào)用的,而賦值函數(shù)只能被已經(jīng)存在了的對(duì)象調(diào)用。以下程序中,第三個(gè)語句和第四個(gè)語句很相似,你分得清楚哪個(gè)調(diào)用了拷貝構(gòu)造函數(shù),哪個(gè)調(diào)用了賦值函數(shù)嗎?

String?a(“hello”);

String?b(“world”);

String?c = a;?//?調(diào)用了拷貝構(gòu)造函數(shù),最好寫成?c(a);

c = b; //?調(diào)用了賦值函數(shù)

本例中第三個(gè)語句的風(fēng)格較差,宜改寫成String c(a)?以區(qū)別于第四個(gè)語句。

9.6?示例:類String的拷貝構(gòu)造函數(shù)與賦值函數(shù)

????//?拷貝構(gòu)造函數(shù)

????String::String(const String &other)

????{??

//?允許操作other的私有成員m_data

????int length = strlen(other.m_data);?

????m_data = new char[length+1];

????strcpy(m_data, other.m_data);

}

?

//?賦值函數(shù)

????String & String::operate =(const String &other)

????{??

????????// (1)?檢查自賦值

????????if(this == &other)

????????????return *this;

???????

????????// (2)?釋放原有的內(nèi)存資源

????????delete [] m_data;

???????

????????//?(3)分配新的內(nèi)存資源,并復(fù)制內(nèi)容

????int length = strlen(other.m_data);?

????m_data = new char[length+1];

????????strcpy(m_data, other.m_data);

???????

????????//?(4)返回本對(duì)象的引用

????????return *this;

}??

???

????類String拷貝構(gòu)造函數(shù)與普通構(gòu)造函數(shù)(參見9.4節(jié))的區(qū)別是:在函數(shù)入口處無需與NULL進(jìn)行比較,這是因?yàn)椤耙谩辈豢赡苁荖ULL,而“指針”可以為NULL。

????類String的賦值函數(shù)比構(gòu)造函數(shù)復(fù)雜得多,分四步實(shí)現(xiàn):

(1)第一步,檢查自賦值。你可能會(huì)認(rèn)為多此一舉,難道有人會(huì)愚蠢到寫出?a = a?這樣的自賦值語句!的確不會(huì)。但是間接的自賦值仍有可能出現(xiàn),例如

???

//?內(nèi)容自賦值

b = a;

c = b;

a = c;?

//?地址自賦值

b = &a;

a = *b;

?

也許有人會(huì)說:“即使出現(xiàn)自賦值,我也可以不理睬,大不了化點(diǎn)時(shí)間讓對(duì)象復(fù)制自己而已,反正不會(huì)出錯(cuò)!”

他真的說錯(cuò)了。看看第二步的delete,自殺后還能復(fù)制自己?jiǎn)?#xff1f;所以,如果發(fā)現(xiàn)自賦值,應(yīng)該馬上終止函數(shù)。注意不要將檢查自賦值的if語句

if(this == &other)

錯(cuò)寫成為

????if( *this == other)

(2)第二步,用delete釋放原有的內(nèi)存資源。如果現(xiàn)在不釋放,以后就沒機(jī)會(huì)了,將造成內(nèi)存泄露。

(3)第三步,分配新的內(nèi)存資源,并復(fù)制字符串。注意函數(shù)strlen返回的是有效字符串長(zhǎng)度,不包含結(jié)束符‘\0’。函數(shù)strcpy則連‘\0’一起復(fù)制。

(4)第四步,返回本對(duì)象的引用,目的是為了實(shí)現(xiàn)象?a = b = c?這樣的鏈?zhǔn)奖磉_(dá)。注意不要將?return *this?錯(cuò)寫成?return this?。那么能否寫成return other?呢?效果不是一樣嗎?

不可以!因?yàn)槲覀儾恢绤?shù)other的生命期。有可能other是個(gè)臨時(shí)對(duì)象,在賦值結(jié)束后它馬上消失,那么return other返回的將是垃圾。

9.7?偷懶的辦法處理拷貝構(gòu)造函數(shù)與賦值函數(shù)

???????如果我們實(shí)在不想編寫拷貝構(gòu)造函數(shù)和賦值函數(shù),又不允許別人使用編譯器生成的缺省函數(shù),怎么辦?

???????偷懶的辦法是:只需將拷貝構(gòu)造函數(shù)和賦值函數(shù)聲明為私有函數(shù),不用編寫代碼。

例如:

????class A

????{?…

?????private:

????????A(const A &a);?????????????//?私有的拷貝構(gòu)造函數(shù)

????????A & operate =(const A &a);?//?私有的賦值函數(shù)

????};

?

如果有人試圖編寫如下程序:

????A?b(a);????//?調(diào)用了私有的拷貝構(gòu)造函數(shù)

????b = a;??????//?調(diào)用了私有的賦值函數(shù)

編譯器將指出錯(cuò)誤,因?yàn)橥饨绮豢梢圆僮鰽的私有函數(shù)。

9.8?如何在派生類中實(shí)現(xiàn)類的基本函數(shù)

???????基類的構(gòu)造函數(shù)、析構(gòu)函數(shù)、賦值函數(shù)都不能被派生類繼承。如果類之間存在繼承關(guān)系,在編寫上述基本函數(shù)時(shí)應(yīng)注意以下事項(xiàng):

u???????派生類的構(gòu)造函數(shù)應(yīng)在其初始化表里調(diào)用基類的構(gòu)造函數(shù)。

u???????基類與派生類的析構(gòu)函數(shù)應(yīng)該為虛(即加virtual關(guān)鍵字)。例如

#include <iostream.h>

class Base

{

?public:

????virtual ~Base() { cout<< "~Base" << endl ; }

};

?

class Derived : public Base

{

?public:

????virtual ~Derived() { cout<< "~Derived" << endl ; }

};

?

void main(void)

{

????Base * pB = new Derived;?// upcast

????delete pB;

}

?

輸出結(jié)果為:

???????~Derived

???????~Base

如果析構(gòu)函數(shù)不為虛,那么輸出結(jié)果為

???????~Base

?

u???????在編寫派生類的賦值函數(shù)時(shí),注意不要忘記對(duì)基類的數(shù)據(jù)成員重新賦值。例如:

class Base

{

?public:

????Base & operate =(const Base &other);????//?類Base的賦值函數(shù)

?private:

????int?m_i, m_j, m_k;

};

?

class Derived : public Base

{

?public:

????Derived & operate =(const Derived &other);?//?類Derived的賦值函數(shù)

?private:

????int?m_x, m_y, m_z;

};

?

Derived & Derived::operate =(const Derived &other)

{

????//(1)檢查自賦值

????if(this == &other)

????????return *this;

?

????//(2)對(duì)基類的數(shù)據(jù)成員重新賦值

????Base::operate =(other);?//?因?yàn)椴荒苤苯硬僮魉接袛?shù)據(jù)成員

?

????//(3)對(duì)派生類的數(shù)據(jù)成員賦值

????m_x = other.m_x;

????m_y = other.m_y;

????m_z = other.m_z;

?

????//(4)返回本對(duì)象的引用

????return *this;

}

?

9.9?一些心得體會(huì)

有些C++程序設(shè)計(jì)書籍稱構(gòu)造函數(shù)、析構(gòu)函數(shù)和賦值函數(shù)是類的“Big-Three”,它們的確是任何類最重要的函數(shù),不容輕視。

也許你認(rèn)為本章的內(nèi)容已經(jīng)夠多了,學(xué)會(huì)了就能平安無事,我不能作這個(gè)保證。如果你希望吃透“Big-Three”,請(qǐng)好好閱讀參考文獻(xiàn)[Cline] [Meyers] [Murry]。

?

?

?

第10章?類的繼承與組合

?

對(duì)象(Object)是類(Class)的一個(gè)實(shí)例(Instance)。如果將對(duì)象比作房子,那么類就是房子的設(shè)計(jì)圖紙。所以面向?qū)ο笤O(shè)計(jì)的重點(diǎn)是類的設(shè)計(jì),而不是對(duì)象的設(shè)計(jì)。

對(duì)于C++程序而言,設(shè)計(jì)孤立的類是比較容易的,難的是正確設(shè)計(jì)基類及其派生類。本章僅僅論述“繼承”(Inheritance)和“組合”(Composition)的概念。

注意,當(dāng)前面向?qū)ο蠹夹g(shù)的應(yīng)用熱點(diǎn)是COM和CORBA,這些內(nèi)容超出了C++教材的范疇,請(qǐng)閱讀COM和CORBA相關(guān)論著。

10.1?繼承

如果A是基類,B是A的派生類,那么B將繼承A的數(shù)據(jù)和函數(shù)。例如:

???????class A

{

?public:

??????????????void?Func1(void);

??????????????void?Func2(void);

};

?

class B : public A

{

?public:

??????????????void?Func3(void);

??????????????void?Func4(void);

};

?

???????main()

{

??????????????B?b;???????????????????

??????????????b.Func1();?????????????// B從A繼承了函數(shù)Func1

??????????????b.Func2();?????????????// B從A繼承了函數(shù)Func2

??????????????b.Func3();

??????????????b.Func4();

}

?

這個(gè)簡(jiǎn)單的示例程序說明了一個(gè)事實(shí):C++的“繼承”特性可以提高程序的可復(fù)用性。正因?yàn)椤袄^承”太有用、太容易用,才要防止亂用“繼承”。我們應(yīng)當(dāng)給“繼承”立一些使用規(guī)則。

?

l?????????【規(guī)則10-1-1】如果類A和類B毫不相關(guān),不可以為了使B的功能更多些而讓B繼承A的功能和屬性。不要覺得“白吃白不吃”,讓一個(gè)好端端的健壯青年無緣無故地吃人參補(bǔ)身體。

l?????????【規(guī)則10-1-2】若在邏輯上B是A的“一種”(a kind of?),則允許B繼承A的功能和屬性。例如男人(Man)是人(Human)的一種,男孩(Boy)是男人的一種。那么類Man可以從類Human派生,類Boy可以從類Man派生。

?????????class Human

{

??????????????????…

};

?????????class Man : public Human

{

??????????????????…

};

?????????class Boy : public Man

{

??????????????????…

};

?

u???????注意事項(xiàng)

【規(guī)則10-1-2】看起來很簡(jiǎn)單,但是實(shí)際應(yīng)用時(shí)可能會(huì)有意外,繼承的概念在程序世界與現(xiàn)實(shí)世界并不完全相同。

例如從生物學(xué)角度講,鴕鳥(Ostrich)是鳥(Bird)的一種,按理說類Ostrich應(yīng)該可以從類Bird派生。但是鴕鳥不能飛,那么Ostrich::Fly是什么東西?

class Bird

{

public:???

???????virtual void Fly(void);

};

?

class Ostrich : public Bird

{

};

?

例如從數(shù)學(xué)角度講,圓(Circle)是一種特殊的橢圓(Ellipse),按理說類Circle應(yīng)該可以從類Ellipse派生。但是橢圓有長(zhǎng)軸和短軸,如果圓繼承了橢圓的長(zhǎng)軸和短軸,豈非畫蛇添足?

???????所以更加嚴(yán)格的繼承規(guī)則應(yīng)當(dāng)是:若在邏輯上B是A的“一種”,并且A的所有功能和屬性對(duì)B而言都有意義,則允許B繼承A的功能和屬性。

10.2?組合

l?????????【規(guī)則10-2-1】若在邏輯上A是B的“一部分”(a part of),則不允許B從A派生,而是要用A和其它東西組合出B。

例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是頭(Head)的一部分,所以類Head應(yīng)該由類Eye、Nose、Mouth、Ear組合而成,不是派生而成。如示例10-2-1所示。

?

class Eye

{
?public:

void?Look(void);?

};

class Nose

{
?public:

void?Smell(void);

};

class Mouth

{
?public:

void?Eat(void);????

};

class Ear

{
?public:

void?Listen(void);

};

//?正確的設(shè)計(jì),雖然代碼冗長(zhǎng)。

class Head

{

?public:

??????????????void???????Look(void)?????{?m_eye.Look();?}

??????????????void???????Smell(void)?????{?m_nose.Smell();?}

??????????????void???????Eat(void)?{?m_mouth.Eat();?}

??????????????void???????Listen(void)????{?m_ear.Listen();?}

?private:

??????????????Eye??????m_eye;

??????????????Nose????m_nose;

??????????????Mouth?m_mouth;

??????????????Ear???????m_ear;

};

示例10-2-1 Head由Eye、Nose、Mouth、Ear組合而成

??????

如果允許Head從Eye、Nose、Mouth、Ear派生而成,那么Head將自動(dòng)具有Look、?Smell、Eat、Listen這些功能。示例10-2-2十分簡(jiǎn)短并且運(yùn)行正確,但是這種設(shè)計(jì)方法卻是不對(duì)的。

?

???????//?功能正確并且代碼簡(jiǎn)潔,但是設(shè)計(jì)方法不對(duì)。

class Head : public Eye, public Nose, public Mouth, public Ear

{

};

示例10-2-2?Head從Eye、Nose、Mouth、Ear派生而成

?

一只公雞使勁地追打一只剛下了蛋的母雞,你知道為什么嗎?

因?yàn)槟鸽u下了鴨蛋。

很多程序員經(jīng)不起“繼承”的誘惑而犯下設(shè)計(jì)錯(cuò)誤。“運(yùn)行正確”的程序不見得是高質(zhì)量的程序,此處就是一個(gè)例證。

?

?

第11章?其它編程經(jīng)驗(yàn)

11.1?使用const提高函數(shù)的健壯性

看到const關(guān)鍵字,C++程序員首先想到的可能是const常量。這可不是良好的條件反射。如果只知道用const定義常量,那么相當(dāng)于把火藥僅用于制作鞭炮。const更大的魅力是它可以修飾函數(shù)的參數(shù)、返回值,甚至函數(shù)的定義體。

const是constant的縮寫,“恒定不變”的意思。被const修飾的東西都受到強(qiáng)制保護(hù),可以預(yù)防意外的變動(dòng),能提高程序的健壯性。所以很多C++程序設(shè)計(jì)書籍建議:“Use const whenever you need”。

?

11.1.1?用const修飾函數(shù)的參數(shù)

如果參數(shù)作輸出用,不論它是什么數(shù)據(jù)類型,也不論它采用“指針傳遞”還是“引用傳遞”,都不能加const修飾,否則該參數(shù)將失去輸出功能。

const只能修飾輸入?yún)?shù):

u???????如果輸入?yún)?shù)采用“指針傳遞”,那么加const修飾可以防止意外地改動(dòng)該指針,起到保護(hù)作用。

例如StringCopy函數(shù):

????????void StringCopy(char *strDestination, const char *strSource);

其中strSource是輸入?yún)?shù),strDestination是輸出參數(shù)。給strSource加上const修飾后,如果函數(shù)體內(nèi)的語句試圖改動(dòng)strSource的內(nèi)容,編譯器將指出錯(cuò)誤。

?

u???????如果輸入?yún)?shù)采用“值傳遞”,由于函數(shù)將自動(dòng)產(chǎn)生臨時(shí)變量用于復(fù)制該參數(shù),該輸入?yún)?shù)本來就無需保護(hù),所以不要加const修飾。

例如不要將函數(shù)void Func1(int x)?寫成void Func1(const int x)。同理不要將函數(shù)void Func2(A a)?寫成void Func2(const A a)。其中A為用戶自定義的數(shù)據(jù)類型。

?

u???????對(duì)于非內(nèi)部數(shù)據(jù)類型的參數(shù)而言,象void Func(A a)?這樣聲明的函數(shù)注定效率比較底。因?yàn)楹瘮?shù)體內(nèi)將產(chǎn)生A類型的臨時(shí)對(duì)象用于復(fù)制參數(shù)a,而臨時(shí)對(duì)象的構(gòu)造、復(fù)制、析構(gòu)過程都將消耗時(shí)間。

為了提高效率,可以將函數(shù)聲明改為void Func(A &a),因?yàn)椤耙脗鬟f”僅借用一下參數(shù)的別名而已,不需要產(chǎn)生臨時(shí)對(duì)象。但是函數(shù)void Func(A &a)?存在一個(gè)缺點(diǎn):“引用傳遞”有可能改變參數(shù)a,這是我們不期望的。解決這個(gè)問題很容易,加const修飾即可,因此函數(shù)最終成為void Func(const A &a)。

以此類推,是否應(yīng)將void Func(int x)?改寫為void Func(const int &x),以便提高效率?完全沒有必要,因?yàn)閮?nèi)部數(shù)據(jù)類型的參數(shù)不存在構(gòu)造、析構(gòu)的過程,而復(fù)制也非常快,“值傳遞”和“引用傳遞”的效率幾乎相當(dāng)。

????問題是如此的纏綿,我只好將“const &”修飾輸入?yún)?shù)的用法總結(jié)一下,如表11-1-1所示。

?

對(duì)于非內(nèi)部數(shù)據(jù)類型的輸入?yún)?shù),應(yīng)該將“值傳遞”的方式改為“const引用傳遞”,目的是提高效率。例如將void Func(A a)?改為void Func(const A &a)。

?

對(duì)于內(nèi)部數(shù)據(jù)類型的輸入?yún)?shù),不要將“值傳遞”的方式改為“const引用傳遞”。否則既達(dá)不到提高效率的目的,又降低了函數(shù)的可理解性。例如void Func(int x)?不應(yīng)該改為void Func(const int &x)。

?

表11-1-1?“const &”修飾輸入?yún)?shù)的規(guī)則

?

11.1.2?用const修飾函數(shù)的返回值

u???????如果給以“指針傳遞”方式的函數(shù)返回值加const修飾,那么函數(shù)返回值(即指針)的內(nèi)容不能被修改,該返回值只能被賦給加const修飾的同類型指針。

例如函數(shù)

????????const char * GetString(void);

如下語句將出現(xiàn)編譯錯(cuò)誤:

????????char *str = GetString();

正確的用法是

????????const char *str = GetString();

?

u???????如果函數(shù)返回值采用“值傳遞方式”,由于函數(shù)會(huì)把返回值復(fù)制到外部臨時(shí)的存儲(chǔ)單元中,加const修飾沒有任何價(jià)值。

????例如不要把函數(shù)int GetInt(void)?寫成const int GetInt(void)。

????同理不要把函數(shù)A GetA(void)?寫成const A GetA(void),其中A為用戶自定義的數(shù)據(jù)類型。

????如果返回值不是內(nèi)部數(shù)據(jù)類型,將函數(shù)A GetA(void)?改寫為const A & GetA(void)的確能提高效率。但此時(shí)千萬千萬要小心,一定要搞清楚函數(shù)究竟是想返回一個(gè)對(duì)象的“拷貝”還是僅返回“別名”就可以了,否則程序會(huì)出錯(cuò)。見6.2節(jié)“返回值的規(guī)則”。

?

u???????函數(shù)返回值采用“引用傳遞”的場(chǎng)合并不多,這種方式一般只出現(xiàn)在類的賦值函數(shù)中,目的是為了實(shí)現(xiàn)鏈?zhǔn)奖磉_(dá)。

例如

????class A

????{…

????????A & operate = (const A &other);?//?賦值函數(shù)

????};

????A a, b, c;??????// a, b, c?為A的對(duì)象

????…

????a = b = c;??????????//?正常的鏈?zhǔn)劫x值

????(a = b) = c;????????//?不正常的鏈?zhǔn)劫x值,但合法

如果將賦值函數(shù)的返回值加const修飾,那么該返回值的內(nèi)容不允許被改動(dòng)。上例中,語句?a = b = c仍然正確,但是語句?(a = b) = c?則是非法的。

?

11.1.3 const成員函數(shù)

????任何不會(huì)修改數(shù)據(jù)成員的函數(shù)都應(yīng)該聲明為const類型。如果在編寫const成員函數(shù)時(shí),不慎修改了數(shù)據(jù)成員,或者調(diào)用了其它非const成員函數(shù),編譯器將指出錯(cuò)誤,這無疑會(huì)提高程序的健壯性。

以下程序中,類stack的成員函數(shù)GetCount僅用于計(jì)數(shù),從邏輯上講GetCount應(yīng)當(dāng)為const函數(shù)。編譯器將指出GetCount函數(shù)中的錯(cuò)誤。

????class Stack

{

?????public:

????????void????Push(int elem);

????????int?????Pop(void);

????????int?????GetCount(void)?const;?// const成員函數(shù)

?????private:

????????int?????m_num;

????????int?????m_data[100];

};

?

????int Stack::GetCount(void)?const

{
????????++ m_num;???//?編譯錯(cuò)誤,企圖修改數(shù)據(jù)成員m_num

????Pop();?????//?編譯錯(cuò)誤,企圖調(diào)用非const函數(shù)

????return m_num;

????}

????const成員函數(shù)的聲明看起來怪怪的:const關(guān)鍵字只能放在函數(shù)聲明的尾部,大概是因?yàn)槠渌胤蕉家呀?jīng)被占用了。

11.2?提高程序的效率

程序的時(shí)間效率是指運(yùn)行速度,空間效率是指程序占用內(nèi)存或者外存的狀況。

全局效率是指站在整個(gè)系統(tǒng)的角度上考慮的效率,局部效率是指站在模塊或函數(shù)角度上考慮的效率。

?

l?????????【規(guī)則11-2-1】不要一味地追求程序的效率,應(yīng)當(dāng)在滿足正確性、可靠性、健壯性、可讀性等質(zhì)量因素的前提下,設(shè)法提高程序的效率。

?

l?????????【規(guī)則11-2-2】以提高程序的全局效率為主,提高局部效率為輔。

?

l?????????【規(guī)則11-2-3】在優(yōu)化程序的效率時(shí),應(yīng)當(dāng)先找出限制效率的“瓶頸”,不要在無關(guān)緊要之處優(yōu)化。

?

l?????????【規(guī)則11-2-4】先優(yōu)化數(shù)據(jù)結(jié)構(gòu)和算法,再優(yōu)化執(zhí)行代碼。

?

l?????????【規(guī)則11-2-5】有時(shí)候時(shí)間效率和空間效率可能對(duì)立,此時(shí)應(yīng)當(dāng)分析那個(gè)更重要,作出適當(dāng)?shù)恼壑浴@缍嗷ㄙM(fèi)一些內(nèi)存來提高性能。

?

l?????????【規(guī)則11-2-6】不要追求緊湊的代碼,因?yàn)榫o湊的代碼并不能產(chǎn)生高效的機(jī)器碼。

?

11.3?一些有益的建議

2????????【建議11-3-1】當(dāng)心那些視覺上不易分辨的操作符發(fā)生書寫錯(cuò)誤。

我們經(jīng)常會(huì)把“==”誤寫成“=”,象“||”、“&&”、“<=”、“>=”這類符號(hào)也很容易發(fā)生“丟1”失誤。然而編譯器卻不一定能自動(dòng)指出這類錯(cuò)誤。

?

2????????【建議11-3-2】變量(指針、數(shù)組)被創(chuàng)建之后應(yīng)當(dāng)及時(shí)把它們初始化,以防止把未被初始化的變量當(dāng)成右值使用。

?

2????????【建議11-3-3】當(dāng)心變量的初值、缺省值錯(cuò)誤,或者精度不夠。

?

2????????【建議11-3-4】當(dāng)心數(shù)據(jù)類型轉(zhuǎn)換發(fā)生錯(cuò)誤。盡量使用顯式的數(shù)據(jù)類型轉(zhuǎn)換(讓人們知道發(fā)生了什么事),避免讓編譯器輕悄悄地進(jìn)行隱式的數(shù)據(jù)類型轉(zhuǎn)換。

?

2????????【建議11-3-5】當(dāng)心變量發(fā)生上溢或下溢,數(shù)組的下標(biāo)越界。

?

2????????【建議11-3-6】當(dāng)心忘記編寫錯(cuò)誤處理程序,當(dāng)心錯(cuò)誤處理程序本身有誤。

?

2????????【建議11-3-7】當(dāng)心文件I/O有錯(cuò)誤。

?

2????????【建議11-3-8】避免編寫技巧性很高代碼。

?

2????????【建議11-3-9】不要設(shè)計(jì)面面俱到、非常靈活的數(shù)據(jù)結(jié)構(gòu)。

?

2????????【建議11-3-10】如果原有的代碼質(zhì)量比較好,盡量復(fù)用它。但是不要修補(bǔ)很差勁的代碼,應(yīng)當(dāng)重新編寫。

?

2????????【建議11-3-11】盡量使用標(biāo)準(zhǔn)庫函數(shù),不要“發(fā)明”已經(jīng)存在的庫函數(shù)。

?

2????????【建議11-3-12】盡量不要使用與具體硬件或軟件環(huán)境關(guān)系密切的變量。

?

2????????【建議11-3-13】把編譯器的選擇項(xiàng)設(shè)置為最嚴(yán)格狀態(tài)。

?

2????????【建議11-3-14】如果可能的話,使用PC-Lint、LogiScope等工具進(jìn)行代碼審查。

?

?

?

參考文獻(xiàn)

[Cline]?Marshall P. Cline and Greg A. Lomow, C++ FAQs, Addison-Wesley, 1995

?

[Eckel]?Bruce Eckel, Thinking in C++(C++?編程思想,劉宗田?等譯),機(jī)械工業(yè)出版社,2000

?

[Maguire]?Steve Maguire, Writing Clean Code(編程精粹,姜靜波?等譯),電子工業(yè)出版社,1993

?

[Meyers]?Scott Meyers, Effective C++, Addison-Wesley, 1992

?

[Murry]?Robert B. Murry, C++ Strategies and Tactics, Addison-Wesley, 1993

?

[Summit]?Steve Summit, C Programming FAQs, Addison-Wesley, 1996

?

?

附錄A?:C++/C代碼審查表

文件結(jié)構(gòu)

重要性

審查項(xiàng)

結(jié)論

?

頭文件和定義文件的名稱是否合理?

?

?

頭文件和定義文件的目錄結(jié)構(gòu)是否合理?

?

?

版權(quán)和版本聲明是否完整?

?

重要

頭文件是否使用了?ifndef/define/endif?預(yù)處理塊?

?

?

頭文件中是否只存放“聲明”而不存放“定義”

?

?

……

?

程序的版式

重要性

審查項(xiàng)

結(jié)論

?

空行是否得體?

?

?

代碼行內(nèi)的空格是否得體?

?

?

長(zhǎng)行拆分是否得體?

?

?

“{”?和?“}”?是否各占一行并且對(duì)齊于同一列?

?

重要

一行代碼是否只做一件事?如只定義一個(gè)變量,只寫一條語句。

?

重要

If、for、while、do等語句自占一行,不論執(zhí)行語句多少都要加“{}”。

?

重要

在定義變量(或參數(shù))時(shí),是否將修飾符?*?和?&?緊靠變量名?

?

?

注釋是否清晰并且必要?

?

重要

注釋是否有錯(cuò)誤或者可能導(dǎo)致誤解?

?

重要

類結(jié)構(gòu)的public, protected, private順序是否在所有的程序中保持一致?

?

?

……

?

命名規(guī)則

重要性

審查項(xiàng)

結(jié)論

重要

命名規(guī)則是否與所采用的操作系統(tǒng)或開發(fā)工具的風(fēng)格保持一致?

?

?

標(biāo)識(shí)符是否直觀且可以拼讀?

?

?

標(biāo)識(shí)符的長(zhǎng)度應(yīng)當(dāng)符合“min-length && max-information”原則?

?

重要

程序中是否出現(xiàn)相同的局部變量和全部變量?

?

?

類名、函數(shù)名、變量和參數(shù)、常量的書寫格式是否遵循一定的規(guī)則?

?

?

靜態(tài)變量、全局變量、類的成員變量是否加前綴?

?

?

……

?

表達(dá)式與基本語句

重要性

審查項(xiàng)

結(jié)論

重要

如果代碼行中的運(yùn)算符比較多,是否已經(jīng)用括號(hào)清楚地確定表達(dá)式的操作順序?

?

?

是否編寫太復(fù)雜或者多用途的復(fù)合表達(dá)式?

?

重要

是否將復(fù)合表達(dá)式與“真正的數(shù)學(xué)表達(dá)式”混淆?

?

重要

是否用隱含錯(cuò)誤的方式寫if語句??例如

(1)將布爾變量直接與TRUE、FALSE或者1、0進(jìn)行比較。

(2)將浮點(diǎn)變量用“==”或“!=”與任何數(shù)字比較。

(3)將指針變量用“==”或“!=”與NULL比較。

?

?

如果循環(huán)體內(nèi)存在邏輯判斷,并且循環(huán)次數(shù)很大,是否已經(jīng)將邏輯判斷移到循環(huán)體的外面?

?

重要

Case語句的結(jié)尾是否忘了加break?

?

重要

是否忘記寫switch的default分支?

?

重要

使用goto?語句時(shí)是否留下隱患??例如跳過了某些對(duì)象的構(gòu)造、變量的初始化、重要的計(jì)算等。

?

?

……

?

常量

重要性

審查項(xiàng)

結(jié)論

?

是否使用含義直觀的常量來表示那些將在程序中多次出現(xiàn)的數(shù)字或字符串?

?

?

在C++?程序中,是否用const常量取代宏常量?

?

重要

如果某一常量與其它常量密切相關(guān),是否在定義中包含了這種關(guān)系?

?

?

是否誤解了類中的const數(shù)據(jù)成員?因?yàn)閏onst數(shù)據(jù)成員只在某個(gè)對(duì)象生存期內(nèi)是常量,而對(duì)于整個(gè)類而言卻是可變的。

?

?

……

?

函數(shù)設(shè)計(jì)

重要性

審查項(xiàng)

結(jié)論

?

參數(shù)的書寫是否完整?不要貪圖省事只寫參數(shù)的類型而省略參數(shù)名字。

?

?

參數(shù)命名、順序是否合理?

?

?

參數(shù)的個(gè)數(shù)是否太多?

?

?

是否使用類型和數(shù)目不確定的參數(shù)?

?

?

是否省略了函數(shù)返回值的類型?

?

?

函數(shù)名字與返回值類型在語義上是否沖突?

?

重要

是否將正常值和錯(cuò)誤標(biāo)志混在一起返回?正常值應(yīng)當(dāng)用輸出參數(shù)獲得,而錯(cuò)誤標(biāo)志用return語句返回。

?

重要

在函數(shù)體的“入口處”,是否用assert對(duì)參數(shù)的有效性進(jìn)行檢查?

?

重要

使用濫用了assert??例如混淆非法情況與錯(cuò)誤情況,后者是必然存在的并且是一定要作出處理的。

?

重要

return語句是否返回指向“棧內(nèi)存”的“指針”或者“引用”?

?

?

是否使用const提高函數(shù)的健壯性?const可以強(qiáng)制保護(hù)函數(shù)的參數(shù)、返回值,甚至函數(shù)的定義體。“Use const whenever you need”

?

?

……

?

內(nèi)存管理

重要性

審查項(xiàng)

結(jié)論

重要

用malloc或new申請(qǐng)內(nèi)存之后,是否立即檢查指針值是否為NULL?(防止使用指針值為NULL的內(nèi)存)

?

重要

是否忘記為數(shù)組和動(dòng)態(tài)內(nèi)存賦初值?(防止將未被初始化的內(nèi)存作為右值使用)

?

重要

數(shù)組或指針的下標(biāo)是否越界?

?

重要

動(dòng)態(tài)內(nèi)存的申請(qǐng)與釋放是否配對(duì)?(防止內(nèi)存泄漏)

?

重要

是否有效地處理了“內(nèi)存耗盡”問題?

?

重要

是否修改“指向常量的指針”的內(nèi)容?

?

重要

是否出現(xiàn)野指針?例如

(1)指針變量沒有被初始化。

(2)用free或delete釋放了內(nèi)存之后,忘記將指針設(shè)置為NULL。

?

重要

是否將malloc/free?和?new/delete?混淆使用?

?

重要

malloc語句是否正確無誤?例如字節(jié)數(shù)是否正確?類型轉(zhuǎn)換是否正確?

?

重要

在創(chuàng)建與釋放動(dòng)態(tài)對(duì)象數(shù)組時(shí),new/delete的語句是否正確無誤?

?

?

……

?

C++?函數(shù)的高級(jí)特性

重要性

審查項(xiàng)

結(jié)論

?

重載函數(shù)是否有二義性?

?

重要

是否混淆了成員函數(shù)的重載、覆蓋與隱藏?

?

?

運(yùn)算符的重載是否符合制定的編程規(guī)范?

?

?

是否濫用內(nèi)聯(lián)函數(shù)?例如函數(shù)體內(nèi)的代碼比較長(zhǎng),函數(shù)體內(nèi)出現(xiàn)循環(huán)。

?

重要

是否用內(nèi)聯(lián)函數(shù)取代了宏代碼?

?

?

……

?

類的構(gòu)造函數(shù)、析構(gòu)函數(shù)和賦值函數(shù)

重要性

審查項(xiàng)

結(jié)論

重要

是否違背編程規(guī)范而讓C++?編譯器自動(dòng)為類產(chǎn)生四個(gè)缺省的函數(shù):(1)缺省的無參數(shù)構(gòu)造函數(shù);(2)缺省的拷貝構(gòu)造函數(shù);(3)缺省的析構(gòu)函數(shù);(4)缺省的賦值函數(shù)。

?

重要

構(gòu)造函數(shù)中是否遺漏了某些初始化工作?

?

重要

是否正確地使用構(gòu)造函數(shù)的初始化表?

?

重要

析構(gòu)函數(shù)中是否遺漏了某些清除工作?

?

?

是否錯(cuò)寫、錯(cuò)用了拷貝構(gòu)造函數(shù)和賦值函數(shù)?

?

重要

賦值函數(shù)一般分四個(gè)步驟:(1)檢查自賦值;(2)釋放原有內(nèi)存資源;(3)分配新的內(nèi)存資源,并復(fù)制內(nèi)容;(4)返回?*this。是否遺漏了重要步驟?

?

重要

是否正確地編寫了派生類的構(gòu)造函數(shù)、析構(gòu)函數(shù)、賦值函數(shù)?注意事項(xiàng):

(1)派生類不可能繼承基類的構(gòu)造函數(shù)、析構(gòu)函數(shù)、賦值函數(shù)。

(2)派生類的構(gòu)造函數(shù)應(yīng)在其初始化表里調(diào)用基類的構(gòu)造函數(shù)。

(3)基類與派生類的析構(gòu)函數(shù)應(yīng)該為虛(即加virtual關(guān)鍵字)。

(4)在編寫派生類的賦值函數(shù)時(shí),注意不要忘記對(duì)基類的數(shù)據(jù)成員重新賦值。

?

?

……

?

類的高級(jí)特性

重要性

審查項(xiàng)

結(jié)論

重要

是否違背了繼承和組合的規(guī)則?

(1)若在邏輯上B是A的“一種”,并且A的所有功能和屬性對(duì)B而言都有意義,則允許B繼承A的功能和屬性。

(2)若在邏輯上A是B的“一部分”(a part of),則不允許B從A派生,而是要用A和其它東西組合出B。

?

?

……

?

其它常見問題

重要性

審查項(xiàng)

結(jié)論

重要

數(shù)據(jù)類型問題:

(1)變量的數(shù)據(jù)類型有錯(cuò)誤嗎?

(2)存在不同數(shù)據(jù)類型的賦值嗎?

(3)存在不同數(shù)據(jù)類型的比較嗎?

?

重要

變量值問題:

(1)變量的初始化或缺省值有錯(cuò)誤嗎?

(2)變量發(fā)生上溢或下溢嗎?

(3)變量的精度夠嗎?

?

重要

邏輯判斷問題:

(1)由于精度原因?qū)е卤容^無效嗎?

(2)表達(dá)式中的優(yōu)先級(jí)有誤嗎?

(3)邏輯判斷結(jié)果顛倒嗎?

?

重要

循環(huán)問題:

(1)循環(huán)終止條件不正確嗎?

(2)無法正常終止(死循環(huán))嗎?

(3)錯(cuò)誤地修改循環(huán)變量嗎?

(4)存在誤差累積嗎?

?

重要

錯(cuò)誤處理問題:

(1)忘記進(jìn)行錯(cuò)誤處理嗎?

(2)錯(cuò)誤處理程序塊一直沒有機(jī)會(huì)被運(yùn)行?

(3)錯(cuò)誤處理程序塊本身就有毛病嗎?如報(bào)告的錯(cuò)誤與實(shí)際錯(cuò)誤不一致,處理方式不正確等等。

(4)錯(cuò)誤處理程序塊是“馬后炮”嗎?如在被它被調(diào)用之前軟件已經(jīng)出錯(cuò)。

?

重要

文件I/O問題:

(1)對(duì)不存在的或者錯(cuò)誤的文件進(jìn)行操作嗎?

(2)文件以不正確的方式打開嗎?

(3)文件結(jié)束判斷不正確嗎?

(4)沒有正確地關(guān)閉文件嗎?

?

?

?

附錄B?:C++/C試題

???????本試題僅用于考查C++/C程序員的基本編程技能。內(nèi)容限于C++/C常用語法,不涉及數(shù)據(jù)結(jié)構(gòu)、算法以及深?yuàn)W的語法。考試成績(jī)能反映出考生的編程質(zhì)量以及對(duì)C++/C的理解程度,但不能反映考生的智力和軟件開發(fā)能力。

???????筆試時(shí)間90分鐘。請(qǐng)考生認(rèn)真答題,切勿輕視。

?

一、請(qǐng)?zhí)顚態(tài)OOL , float,?指針變量?與“零值”比較的?if?語句。(10分)

提示:這里“零值”可以是0, 0.0 , FALSE或者“空指針”。例如?int?變量?n?與“零值”比較的?if?語句為:

????if ( n == 0 )

????if ( n != 0 )

以此類推。

?

請(qǐng)寫出?BOOL?flag?與“零值”比較的?if?語句:

?

?

請(qǐng)寫出?float?x?與“零值”比較的?if?語句:

?

?

?

請(qǐng)寫出?char ?*p?與“零值”比較的?if?語句:

?

?

?

二、以下為Windows NT下的32位C++程序,請(qǐng)計(jì)算sizeof的值(10分)

?

???????char?str[] = “Hello” ;

???????char???*p = str ;

int?????n = 10;

請(qǐng)計(jì)算

sizeof (str ) =???????

?????????

sizeof ( p ) =???????

??????????

sizeof ( n ) =

void Func ( char str[100])

{

請(qǐng)計(jì)算

?sizeof( str ) =???

}

?

void *p = malloc( 100 );

請(qǐng)計(jì)算

sizeof ( p ) =

?

?

三、簡(jiǎn)答題(25分)

?

1、頭文件中的?ifndef/define/endif?干什么用?

?

?

?

2、#include?<filename.h>???和?#include?“filename.h”?有什么區(qū)別?

?

?

?

3、const?有什么用途?(請(qǐng)至少說明兩種)

?

?

?

4、在C++?程序中調(diào)用被?C編譯器編譯后的函數(shù),為什么要加?extern “C”聲明?

?

?

?

?

5、請(qǐng)簡(jiǎn)述以下兩個(gè)for循環(huán)的優(yōu)缺點(diǎn)

?

//?第一個(gè)

for (i=0; i<N; i++)

{

if (condition)

????DoSomething();

else

????DoOtherthing();

}

//?第二個(gè)

if (condition)

{

for (i=0; i<N; i++)

????DoSomething();

}

else

{

????for (i=0; i<N; i++)

????DoOtherthing();

}

優(yōu)點(diǎn):

?

?

缺點(diǎn):

?

?

?

優(yōu)點(diǎn):

?

?

缺點(diǎn):

?

?

?

四、有關(guān)內(nèi)存的思考題(20分)

?

void GetMemory(char *p)

{

p = (char *)malloc(100);

}

void Test(void)

{

char *str = NULL;

GetMemory(str);??

strcpy(str, "hello world");

printf(str);

}

?

請(qǐng)問運(yùn)行Test函數(shù)會(huì)有什么樣的結(jié)果?

答:

?

?

?

?

char *GetMemory(void)

{??

char p[] = "hello world";

return p;

}

void Test(void)

{

char *str = NULL;

str = GetMemory();???

printf(str);

}

?

請(qǐng)問運(yùn)行Test函數(shù)會(huì)有什么樣的結(jié)果?

答:

Void GetMemory2(char **p, int num)

{

*p = (char *)malloc(num);

}

void Test(void)

{

char *str = NULL;

GetMemory(&str, 100);

strcpy(str, "hello");??

printf(str);???

}

請(qǐng)問運(yùn)行Test函數(shù)會(huì)有什么樣的結(jié)果?

答:

?

?

?

?

?

void Test(void)

{

char *str = (char *) malloc(100);

????strcpy(str,?“hello”);

????free(str);????

????if(str != NULL)

????{

?????strcpy(str,?“world”);

printf(str);

}

}

請(qǐng)問運(yùn)行Test函數(shù)會(huì)有什么樣的結(jié)果?

答:

?

?

?

?

?

?

五、編寫strcpy函數(shù)(10分)

已知strcpy函數(shù)的原型是

???????char *strcpy(char *strDest, const char *strSrc);

???????其中strDest是目的字符串,strSrc是源字符串。

?

(1)不調(diào)用C++/C的字符串庫函數(shù),請(qǐng)編寫函數(shù)?strcpy

?

?

?

?

?

?

?

(2)strcpy能把strSrc的內(nèi)容復(fù)制到strDest,為什么還要char *?類型的返回值?

?

?

?

?

六、編寫類String的構(gòu)造函數(shù)、析構(gòu)函數(shù)和賦值函數(shù)(25分)

已知類String的原型為:

????class String

????{

?????public:

????????String(const char *str = NULL);?//?普通構(gòu)造函數(shù)

????????String(const String &other);????????//?拷貝構(gòu)造函數(shù)

????????~ String(void);?????????????????????//?析構(gòu)函數(shù)

????????String & operate =(const String &other);????//?賦值函數(shù)

?????private:

????????char??*m_data;????????????????//?用于保存字符串

????};

???????請(qǐng)編寫String的上述4個(gè)函數(shù)。

?

附錄C?:C++/C試題的答案與評(píng)分標(biāo)準(zhǔn)

一、請(qǐng)?zhí)顚態(tài)OOL , float,?指針變量?與“零值”比較的?if?語句。(10分)

?

請(qǐng)寫出?BOOL?flag?與“零值”比較的?if?語句。(3分)

標(biāo)準(zhǔn)答案:

????if ( flag )

????if ( !flag )

如下寫法均屬不良風(fēng)格,不得分。

????if (flag == TRUE)??

????if (flag == 1 )????

????if (flag == FALSE) ?

????????if (flag == 0)????

請(qǐng)寫出?float?x?與“零值”比較的?if?語句。(4分)

標(biāo)準(zhǔn)答案示例:

const float EPSINON = 0.00001;

if ((x >= - EPSINON) && (x <= EPSINON)

不可將浮點(diǎn)變量用“==”或“!=”與數(shù)字比較,應(yīng)該設(shè)法轉(zhuǎn)化成“>=”或“<=”此類形式。

??

?

如下是錯(cuò)誤的寫法,不得分。

????if (x == 0.0)??

????if (x != 0.0)??????

???

請(qǐng)寫出?char ?*p?與“零值”比較的?if?語句。(3分)

標(biāo)準(zhǔn)答案:

????if?(p == NULL)

????if?(p != NULL)

如下寫法均屬不良風(fēng)格,不得分。

????if (p == 0)

????if (p != 0)????

????if (p) ?

????????if (!)????

?

二、以下為Windows NT下的32位C++程序,請(qǐng)計(jì)算sizeof的值(10分)

?

???????char?str[] = “Hello” ;

???????char???*p = str ;

int?????n = 10;

請(qǐng)計(jì)算

sizeof (str ) =?6???(2分)

?????????

sizeof ( p ) =???4???(2分)

??????????

sizeof ( n ) =???4???(2分)

void Func ( char str[100])

{

請(qǐng)計(jì)算

?sizeof( str ) =???4?????(2分)

}

?

void *p = malloc( 100 );

請(qǐng)計(jì)算

sizeof ( p ) =?4??????(2分)

?

?

三、簡(jiǎn)答題(25分)

?

1、頭文件中的?ifndef/define/endif?干什么用?(5分)

答:防止該頭文件被重復(fù)引用。

?

2、#include?<filename.h>???和?#include?“filename.h”?有什么區(qū)別?(5分)

答:對(duì)于#include?<filename.h>?,編譯器從標(biāo)準(zhǔn)庫路徑開始搜索?filename.h

????對(duì)于#include?“filename.h”?,編譯器從用戶的工作路徑開始搜索?filename.h

?

3、const?有什么用途?(請(qǐng)至少說明兩種)(5分)

答:(1)可以定義?const?常量

(2)const可以修飾函數(shù)的參數(shù)、返回值,甚至函數(shù)的定義體。被const修飾的東西都受到強(qiáng)制保護(hù),可以預(yù)防意外的變動(dòng),能提高程序的健壯性。

?

4、在C++?程序中調(diào)用被?C編譯器編譯后的函數(shù),為什么要加?extern “C”??(5分)

答:C++語言支持函數(shù)重載,C語言不支持函數(shù)重載。函數(shù)被C++編譯后在庫中的名字與C語言的不同。假設(shè)某個(gè)函數(shù)的原型為:?void foo(int x, int y);

該函數(shù)被C編譯器編譯后在庫中的名字為_foo,而C++編譯器則會(huì)產(chǎn)生像_foo_int_int之類的名字。

C++提供了C連接交換指定符號(hào)extern“C”來解決名字匹配問題。

?

5、請(qǐng)簡(jiǎn)述以下兩個(gè)for循環(huán)的優(yōu)缺點(diǎn)(5分)

?

for (i=0; i<N; i++)

{

if (condition)

????DoSomething();

else

????DoOtherthing();

}

if (condition)

{

for (i=0; i<N; i++)

????DoSomething();

}

else

{

????for (i=0; i<N; i++)

????DoOtherthing();

}

優(yōu)點(diǎn):程序簡(jiǎn)潔

?

缺點(diǎn):多執(zhí)行了N-1次邏輯判斷,并且打斷了循環(huán)“流水線”作業(yè),使得編譯器不能對(duì)循環(huán)進(jìn)行優(yōu)化處理,降低了效率。

優(yōu)點(diǎn):循環(huán)的效率高

?

缺點(diǎn):程序不簡(jiǎn)潔

?

?

?

四、有關(guān)內(nèi)存的思考題(每小題5分,共20分)

?

void GetMemory(char *p)

{

p = (char *)malloc(100);

}

void Test(void)

{

char *str = NULL;

GetMemory(str);??

strcpy(str, "hello world");

printf(str);

}

?

請(qǐng)問運(yùn)行Test函數(shù)會(huì)有什么樣的結(jié)果?

答:程序崩潰。

因?yàn)镚etMemory并不能傳遞動(dòng)態(tài)內(nèi)存,

Test函數(shù)中的?str一直都是?NULL。

strcpy(str, "hello world");將使程序崩潰。

?

char *GetMemory(void)

{??

char p[] = "hello world";

return p;

}

void Test(void)

{

char *str = NULL;

str = GetMemory();???

printf(str);

}

?

請(qǐng)問運(yùn)行Test函數(shù)會(huì)有什么樣的結(jié)果?

答:可能是亂碼。

因?yàn)镚etMemory返回的是指向“棧內(nèi)存”的指針,該指針的地址不是?NULL,但其原現(xiàn)的內(nèi)容已經(jīng)被清除,新內(nèi)容不可知。

void GetMemory2(char **p, int num)

{

*p = (char *)malloc(num);

}

void Test(void)

{

char *str = NULL;

GetMemory(&str, 100);

strcpy(str, "hello");??

printf(str);???

}

請(qǐng)問運(yùn)行Test函數(shù)會(huì)有什么樣的結(jié)果?

答:

(1)能夠輸出hello

(2)內(nèi)存泄漏

?

?

void Test(void)

{

char *str = (char *) malloc(100);

????strcpy(str,?“hello”);

????free(str);????

????if(str != NULL)

????{

?????strcpy(str,?“world”);

printf(str);

}

}

請(qǐng)問運(yùn)行Test函數(shù)會(huì)有什么樣的結(jié)果?

答:篡改動(dòng)態(tài)內(nèi)存區(qū)的內(nèi)容,后果難以預(yù)料,非常危險(xiǎn)。

因?yàn)閒ree(str);之后,str成為野指針,

if(str != NULL)語句不起作用。

?

?

?

五、編寫strcpy函數(shù)(10分)

已知strcpy函數(shù)的原型是

???????char *strcpy(char *strDest, const char *strSrc);

???????其中strDest是目的字符串,strSrc是源字符串。

(1)不調(diào)用C++/C的字符串庫函數(shù),請(qǐng)編寫函數(shù)?strcpy

char *strcpy(char *strDest, const char *strSrc);

{

????assert((strDest!=NULL) && (strSrc?!=NULL));?// 2分

????char *address =?strDest;???????????????????// 2分

????while( (*strDest++ = *?strSrc++) !=?‘\0’?)????// 2分

???????NULL ;

????return address ;??????????????????????????// 2分

}

?

(2)strcpy能把strSrc的內(nèi)容復(fù)制到strDest,為什么還要char *?類型的返回值?

答:為了實(shí)現(xiàn)鏈?zhǔn)奖磉_(dá)式。??????????????????????????????????????????????// 2分

例如???????int length = strlen( strcpy( strDest, “hello world”) );

?

六、編寫類String的構(gòu)造函數(shù)、析構(gòu)函數(shù)和賦值函數(shù)(25分)

已知類String的原型為:

????class String

????{

?????public:

????????String(const char *str = NULL);?//?普通構(gòu)造函數(shù)

????????String(const String &other);????????//?拷貝構(gòu)造函數(shù)

????????~ String(void);?????????????????????//?析構(gòu)函數(shù)

????????String & operate =(const String &other);????//?賦值函數(shù)

?????private:

????????char??*m_data;????????????????//?用于保存字符串

????};

???????請(qǐng)編寫String的上述4個(gè)函數(shù)。

標(biāo)準(zhǔn)答案:

?

// String的析構(gòu)函數(shù)

???????String::~String(void)???????????????// 3分

{

????delete [] m_data;?????????????????????

//?由于m_data是內(nèi)部數(shù)據(jù)類型,也可以寫成?delete m_data;

???????}

?

???????// String的普通構(gòu)造函數(shù)????????????

???????String::String(const char *str)??????// 6分

{

????if(str==NULL)?????????????????????????

????{

???????m_data = new char[1];????//?若能加?NULL?判斷則更好

???????*m_data =?‘\0’;?????????????????????

????}?????????????????????????????????????????

????else

????{

???????int length = strlen(str);??????????

???????m_data = new char[length+1];?//?若能加?NULL?判斷則更好?????

???????strcpy(m_data, str);???????????????

????}

}??

//?拷貝構(gòu)造函數(shù)

????String::String(const String &other)???// 3分

????{??

????int length = strlen(other.m_data);

????m_data = new char[length+1];??????//?若能加?NULL?判斷則更好???

????strcpy(m_data, other.m_data);????????

}

//?賦值函數(shù)

????String & String::operate =(const String &other)????// 13分

????{??

???????// (1)?檢查自賦值?????????????????????// 4分

???????if(this == &other)

???????????return *this;

???

// (2)?釋放原有的內(nèi)存資源????????????// 3分

???????delete [] m_data;

??????

???????//?(3)分配新的內(nèi)存資源,并復(fù)制內(nèi)容?// 3分

????int length = strlen(other.m_data);

????m_data = new char[length+1];?????????//?若能加?NULL?判斷則更好

????????strcpy(m_data, other.m_data);

??????

???????//?(4)返回本對(duì)象的引用????????????// 3分

???????return *this;

}??

總結(jié)

以上是生活随笔為你收集整理的高质量C++/C编程指南(林锐)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。