讀Thinking in C++卷一後的總結(1)

斷斷續續的學習C++編程已經有一年多的歷史了,在學習了一些基本的語法後,基本可以使用C++語言編程,也拿這個語言參加比賽實現了比較複雜的算法。當時很得意,覺得C++語言也就那麼回事,殊不知在大三實習面試的過程中,大部分面試在考察對C++的理解和編程實踐方面都會問看了C++方面的哪些書籍,很多都會問有沒有看過Thinking in C++,還會考察對STL的理解,如容器,迭代器,算法等。而這些都是我沒注意過的,備受打擊的我決定要仔細研讀Thinking in C++。當我在Internet上搜索Thinking in C++時,我才發現大家這本書的評價非常高,這更堅定了我讀這本書的決心。隨着對這本書的深入,我才發現作爲一個計算機專業的學生,我以前對編程語言的理解是多麼的膚淺。這本書讓我明白對編程語言的理解最起碼要從編譯器的角度來思考,只有從編譯器的角度來思考自己編寫的程序了,自己的編程技藝纔算真正的入了門。但是要記住對編程語言的熟練掌握也僅僅是掌握了一種使用工具的技能,而算法纔是一切的靈魂。

Thinking in C++共有兩卷,第一卷主要講解了C++的一些基本特性(可概括爲面向對象的三大特性和模板機制),作者對這些特性剖析的非常深入,不僅告訴你這些特性是這樣的,還告訴你這些特性爲什麼會被設計成這樣,瞭解了第一卷的內容,使用C++語言編程時你將擁有更多的自信,敲代碼時會覺得非常有力,而且時刻會考慮和審視自己的代碼,會思考爲什麼要這樣實現設計;第二卷主要講解了C++的一些高級特性,如如何構建穩定的系統,STL,以及一些專題部分,本人還未完成該卷的研讀,現僅完成了5章的內容,所以不做過多的陳述。這裏主要對第一卷的內容做做總結,總結C++的基本特性的重要內容以及自己的一些體會,以後也會對第二卷做一個總結。

Thinking in C++第一卷中共有16章內容,前3章內容是對象導言,對象的創建與使用以及C++中的C,對於這3章內容這裏不做總結,因爲第2章和第3章的內容都是一些基本的內容,很容易掌握,而對於第1章的內容,我的建議是讀完整本書後對什麼是面向對象編程有深刻理解後再看也不遲。我還建議,研讀Thinking in C++最好看英文版的,因爲中文版的翻譯很多地方有錯誤,如果英文不太好,可以中文英文對比着看。

下面開始進入主題吧,對於第一卷的416章的內容,我分爲四個部分來做出總結,具體如下:

一、面向對象特性之一封裝,包括下列章節內容:

4章:數據抽象、第5章:隱藏實現、第6章:初始化和清除、第7章:函數重載和默認參數、第8章:常量、第9章:內聯函數、第10章:名字控制、第11章:引用和複製構造函數、第12章:運算符重載、第13章:動態對象創建。

這個部分的內容多而且雜,但整體上來看都是爲封裝服務的,於是將它們歸於一類,後面會對每一章節做出總結。

二、面向對象特性之二繼承,包括一章的內容:

14章:繼承和組合。

   這章的內容會討論如何選擇繼承和組合的問題。

三、面向對象特性之三多態,包括一章的內容:

    15章:多態性和函數。

    實際上前面封裝和繼承兩部分的內容都是爲多態服務的,它們是多態機制實現的基礎。這章的內容將會討論多態的很多特性。

四、面向對象編程很重要的一個內容,模板機制,包括一章的內容:

16章:模板介紹。

模板是面向對象編程不可或缺的一個特性,它提供了重用源代碼的方法,有了模板機制纔有了像STL這樣優秀的庫,極大的提高了編程效率。

 

一、封裝

1.  數據抽象

數據抽象,簡單一點來講就ADT,就是將作用在一個數據集合上的一些操作函數和該數據集合放在一起,這個“放在一起”指的是邏輯代碼上的,而非物理內存上的。如將作用在結構體數據上的操作函數,放到結構體內部,這就形成了一個ADT。使用這個結構創建的變量就叫做這個結構類型的對象,調用該對象的成員函數就是向該對象發送消息。將數據和函數捆綁在一起的好處是可以防止名字衝突。

該章內容除了數據抽象外,還應該注意的內容包括:C++中的無數據結構應當擁有最小的非零長度(C中是不合法的);C++中有嚴格的類型檢查(解決這個問題可以使用C++的顯示轉換);頭文件和實現文件分離,頭文件是放置接口規範的地方(聲明),而實現文件對聲明定義,且只能定義一次,頭文件中不應當使用using指令,這將破壞名字空間保護。

 

2.       隱藏實現

該章重點討論了結構中的邊界問題。這讓我想起了搞研究,對於研究對象,我們首先應思考的是該對象的外延,而後內涵。外延實際上就是研究對象的範圍內的元素組成的集合,在這個集合外的東西是跟對該對象的研究無關的,也就是確定我們研究的重點範圍是什麼。這裏的邊界問題和這個類似,不同的人對一個結構的關注重點是不一樣的,如開發該結構的人重點關注該結構的實現,如publicprotectedprivate成員和函數的確定或實現,他們研究對象的外延就是整個結構中的元素(成員和函數);而對於使用該結構的人來說,這個結構是怎麼實現的並不關注,他們關注的是這個結構提供給我哪些功能,他們研究對象的外延就是該結構中的public元素。C++就是通過publicprotectedprivate訪問控制確定了結構中的邊界問題(當然也是邏輯代碼上的,而不是物理內存上的,訪問控制只在編譯期間有效,而運行期間不再檢查)。

C++中還有友元的概念,當在一個結構中聲明瞭友元(全局函數或結構中的函數或整個結構)時,那麼該友元就可以訪問該結構中的成員或函數,從friend的角度來看,C++不是一個純面向對象的語言。

如果將一個結構的struct關鍵字換成class,那麼這就是類了。結構和類的區別在於,結構默認訪問控制是public,而類的默認訪問控制是private

對於有些保密性比較強的項目,雖然客戶不能夠看到其實現部分,但可能在頭文件中看到一些策略信息。如頭文件中可以看到的一些private函數的聲明,客戶雖然不能直接調用它們,但仍然看得見其聲明。如何將這些private可見的函數或成員聲明也隱藏起來呢?可以使用句柄類技術,其簡單的一個例子描述如下:

頭文件:

#ifndef HANDLE_H
#define HANDLE_H
class Handle {
    struct Cheshire; // Class declaration only
    Cheshire* smile;
public:
    void initialize();
    void cleanup();
    int read();
    void change(int);
};
#endif //HANDLE_H

實現文件:

#include"Handle.h"
#include"../require.h"
// DefineHandle's implementation:
structHandle::Cheshire {
    int i;
};
voidHandle::initialize() {
    smile = new Cheshire;
    smile->i = 0;
}
voidHandle::cleanup() {
    delete smile;
}
int Handle::read(){
    return smile->i;
}
voidHandle::change(int x) {
    smile->i = x;
}

    可以看到,在實現文件中定義Cheshire結構,然後在類的構造函數中初始化Cheshire結構類型的smile指針,這樣在public的成員函數中通過smile指針來調用Cheshire結構中的數據或函數。這樣客戶在頭文件中只能看見public數據和函數,而不見private的數據或函數,因此也看不見可能會泄露的策略信息。

    句柄類技術還解決了重複編譯的問題。當一個文件被修改或其包含的頭文件(共有或私有成員的聲明部分)被修改,那麼該文件都要重新編譯。而句柄類技術將更多的聲明部分放到了實現文件中,這樣只要頭文件不變,那麼包含該頭文件的文件就需要變動,而實現文件可以任意的改動,完成後只需要對實現文件進行重編譯。

 

3.       初始與清除

編程中很多錯誤的發生都是錯誤的初始化或清除引起的,因此C++在初始和清除做了很多工作。C++對象的初始化是構造函數完成的,清除對象是析構函數完成的。當一個對象被聲明後C++總是試圖調用構造函數對它進行初始,當該對象超出作用域後總會調用析構函數。構造函數調用時,編譯器會傳遞一個this指針到構造函數,這個this指針指向的是對象數據成員的內存地址,也就是對象的地址。

當一個結構中沒有構造函數,編譯器會自動爲它創建一個默認構造函數,但這個編譯器合成的默認構造函數很多時候完成的工作並不是我們期待的。編譯器合成的默認構造函數只會對全局變量(未初始化或對象默認初始)進行初始,如果全局變量是對象並默認初始,則該對象須有默認構造函數;而對於結構中的成員數據,如果成員數據是內置類型(int,float,char,*等)是不會對它們進行初始的,如果成員數據是對象則該對象必須有默認構造函數。因此我們應該明確自己的構造函數,而不讓編譯器來完成。對於結構中的成員數據,它們的初始都應該在構造函數參數列表中完成,有的情況下應該在函數體中完成。如數據成員爲內部類型的數組時,因爲其沒有默認構造函數,因此放到函數體中是合理的,當然一個更好的方法是將內置類型封裝成類,再添加一個默認構造函數,這樣不需要在參數列表中或函數體中顯示初始化,編譯器就能夠幫助完成默認的初始化。

 

4.       函數重載與默認參數

函數重載的主要目的是方便的使用函數名,根據函數參數列表的不同來選擇調用函數,也就是同一個函數名的函數實現有區別的功能,但它們的功能又有共性,如構造函數,它們都是完成初始化工作的。默認參數的作用則是減少了函數重載的數量。如:stash(int size); stash(int size, int initQuantity);可以用一個函數來代替stash(intsize, int initQuantity=0);。默認參數只能放在函數聲明中,編譯器必須在使用函數前知道默認值,有時爲了閱讀方便在函數定義處放一些默認值的註釋。

函數重載和默認參數選擇的問題:1)如果一個默認值成了一個標識,就意味着函數體中使用了兩個不同的有效版本,一個用於正常情況,一個用於錯誤情況。這種情況還不如使用兩個不相干的函數來維護兩段代碼,這樣維護更方便一些,尤其函數比較大時。2)當發現兩個函數在使用默認參數後根本不會導致函數的定義改變時,可以使用默認參數將兩個函數合併。

 

5.       常量

const最初的動機是代替預處理來進行值代替。從這以後它被用到指針、函數參數傳遞和返回、類對象及類成員數據和函數。

    1)C++中的constC中的const的區別

當在C++中定義一個constint a = 5;時,編譯器不僅會爲a分配空間而且會將a的定義保存到符號表裏,這就是常量摺疊,編譯期間是可以確定a的值的,因此再用a來定義數組長度時不會有什麼錯誤,如int b[a];。而在C中這樣是不可以的,因爲C編譯器會分配空間但不會進行常量摺疊。C++中的const用於集合時,如數組,C++編譯器不會複雜到將整個集合保存到符號表中,因此編譯期間是不知道集合中的元素值的,即使以對該集合初始化了。

C++中的constC中的const的另一個區別是它們的鏈接方式,C++中默認內部鏈接,而C中默認外部鏈接。要使C++中的const具有外部鏈接,那麼在定義該const變量的文件中要類似這樣定義:extern const int a = 5;,該文件中編譯器仍然常量摺疊。在其它文件中要使用該const變量時,需先聲明如下:extern const int a;,但在該文件中編譯器就不能常量摺疊了。

    2)const和指針

const和指針一起可以構成指向const的指針和cosnt指針。指向const的指針聲明可以如下:cosntint* u;int cons* u;,其意義是不能通過u指針修改u指向的空間的內容,因爲該內容是cosnt的。Const指針聲明如下:int* const w;,其意義是不能修改w的指向,因爲該指針變量是const的。

C++對類型檢查是非常精細的,這也擴展到指針賦值,如不可以把一個const對象的地址賦給一個非const指針。當然總能用強制類型轉換實現這樣的賦值。

一個沒有強調const的地方是字符串常量,如char*p = “howdy”;,這不會報錯,但當通過p指針對”howdy”進行修改是往往會導致運行時錯誤,當然不是所有的編譯器都可以做到這一點的。

    3)const和函數的參數和返回值

當按值傳遞或返回一個內部類型時,const是沒有用的。值傳遞很明顯,const沒有意義,當按值返回一個內部類型時,const沒有意義的原因是,它是一個值而不是一個變量。

如果按值傳遞對象,用const限制沒有意義; 如果按值返回一個用戶自定義的const對象,這意味着返回值不會被修改,不能爲左值。

如果傳遞並返回地址,則const將保證該地址的內容不會被改變。當const限定傳遞的指針或引用時,則表明在該函數中不能對指針指向的空間或與引用相關聯的空間進行修改。而返回一個const指針或引用則取決於設計該函數的程序員想幹什麼。

從上面的內容可以看出:當按值返回內部類型或任何類型的指針時,均不能爲左值;而返回自定義對象或引用則可以爲左值,除非使用const進行限定。

標準參數傳遞:參數傳遞時,如果想在函數體中對該傳遞參數的值進行改變最好用指針的方式傳遞,如果不想改變則使用const引用的方式傳遞。

    4)const和類

在類裏創建一個非staticconst變量時,不能給它初值,這個初值必須在構造函數參數列表中進行,這裏是初始所有類裏的const的地方。而當在類裏創建一個static const時,如果是一個內部類型,可以直接初始化,則該常量可以被看作一個編譯期間的常量,如可以使用該常量去定義數組的長度。個人覺得在類裏的const成員都應該同時爲static,能在聲明時初始化的可以直接初始化,不能夠在聲明時直接初始化的(見7中的3)部分)可以使用標準的static數據成員的初始化方法,因爲當類數據成員爲一個const數組時,產生多個對象時不僅浪費空間(每個對象都要爲const數組成員分配空間,卻又不對其做任何改變),而且在構造函數中對其初始化也同樣很麻煩(見3的第二段),若爲static const則更爲合理。

const也可用於對象或類中的成員函數。當const修飾類中的成員函數時,意義是該函數不會對對象的數據部分做任何的修改,當希望某個數據成員能夠在const函數中被修改則可以將該數據成員聲明爲mutable;通過const對象只能調用公有的const成員函數或數據。

    5)volatile

volatileconst類似,它可用於對象或類裏的成員函數和數據。volatile的含義是,對於volatile修飾的變量,編譯器將不對它做任何的優化,每一次對它的引用都會直接從內存中讀取。通過volatile對象只能調用volatile成員函數。

 

6.       內聯函數

內聯函數的出現主要是爲了消除預處理器的缺陷,預處理器不僅會引起一些side effect,而且也沒有類作用域的概念。內聯函數在具備宏高效的性質外,同時又有類作用域的概念和接受C++訪問控制(private等)的保護。使用inline時,必須在函數定義是給出,聲明時給出inline關鍵字是無效的。

    1)內聯函數與編譯器

編譯器遇到一個內聯函數時,它會檢查函數參數列表,編譯器一定要能夠進行參數類型轉換,返回類型正確,然後讓內聯函數代碼代替這個函數調用語句,而後把對象的this指針放到恰當的位置,而預處理器只會替換而不會做其它的工作。

使用inline關鍵字也有一些限制。假如函數太複雜編譯器是不能執行內聯的;假如要顯式或隱式的取函數的地址,編譯器也拒絕內聯,因爲取地址意味着要給函數代碼分配空間,但當地址不需要時,編譯器仍可能內聯代碼,這樣就造成了取地址時分配空間的浪費;內聯函數要是小的,簡單的,纔會具有宏效率的函數調用,這樣的話鑑於構造函數和析構函數中會有隱藏的其它的初始化和清除操作,因此當對程序效率做出考慮時,那麼構造函數和析構函數是否內聯是一個值得注意的問題。

    2)預處理器更多的特性

當然預處理器也有它自己的優點,瞭解預處理器更多的特性對於代碼調試階段是很有幫助的。如字符串組合:#define DEBUG(x) cout <<  #x”=”  << x  << endl;,其中#表示字符串定義,輸出x的字面值。

標識聯貼使用##實現,它允許設兩個標識符並把他們聯貼在一起自動產生一個新的標識符。例如:

#define FIELD(a) char* a##_string; int a##_size;
 class Record {
     FIELD(one)
     FIELD(two)
     //…..
 };

 

7.       名字控制

標識符命名衝突在早期的編程中是一個很大的問題,特別是項目比較大的時候。儘管類的出現減少了標識符命名衝突的可能性,但這還遠遠不夠,爲了更好的解決名字衝突帶來的麻煩,C++在將來自Cstatic關鍵字擴展到對象和類中的成員外,也引入了命名空間。

本章內容主要包括:static如何控制存儲和可見性;C++的命名空間控制訪問;使用採用C語言編寫和編譯過的函數。

1C++中的static關鍵字

C++中的static關鍵字和Cstatic關鍵字都有兩個基本意思:在靜態區分配存儲;對於一個特定編譯單元是局部可見的。對C++中的static關鍵字總結如下:

static修飾全局變量:靜態區存儲;未初始化會被初始化爲0;僅本文件可見。

static修飾局部變量:靜態區存儲;第一次調用函數是初始化,未初始化會被初始化爲0;作用域僅限於函數內部。

static修飾全局對象,局部對象:一一對應於全局變量和局部變量,只是若未給出初始化定義則需提供默認構造函數。

static修飾全局函數:靜態區存儲;無普通函數的進棧出棧,速度快;避免與其它文件函數的名字衝突;聲明時需給出定義。

static修飾類中的成員:類的所有對象共享static成員(變量,對象,函數);變量、對象成員的定義必須在類的外部;函數成員調用時不會傳遞this指針,因此它是不能訪問類中的非靜態成員的。

2)命名空間

一個命名空間可以在多個文件中使用;一個命名空間可以用另一個名字來作爲它的別名,這樣就不必敲那些開發商提供的冗長的命名空間名字,如namespace Bob = Bob-------library;Bob-------library命名空間中是開發商提供的庫文件;在一個命名空間中的類中使用友元,也就意味着該友元也就是該命名空間的一個成員了。

每個翻譯單元都可以包含一個未命名的命名空間,可以不用標識符而只用”namespace”增加一個名字空間,每個翻譯單元都只有一個未命名的命名空間。如果把一個局部名字放在一個未命名的命名空間中,不需要加static說明就可以讓他們內部鏈接。

使用命名空間的三種方法:作用域運算符;用using指令將整個命名空間引入;使用using聲明一次性引用。using聲明是在當前範圍之內進行的一個聲明,這就意味着它可以不顧來自using指令的名字。

不要把一個全局的using指令放到頭文件中,因爲包含這個頭文件的其它文件也會打開這個命名空間。因此在頭文件中應當使用明確限定或在一定範圍內的using指令和using聲明。

3C++類中的靜態數據成員初始化

C++類中的靜態數據成員的初始化必須在類的外部進行顯式初始化。如果是內置類型的static const數據成員是可以在類中給出定義的,但是對於數組或其它類型的靜態常量(static const)必須爲他們提供專門的外部定義,它們的定義語法遵循典型的靜態數據成員定義語法。

因爲靜態數據成員的初始化特點,使得一個類只能創建一個對象成爲可能,這就是Singleton模式。一個簡單的例子如下:

class Egg {
   staticEgg e;
   inti;
   Egg(intii) : i(ii) {}
   Egg(constEgg&); //prevent copy constructor
public:
   staticEgg* instance() {
       return&e;
   }
   int val() const{
       return i;
   }
};
Egg Egg::e(47);

這樣Egg類只有一個對象存在,可以訪問那個對象,但不能產生任何新的對象。必須使拷貝構造函數私有,不然就可以用如下方法創建新的Egg對象:Egg e= *Egg::instance();

4)靜態初始化的相依性

靜態初始的相依性問題,可以用這樣一個例子來說明,假設一個cpp文件中有如下兩行代碼:extern int y; int x = y + 1;,另一個cpp文件中有如下兩行代碼:extern int x; int y = x + 1;;如果第一個cpp文件先編譯,第二個cpp文件後編譯,那麼在第一個文件中y先被初始爲0,接着x初始化爲1,然後在第二個文件中,y被初始爲2;如果第二個cpp文件先編譯,第一個cpp文件後編譯,那麼在第二個文件中x被初始爲0y被初始化爲1,然後在第一個文件中x被初始化爲2。這樣編譯的順序不同會導致初始結果的不同,這是一個問題。

解決這個問題的方法有三種:不用;如果實在要用,把那些關鍵的靜態變量的定義放到一個文件中(同一個翻譯單元),這樣只要他們在文件中的順序正確就可以保證他們正確的初始化;如果有時必須把靜態變量放在幾個不同的翻譯單元中(如寫一個庫的時候),可以通過兩種程序設計技術解決。

技術一例子如下:

//Initializer.h
extern int x;
extern int y;
classInitializer {
    static int initCount;
public:
    Initializer() {
        if(initCount++==0) {
            x = 100;
            y = 200;
        }
    }
    ~Initializer(){
        if(--initCount) {
             //anynecessary clean up
        }
    }
};
staticInitializer init;
//Initializer.cpp   #include“Initializer.h”
int x; int y; int Initializer::initCount;

 

現假設使用該庫的程序員產生了兩個其它文件:

//Initializer1.cpp
#include “Initializer.h”
 
//Initializer2.cpp
#include “Initializer.h”
    int main(void) {
}

現在哪個翻譯單元先初始化staticInitializer init都沒有關係。當第一次包含Initializer.h的翻譯單元被初始化時,initCount0,這時對xy的初始化就已經完成了。解釋這個問題,需要知道main函數執行之前發生了哪些事,在main函數執行之前編譯器插入的start_函數會對應用程序運行時所需資源進行初始化,如對棧的分配,堆的分配,static變量有定義的放在data段,static變量無定義的置爲0bss段,運行全局構造函數,傳遞main函數參數,然後纔開始運行mian函數。這裏的initCount沒有定義其初始值,那麼它會被置0放到bss段,然後運行全局構造函數時纔會對各個cpp文件中的static Initializerinit對象初始化,這樣除第一次包含Initializer.h翻譯單元被初始化時initCount0外,對於其餘單元,initCount不會爲0,忽略對xy的初始化操作。清除時將按包含頭文件的各個翻譯單元中的static Initializer init的初始化順序相反的順序發生,且~Initializer()可確保清除只發生一次。

技術二例子如下:

//Dependency1.h
class Dependency1 {
         boolinit;
public:
         Dependency1(): init(true) {}
};
 
//Dependency2.h
#include “Dependency1.h”
class Dependency2 {
         Dependency1d1;
public:
         Dependency2(constDependency1 dep1) : d1(dep1) {}
};
 
//Dependency1StatFun.h
#include “Dependency1.h”
extern Dependency1& d1();
 
//Dependency2StatFun.h
#include “Dependency2.h”
extern Dependency2& d2();
 
//Dependency1StatFun.cpp
#include “Dependency1StatFun.h”
Dependency1& d1() {
     staticDependency1 dep1;
     returndep1;
}
 
//Dependency2StatFun.cpp
#include “Dependency2StatFun.h”
Dependency2& d2() {
     staticDependency2 dep2(d1());
     returndep2;
}

    通過對各個靜態變量進行封裝成類,在構造函數中對各個靜態變量進行正確順序的初始化。然後爲各個靜態變量定義全局函數,在各自函數中有一個各靜態變量被封裝成的類的一個靜態對象,通過全局函數返回其函數內部的靜態對象的引用來達到對相關靜態變量的訪問。當第一次調用這些全局函數時,函數中的靜態對象的構造函數總能夠保證與其相關聯的靜態變量被正確的初始化。

5)代替連接說明

如果在C++中編寫一個程序需要用到C庫,那麼該怎麼辦?如果聲明這樣一個C函數:float f(int a, char b);C++的編譯器就會將這個名字變成類似_f_int_char這樣的名字,而C編譯器一般不做這樣的轉換,所以它的庫內部名字爲_f。這樣C++連接器將無法解釋對C庫中的f的調用。

C++提供了一個代替連接說明,它是通過重載extern關鍵字來實現的。extern後跟一個字符串,指定箱聲明的函數的連接類型,後面是函數聲明,如下:

extern “C” floatf(int a, char b);

這就告訴編譯器f()C連接,這樣就不會轉換函數名。標準的連接類型指定符有”C””C++”兩種。

如果有一組替代連接的說明,可以把它們放在花括號內:

extern “C” {
       float f(int a, char b);
       double d(int a, char);
}

或在頭文件中:

extern “C” {
       #include “header.h”
}

 

8.       引用和拷貝構造函數

C++中引用和指針的區別在於,指針強調的是指向,而引用強調的是關聯。引用的引入豐富了函數參數傳遞和返回的方式,引用在複製構造函數和重載運算符函數的使用,大大的豐富了C++的特性,使得C++提供給了我們更多方便的程序設計方式。

    1)C++中的引用

C++是一個類型嚴格檢查的語言,如果要把某種類型當做別的類型處理,必須顯式的轉換。這阻止了在C中的void*指針使得任意類型的指針之間進行賦值的功能(如:bird* b; rock* r; void* v; v=r; b = v;)。

C++除了對指針的使用限制的更加嚴格外,還引入了引用。指針與引用最大的區別在於,指針強調指向,而引用強調關聯,指針的指向可以改變,而引用的關聯一旦建立就不會改變。因此一個引用創建時,必須爲它初始化,而指針則可以在任何時候初始化;一旦一個引用被初始化關聯到一個對象後,它就只能和該對象關聯,而不能通過修改它而關聯到其它的對象,相當於爲其關聯到的對象取了另外一個名字;引用不可能爲null,必須保證引用和一塊合法的存儲單元相關聯。

引用也常見於函數的參數和返回值中,在參數中使用指針也可以,但引用更爲簡潔。在函數內部通過修改參數中的引用來達到修改函數外部變量的目的。當返回一個引用時,必須同返回一個指針一樣注意,和一個引用相關聯的存儲空間在函數返回時不會消亡,否則將引用到一個未知空間。引用在函數參數傳遞和返回中的語法和按值傳遞參數和返回的語法類似,但其區別在於,按值的方式傳遞或返回自定義對象時會調用構造函數和析構函數,而引用只需傳遞或返回地址。

const限定的引用作爲函數的參數時,對於內部類型,意味着在函數中不能夠通過引用修改函數外部的變量,而對於自定義類型,則在該函數內只能調用const成員函數,而且不能夠改變任何公共的數據成員。在函數參數中使用const引用特別重要,因爲我們的函數也會接受臨時對象,這個臨時對象可能是另一個函數的返回值或由函數使用者顯式建立的,臨時變量總是const的,修改它沒有任何意義。

    2)拷貝構造函數

當按值的方式向函數傳遞或從函數返回內置類型時,傳遞時直接拷貝,函數返回時可以將其返回值放到寄存器中。而當傳遞或返回自定義對象時,傳遞時仍是直接拷貝,返回時寄存器沒有足夠大的空間存放整個對象,而是將返回值的目的地址像一個函數參數一樣壓棧,讓函數直接把返回值拷貝到目的地址,這些地方對對象的拷貝就需要拷貝構造函數。

這裏就有這樣一個問題,如果是像a =f();這樣的方式調用函數,那麼可以將a的地址壓棧,f()返回時直接將返回值拷貝到a中,但如果像f();這樣調用函數,f()的返回值又會去哪呢?編譯器爲了計算一個表達式,而創建一個看不見的臨時對象作爲函數的返回的目的地址,這個臨時對象的生存週期應當儘量的短。在一些情況下,臨時變量會傳遞給其它的函數,但像f();這樣的情況,一旦函數調用結束就對臨時對象就被析構了。

在沒有提供拷貝構造函數時,編譯器會自動的完成拷貝,但編譯器所做的僅僅是位拷貝,就是從一個空間拷貝到另一個空間,這樣的拷貝是沒有調用構造函數的,這會造成構造函數調用與析構函數調用的不對稱,而且C++中的對象比一組比特位要複雜的多,對象具有含義,比如一個對象的一個數據成員是一個指針時,這時的對象拷貝就要求我們需要考慮更多,而不是僅僅的位拷貝(後面會討論這個問題)。

    當自定義了拷貝構造函數,那麼編譯器的自動拷貝就無效了。對於使用組合和繼承的方法創建的類創建拷貝構造函數,編譯器遞歸的爲所有的成員和基類調用拷貝構造函數。如果成員對象還有別的對象,那麼後者的拷貝構造函數也將被調用。

    拷貝構造函數是僅當在按值傳遞對象或返回對象或用一個對象創建另一個對象時使用,如果不是這樣的情況就不需要。可以通過將拷貝構造函數私有聲明,這樣就可以防止前面的情況出現(7小節3)中有一個拷貝構造函數私有聲明的一個例子)。

    3)指向成員的指針

C++提供了一種對對象共有成員的一種靈活的訪問方式,這就是指向成員的指針。下面先看兩個例子,指向數據成員和函數成員的指針例子。

1:指向數據成員的指針

class Data {
public:
       int a, b, c;
       void print() const {
             cout << “a=”<<a<<”b=”<< b <<”c=”<< c<<endl;
       }
};
 
int main(void) {
       Data d, *dp = &d;
       int Data::*pmInt = &Data::a;
       dp->*pmInt = 47;
       pmInt = &Data::b;
       dp.*pmInt = 48;
       pmInt = &Data::c;
       dp->*pmInt = 49;
       dp->print();
}

2:指向函數成員的指針

class Widget {
           void f(int) const {cout << “f(int)”<< endl;}
           void g(int) const {cout << “g(int)”<< endl;}
           void h(int) const {cout << “h(int)”<< endl;}
           void i(int) const {cout << “i(int)”<< endl;}
           enum {cnt = 4};
           void (Widget::*fptr[cnt])(int) const;
public:
           Widget() {
                    fptr[0] =&Widget::f;//&Widget::f這種格式是語法需要的
                    fptr[1] = &Widget::g;
                    fptr[2] = &Widget::h;
                    fptr[3] = &Widget::I;
           }
           void select(int i, in j) {
                    if(i<0 || i>=cnt)return;
                    (this->*fptr[i])(j);//this->語法需要
           }
           int count() {return cnt;}
};
 
int main(void) {
           Widget w;
           for(int i=0; i<w.count(); i++) {
                    w.select(i, 47);
           }
}

從上面的例子中可以看出指向成員的指針使得訪問對類的公有成員的方式變得靈活,可以在運行時改變操作,通過在運行時修改指向成員的指針的指向來選擇類中的共有數據或函數成員,這就爲程序設計提供了重要的靈活性。

指向成員的指針需要給出精確的指向,這種方式使得它顯的過於難用,另外指向成員的指針是受限制的,它們僅能被指定給類中的確定位置,我們不能像使用普通指針那些增加或比較指針。

特別地,用typedef定義一個指針類型可以減少表達式的複雜度。

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章