《C專家程式設計》:全面回顧認識C++(十)

塵虛緣_KY發表於2016-06-19
       如果你覺得C++還不夠複雜,那你知道protected abstract virtual base pur virtual private destructor inheritance是什麼意思嗎?你上次用到它又是什麼時候呢?
                                                                                                  -----Tom Cargill,C++ Journal 1990年秋
       C++在軟體的複用性方面或許可以比以前的語言取得更大的成功。因為C++的繼承的風格基於物件,即允許資料的繼承,也允許程式碼的繼承。
      1985年以前,C++的名字是“C with classes”,但是現在人們已經在其中加入了非常非常多的特性,從當時的角度類看,“C with classes”是C語言的一個相當合理的擴充套件,很容易理解和解釋。隨即,人們對這種語言投入了極大的熱情,至今未曾衰減。現在的C++是一個相當龐大的語言。具體地說,一個C編譯器的前端大約有40000行程式碼左右,而一個C++編譯器的前端的程式碼可能是它的兩倍,甚至更多。
       這一節主要是對C++語言特性和一些概念的概括總結,本來不打算寫了,因為具體的細節問題我在[C++ PP(Edit 6)]中也有具體的解釋,可能大部分有重複,但是為了小結和複習,回顧一下C++語言與C語言的區別和一些新的特性及其相關概念,決定還是寫下來,高手略過。
        物件導向程式設計的特點是封裝、繼承和多型(動態繫結)。C++通過類的派生支援繼承,通過虛擬函式支援動態繫結。虛擬函式提供了一種封裝類體系實現細節的方法。
一、全面認識C++概念及注意的知識點:
(1)抽象(Abstruct):
它是一個去除物件中不重要的細節的過程,只有那些描述了那些物件本質特徵的關鍵點才被保留。抽象是一種設計活動,其他的概念都是提供抽象的OOP(Object-oriented paradigm)特性。
     抽象的概念就是觀察一群事物,並認識到他們具有共同的主題。你可以忽略不重要的區別,只記錄能表現事物特性的關鍵資料。當你這樣做的時候,就是在進行抽象,所儲存的資料型別就是“抽象資料型別”。抽象聽上去像是一個艱深的數學概念,但不要被它糊弄--它只不過是對事物的簡化而已。
在軟體中,抽象是十分有用的,它允許程式設計師實現下列目標:
(1)隱藏不相關的細節,把注意力集中在本質特徵上。
(2)向外部世界提供一個“黑盒子介面”,介面確定了施加在物件之上的有效操作集合,但它並不提示物件在內部是怎樣實現他們的。
(3)把一個複雜的系統分解成幾個相互獨立的組成部分。這可以做到分工明確,避免元件之間不符合規則的相互作用。
(4)重用和程式碼共享。
抽象建立了一種抽象資料型別,C++使用l類(class)這個特性來實現它。
(2)類:就是使用者定義型別加上對該型別進行操作。
    類是一種使用者定義的型別,就好像是int這樣的內建型別一樣。內建型別已經有了一套完善的針對它的操作(如算術運算)等,類機制也必須允許程式設計師規定他所定義的類能夠進行的操作。類裡面的任何東西被稱為類的成員。一個空類有:建構函式,解構函式,拷貝建構函式,賦值操作符operator=四個成員函式。下面會分別介紹。
    C++的類機制實現了OOP的封裝要求,類就是封裝的軟體實現。類也是一種型別,就像是char,int,double和struct st*一樣。因此在使用之前必須要宣告類的變數,類和型別一樣,你可以對它進行很多操作,如取得它的大小或宣告它的變數等。物件和變數一樣,可以對它進行很多操作,如取得它的地址、把它作為引數傳遞、把它作為函式的返回值、使它成為常量值等。
(3)物件(Object):某個類的一個特定變數,就像j可能是int型別的一個變數一樣。物件也可以被稱作類的例項(instance)。
(4)封裝(encapsulation):把型別、資料和函式組合在一起,組成一個類。在C語言中,標頭檔案就是一個非常脆弱的封裝的例項。它之所以是一個微不足道的封裝例子,是因為它的組合形式是純詞法意義上的,編譯器並不知道標頭檔案是一個語義單位。
(5)單繼承(ingheritance):這是一個很大的概念--允許類從一個更簡單的基類中接收資料結構和函式。派生類獲得基類的資料和操作,並可以根據需要對它們進行改寫,也可以在派生類中增加新的資料和函式成員。在C語言裡不存在繼承的概念,沒有任何東西可以模擬這個特性。當一個類沿用或定製它的唯一基類的資料結構和成員函式時,它就成了單繼承。不要把在一個類內部巢狀另一個類與繼承混淆。巢狀只是把一個類嵌入另一個類的內部,稱內部類
(6)多繼承:多重繼承允許把兩個類組合成一個類,這樣的 結果類物件的行為類似於這兩個類的物件中的一個。
(7)虛繼承:主要解決在多繼承中,訪問不明確的問題。虛繼承中所有的子類都指向同一份資料空間。
(8)虛基類:虛繼承的父類稱之為虛基類,它表示積累是被多個多重繼承的類所共享。(程式設計的時候一般避免虛繼承)
(9)內部類(Inner):內部類與外部類的關係:與編譯器有關,VC6.0他們是兩個完全不同的類,不能用對方的東西,如果要用,可以定義為友元類;VS2005編譯器,內部類可以用外部類的東西,外部類不能用內部類的東西。
內部類的建構函式執行的順序:先呼叫外部類的建構函式,如果有父類則先呼叫父類的建構函式。然後再呼叫自己的建構函式。
(10)純虛擬函式(pure virtual function):純虛擬函式是一種特殊的虛擬函式,在許多情況下,基類不能對虛擬函式給出有意義的實現,而把它申明為純虛擬函式,它的實現留給基類派生的類去做。這就是純虛擬函式的作用(類似於java中的介面)。同時含有純虛擬函式的類稱為抽象類(至少含有一個純虛擬函式),它不能生成物件。
class father
{
    public:
    virtual void show()=0;//純虛擬函式;
}
(11)模板(template):這個特性支援引數化型別。是一種泛型技術,用不變的程式碼實現可變的演算法。
#include <iostream>
using namespace std;
template<typename T>//函式模板不能給預設值;
T Min(T a,T b)
{
   return a>b?b:a;
}
template<typename T,typename R>
void show(T a,R b)
{
   cout<<"a:"<<a<<" b:"<<b<<endl;
}
void show1(T c)//一個函式模板也不能被多個函式來使用;
{
   cout<<c<<endl;  //error,不認識;
}
template<typename T,typename R=int>// 只有類别範本才可以給預設值;
class father
{
  public:
     T a;
     R b;
};
template<typename T,typename S>
class Son:public father<S>//在父類的後面加上<S>表示子類用S來給父類進行初始化;
{
//初始化;
};
int main()
{
    cout<<Min(34,2)<<endl;
    show(23,"kongyin");
    system("pause");
    return 0;
}
(12)行內函數(inline):程式設計師可以規定某個特定的函式在行內以指令流的形式展開(就像巨集一樣),而不是產生一個函式呼叫,避免了函式呼叫的過程,提高了程式的執行效率。
(13)異常處理(exception):異常通過發生錯誤時把處理自動切換到程式中用於處理錯誤程式碼的那部分程式碼。C標準庫提供兩個特殊的函式:setjmp() 及 longjmp(),這兩個函式是結構化異常的基礎,正是利用這兩個函式的特性來實現異常。setjmp和longjmp的主要作用是錯誤恢復,前面有詳細的講過。只要還沒有從函式返回,一旦發現一個不可恢復的錯誤,可以把控制轉移到主輸入迴圈,並從那裡重新開始執行。
try{}  cathch{}異常處理是C++特有的。例項如下:
#include <iostream>
using namespace std;
void show(int num)
{
    switch(num)
    {
      case 1:
        cout<<"throw:";
        throw num;
      case 2:
        cout<<"throw:";
        throw "ERROR";
    }
}
int main()
{
    try
    {
      show(1);
    }
    catch (int a)//丟擲是什麼型別的就要用什麼型別的來接收。
    {
     cout<<"catch:"<<a<<endl;
    }
    catch (char *str)
    {
     cout<<"catch:"<<str<<endl;
    }
    system("pause");
    return 0;
}
(14)友元函式(friend):屬於friend的函式不屬於類的成員函式,但可以像成員函式一樣訪問類的private和protected成員。friend可以是一個函式,也可以是一個類。
(15)成員函式(member function):呼叫一個類的物件的成員函式相當於物件導向程式語言所使用的“向物件傳送一條資訊”這個術語。每個成員都有一個this指標引數,它是隱式的賦給該函式的,它允許物件在成員函式內部引用物件本身。注意,在成員函式內部,你應該發現this指標並未顯示出現,這也是語言本身所涉及的。
#include <iostream>
using namespace std;
class father
{
   private:
     int number;
   public:
     void show()
     {
       cout<<"this的地址:"<<this<<endl;
     }
};
int main()
{
   father ff;
   cout<<"物件的地址:"<<&ff<<endl;
   ff.show();
   system("pause");
   return 0;
}
執行結果:


(16)建構函式(Constructor):絕大多數類至少有一個建構函式。當類的一個物件被建立時,建構函式被隱式的呼叫,它負責物件的初始化。建構函式是必要的,因為類通常包含一些結構,二結構又包含很多欄位。這就需要複雜的初始化。當類的一個物件被建立時,建構函式會被自動呼叫。

(17)拷貝建構函式(Copy Constructor):拷貝建構函式也是一種特殊的建構函式,函式的名稱和類名一致,它的唯一一個引數是本型別的一個引用。a,當用一個已初始化的自定義型別的物件去初始化另一個新構造的物件的時候;b,當函式的引數是類的物件的時候;c,函式的返回值是類的物件的時候,拷貝建構函式就被呼叫。

(18)深拷貝和淺拷貝(位拷貝):簡單理解:深拷貝,發生物件的複製,資源的複製;淺拷貝只是指標的複製,沒有資源的複製。

(19)解構函式(DesConstructor):與建構函式相對應的,類也存在一個清理函式,稱為解構函式。當物件被銷燬(超出其生命週期或進行delete操作,回收它所使用的堆記憶體)時,解構函式被自動呼叫。有人把解構函式當作一種保險方法來確保當物件離開適當的範圍時,同步鎖總能夠釋放。所以它們不僅能清楚物件,還清理 物件所持有的鎖。建構函式和解構函式都是必要的,因為類外部的任何函式都不能訪問類的私有成員。因此,你需要類內部有一個特權函式來建立一個物件並對其進行初始化。但是建構函式和解構函式都違背了C語言中“一切工作自己負責”的原則。他們可以使大量的工作在程式執行時被隱式的呼叫完成一些功能,減輕程式設計師的負擔,這也違背了C語言的哲學,也就是語言的任何部分都不應該通過隱藏的執行時程式來實現。
(20)過載(overload):就是簡單的複用一個現存的名字,但使它操作一個不同的型別。可以是函式的名字,也可以是一個操作符。該函式的返回值、引數的個數、型別和順序都可以不同。過載不能是私有的,不能降低訪問許可權,否則不是過載。過載都是在編譯器解析。
(21)重寫(overwrite):函式的名稱、引數和返回值一樣。僅限於父子之間的返回型別可以不同。多型是在重寫的基礎上實現的。
(22)多型(polymorphism):源於希臘語,意思是“多種形狀”。根據不同型別呼叫不同的函式的能力,允許父類的指標可以指向子類的成員函式,允許物件與適當的成員函式在執行時進行繫結,執行時根據物件的型別選擇正確的成員函式。也稱遲後編譯和滯後編譯(late binding)。基類有多少子類,父指標就有多少形態。主要通過虛擬函式來實現,關鍵字為virtual。它用來通知編譯器,該派生類的成員函式有可能取代基類的同名函式。
   原理是:單繼承通常通過在每個物件內包含一個vptr指標來實現虛擬函式。vptr指標指向一個叫做vtbl的函式指標向量(稱為虛擬函式表,也稱V表)。每個類都有這樣一個向量,類中的每個虛擬函式在該向量中都有一條記錄。使用這種方法,該類的所有物件共享實現程式碼。通過此虛表來實現。是一種泛型技術。虛表的首地址是物件的首地址。
(23)虛擬函式(virtual):不能被宣告為虛擬函式的是:普通函式,類的靜態成員函式,inline函式,friend函式(不是類的成員函式,C++不支援)和建構函式。能宣告為虛擬函式的條件是:(1)能被繼承;(2)類的成員函式
(24)靜態成員函式:靜態成員函式是類的組成部分,但是不是任何物件的組成部分,所有物件共享一份,沒有必要動態繫結,也不能被繼承【效果能,但機理不能。靜態成員函式就一份實體,在父類裡;子類繼承父類時也無需拷貝父類的靜態函式,但子類可以使用父類的靜態成員函式】,並且靜態成員函式只能訪問靜態變數。
(25)new和delete操作符:用於取代free和malloc函式。這兩個操作符用起來更方便一點。如能夠自動完成sizeof的計算工作,並能呼叫合適的建構函式和解構函式。new能真正的建立一個物件,則malloc只是簡單的分配記憶體。
(26)傳引用呼叫(call-by-reference):相當於傳址呼叫,C語言只使用傳值呼叫(call-by-value)。C++在語言中引入了傳引用呼叫,可以把物件的引用作為引數傳遞。
二、C++對C語言的改進
(1)型別轉換既可以寫成像float(i)這樣看上去更順眼的形式,也可以寫成像(float)i這樣稍顯怪異的C語言風格的形式。
(2)C++允許一個常量整數來定義陣列的大小;
const int size =128;
char str[size];
在C++中這是允許的,但是在C中是不允許的。
(3)宣告可以穿插在語句之間。在C語言中,一個語句塊中所有的宣告都必須放在所有的語句的前面。C++去掉了這個專橫的限制,宣告可以出現在語句可以出現的任何地方。
    雖然C++顯的複雜,但是它是對C語言唯一成功的改造方案,擁有大群的支持者。
三、C++小知識點彙集(參考 錢能 著《C++程式設計教程(修訂版)》)<知識點可能有重複>
1.class裡預設的情況下的成員是private;struct預設是public;結構體中不能有成員函式;
2.::叫作用域區分符。黨成員函式在類外實現時,前面必須附加此字首“::”,它彷彿在大聲吶喊,“嗨,我很重要,我表示有一些東西屬於這個類”,
指明一個函式和成員屬於哪個類。::可以不跟類名,表示全域性函式,即非成員函式;
3.類中定義的成員函式都是內聯inline函式,這樣在呼叫的時候就不會拷貝一份,而是指標直接指向函式的入口地址,直接呼叫,提高了呼叫的效率;
4.成員函式一般規模都比較小,所以switch語句不允許用;
5.函式宣告一般在標頭檔案,而函式的定義不能再標頭檔案,因為它們將被編譯多次;使用#pragma once 表示只呼叫一次;
6.inline函式對編譯器而言必須是可見的,以便它能夠在呼叫點內展開該函式。與非inline函式不同的是,inline函式不是一次函式的跳轉,
而是指令的展開(從而提高執行效率)。如果行內函數過大,就會導致目標碼過大,增加額外的換頁行為,降低指令快取記憶體裝置的擊中率。
所以編譯器會自動變為非內聯;
7.由於類中的成員函式都是內聯inline的,所以避免了不能被包含早標頭檔案的問題;
8.因為類名是成員函式的一部分,所以一個類的成員函式與另一個成員函式重名也不能認為是過載;
9.類的物件所佔的空間由他的資料成員所佔據的空間綜合決定,類的成員函式不佔據物件的空間;
10.類的物件有全域性物件,區域性物件,靜態物件和堆物件--指標物件;
11.建構函式是類的成員函式,可以有任意多個引數,可以過載;所以可以放在類外定義;沒有返回型別,當然也沒有返回值;但可以有無值返回語句return;
沒有建構函式就不能建立任何物件;
12.解構函式也是特殊的類成員函式,它沒有返回型別,沒有引數,不能過載,物件生命週期結束的時候,系統自動呼叫;
13.類中物件和函式的定義與宣告:
class Tdate{}
Tdate day();這是宣告瞭一個day的普通函式,返回的Tday類物件;
Tdate day(int);建立了一個函式;
Tdate day(10);建立物件;
14.類的解構函式將自動地為各個具有解構函式的資料成員呼叫解構函式;
15.類的定義是不分配空間和初始化的;
16.類是一個抽象的概念,並不是一個實體,並不含有屬性值,而只有物件才佔有一定的空間,含有明確的屬性值。
17.在建構函式的後面,冒號表示要對類的資料成員的建構函式進行呼叫;冒號語法是的常量資料成員和應用資料成員的初始化成為可能;因為常量是不能被賦值的,引用變數也是不可重新指派的,初始化之後,其值就固定不變了。
class student{
public:
   student(int &i):ten(10),refi(i){}
   protected:
   const int ten;//常量資料成員;
   int &refi;  //引用資料成員;
   student()
   {
    ten=10;  //error:常量不能賦值;
    refi=i;  //error:引用不能重新指派;
   }
}
18.對於類的的資料成員是一般變數的情況,則放在冒號後面與放在函式體重初始化都一樣;
19.建立物件的唯一途徑是呼叫建構函式;
20.區域性和靜態物件是指塊作用域和檔案作用域的物件;
靜態物件只被構造一次:檔案作用域的靜態物件在主函式開始執行前全部構造完畢。塊作用域的靜態物件,則在首次進入到定義該靜態物件的函式時,進行構造;
class student{
public:
  int n;
  student( int m_n)
  {
    cout<<"construct student"<<m_n<<endl;
   }
}
   void fn(int m)
  {
    static student st(m);  //區域性靜態建構函式;
    cout<<"fn: "<<m<<endl;
  }
int main()
{ 
   fn(10);
   fn(20);
}
執行結果:
construct student 10;  //建構函式只執行一次;
fn: 10;
fn: 20;
21:成員變數和成員物件的初始化和構造順序以其在類中宣告的順序構造;不是按照建構函式冒號後面的順序執行;
22.物件導向程式設計基於兩個原則:抽象和分類;---結構化程式設計;效率的比較:
一個人體力很好:他可以騎車穿越大街小巷到達目的地,效率是比較高,但是要是兩地相隔甚遠,那麼就不那麼容易了,,就要選擇火車,飛機,只要付錢坐上去就可以了,不用考慮中間的過程,這就是面相物件;
23.程式執行的效率是指佔盡可能少的資源【儲存空間】而執行的速度相對較快,不能片面的看某一方面,要綜合比較時間和空間客觀的評價一個程式執行的效率;
24.全域性資料區:全域性變數,靜態資料,常量;
程式碼區:類的成員函式和非成員程式碼;
棧區:為執行函式兒分配的區域性變數,函式引數,返回資料,返回地址;
堆區:餘下的空間放在堆區;
25 類中需要new和delete的原因:
   因為malloc在分配空間的時候不能呼叫建構函式,類物件的建立是分配空間,構造結構以及初始化的三位一體,他們統一是由建構函式完成;而malloc只是獲得一個含有隨機資料的類物件空間而已;對應的物件空間中的值不確定,為此,必須在記憶體分配之後再進行初始化;這從根本上來說,不是類物件的建立,它繞過了建構函式;
指標物件只有在呼叫delete時,才呼叫解構函式;如果是區域性物件,則在該區域性物件退出作用域時【結束語句},或者return時】,自動呼叫解構函式;
26.在分配物件陣列時,型別的後面只能跟[元素的個數],不能再跟建構函式的引數,所以,從堆上分配物件陣列,只能呼叫預設建構函式,不能呼叫其他;如果該類沒有預設的建構函式則不能分配兌現該陣列;
student st[4];
student *st[4];
delete[] st; //其中的[]是告訴c++這是一個陣列,填上數字,編譯器會忽略;但是如果不寫,就會產生錯誤;
27.一個空類的預設的函式:建構函式,拷貝建構函式,解構函式,運算子過載函式,操作符過載函式operator=;
28.例項物件,指標物件【堆物件】,臨時物件,無名物件
class student
{
     int numnber;
}

studnet fn()
{
  studnet ms(200);
  return ms;
}
student st(100);//例項物件
studnet *st =new studnet(100);//指標物件
student(100);//無名物件
studnet &sts=fn();//非常危險,不再有效;
29,無名物件的典型三種用法:注意初始化引用和初始化物件的區別,還有賦值和初始化的區別;
void fn(studnet &s);
int main()
{
    student &refs=studnet("Jenny");  //初始化引用; 在函式內部,無名物件作為區域性物件產生在棧空間中;  studnet refs=Jenny;
     student s=student("Jenny");//初始化物件定義; 用無名物件去拷貝構造一個物件s;studnet s=Jenny;省略建立無名物件這一步;按理說:C++先呼叫建構函式 “studnet (char *);”建立一個無名物件,然後再呼叫一個拷貝建構函式 studnet (student &)來建立物件S;但是由於是用無名物件去拷貝構造,完事兒後該無名物件就失去了意義,所以省略了建立無名物件這一步;
   fn(studnet("Jenny")); //函式引數; 無名物件作為實參傳遞給形參s,先呼叫建構函式建立一個無名物件,然後將該無名物件初始化給引用引數s物件,有於實參是在主函式中,所以無名物件是在主函式的棧區中建立,函式fn()的形參s引用的是主函式棧中的一個物件;
  student s("Jenny");
  fn(s);
}
30.由於C++預設的建構函式只是對物件進行了淺拷貝複製,如果物件的資料成員包括指向對空間的指標(堆空間),就不能用這種方式,必須自己定義拷貝構造為其分配空間,也就是深拷貝,資源的拷貝;






相關文章