瘋子C++筆記

 

瘋耔C++筆記html

 

 

歡迎關注瘋耔新浪微博:http://weibo.com/cpjphoneios

參考:C++程序設計(譚浩強)程序員

參考:http://c.biancheng.net/cpp/biancheng/cpp/rumen_8/編程

 

博客原文:http://www.cnblogs.com/Ph-one/p/3974707.html數組

 

C++主要比C多了繼承,多態,模板等特性;安全

 

一.C++初步認識數據結構

1.C++輸入、輸出、頭文件解釋編程語言

#include<iostream>函數

using namespace std ;學習

int mian()

{

  cout << 「This is a C++ program」;

  return 0;

}

程序運行結果:This is a C++ program

(1)"<<"的做用是將引號中的內容插入到輸出的隊列cout中(輸出隊列也稱做「輸出流」)至關於C語言中的printf。

(2)標準C++規定main函數必須聲明爲int型。

(3)程序正常執行,則先系統返回數值 0;

(4)文件iostream的做用是向程序提供輸入輸出時所須要的一些信息。

(5)">>"輸入流對象。如(int a,b;  cin >> a >> b;第一個輸入賦給a,第二個輸入賦給b)

(6)cout << 「a + b =」<< sum << endl;(「endl:換行,及end line的縮寫」)

(7)

#include<iostream>

using namespace std ;

等價於

#include<iostream.h>

2.類

 #include<iostream>

using namespace std ;

class Student                //聲明一個類,類的名字爲 Student

{

  private:               //此名稱表示下面爲私有部分(只有在該類裏面能夠使用)

    int num;              //私有變量num 

    int score;            //私有變量score

  public:                //一下爲類中公用部分

    void setdata()          //定義公用函數setdata

    {

      cin >> num;        //輸入num的值

      cin >> score;        //輸入score的值

    }

    void display()                 //定義公用函數dispaly

    {

      cout << "num = " << num << endl;   //輸出num的值

      cout << "score = " << score << endl;  //輸出score的值

    };

};

//解析:一個類有兩部分1.數據2.函數

Student stud1,stud2;

int mian()

{

  stud1.setdata();      //調用對象stud1的setdata函數

  stud2.setdata();

  stud1.display();

  stud2.dispaly();

  return 0;

}

1.具備「類」類型特徵的變量稱爲「對象」(object)

對象是佔實際存儲空間的,而類型並不佔實際存儲空間,它只是給出一種「模型」,供用戶定義實際的對象。

2.對於「.」是一個「成員運算符」

3.用於高級語言編寫的程序屬於「源程序」(source program)。C++的源程序是以.cpp做爲後綴的(cpp是 c plus plus的縮寫)。

二.數據類型(易遺忘和混淆的)

1.布爾型(bool):邏輯型0、1

2.空類型(void):無值型

3.切記:若是指定爲signed,則數值以補碼形式存放(高位表示符號位,所以和unsigned相比,正數範圍小了一倍)

4.常量:數值型常量和字符常量,兩個單撇號之間的爲字符常量如:'a','X',從字面形式便可識別的常量稱爲「字面常量」或「直接常量」。cout << 'a';輸出的是一個字母「a」,而不是3個字符「 ‘ a ’ 」;

5.在整形常量後加l或L,則認爲是 long int;加f或F,單精度浮點型

6.對於轉義字符來說:cout << i <<' '<< j << '\n';  「 \n 」只佔用一個字符

7.cout << c1 << '' << c2 << endl;(加C後可輸出字符)

8.一個「 \ 」續行符,這樣二者更親密,如:

cout << "I say \「Thank you!\」\n" 析:!後面的「\」要在引號中

9.常變量:在變量的基礎上加上一個限定:存儲單元中的值不容許變化,所以常變量又稱爲只讀變量

10.不一樣類型的整形數據間的賦值歸根到底就是一條:按存儲單元中的存儲形式直接傳送

三.C++運算符

1.   「.」成員運算符

2.「->」指向成員運算符

3.邏輯常量:false(假)、true(真);洛基變量:bool(他的值只有false、true)

 

 控制符  做用 
 dec  設置數值的基數爲10 
 hex  設置數值的基數爲16
 oct  設置數值的基數爲8
 setfill(c) 設置填充字符c,c能夠是字符常量或字符變量 
 setprecision(n) 設置浮點數的精度爲n位。在以通常十進制小數形式輸出時,n表明有效數字。在以fixed(固定小數位)形式和scientific(指數)形式輸出時,n爲小數位數 
 setw(n)  設置字段寬度爲n位
 setiosflags(ios::fixed) 設置浮點數以固定的小數位顯示 
 setiosflags(ios::scientific)  設置浮點數以科學計數法(即指數形式)顯示
 setiosflags(ios::left)  輸出數據左對齊
 setiosflags(ios::right) 輸出數據右對齊 
 setiosflags(ios::skipws) 忽略前導的空格 
 setiosflags(ios::uppercase) 數據以十六進制形式輸出時字母以大寫表示 

 setiosflags(ios::lowercase)

 數據以十六進制形式輸出時字母以小寫表示 
 setiosflags(ios::showpos)  輸出正數時給出「+」號

 

 舉例:

1.對於浮點數

double a = 123.456789012345      對a賦初值

①cout << a;               輸出123.456(默認精度爲6)

②cout << setprecision(9) << a;       輸出123.456789

③cout << setprecision(6);        恢復默認格式(精度爲6)               

④cout << setiosflags(ios::fixed);       輸出123.456789(默認6位小數位)

⑤cout << setiosflags(ios::fixed)<< setprecision(8) << a;    輸出123.45678901(8位小數位)

⑥cout << setiosflags(ios::scientific) << a;               輸出1.234568e+02(默認小數位6位)

⑦cout << setiosflags(ios::scientific) << setprecision(4) << a;  輸出1.2346e02

 2.對於整數

int b = 123456;

①cout << b;                       輸出123456

②cout << hex << b;                  輸出:1e240(對應的16位數字大小)

③cout << setiosflags(ios::uppercase) << b;        輸出:1E240(對應的16位數字大小大寫字母)

④cout << setw(10) << b << ',' << b;         輸出: ————123456,123456 (和setw(10)緊挨着的第一個前四位留4個空格,後一個沒有)

⑤cout << setfill('*') << setw(10) << b;        輸出:****123456

⑥cout << setiosflags(ios::showpos) << b;          輸出:+123456

總結:

1.單setprecision(x) 精度爲x位,默認6位;

2.見setiosflags精度按小數後算,默認也是6位;

一個簡單的例子

求一元二次方程式ax2 +bx +c = 0 的跟

 

 看以下程序:

#include <iostream>

#include <cmath>    //等價於#include math.h

using namespace std;

int main()

{

  float a,b,c,x1,x2;

  cin >> a >> b >> c;

  x1 = (-b + sqrt(b*b - 4*a*c))/(2*a); 

  x1 = (-b - sqrt(b*b - 4*a*c))/(2*a); 

  cout << "x1 = " << x1 << endl;

    cout << "x2 = " << x2 << endl;

  

  return 0;

}

運行結果:

4.5  8.8  2.4(輸入)

x1 = -0.327612

x2 = -1.17794

 

四.與C語言不一樣的函數

與C語言相比C++不只有嵌套、遞歸,還有內置函數、函數的仲裁

1.內置函數:

在函數調用以後,流程返回到先前記下的地址處,而且根據記下的信息「恢復現場」,而後繼續執行。這些都須要花費時間。若是有的函數須要頻繁使用,則所用時間會很長,從而下降程序的執行效率。

內置函數:在編譯時將所調用函數的代碼直接敲入到主函數中,而不是將流程轉出去。(至關於把子函數裏面的語句放到調用函數中,或者說就用max函數體的代碼代替「max(i,j,k),同時將實參代替形參」)

#include <iostream>

using namespace std;

inline int max(int,int,int);  //聲明內置函數,注意左端有inline

int main()

{

  int i = 10,j = 20,k = 30,m;

  m = max(i,j,k);

  cout << "max = " << m <<endl;

  

  return 0;

}

 inline int max(int a,int b,int c)    //定義max爲內置函數

{

  if(b > a) a = b;          //求a,b,c中的最大者

  if(c > a) a = c;

  return a;

}

一樣在函數聲明時加inline,而定義函數時不加inline。

使用內置函數能夠節省運行時間,但卻增長了目標程序的長度。

所以只將規模很小(通常5句如下)而使用頻繁的函數(如定時採集數據的函數)聲明爲內置函數。

在函數規模很小的狀況下,函數調用的時間開銷可能至關於甚至超過執行函數自己的時間。

切記:內置函數中不能包括複雜的控制語句,如循環語句和switch語句。固然若不適合用inline,編譯系統會忽略inline,而按普通函數處理。

2.函數的重載

定義:

重載函數的參數個數、參數類型、或參數順序三者中必須至少有一種不一樣;函數的返回值類型能夠相同也能夠不一樣;

舉例:

①個數不一樣:

#include <iostream>

using namespace std;

int max(int,int,int);

int main()

{

  int max(int a,int b,int c);

  int max(int a,int b);

  int a = 8,b = -12,c = 27;

  cout << "max (a,b,c) = " << max(a,b,c) <<endl;

  cout << "max (a,b) = " << max(a,b) <<endl; 

}

int max(int a,int b,int c)   

{

  if(b > a) a = b;          //求a,b,c中的最大者

  if(c > a) a = c;

  return a;

}

int max(int a,int b)    //定義max爲內置函數

{

  if(a>b) return a;

  else return b;

}

運行結果:

max(a,b,c) = 27

max(a/b) = 8

②輸入參數類型不一樣

#include<iostream>
using namespace std;

int max(int a,int b)
{
        return a>=b?a:b;
}

double max(double a,double b)
{
        return a>=b?a:b;
}
int main()
{
        cout<<"max int is: "<<max(1,3)<<endl;
        cout<<"max double is: "<<max(1.2,1.3)<<endl;
        return 0;
}

 3.函數模板

定義:由於函數的類型(輸出)和參數類型(輸入)各不相同,爲更好的統一,引入函數模板,用一個虛擬的類型來表明;

例子:

#include <iostream>
using namespace std;
template<typename T>  //模板聲明,其中T爲類型參數
T max(T a,T b,T c)   //定義一個通用函數,用T做虛擬的類型名
{
   if(b>a) a=b;
   if(c>a) a=c;
   return a;
}

int main( )
{
   int i1=185,i2=-76,i3=567,i;
   double d1=56.87,d2=90.23,d3=-3214.78,d;
   long g1=67854,g2=-912456,g3=673456,g;
   i=max(i1,i2,i3); //調用模板函數,此時T被int取代
   d=max(d1,d2,d3); //調用模板函數,此時T被double取代
   g=max(g1,g2,g3); //調用模板函數,此時T被long取代
   cout<<"i_max="<<i<<endl;
   cout<<"f_max="<<f<<endl;
   cout<<"g_max="<<g<<endl;
   return 0;
}

 

 

 運行結果:

輸入:

185  -76  567

輸出:

i_max = 567

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

輸入:

56.87  90.23  -3214.78

輸出:

d_max = 90.23

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

輸入:

67854  -912456  673456

輸出:

g_max = 673456

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

 

 定義函數模板的通常形式爲:
    template < typename T>
    通用函數定義  通用函數定義
或(二者等價)
    template <class T>(老版)
    通用函數定義  通用函數定義

注意:

①類型名:(此處用typename 或class表示類型名,T只是一個符號(Type)能夠換成其餘的)

②類名   :

 類型參數能夠不僅一個,能夠根據須要肯定個數。如:
    template <class T1, typename T2>
能夠看到,用函數模板比函數重載更方便,程序更簡潔。但應注意它只適用於函數的參數個數相同而類型不一樣,且函數體相同的狀況,若是參數的個數不一樣,則不能用函數模板

 4.有默認參數的函數

函數聲明時,填入輸入參數

如:

float area(float r = 6.5);

調用時候:

area();  //至關於area(float r = 6.5);

聲明

float volume(float h,float r = 12.5);

調用:

volume(45.6);   //至關於volume(float 45.6,float r = 12.5);

特別注意

void f1(float a, int b = 0, int c, char d = 'a');         //錯誤

void f2(float a, int c, int b = 0, char d = 'a');     //正確

實參與造成的結合是從左至右進行的,因此爲方便調用,指定默認的參數值必須放在形參列表中的最右端,不然出錯

默認函數定義時,參數賦值自右向左

一個函數不能既做重載函數,又做爲有默認參數的函數。(編譯系統沒法區分)

 

五.特殊的變量聲明

register:用register聲明的變量沒必要從內存中讀取,它是直接放在CPU中的,這樣效率快不少(固然這些變量是及其經常使用的在運行當中)P117;

六.數組

1)

cout << str;  //用字符數組名,輸出一個字符串

cout << str[4]; //用數組元素名,輸出一個字符

2)C++中提供了一種新的數據類型----------字符串類型(string類型)

如:

string string1;   //定義字符串變量

string string2 = 「China」;

3)

字符串變量的運算

①複製

string1 = string2;等價於 strcpy(string1,string2);

②相加

 string1 = 「C++」;

string2 = 「Language」;

string1 = string1 + string2;

鏈接後string1 的內容爲「C++ Language」。

③字符串數組

string name[5];

string name[5] = {「Zhang」,「Li」,「Sun」,「Wang」,「Tan」}

④字符串可直接比較大小。

  

七.類和對象特徵

1)

類是對象的抽樣(不佔空間),對象是類的具體實例(instance)。

2)

若是在類的定義中即不指定private,也不指定public,則系統就默認爲是私有的。

好比:

int 是個類型

 

int a;

a是個對象;(可能不恰當,但好理解)

class student

  {

  ......

  };

student stud1,stud2;

student(類型)至關於int;

stud1(對象)至關於 a;

3)在C++中能夠用struct來聲明一個類,和class聲明的類所不一樣的是若是對其成員不做private或public的聲明,

struct:系統將其默認爲public(公用的)。 (固然在C語言中,結構體中沒有見有函數的,類中卻能夠有函數)

class :系統將其默認爲private()。 

 4) 外置

在類外定義類當中成員函數

如:

class Student

{

  public:

    void display();

    //inline void display();

  private:

  int num;

  string name;

  char sex;

};

 

void Student::display()

//inline void Student::display()

{

   cout << "num = " << num << endl;   //輸出num的值

   cout << "score = " << score << endl;  //輸出score的值

}

student stud1,stud2;

 

解釋:

「::」是做用域限定符(field qualifier)或稱 做用域運算符,用它聲明函數屬於哪一個類中的,

 Student::display()表示Student類中的display函數。說白了就是限定這個display函數是Student類中的。

 

5)內置

 在類中的public中

    void display();

    //inline void display();

二者等效;

 若在外聲明inline void Student::display()

 則須要類內部也寫成顯性;

而且類定義和成員函數的定義 放在同一個頭文件中(或者同一個源文件中);

 

6)成員函數的存儲方式

成員函數沒有放在對象的存儲空間中,但從邏輯的角度,成員函數是和數據一塊兒封裝在一個對象中的,只容許本對象中成員的函數訪問同一對象中的私有數據。

 

7)類成員的引用

如:

stud1.num = 1001;

指針法:

stud1 t,*p;

p = &t;

cout << p -> hour;

 

8)三個名詞:對象,方法(method),消息(message)

如:

stud1.display();是一個消息;

stud1 是對象;display()是方法(必須是對外的,公用的)。

 

八.對象的使用

1)對象的初始化

class Time
{
   hour = 0;
   minute = 0;
   sec = 0;
};   

 

在類聲明中對數據成員初始化是錯誤的!

 

class Time
{
   hour ;
   minute ;
   sec ;
};  
Time t1 = {1456,30};

 

這是正確的

 

2)構造函數實現數據成員的初始化

(法1)

 #include<iostream>
using namespace std ;

class Time                //聲明一個類,類的名字爲 Student

{
  private:               //此名稱表示下面爲私有部分(只有在該類裏面能夠使用)
  int hour;
  int minute;
  int sec;
  public:                //一下爲類中公用部分
     Time()
    {
      hour = 0;
      minute = 0;
      sec = 0;
    }
    void set_time();
    void show_time();
};

void Time::set_show()
{
  cin >> hour;
  cin >> minute;
  cin >> sec;
}

void Time::show_time()
{
  cout << hour << ":" << minute << ":" << sec << endl;
}
int mian() {   Time t1;    //創建對象t1,同時調用構造函數t1.Time()
  t1.set_time();
  t1.show_time();
  Time t2;
 
t2.show_time();
  return 0;
}

 

(法2)

 #include<iostream>
using namespace std ;

class Time                //聲明一個類,類的名字爲 Student

{
  private:               //此名稱表示下面爲私有部分(只有在該類裏面能夠使用)
  int hour;
  int minute;
  int sec;

  public:                //一下爲類中公用部分
     Time();
   
    void set_time();
    void show_time();
};

  Time::Time()
    {
      hour = 0;       minute = 0;       sec = 0;     }
void Time::set_show()
{
  cin >> hour;
  cin >> minute;
  cin >> sec;
}

void Time::show_time()
{
  cout << hour << ":" << minute << ":" << sec << endl;
}

int mian()

{
  Time t1;    //創建對象t1,同時調用構造函數t1.Time()
  t1.set_time();
  t1.show_time();
  Time t2;
  t2.show_time();
  return 0;
}

 

在類中定義了構造函數Time,它和所在的類同名。在創建對象時自動執行構造函數,賦值語句是寫在構造函數的函數體中的,只有在調用構造函數時才執行這些賦值語句。

輸入,輸出以下:

10 25 54 (輸入)

10:25:54(輸出)

0:0:0

 構造函數理解:

創建對象時系統爲該對象分配存儲單元,此時執行構造函數,每創建一個對象,就調用一次構造函數。

構造函數沒有返回值,所以也沒有類型,它的做用只是對 對象進行初始化。

不能寫成:

int Time()

{......}

void Time()

{......}

構造函數不需用戶調用,也不能被用戶調用。

t1.Time(); 錯誤

能夠用一個類對象初始化另外一個類對象。

如:

Time t1;        //創建對象t1,同時調用構造函數t1.Time()

Time t2 = t1;  //創建對象t2,並用一個t1初始化t2

此時,把t1的各數據成員的值複製到t2相應各成員,而不調用構造函數t2.Time()。

在構造函數內部不只能夠對數據成員賦初值(說白了內部怎麼用和日常的函數每什麼兩樣,只是函數類型是無類型)能夠包含其餘語句,例如cout語句。但通常不提倡在構造函數中加入其餘與初始化無關的內容,以保持程序的清晰。

若是用戶本身沒有定義構造函數,則C++系統會自動生成一個構造函數,只是這個構造函數的函數體是空的,也沒有參數,不執行初始化操做。

 

3)帶參數的構造函數

 

#include <iostream>
using namespace std;
class Box
{
   public :
   Box(int,int,int);
   int volume( );
   private :
   int height;
   int width;
   int length;
};
//聲明帶參數的構造函數//聲明計算體積的函數
Box::Box(int h,int w,int len) //在類外定義帶參數的構造函數
{
   height=h;
   width=w;
   length=len;
}
 //Box::Box(int h,int w,int len):height(h),width(w), length(len){ } //或者用這個方法也能夠
int Box::volume( ) //定義計算體積的函數 { return (height*width*length); } int main( ) { Box box1(12,25,30); //創建對象box1,並指定box1長、寬、高的值 cout<<"The volume of box1 is "<<box1.volume( )<<endl; Box box2(15,30,21); //創建對象box2,並指定box2長、寬、高的值 cout<<"The volume of box2 is "<<box2.volume( )<<endl; return 0; }

 程序運行結果以下:
The volume of box1 is 9000
The volume of box2 is 9450

能夠知道:

  • 帶參數的構造函數中的形參,其對應的實參在定義對象時給定。
  • 用這種方法能夠方便地實現對不一樣的對象進行不一樣的初始化。

用參數初始化表對數據成員初始化

上面介紹的是在構造函數的函數體內經過賦值語句對數據成員實現初始化。C++還提供另外一種初始化數據成員的方法——參數初始化表來實現對數據成員的初始化。這種方法不在函數體內對數據成員初始化,而是在函數首部實現。

例9.2中定義構造函數能夠改用如下形式:
    Box::Box(int h,int w,int len):height(h),width(w), length(len){ }
這種寫法方便、簡練,尤爲當須要初始化的數據成員較多時更顯其優越性。甚至能夠直接在類體中(而不是在類外)定義構造函數。 

 

4)構造函數的重載

#include <iostream>
using namespace std;
class Box
{
   public :
Box( ); //聲明一個無參的構造函數 //聲明一個有參的構造函數,用參數的初始化表對數據成員初始化 Box(int h,int w,int len):height(h),width(w),length(len){ } int volume( ); private : int height; int width; int length; }; Box::Box( ) //定義一個無參的構造函數 { height=10; width=10; length=10; } int Box::volume( ){ return (height*width*length); } int main( ) { Box box1; //創建對象box1,不指定實參 cout<<"The volume of box1 is "<<box1.volume( )<<endl; Box box2(15,30,25); //創建對象box2,指定3個實參 cout<<"The volume of box2 is "<<box2.volume( )<<endl; return 0; }

 

第一次輸出  10 10 10

第二次輸出 15 30 25

 關於構造函數的重載的幾點說明:

  1. 調用構造函數時沒必要給出實參的構造函數,稱爲默認構造函數(default constructor)。顯然,無參的構造函數屬於默認構造函數。一個類只能有一個默認構造函數。
  2. 若是在創建對象時選用的是無參構造函數,應注意正確書寫定義對象的語句。
  3. 儘管在一個類中能夠包含多個構造函數,可是對於每個對象來講,創建對象時只執行其中一個構造函數,並不是每一個構造函數都被執行

5)構造函數有默認參數

#include <iostream>
using namespace std;
class Box
{
   public :
   Box(int h=10,int w=10,int len=10); //在聲明構造函數時指定默認參數
   int volume( );
   private :
   int height;
   int width;
   int length;
};
Box::Box(int h,int w,int len) //在定義函數時能夠不指定默認參數 { height=h; width=w; length=len; } int Box::volume( )
{
   return (height*width*length);
}
int main( )
{
   Box box1; //沒有給實參
   cout<<"The volume of box1 is "<<box1.volume( )<<endl;
   Box box2(15); //只給定一個實參   這樣對二個,第三個參數仍是 10 ;
   cout<<"The volume of box2 is "<<box2.volume( )<<endl;
   Box box3(15,30); //只給定2個實參
   cout<<"The volume of box3 is "<<box3.volume( )<<endl;
   Box box4(15,30,20); //給定3個實參
   cout<<"The volume of box4 is "<<box4.volume( )<<endl;
   return 0;
}

 

程序運行結果爲:
The volume of box1 is 1000
The volume of box2 is 1500
The volume of box3 is 4500
The volume of box4 is 9000

程序中對構造函數的定義(第12-16行)也能夠改寫成參數初始化表的形式:
    Box::Box(int h,int w,int len):height(h),width(w),length(len){ }

 

6)析構函數

建立對象時系統會自動調用構造函數進行初始化工做,一樣,銷燬對象時系統也會自動調用一個函數來進行清理工做(例如回收建立對象時消耗的各類資源),這個函數被稱爲析構函數。

析構函數(Destructor)也是一種特殊的成員函數,沒有返回值,不須要用戶調用,而是在銷燬對象時自動執行。與構造函數不一樣的是,析構函數的名字是在類名前面加一個」~「符號。

注意:析構函數沒有參數,不能被重載,所以一個類只能有一個析構函數。若是用戶沒有定義,那麼編譯器會自動生成。

析構函數舉例:

#include <iostream>
using namespace std;

class Student{
private:
    char *name;
    int age;
    float score;

public:
    //構造函數
    Student(char *, int, float);
    //析構函數
    ~Student();
    //普通成員函數
    void say();
};

Student::Student(char *name1, int age1, float score1):name(name1), age(age1), score(score1){}
Student::~Student(){
    cout<<name<<"再見"<<endl;
}
void Student::say(){
    cout<<name<<"的年齡是 "<<age<<",成績是 "<<score<<endl;
}

int main(){
    Student stu1("小明", 15, 90.5f);
    stu1.say();
   
    Student stu2("李磊", 16, 95);
    stu2.say();
   
    Student stu3("王爽", 16, 80.5f);
    stu3.say();

    cout<<"main 函數即將運行結束"<<endl;
   
    return 0;
}

 

運行結果:
小明的年齡是 15,成績是 90.5
李磊的年齡是 16,成績是 95
王爽的年齡是 16,成績是 80.5
main 函數即將運行結束
王爽再見
李磊再見
小明再見

能夠看出,析構函數在 main 函數運行結束前被執行,而且調用順序和構造函數正好相反,爲了方便記憶,咱們能夠將之理解爲一個棧,先入後出

析構函數在對象被銷燬前執行;要知道析構函數何時被調用,就要先知道對象何時被銷燬。

對象能夠認爲是經過類這種數據類型定義的變量,它的不少特性和普通變量是同樣的,例如做用域、生命週期等。由此能夠推斷,對象這種變量的銷燬時機和普通變量是同樣的。

 

析構函數的執行順序

上面的例子中,咱們依次建立了3個對象,分別是 stu一、stu二、stu3,但它們對應的析構函數的執行順序倒是相反的,這是爲何呢?

要搞清楚這個問題,首先要明白C++內存模型,也就是C++的代碼和數據在內存中是如何存儲的。C++內存模型和C語言類似(有部分細節不一樣),你能夠參照C語言內存模型來理解。

在內存模型中有一塊區域叫作棧區,它是由系統維護的(程序員沒法操做),用來存儲函數的參數、局部變量等,相似於數據結構中的棧,也是先進後出。

當遇到函數調用時,首先將下一條指令的地址壓入棧區,而後將函數參數壓入棧區,隨着函數的執行,再將局部變量(或對象)按順序壓入棧區。棧區是先進後出的結構,當函數執行結束後,先把最後壓入的變量(或對象)彈出,以此類推,最後把第一個壓入的變量彈出。接下來,再按照先進後出的規則彈出函數參數,彈出下一條指令地址。有了下一條指令的地址,函數調用結束後纔可以繼續執行後面的代碼。

所謂彈出變量,就是銷燬變量,清空變量所佔用的資源。若是這個變量是一個對象,那麼就會執行析構函數。

上面的例子中,三個對象入棧的順序依次是 stu一、stu二、stu3,出棧(銷燬)的順序依次是 stu三、stu二、stu1,它們對應的析構函數的執行順序也就一目瞭然了。

 

總結起來,有下面幾種狀況:
1) 若是在一個函數中定義了一個對象(auto 局部變量),當這個函數運行結束時,對象就會被銷燬,在對象被銷燬前自動執行析構函數。

2) static 局部對象在函數調用結束時並不銷燬,所以也不調用析構函數,只有在程序結束時(如 main 函數結束或調用 exit 函數)才調用 static 局部對象的析構函數。

3) 若是定義了一個全局對象,也只有在程序結束時纔會調用該全局對象的析構函數。

4) 若是用 new 運算符動態地創建了一個對象,當用 delete 運算符釋放該對象時,先調用該對象的析構函數。

若是你對 auto、static、extern 等關鍵字不理解,請猛擊: C語言動態內存分配及變量存儲類別

注意:析構函數的做用並非刪除對象,而是在撤銷對象佔用的內存以前完成一些清理工做,使這部份內存能夠分配給新對象使用。

 

7)調用構造函數和析構造函數的順序

在使用構造函數和析構函數時,須要特別注意對它們的調用時間和調用順序。在通常狀況下,調用析構函數的次序正好與調用構造函數的次序相反:最早被調用的構造函數,其對應的(同一對象中的)析構函數最後被調用,而最後被調用的構造函數,其對應的析構函數最早被調用。

能夠簡記爲:先構造的後析構,後構造的先析構,它至關於一個棧,先進後出。

可是,並非在任何狀況下都是按這一原則處理的。咱們已經介紹過做用域(請查看:C++局部變量和全局變量)和存儲類別(請查看:C++變量的存儲類別)的概念,這些概念對於對象也是適用的。對象能夠在不一樣的做用域中定義,能夠有不一樣的存儲類別。這些會影響調用構造函數和析構函數的時機。

下面概括一下何時調用構造函數和析構函數:
1) 在全局範圍中定義的對象(即在全部函數以外定義的對象),它的構造函數在文件中的全部函數(包括main函數)執行以前調用。但若是一個程序中有多個文件,而不一樣的文件中都定義了全局對象,則這些對象的構造函數的執行順序是不肯定的。當main函數執行完畢或調用exit函數時(此時程序終止),調用析構函數。

2) 若是定義的是局部自動對象(例如在函數中定義對象),則在創建對象時調用其構造函數。若是函數被屢次調用,則在每次創建對象時都要調用構造函數。在函數調用結束、對象釋放時先調用析構函數。

3) 若是在函數中定義靜態(static )局部對象,則只在程序第一次調用此函數創建對象時調用構造函數一次,在調用結束時對象並不釋放,所以也不調用析構函數,只在main函數結束或調用exit函數結束程序時,才調用析構函數。

例如,在一個函數中定義了兩個對象:
void fn(){
    Student stud1;  //定義自動局部對象
    static Student stud2;  //定義靜態局部對象
}
在調用fn函數時,先調用stud1的構造函數,再調用stud2的構造函數,在fn調用結束時,stud1是要釋放的(由於它是自動局部對象),所以調用stud1的析構函數。而stud2 是靜態局部對象,在fn調用結束時並不釋放,所以不調用stud2的析構函數。直到程序結束釋放stud2時,才調用stud2的析構函數。能夠看到stud2是後調用構造函數的,但並不先調用其析構函數。緣由是兩個對象的存儲類別不一樣、生命週期不一樣。

(說白了仍是根據變量的做用域,存儲方式來決定先釋放誰,固然總的邏輯是 先入後出)

 

8)對象數組

若是構造函數有3個參數,分別表明學號、年齡、成績。則能夠這樣定義對象數組:

Student Stud[3]={ //定義對象數組
    Student(1001,18,87),  //調用第1個元素的構造函數,爲它提供3個實參
    Student(1002,19,76),  //調用第2個元素的構造函數,爲它提供3個實參
    Student(1003,18,72)  //調用第3個元素的構造函數,爲它提供3個實參
};

在創建對象數組時,分別調用構造函數,對每一個元素初始化。每個元素的實參分別用括號包起來,對應構造函數的一組形參,不會混淆。

[例9.6] 對象數組的使用方法。

#include <iostream>
using namespace std;
class Box
{
public :
   //聲明有默認參數的構造函數,用參數初始化表對數據成員初始化
   Box(int h=10,int w=12,int len=15): height(h),width(w),length(len){ }
   int volume( );
private :
   int height;
   int width;
   int length;
};
int Box::volume( )
{
   return (height*width*length);
}
int main( )
{
   Box a[3]={ //定義對象數組
      Box(10,12,15), //調用構造函數Box,提供第1個元素的實參
      Box(15,18,20), //調用構造函數Box,提供第2個元素的實參
      Box(16,20,26) //調用構造函數Box,提供第3個元素的實參
   };
   cout<<"volume of a[0] is "<<a[0].volume( )<<endl;
   cout<<"volume of a[1] is "<<a[1].volume( )<<endl;
   cout<<"volume of a[2] is "<<a[2].volume( )<<endl;
   return 0;
}

運行結果以下:
volume of a[0] is 1800
volume of a[1] is 5400
volume of a[2] is 8320

 

9)對象指針

指向對象的指針

在創建對象時,編譯系統會爲每個對象分配必定的存儲空間,以存放其成員。對象空間的起始地址就是對象的指針。能夠定義一個指針變量,用來存放對象的指針。

class Time
{
   public :
   int hour;
   int minute;
   int sec;
   void get_time( );
};
void Time::get_time( )
{
   cout<<hour<<":"<<minute<<":"<<sec<<endl;
}

在此基礎上有如下語句:
    Time *pt;  //定義pt爲指向Time類對象的指針變量
    Time t1;  //定義t1爲Time類對象
    pt=&t1;  //將t1的起始地址賦給pt
這樣,pt就是指向Time類對象的指針變量,它指向對象t1。

定義指向類對象的指針變量的通常形式爲:
    類名 *對象指針名;

能夠經過對象指針訪問對象和對象的成員。如:
    *pt   //pt所指向的對象,即t1
    (*pt).hour  //pt所指向的對象中的hour成員,即t1.hour
    pt->hour  //pt所指向的對象中的hour成員,即t1.hour
    (*pt).get_time ( )   //調用pt所指向的對象中的get_time函數,即t1.get_time
    pt->get_time ( )  //調用pt所指向的對象中的get_time函數,即t1.get_time

上面第2, 3行的做用是等價的,第4, 5兩行也是等價的。

指向對象成員的指針

對象有地址,存放對象初始地址的指針變量就是指向對象的指針變量。對象中的成員也有地址,存放對象成員地址的指針變量就是指向對象成員的指針變量。

1) 指向對象數據成員的指針
定義指向對象數據成員的指針變量的方法和定義指向普通變量的指針變量方法相同。例如:
    int *p1; //定義指向整型數據的指針變量
定義指向對象數據成員的指針變量的通常形式爲:
    數據類型名 *指針變量名;
若是Time類的數據成員hour爲公用的整型數據,則能夠在類外經過指向對象數據成員的指針變量訪問對象數據成員hour:
    p1=&t1.hour;  //將對象t1的數據成員hour的地址賦給p1,p1指向t1.hour
    cout<<*p1<<endl;  //輸出t1.hour的值

2) 指向對象成員函數的指針
須要提醒讀者注意: 定義指向對象成員函數的指針變量的方法和定義指向普通函數的指針變量方法有所不一樣。這裏重溫一個指向普通函數的指針變量的定義方法:
    數據類型名 (*指針變量名) (參數表列);

    void ( *p)( );  //p是指向void型函數的指針變量
能夠使它指向一個函數,並經過指針變量調用函數:
    p = fun;  //將fun函數的人口地址傳給指針變童p,p就指向了函數fn
    (*P)( );  //調用fn函數

而定義一個指向對象成員函數的指針變量則比較複雜一些。若是模仿上面的方法將對象成員函數名賦給指針變最P:
    p = t1.get_time;
則會出現編譯錯誤。爲何呢?

成員函數與普通函數有一個最根本的區別: 它是類中的一個成員。編譯系統要求在上面的賦值語句中,指針變量的類型必須與賦值號右側函數的類型相匹配,要求在如下3方面都要匹配:
①函數參數的類型和參數個數;
②函數返回值的類型;
③所屬的類。

如今3點中第①②兩點是匹配的,而第③點不匹配。指針變量p與類無關,面get_ time函數卻屬於Time類。所以,要區別普通函數和成員函數的不一樣性質,不能在類外直接用成員函數名做爲函數入口地址去調用成員函數。

那麼,應該怎樣定義指向成員函數的指針變量呢?應該採用下面的形式:
    void (Time::*p2)( );  //定義p2爲指向Time類中公用成員函數的指針變量
注意:(Time:: *p2) 兩側的括號不能省略,由於()的優先級高於*。若是無此括號,就至關於:
    void Time::*(p2())  //這是返回值爲void型指針的函數

定義指向公用成員函數的指針變量的通常形式爲:
   數據類型名 (類名::*指針變量名)(參數表列);

可讓它指向一個公用成員函數,只需把公用成員函數的入口地址賦給一個指向公用成員函數的指針變量便可。如:
    p2=&Time::get_time;
使指針變量指向一個公用成員函數的通常形式爲
    指針變量名=&類名::成員函數名;
在VC++系統中,也能夠不寫&,以和C語言的用法一致,但建議在寫C++程序時不要省略&。

 

[例9.7]有關對象指針的使用方法。

#include <iostream>
using namespace std;
class Time
{
   public:
   Time(int,int,int);
   int hour;
   int minute;
   int sec;
   void get_time( );
};
Time::Time(int h,int m,int s)
{
   hour=h;
   minute=m;
   sec=s;
}
void Time::get_time( ) //聲明公有成員函數
//定義公有成員函數
{
   cout<<hour<<":"<<minute<<":" <<sec<<endl;
}
int main( )
{
   Time t1(10,13,56); //定義Time類對象t1
   int *p1=&t1.hour; //定義指向整型數據的指針變量p1,並使p1指向t1.hour
   cout<<* p1<<endl; //輸出p1所指的數據成員t1.hour
   t1.get_time( ); //調用對象t1的成員函數get_time
   Time *p2=&t1; //定義指向Time類對象的指針變量p2,並使p2指向t1
   p2->get_time( ); //調用p2所指向對象(即t1)的get_time函數
   void (Time::*p3)( ); //定義指向Time類公用成員函數的指針變量p3
   p3=&Time::get_time; //使p3指向Time類公用成員函數get_time
   (t1.*p3)( ); //調用對象t1中p3所指的成員函數(即t1.get_time( ))
   return 0;
}

程序運行結果爲:
10 (main函數第4行的輸出)
10:13:56 (main函數第5行的輸出)
10:13:56 (main函數第7行的輸出)
10:13:56 (main函數第10行的輸出)
能夠看到爲了輸出t1中hour,minute和sec的值,能夠採用3種不一樣的方法。

幾點說明:
1) 從main函數第9行能夠看出,成員函數的入口地址的正確寫法是:
    &類名::成員函數名
不該該寫成:
    p3 =&t1.get_time;  //t1爲對象名

成員函數不是存放在對象的空間中的,而是存放在對象外的空間中的。若是有多個同類的對象,它們共用同一個函數代碼段。所以賦給指針變量p3的應是這個公用的函數代碼段的入口地址。

調用t1的get_time函數能夠用t1.get_time()形式,那是從邏輯的角度而言的,經過對象名能調用成員函數。而如今程序語句中須要的是地址,它是物理的,具體地址是和類而不是對象相聯繫的。

2) main函數第8, 9兩行能夠合寫爲一行:
   void (Time::*p3)( )=&Time::get_time;  //定義指針變量時指定其指向

 

 10)this指針詳解

this 是C++中的一個關鍵字,也是一個常量指針,指向當前對象(具體說是當前對象的首地址,使用中不是指針變量,它就是一個地址)。經過 this,能夠訪問當前對象的成員變量和成員函數。

所謂當前對象,就是正在使用的對象,例如對於 stu.say();,stu 就是當前對象,系統正在經過 stu 訪問成員函數 say()。

下面的語句中,this 就和 pStu 的值相同:

Student stu;  //經過Student類來建立對象
Student *pStu = &stu;

[示例] 經過 this 來訪問成員變量:

class Student{
private:
    char *name;
    int age;
    float score;

public:
    void setname(char *);
    void setage(int);
    void setscore(float);
};

void Student::setname(char *name){
    this->name = name;
}
void Student::setage(int age){
    this->age = age;
}
void Student::setscore(float score){
    this->score = score;
}

本例中,函數參數和成員變量重名是沒有問題的,由於經過 this 訪問的是成員變量,而沒有 this 的變量是函數內部的局部變量。例如對於this->name = name;語句,賦值號左邊是類的成員變量,右邊是 setname 函數的局部變量,也就是參數。

下面是一個完整的例子:

#include <iostream>
using namespace std;

class Student{
private:
    char *name;
    int age;
    float score;

public:
    void setname(char *);
    void setage(int);
    void setscore(float);
    void say();
};

void Student::setname(char *name){
    this->name = name;
}
void Student::setage(int age){
    this->age = age;
}
void Student::setscore(float score){
    this->score = score;
}
void Student::say(){
    cout<<this->name<<"的年齡是 "<<this->age<<",成績是 "<<this->score<<endl;
}

int main(){
    Student stu1;
    stu1.setname("小明");
    stu1.setage(15);
    stu1.setscore(90.5f);
    stu1.say();
   
    Student stu2;
    stu2.setname("李磊");
    stu2.setage(16);
    stu2.setscore(80);
    stu2.say();

    return 0;
}

運行結果:
小明的年齡是 15,成績是 90.5
李磊的年齡是 16,成績是 80

對象和普通變量相似;每一個對象都佔用若干字節的內存,用來保存成員變量的值,並且不一樣對象佔用的內存互不重疊,因此操做對象A不會影響對象B。

上例中,建立對象 stu1 時,this 指針就指向了 stu1 所在內存的首字節,它的值和 &stu1 是相同的;建立對象 stu2 時,也是同樣的。

咱們不妨來證實一下,給 Student 類添加一個成員函數,輸出 this 的值,以下所示:

void Student::printThis(){
    cout<<this<<endl;   //this就是該對象的首地址
}

而後在 main 函數中建立對象並調用 printThis

Student stu1, *pStu1 = &stu1;
stu1.printThis();
cout<<pStu1<<endl;

Student stu2, *pStu2 = &stu2;
stu2.printThis();
cout<<pStu2<<endl;

運行結果:
0x28ff30
0x28ff30
0x28ff10
0x28ff10

能夠發現,this 確實指向了當前對象的首地址,並且對於不一樣的對象,this 的值也不同。

幾點注意:

  • this 是常量指針,它的值是不能被修改的,一切企圖修改該指針的操做,如賦值、遞增、遞減等都是不容許的
  • this 只能在成員函數內部使用,其餘地方沒有意義,也是非法的。
  • 只有當對象被建立後 this 纔有意義,所以不能在 static 成員函數中使用,後續會講到。

this 究竟是什麼

實際上,this 指針是做爲函數的參數隱式傳遞的。也就是說,this 並不出如今參數列表中,調用成員函數時,系統自動獲取當前對象的地址,賦值給 this,完成參數的傳遞,無需用戶干預。

this 只是隱式參數,不在對象的內存空間中,建立對象時也不爲 this 分配內存,只有在發生成員函數調用時纔會給 this 賦值,函數調用結束後,this 被銷燬。

正由於 this 是參數,表示對象首地址,因此只能在函數內部使用,而且對象被實例化之後纔有意義。

 

 11)常對象(const的對象,至關於枚舉)

C++雖然採起了很多有效的措施(如設private保護)以增長數據的安全性,可是有些數據卻每每是共享的,人們能夠在不一樣的場合經過不一樣的途徑訪問同一個數據對象。有時在無心之中的誤操做會改變有關數據的情況,而這是人們所不但願出現的。

既要使數據能在必定範圍內共享,又要保證它不被任意修改,這時能夠使用const,即把有關的數據定義爲常量。

常對象

在定義對象時指定對象爲常對象。常對象必需要有初值,如:
    Time const t1(12,34,46); //t1是常對象
這樣,在全部的場合中,對象t1中的全部成員的值都不能被修改。凡但願保證數據成員不被改變的對象,能夠聲明爲常對象。

定義常對象的通常形式爲:
    類名 const 對象名[(實參表列)];
也能夠把const寫在最左面:
    const 類名 對象名[(實參表列)];
兩者等價。

若是一個對象被聲明爲常對象,則不能調用該對象的非const型的成員函數(除了由系統自動調用的隱式的構造函數和析構函數)。例如,對於例9.7中已定義的Time類,若是有
    const Time t1(10,15,36); //定義常對象t1
    t1.get_time( ); //企圖調用常對象t1中的非const型成員函數,非法
這是爲了防止這些函數會修改常對象中數據成員的值。

不能僅依靠編程者的細心來保證程序不出錯,編譯系統充分考慮到可能出現的狀況,對不安全的因素予以攔截。如今,編譯系統只檢查函數的聲明,只要發現調用了常對象的成員函數,並且該函數未被聲明爲const,就報錯,提請編程者注意。

引用常對象中的數據成員很簡單,只需將該成員函數聲明爲const便可。如:
    void get_time( ) const ; //將函數聲明爲const
這表示get_time是一個const型函數,即常成員函數。

常成員函數能夠訪問常對象中的數據成員,但仍然不容許修改常對象中數據成員的值。有時在編程時有要求,必定要修改常對象中的某個數據成員的值,ANSI C++考慮到實際編程時的須要,對此做了特殊的處理,對該數據成員聲明爲mutable,如:
    mutable int count;  (常對象中的變量用 mutable 聲明)
把count聲明爲可變的數據成員,這樣就能夠用聲明爲const的成員函數來修改它的值。

常對象成員

能夠將對象的成員聲明爲const,包括常數據成員和常成員函數。

1) 常數據成員
其做用和用法與通常常變量類似,用關鍵字const來聲明常數據成員。常數據成員的值是不能改變的。

有一點要注意: 只能經過構造函數的參數初始化表對常數據成員進行初始化。如在類體中定義了常數據成員hour:
    const int hour; //聲明hour爲常數據成員

不能採用在構造函數中對常數據成員賦初值的方法,下面的作法是非法的:
    Time::Time(int h){
        hour=h;
    }  // 非法
由於常數據成員是不能被賦值的。

在類外定義構造函數,應寫成如下形式:
   Time::Time(int h):hour(h){} //經過參數初始化表對常數據成員hour初始化
常對象的數據成員都是常數據成員,所以常對象的構造函數只能用參數初始化表對常數據成員進行初始化。


2) 常成員函數
前面已提到,通常的成員函數能夠引用本類中的非const數據成員,也能夠修改它們。若是將成員函數聲明爲常成員函數,則只能引用本類中的數據成員,而不能修改它們,例如只用於輸出數據等。如
    void get_time( ) const ; //注意const的位置在函數名和括號以後
const是函數類型的一部分,在聲明函數和定義函數時都要有const關鍵字,在調用時沒必要加const。常成員函數能夠引用const數據成員,也能夠引用非const的數據成員。const數據成員能夠被const成員函數引用,也能夠被非const的成員函數引用。具體狀況能夠用表9.1表示。

表 9.1
數據成員 非const成員函數 const成員函數
非const的數據成員 能夠引用,也能夠改變值 能夠引用,但不能夠改變值
const數據成員 能夠引用,但不能夠改變值 能夠引用,但不能夠改變值
const對象的數據成員 不容許 能夠引用,但不能夠改變值


那麼怎樣利用常成員函數呢?

  1. 若是在一個類中,有些數據成員的值容許改變,另外一些數據成員的值不容許改變,則能夠將一部分數據成員聲明爲const,以保證其值不被改變,能夠用非const的成員函數引用這些數據成員的值,並修改非const數據成員的值。
  2. 若是要求全部的數據成員的值都不容許改變,則能夠將全部的數據成員聲明爲const,或將對象聲明爲const(常對象),而後用const成員函數引用數據成員,這樣起到「雙保險」的做用,切實保證
  3. 若是已定義了一個常對象,只能調用其中的const成員函數,而不能調用非const成員函數(不論這些函數是否會修改對象中的數據)。這是爲了保證數據的安全。若是須要訪問對象中的數據成員,可將常對象中全部成員函數都聲明爲const成員函數,但應確保在函數中不修改對象中的數據成員。


不要誤認爲常對象中的成員函數都是常成員函數。常對象只保證其數據成員是常數據成員,其值不被修改。若是在常對象中的成員函數未加const聲明,編譯系統把它做爲非const成員函數處理。

還有一點要指出,常成員函數不能調用另外一個非const成員函數。

 

12)指向對象的常指針

 將指針變量聲明爲const型,這樣指針值始終保持爲其初值,不能改變。(說白就是一個不變的地址)

如:
    Time t1(10,12,15),t2; //定義對象
    Time * const ptr1; //const位置在指針變量名前面,規定ptr1的值是常值
    ptr1=&t1; //ptr1指向對象t1,此後不能再改變指向 (只能賦值一次,以後不變,這其實並不符合邏輯)
    ptr1=&t2; //錯誤,ptr1不能改變指向

定義指向對象的常指針的通常形式爲:
    類名 * const 指針變量名;
也能夠在定義指針變量時使之初始化,如將上面第2, 3行合併爲:
   Time * const ptr1=&t1; //指定ptr1指向t1

請注意,指向對象的常指針變量的值不能改變,即始終指向同一個對象,但能夠改變其所指向對象(如t1)的值。

何時須要用指向對象的常指針呢?若是想將一個指針變量固定地與一個對象相聯繫(即該指針變量始終指向一個對象),能夠將它指定爲const型指針變量,這樣能夠防止誤操做,增長安全性。

每每用常指針做爲函數的形參,目的是不容許在函數執行過程當中改變指針變量的值, 使其始終指向原來的對象。若是在函數執行過程當中修改了該形參的值,編譯系統就會發現錯誤,給出出錯信息,這樣比用人工來保證形參值不被修改更可靠。

 

13)指向常對象的指針變量(說白了就是 對象內容不變,指向他的指針可變可不變,下表也說明這點)

 

爲了更容易理解指向常對象的指針變量的概念和使用,首先了解指向常變量的指針變量,而後再進一步研究指向常對象的指針變量。下面定義了一個指向常變量的指針變量ptr:
    const char *ptr;
注意const的位置在最左側,它與類型名char緊連,表示指針變量ptr指向的char變量是常變量,不能經過ptr來改變其值的。

定義指向常變量的指針變量的通常形式爲:
    const 類型名 *指針變量名;

幾點說明:
1)  若是一個變量已被聲明爲常變量,只能用指向常變量的指針變量指向它,(這一點是否是有點扯淡啊?)而不能用通常的(指向非const型變量的)指針變量去指向它。如:
    const char c[] ="boy";  //定義 const 型的 char 數組
    const char * pi;   //定義pi爲指向const型的char變量的指針變量
    pi =c;   //合法,pi指向常變量(char數組的首元素)
    char *p2=c;  //不合法,p2不是指向常變量的指針變量

2) 指向常變量的指針變量除了能夠指向常變量外,還能夠指 向 未被聲明爲const的變量。此時不能經過此指針變量改變該變量的值。如:
    char cl ='a'; //定義字符變量cl,它並未聲明爲const
    const char *p;  //定義了一個指向常變量的指針變量p
    p = &cl;  //使p指向字符變量cl
    *p = 'b'; //非法,不能經過p改變變量cl的值
    cl = 'b';  //合法,沒有經過p訪問cl,cl不是常變量


3) 若是函數的形參是指向非const型變量的指針,實參只能用指向非const變量的指針,而不能用指向const變量的指針,這樣,在執行函數的過程當中能夠改變形參指針變量所指向的變量(也就是實參指針所指向的變量)的值。
(上述三條說白了就是,常指針(const)和變指針絕對空間不同)
若是函數的形參是指向const型變量的指針,在執行函數過程當中顯然不能改變指針變量所指向的變量的值,所以容許實參是指向const變量的指針,或指向非const變量的指針。如:
    const char str[ ] = "boy";  //str 是 const 型數組名
    void fun( char * ptr) ;  //函數fun的形參是指向非const型變量的指針
    fun(str);  //調用fun函數,實參是const變量的地址, 非法

由於形參是指向非const型變量的指針變量,按理說,在執行函數過程當中它所指向的變量的值是能夠改變的。可是形參指針和實參指針指向的是同一變量,而實參是const 變量的地址,它指向的變量的值是不可改變的。這就發生矛盾。所以C++要求實參用非const變量的地址(或指向非const變量的指針變量)。

表9.2 用指針變量做形參時形參和實參的對應關係
形參 實參 合法否 改變指針所指向的變量的值
指向非const型變量的指針 非const變量的地址 合法 能夠
指向非const型變量的指針 const變量的地址 非法 /
指向const型變量的指針 const變量的地址 合法 不能夠
指向const型變量的指針 非const變量的地址 合法 不能夠

上表的對應關係與在(2)中所介紹的指針變量和其所指向的變量的關係是一致的: 指向常變量的指針變量能夠指向const和非const型的變量,而指向非const型變量的指針變量只能指向非const的變量。

以上介紹的是指向常變量的指針變量,指向常對象的指針變量的概念和使用是與此相似的,只要將「變量」換成「對象」便可。

1) 若是一個對象已被聲明爲常對象,只能用指向常對象的指針變量指向它,而不能用通常的(指向非const型對象的)指針變量去指向它。

2) 若是定義了一個指向常對象的指針變量,並使它指向一個非const的對象,則其指向的對象是不能經過指針來改變的。如:
    Time t1(10,12,15);  //定義Time類對象t1,它是非const型對象
    const Time *p = &t1;  //定義p是指向常對象的指針變量,並指向t1
    t1.hour = 18;  //合法,t1不是常變量
    (* p).hour = 18;  //非法,不齙經過指針變量改變t1的值

若是但願在任何狀況下t1的值都不能改變,則應把它定義爲const型,如:
    const Time t1(lO,12,15);

請注意指向常對象的指針變量與指向對象的常指針變量在形式上和做用上的區別。
    Time * const p; //指向對象的常指針變量
    const Time *p; //指向常對象的指針變量

3) 指向常對象的指針最經常使用於函數的形參,目的是在保護形參指針所指向的對象,使它在函數執行過程當中不被修改。

請記住這樣一條規則: 當但願在調用函數時對象的值不被修改,就應當把形參定義爲指向常對象的指針變量,同時用對象的地址做實參(對象能夠是const或非const型)。若是要求該對象不只在調用函數過程當中不被改變,並且要求它在程序執行過程當中都不改變,則應把它定義爲const型。

4) 若是定義了一個指向常對象的指針變量,是不能經過它改變所指向的對象的值的,可是指針變量自己的值是能夠改變的
 

 14)對象的常引用

 咱們知道,一個變量的引用就是變量的別名。實質上,變量名和引用名都指向同一段內存單元。

若是形參爲變量的引用名,實參爲變量名,則在調用函數進行虛實結合時,並非爲形參另外開闢一個存儲空間(常稱爲創建實參的一個拷貝),而是把實參變量的地址傳給形參(引用名),這樣引用名也指向實參變量。

[例9.8] 對象的常引用。

#include <iostream>
using namespace std;
class Time
{
   public:
   Time(int,int,int);
   int hour;
   int minute;
   int sec;
};
Time::Time(int h,int m,int s) //定義構造函數
{
   hour=h;
   minute=m;
   sec=s;
}
void fun(Time &t)
{
   t.hour=18;
}
int main( )
{
   Time t1(10,13,56);
   fun(t1);
   cout<<t1.hour<<endl;
   return 0;
}

 

 若是不但願在函數中修改實參t1的值,能夠把引用變量t聲明爲const(常引用),函數原型爲
    void fun(const Time &t);
則在函數中不能改變t的值,也就是不能改變其對應的實參t1的值。

在C++面向對象程序設計中,常常用常指針和常引用做函數參數。這樣既能保證數據安全,使數據不能被隨意修改,在調用函數時又沒必要創建實參的拷貝。

每次調用函數創建實參的拷貝時,都要調用複製構造函數,要有時間開銷。用常指針和常引用做函數參數,能夠提升程序運行效率。

 

15)對象的動態創建和釋放

使用類名定義的對象(請查看:C++類的聲明和對象的定義)都是靜態的,在程序運行過程當中,對象所佔的空間是不能隨時釋放的。但有時人們但願在須要用到對象時才創建對象,在不須要用該對象時就撤銷它,釋放它所佔的內存空間以供別的數據使用。這樣可提升內存空間的利用率。

在C++中,能夠使用new運算符動態地分配內存,用delete運算符釋放這些內存空間(請查看:C++動態分配內存(new)和撤銷內存(delete))。這也適用於對象,能夠用new運算符動態創建對象,用delete運算符撤銷對象。

若是已經定義了一個Box類,能夠用下面的方法動態地創建一個對象:
    new Box; 
編譯系統開闢了一段內存空間,並在此內存空間中存放一個Box類對象,同時調用該類的構造函數,以使該對象初始化(若是已對構造函數賦予此功能的話)。

可是此時用戶還沒法訪問這個對象,由於這個對象既沒有對象名,用戶也不知道它的地址。這種對象稱爲無名對象,它確實是存在的,但它沒有名字。

用new運算符動態地分配內存後,將返回一個指向新對象的指針的值,即所分配的內存空間的起始地址。用戶能夠得到這個地址,並經過這個地址來訪問這個對象。須要定義一個指向本類的對象的指針變量來存放該地址。如
    Box *pt;  //定義一個指向Box類對象的指針變量pt
    pt=new Box;  //在pt中存放了新建對象的起始地址
在程序中就能夠經過pt訪問這個新建的對象。如
    cout<<pt->height;  //輸出該對象的height成員
    cout<<pt->volume( );  //調用該對象的volume函數,計算並輸出體積

C++還容許在執行new時,對新創建的對象進行初始化。如
    Box *pt=new Box(12,15,18);
這種寫法是把上面兩個語句(定義指針變量和用new創建新對象)合併爲一個語句,並指定初值。這樣更精煉。

新對象中的height,width和length分別得到初值12,15,18。調用對象既能夠經過對象名,也能夠經過指針。

用new創建的動態對象通常是不用對象名的,是經過指針訪問的,它主要應用於動態的數據結構,如鏈表。訪問鏈表中的結點,並不須要經過對象名,而是在上一個結點中存放下一個結點的地址,從而由上一個結點找到下一個結點,構成連接的關係。

在執行new運算時,若是內存量不足,沒法開闢所需的內存空間,目前大多數C++編譯系統都使new返回一個0指針值。只要檢測返回值是否爲0,就可判斷分配內存是否成功。

ANSI C++標準提出,在執行new出現故障時,就「拋出」一個「異常」,用戶可根據異常進行有關處理。但C++標準仍然容許在出現new故障時返回0指針值。當前,不一樣的編譯系統對new故障的處理方法是不一樣的。

在再也不須要使用由new創建的對象時,能夠用delete運算符予以釋放。如
    delete pt; //釋放pt指向的內存空間
這就撤銷了pt指向的對象。此後程序不能再使用該對象。

若是用一個指針變量pt前後指向不一樣的動態對象,應注意指針變量的當前指向,以避免刪錯了對象。在執行delete運算符時,在釋放內存空間以前,自動調用析構函數,完成有關善後清理工做。

 

16)對象賦值

對象賦值的通常形式爲:

    對象名1 = 對象名2;
注意對象名1和對象名2必須屬於同一個類。例如
    Student stud1,stud2; //定義兩個同類的對象
    stud2=stud1; //將stud1賦給stud2

經過下面的例子能夠了解怎樣進行對象的賦值。

[例9.9] 對象的賦值。

#include <iostream>
using namespace std;
class Box
{
   public :
   Box(int =10,int =10,int =10); //聲明有默認參數的構造函數
   int volume( );
   private :
   int height;
   int width;
   int length;
};
Box::Box(int h,int w,int len)
{
   height=h;
   width=w;
   length=len;
}
int Box::volume( )
{
   return (height*width*length); //返回體積
}
int main( )
{
   Box box1(15,30,25),box2; //定義兩個對象box1和box2
   cout<<"The volume of box1 is "<<box1.volume( )<<endl;
   box2=box1; //將box1的值賦給box2
   cout<<"The volume of box2 is "<<box2.volume( )<<endl; return 0;
}

 

運行結果以下:
The volume of box1 is 11250
The volume of box2 is 11250

說明:(謹記)

    • 對象的賦值只對其中的數據成員賦值,而不對成員函數賦值。數據成員是佔存儲空間的,不一樣對象的數據成員佔有不一樣的存儲空間,賦值的過程是將一個對象的數據成員在存儲空間的狀態複製給另外一對象的數據成員的存儲空間。而不一樣對象的成員函數是同一個函數代碼段,不須要、也沒法對它們賦值。
    • 類的數據成員中不能包括動態分配的數據不然在賦值時可能出現嚴重後果 (在此不做詳細分析,只需記住這一結論便可)。

 

17)對象的複製

有時須要用到多個徹底相同的對象,例如,同一型號的每個產品從外表到內部屬性都是同樣的,若是要對每個產品分別進行處理,就須要創建多個一樣的對象,並要進行相同的初始化,用之前的辦法定義對象(同時初始化)比較麻煩。此外,有時須要將對象在某一瞬時的狀態保留下來。

C++提供了克隆對象的方法,來實現上述功能。這就是對象的複製機制。

用一個已有的對象快速地複製出多個徹底相同的對象。如
   Box box2(box1); 1克隆2;
其做用是用已有的對象box1去克隆出一個新對象box2。(1克隆2,2和1同樣)

其通常形式爲:
    類名  對象2(對象1);
用對象1複製出對象2。

能夠看到,它與定義對象的方式相似,可是括號中給出的參數不是通常的變量,而是對象。在創建對象時調用一個特殊的構造函數——複製構造函數(copy constructor)。這個函數的形式是這樣的:

 

//The copy constructor definition.
Box::Box(const Box& b)
{
    height=b.height; width=b.width; length=b.length;
}

 

 複製構造函數也是構造函數,但它只有一個參數,這個參數是本類的對象(不能是其餘類的對象), 並且採用對象的引用的形式(通常約定加const聲明,使參數值不能改變,以避免在調用此函數時因不慎而使對象值被修改)。此複製構造函數的做用就是將實參對象的各成員值一一賦給新的對象中對應的成員。

複製對象的語句
    Box box2(box1);
這實際上也是創建對象的語句,創建一個新對象box2。因爲在括號內給定的實參是對象,所以編譯系統就調用複製構造函數(它的形參也是對象), 而不會去調用其餘構造函數。實參box1的地址傳遞給形參b(b是box1的引用),所以執行複製構造函數函數體時,將box1對象中各數據成員的值賦給box2中各數據成員。

若是用戶本身未定義複製構造函數,則編譯系統會自動提供一個默認的複製構造函數,其做用只是簡單地複製類中每一個數據成員。C++還提供另外一種方便用戶的複製形式,用賦值號代替括號,如
    Box box2=box1; //用box1初始化box2
其通常形式爲
   類名 對象名1 = 對象名2;
能夠在一個語句中進行多個對象的複製。如
   Box box2=box1,box3=box2;
按box1來複制box2和box3。能夠看出,這種形式與變量初始化語句相似,請與下面定義變量的語句做比較:
    int a=4,b=a;
這種形式看起來很直觀,用起來很方便。可是其做用都是調用複製構造函數。

賦值:對象二者已經存在

複製:對象一方從無到有


能夠對例9.7程序中的主函數做一些修改:

int main( )
{
   Box box1(15,30,25); //定義box1
   cout<<"The volume of box1 is "<<box1.volume( )<<endl;
   Box box2=box1,box3=box2; //按box1來複制box2,box3
   cout<<"The volume of box2 is "<<box2.volume( )<<endl;
   cout<<"The volume of box3 is "<<box3.volume( )<<endl;
}

 

執行完第3行後,3個對象的狀態徹底相同。

下面說一下普通構造函數和複製構造函數的區別。

1) 在形式上
類名(形參表列); //普通構造函數的聲明,如Box(int h,int w,int len);
類名(類名& 對象名); //複製構造函數的聲明,如Box(Box &b);

2) 在創建對象時,實參類型不一樣
系統會根據實參的類型決定調用普通構造函數或複製構造函數。如
    Box box1(12,15,16); //實參爲整數,調用普通構造函數
    Box box2(box1); //實參是對象名,調用複製構造函數(在進行對象複製時候,對象調用的構造函數就不同了)

3) 在什麼狀況下被調用
普通構造函數在程序中創建對象時被調用。複製構造函數在用已有對象複製一個新對象時被調用,在如下3種狀況下須要克隆對象:
① 程序中須要新創建一個對象,並用另外一個同類的對象對它初始化,如上面介紹的那樣。

當函數的參數爲類的對象時。在調用函數時須要將實參對象完整地傳遞給形參,也就是須要創建一個實參的拷貝,這就是按實參複製一個形參,系統是經過調用複製構造函數來實現的,這樣能保證形參具備和實參徹底相同的值。如

void fun(Box b) //形參是類的對象
{ }
int main( )
{
   Box box1(12,15,18);
   fun(box1); //實參是類的對象,調用函數時將複製一個新對象b
   return 0;
}

 

③ 函數的返回值是類的對象。在函數調用完畢將返回值帶回函數調用處時。此時須要將函數中的對象複製一個臨時對象並傳給該函數的調用處。如

Box f( ) //函數f的類型爲Box類類型
{
   Box box1(12,15,18);
   return box1; //返回值是Box類的對象
}
int main( )
{
   Box box2; //定義Box類的對象box2
   box2=f( ); //調用f函數,返回Box類的臨時對象,並將它賦值給box2
}

 

以上幾種調用複製構造函數都是由編譯系統自動實現的,沒必要由用戶本身去調用,讀者只要知道在這些狀況下須要調用複製構造函數就能夠了。

 

18)C++ static靜態成員變量和靜態成員函數

若是想在同類的多個對象之間實現數據共享,也不要用全局變量,那麼能夠使用靜態成員變量。

static靜態成員變量

靜態成員變量是一種特殊的成員變量,它以關鍵字 static 開頭。例如:

 

class Student{
private:
    char *name;
    int age;
    float score;
    static int num;  //將num定義爲靜態成員變量

public:
    Student(char *, int, float);
    void say();
};

 

這段代碼聲明瞭一個靜態成員變量 num,用來統計學生的人數。

static 成員變量屬於類,不屬於某個具體的對象,這就意味着,即便建立多個對象,也只爲 num 分配一分內存,全部對象使用的都是這分內存中的數據。當某個對象修改了 num,也會影響到其餘對象。

static 成員變量必須先初始化才能使用,不然連接錯誤。例如:

int Student::num;  //初始化
或者:
int Student::num = 10;  //初始化同時賦值

初始化時能夠不加 static,但必需要有數據類型。被 private、protected、public 修飾的 static 成員變量均可以用這種方式初始化。

注意:static 成員變量的內存空間既不是在聲明類時分配,也不是在建立對象時分配,而是在初始化時分配

static 成員變量既能夠經過對象來訪問,也能夠經過類來訪問。經過類來訪問的形式爲:

類名::成員變量;

例如:

//經過類來訪問
Student::num = 10;
//經過對象來訪問
Student stu;
stu.num = 10;

這兩種方式是等效的。
注意:static 成員變量與對象無關,不佔用對象的內存,而是在全部對象以外開闢內存,即便不建立對象也能夠訪問。
下面來看一個完整的例子:

#include <iostream>
using namespace std;

class Student{
private:
    char *name;
    int age;
    float score;
    static int num;  //將num定義爲靜態成員變量 

public:
    Student(char *, int, float);
    void say();
};

int Student::num = 0;  //初始化靜態成員變量   ② 這時候分配內存

Student::Student(char *name, int age, float score){
    this->name = name;
    this->age = age;
    this->score = score;
    num++;    
}
void Student::say(){
    //在普通成員函數中能夠訪問靜態成員變量  
    cout<<name<<"的年齡是 "<<age<<",成績是 "<<score<<"(當前共"<<num<<"名學生)"<<endl;
}

int main(){
    //使用匿名對象
    (new Student("小明", 15, 90))->say();
    (new Student("李磊", 16, 80))->say();
    (new Student("張華", 16, 99))->say();
    (new Student("王康", 14, 60))->say();

    return 0;
}

 

運行結果:
小明的年齡是 15,成績是 90(當前共1名學生)
李磊的年齡是 16,成績是 80(當前共2名學生)
張華的年齡是 16,成績是 99(當前共3名學生)
王康的年齡是 14,成績是 60(當前共4名學生)

本例中將 num 聲明爲靜態成員變量,每次建立對象時,會調用構造函數,將 num 的值加 1。之因此使用匿名對象,是由於每次建立對象後只會使用它的 say 函數,再也不進行其餘操做。不過請注意,使用匿名對象有內存泄露的風險

關於靜態數據成員的幾點說明: 
1) 一個類中能夠有一個或多個靜態成員變量,全部的對象都共享這些靜態成員變量,均可以引用它。

2) static 成員變量和普通 static 變量同樣,編譯時在靜態數據區分配內存,到程序結束時才釋放。這就意味着,static 成員變量不隨對象的建立而分配內存,也不隨對象的銷燬而釋放內存。而普通成員變量在對象建立時分配內存,在對象銷燬時釋放內存。

3) 靜態成員變量必須初始化,並且只能在類體外進行。例如:

int Student::num = 10;

初始化時能夠賦初值,也能夠不賦值。若是不賦值,那麼會被默認初始化,通常是 0。靜態數據區的變量都有默認的初始值,而動態數據區(堆區、棧區)的變量默認是垃圾值。

4) 靜態成員變量既能夠經過對象名訪問,也能夠經過類名訪問,但要遵循 private、protected 和 public 關鍵字的訪問權限限制。當經過對象名訪問時,對於不一樣的對象,訪問的是同一分內存。

static靜態成員函數

在類中,static 除了聲明靜態成員變量,還能夠聲明靜態成員函數。普通成員函數能夠訪問全部成員變量,而靜態成員函數只能訪問靜態成員變量。

咱們知道,當調用一個對象的成員函數(非靜態成員函數)時,系統會把當前對象的起始地址賦給 this 指針。而靜態成員函數並不屬於某一對象,它與任何對象都無關,所以靜態成員函數沒有 this 指針既然它沒有指向某一對象,就沒法對該對象中的非靜態成員進行訪問。this並不能指向一個靜態變量;,靜態成員函數沒法對非靜態變量進行訪問;

能夠說,靜態成員函數與非靜態成員函數的根本區別是:非靜態成員函數有 this 指針,而靜態成員函數沒有 this 指針。由此決定了靜態成員函數不能訪問本類中的非靜態成員

靜態成員函數能夠直接引用本類中的靜態數據成員,由於靜態成員一樣是屬於類的,能夠直接引用。在C++程序中,靜態成員函數主要用來訪問靜態數據成員,而不訪問非靜態成員。

若是要在類外調用 public 屬性的靜態成員函數,要用類名和域解析符「::」。如:

Student::getNum();

固然也能夠經過對象名調用靜態成員函數,如:

stu.getNum();


下面是一個完整的例子,經過靜態成員函數得到學生的平均成績:

#include <iostream>
using namespace std;

class Student{
private:
    char *name;
    int age;
    float score;
    static int num;  //學生人數
    static float total;  //總分

public:
    Student(char *, int, float);
    void say();
    static float getAverage();  //靜態成員函數,用來得到平均成績
};

int Student::num = 0;
float Student::total = 0;

Student::Student(char *name, int age, float score){
    this->name = name;
    this->age = age;      
    this->score = score;
    num++;
    total += score;
}
void Student::say(){
    cout<<name<<"的年齡是 "<<age<<",成績是 "<<score<<"(當前共"<<num<<"名學生)"<<endl;
}
float Student::getAverage(){
    return total / num;    //裏面只能用 靜態變量
}

int main(){
    (new Student("小明", 15, 90))->say();
    (new Student("李磊", 16, 80))->say();
    (new Student("張華", 16, 99))->say();
    (new Student("王康", 14, 60))->say();

    cout<<"平均成績爲 "<<Student::getAverage()<<endl;
   
    return 0;
}

 運行結果:
小明的年齡是 15,成績是 90(當前共1名學生)
李磊的年齡是 16,成績是 80(當前共2名學生)
張華的年齡是 16,成績是 99(當前共3名學生)
王康的年齡是 14,成績是 60(當前共4名學生)
平均成績爲 82.25

上面的代碼中,將 num、total 聲明爲靜態成員變量,將 getAverage 聲明爲靜態成員函數。在 getAverage 函數中,只使用了 total、num 兩個靜態成員變量。

 

19)靜態成員函數

 

 與數據成員相似,成員函數也能夠定義爲靜態的,在類中聲明函數的前面加static就成了靜態成員函數。如
    static int volume( );
和靜態數據成員同樣,靜態成員函數是類的一部分,而不是對象的一部分。

若是要在類外調用公用的靜態成員函數,要用類名和域運算符「::」。如
    Box::volume( );
實際上也容許經過對象名調用靜態成員函數,如
    a.volume( );
但這並不意味着此函數是屬於對象a的,而只是用a的類型而已。

與靜態數據成員不一樣,靜態成員函數的做用不是爲了對象之間的溝通,而是爲了能處理靜態數據成員。

咱們知道,當調用一個對象的成員函數(非靜態成員函數)時,系統會把該對象的起始地址賦給成員函數的this指針。而靜態成員函數並不屬於某一對象,它與任何對象都無關,所以靜態成員函數沒有this指針。既然它沒有指向某一對象,就沒法對一個對象中的非靜態成員進行默認訪問(即在引用數據成員時不指定對象名)。

能夠說,靜態成員函數與非靜態成員函數的根本區別是:非靜態成員函數有this指針,而靜態成員函數沒有this指針。由此決定了靜態成員函數不能訪問本類中的非靜態成員。

靜態成員函數能夠直接引用本類中的靜態數據成員,由於靜態成員一樣是屬於類的,能夠直接引用。在C++程序中,靜態成員函數主要用來訪問靜態數據成員,而不訪問非靜態成員。

假如在一個靜態成員函數中有如下語句:
    cout<<height<<endl;  //若height已聲明爲static,則引用本類中的靜態成員,合法
    cout<<width<<endl;  //若width是非靜態數據成員,不合法
可是,並非絕對不能引用本類中的非靜態成員,只是不能進行默認訪問,由於沒法知道應該去找哪一個對象。

若是必定要引用本類的非靜態成員,應該加對象名和成員運算符「.」。如
    cout<<a.width<<endl; //引用本類對象a中的非靜態成員
假設a已定義爲Box類對象,且在當前做用域內有效,則此語句合法。

經過例9.11能夠具體瞭解有關引用非靜態成員的具體方法。

[例9.11] 靜態成員函數的應用。

#include <iostream>
using namespace std;
class Student                   //定義Student類
{
public:
   Student(int n,int a,float s):num(n),age(a),score(s){ }      //定義構造函數
   void total( );
   static float average( );      //聲明靜態成員函數
private:
   int num;
   int age;
   float score;
   static float sum;            //靜態數據成員
   static int count;            //靜態數據成員
};
void Student::total( )                      //定義非靜態成員函數
{
   sum+=score;                            //累加總分
   count++;                               //累計已統計的人數
}
float  Student::average( )                  //定義靜態成員函數
{
   return(sum/count);
}

float Student::sum=0;                     //對靜態數據成員初始化
int Student::count=0;                     //對靜態數據成員初始化

int main( )
{
   Student stud[3]={                      //定義對象數組並初始化
      Student(1001,18,70),
      Student(1002,19,78),
      Student(1005,20,98)
   };
   int n;
   cout<<"please input the number of students:";
   cin>>n;                               //輸入須要求前面多少名學生的平均成績
   for(int i=0;i<n;i++)                  //調用3次total函數
      stud[i].total( );
   cout<<"the average score of "<<n<<" students is "<<Student::average( )<<endl;
   //調用靜態成員函數
   return 0;
}

 

運行結果爲:
please input the number of students:3↙
the average score of 3 students is 82.3333

關於靜態成員函數成員的幾點說明:

  1. 在主函數中定義了stud對象數組,爲了使程序簡練,只定義它含3個元素,分別存放3個學生的數據。程序的做用是先求用戶指定的n名學生的總分,而後求平均成績(n由用戶輸入)。
  2. 在Student類中定義了兩個靜態數據成員sum(總分)和count(累計須要統計的學生人數), 這是因爲這兩個數據成員的值是須要進行累加的,它們並非只屬於某一個對象元素,而是由各對象元素共享的,能夠看出: 它們的值是在不斷變化的,並且不管對哪一個對象元素而言,都是相同的,並且始終不釋放內存空間
  3. total是公有的成員函數,其做用是將一個學生的成績累加到sum中。公有的成員函數能夠引用本對象中的通常數據成員(非靜態數據成員),也能夠引用類中的靜態數據成員。score是非靜態數據成員,sum和count是靜態數據成員。
  4. average是靜態成員函數,它能夠直接引用私有的靜態數據成員(沒必要加類名或對象名), 函數返回成績的平均值。
  5. 在main函數中,引用total函數要加對象名(今用對象數組元素名), 引用靜態成員函數average函數要用類名或對象名。
  6. 請思考,若是不將average函數定義爲靜態成員函數行不行?程序可否經過編譯?須要做什麼修改?爲何要用靜態成員函數?請分析其理由。


最後請注意,做爲C++程序員,最好養成這樣的習慣:只用靜態成員函數引用靜態數據成員,而不引用非靜態數據成員。這樣思路清晰,邏輯清楚,不易出錯。

 

 

20)C++友元函數和友元類

在一個類中能夠有公用的(public)成員和私有的(private)成員,在類外能夠訪問公用成員,只有本類中的函數能夠訪問本類的私有成員。如今,咱們來補充介紹一個例外——友元(friend)。

fnend 的意思是朋友,或者說是好友,與好友的關係顯然要比通常人親密一些。有的家庭可能會這樣處理:客廳對全部來客開放,而臥室除了本家庭的成員能夠進人之外,還容許好朋友進入。在C++中,這種關係以關鍵宇 friend 聲明,中文多譯爲友元。友元能夠訪問與其有好友關係的類中的私有成員,友元包括友元函數和友元類。若是您對友元這個名詞不習慣,能夠按原文 friend 理解爲朋友便可。

友元函數

在當前類之外定義的、不屬於當前類的函數也能夠在類中聲明,但要在前面加 friend 關鍵字,這樣就構成了友元函數。友元函數能夠是不屬於任何類的非成員函數,也能夠是其餘類的成員函數。

友元函數能夠訪問當前類中的全部成員,包括 private 屬性的。

1) 將普通函數聲明爲友元函數。

#include<iostream>
using namespace std;

class Student{
private:
    char *name;
    int age;
    float score;
public:
    Student(char*, int, float);
    friend void display(Student &);  //將display聲明爲友元函數
};
Student::Student(char *name, int age, float score){
    this->name = name;
    this->age= age;
    this->score = score;
}

//普通成員函數
void display(Student &stu){
    cout<<stu.name<<"的年齡是 "<<stu.age<<",成績是 "<<stu.score<<endl;
}

int main(){
    Student stu("小明", 16, 95.5f);
    display(stu);

    return 0;
}

 

運行結果:
小明的年齡是 16,成績是 95.5

請注意 display 是一個在類外定義的且沒有使用 Student 做限定的函數,它是非成員函數,不屬於任何類,它的做用是輸出學生的信息。若是在 Student 類中未聲明 display 函數爲 friend 函數,它是不能引用 Student 中的私有成員 name、age、score 的。你們能夠親測一下,將上面程序中的第11行刪去,觀察編譯時的信息。

如今因爲聲明瞭 display 是 Student 類的 friend 函數,因此 display 能夠使用 Student 中的私有成員 name、age、score。但注意在使用這些成員變量時必須加上對象名,不能寫成:

cout<<name<<"的年齡是 "<<age<<",成績是 "<<score<<endl;   //錯誤

 

2) 將其餘類的成員函數聲明爲友元函數
friend 函數不只能夠是普通函數(非成員函數),還能夠是另外一個類中的成員函數。請看下面的例子:

#include<iostream>
using namespace std;

class Address;  //對Address類的提早引用聲明 

//聲明Student類
class Student{
private:
    char *name;
    int age;
    float score;
public:
    Student(char*, int, float);
    void display(Address &);
};

//聲明Address類
class Address{
private:
    char *province;
    char *city;
    char *district;
public:
    Address(char*, char*, char*);
    //將Student類中的成員函數display聲明爲友元函數
    friend void Student::display(Address &);
};
Address::Address(char *province, char *city, char *district){
    this->province = province;
    this->city = city;
    this->district = district;
}

//聲明Student類成構造函數和成員函數
Student::Student(char *name, int age, float score){
    this->name = name;
    this->age= age;
    this->score = score;
}
void Student::display(Address &add){
    cout<<name<<"的年齡是 "<<age<<",成績是 "<<score<<endl;
    cout<<"家庭住址:"<<add.province<<""<<add.city<<""<<add.district<<""<<endl;
}

int main(){
    Student stu("小明", 16, 95.5f);
    Address add("陝西", "西安", "雁塔");
    stu.display(add);

    return 0;
}

運行結果:
小明的年齡是 16,成績是 95.5
家庭住址:陝西省西安市雁塔區

在本例中定義了兩個類 Student 和 Address。程序第 26 行將 Student 類中的成員函數 display 聲明爲友元函數,由此,display 就能夠訪問 Address 類的私有成員變量了(只是這個函數能夠訪問address了)。

兩點注意:
① 程序第4行對Address類進行了提早聲明,是由於在Address類定義以前、在Student類中使用到了它,若是不提早聲明,編譯會報錯,提示"Address" has not been declared。類的提早聲明和函數的提早聲明是一個道理。

② 程序中將 Student 類的聲明和定義分開了,而將 Address 放在了中間,是由於 Student::display() 函數體中用到了 Address 類的成員,必須出如今 Address 類的類體以後(類體說明了有哪些成員)。

這裏簡單介紹一下類的提早聲明。通常狀況下,類必須在正式聲明以後才能使用;可是某些狀況下(如上例所示),只要作好提早聲明,也能夠先使用。

可是應當注意,類的提早聲明的使用範圍是有限的。只有在正式聲明一個類之後才能用它去建立對象。若是在上面程序第4行後面增長一行:

Address obj;  //企圖定義一個對象

 

會在編譯時出錯。由於建立對象時是要爲對象分配內存空間的,在正式聲明類以前,編譯系統沒法肯定應該爲對象分配多大的空間。編譯器只有在「見到」類體後(實際上是見到成員變量),才能肯定應該爲對象預留多大的空間。在對一個類做了提早引用聲明後,能夠用該類的名字去定義指向該類型對象的指針變量或對象的引用變量(如在本例中,定義了Address類對象的引用變量)。這是由於指針變量和引用變量自己的大小是固定的,與它所指向的類對象的大小無關。

請注意程序是在定義 Student::display() 函數以前正式聲明 Address 類的。這是由於在 Student::display() 函數體中要用到 Address 類的成員變量 province、city、district,若是不正式聲明 Address 類,編譯器就沒法識別這些成員變量。

③ 一個函數能夠被多個類聲明爲「朋友」,這樣就能夠引用多個類中的私有成員。

友元類

不只能夠將一個函數聲明爲一個類的「朋友」,並且能夠將整個類(例如B類)聲明爲另外一個類(例如A類)的「朋友」。這時B類就是A類的友元類。

友元類B中的全部函數都是A類的友元函數,能夠訪問A類中的全部成員。在A類的類體中用如下語句聲明B類爲其友元類:

friend B;

 

聲明友元類的通常形式爲:

friend 類名;

關於友元,有兩點須要說明:

  • 友元的關係是單向的而不是雙向的。若是聲明瞭 B類是A類的友元類,不等於A類是B類的友元類,A類中的成員函數不能訪問B類中的私有數據。
  • 友元的關係不能傳遞,若是B類是A類的友元類,C類是B類的友元類,不等於 C類是A類的友元類。


在實際開發中,除非確有必要,通常並不把整個類聲明爲友元類,而只將確實有須要的成員函數聲明爲友元函數,這樣更安全一些。

 

21)類模塊

有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不一樣,以下面語句聲明瞭一個類:

class Compare_int
{
public :
   Compare(int a,int b)
   {
      x=a;
      y=b;
   }
   int max( )
   {
      return (x>y)?x:y;
   }
   int min( )
   {
      return (x<y)?x:y;
   }
private :
   int x,y;
};

其做用是對兩個整數做比較,能夠經過調用成員函數max和min獲得兩個整數中的大者和小者。

若是想對兩個浮點數(float型)做比較,須要另外聲明一個類:

class Compare_float
{
public :
   Compare(float a,float b)
   {
      x=a;y=b;
   }
   float max( )
   {
      return (x>y)?x:y;
   }
   float min( )
   {
      return (x<y)?x:y;
   }
private :
   float x,y;
}

顯然這基本上是重複性的工做,應該有辦法減小重複的工做。

C++在發展的後期增長了模板(template )的功能,提供瞭解決這類問題的途徑。能夠聲明一個通用的類模板,它能夠有一個或多個虛擬的類型參數,如對以上兩個類能夠綜合寫出如下的類模板:

template <class numtype> //聲明一個模板,虛擬類型名爲numtype
class Compare //類模板名爲Compare
{
public :
   Compare(numtype a,numtype b)
   {
      x=a;y=b;
   }
   numtype max( )
   {
      return (x>y)?x:y;
   }
   numtype min( )
   {
      return (x<y)?x:y;
   }
private :
   numtype x,y;
};

 

請將此類模板和前面第一個Compare_int類做一比較,能夠看到有兩處不一樣。

1) 聲明類模板時要增長一行
    template <class 類型參數名>
template意思是「模板」,是聲明類模板時必須寫的關鍵字。在template後面的尖括號內的內容爲模板的參數表列,關鍵字class表示其後面的是類型參數。在本例中numtype就是一個類型參數名。這個名宇是能夠任意取的,只要是合法的標識符便可。這裏取numtype只是表示「數據類型」的意思而已。此時,mimtype並非一個已存在的實際類型名,它只是一個虛擬類型參數名。在之後將被一個實際的類型名取代。

2) 原有的類型名int換成虛擬類型參數名numtype。
在創建類對象時,若是將實際類型指定爲int型,編譯系統就會用int取代全部的numtype,若是指定爲float型,就用float取代全部的numtype。這樣就能實現「一類多用」。

因爲類模板包含類型參數,所以又稱爲參數化的類。若是說類是對象的抽象,對象是類的實例,則類模板是類的抽象,類是類模板的實例。利用類模板能夠創建含各類數據類型的類。

那麼,在聲明瞭一個類模板後,怎樣使用它呢?怎樣使它變成一個實際的類?

先回顧一下用類來定義對象的方法:
    Compare_int cmp1(4,7); // Compare_int是已聲明的類
其做用是創建一個Compare_int類的對象,並將實參4和7分別賦給形參a和b,做爲進 行比較的兩個整數。

用類模板定義對象的方法與此類似,可是不能直接寫成
   Compare cmp(4,7); // Compare是類模板名
Compare是類模板名,而不是一個具體的類,類模板體中的類型numtype並非一個實際的類型,只是一個虛擬的類型,沒法用它去定義對象。必須用實際類型名去取代虛擬的類型,具體的作法是:
    Compare <int> cmp(4,7);
即在類模板名以後在尖括號內指定實際的類型名,在進行編譯時,編譯系統就用int取代類模板中的類型參數numtype,這樣就把類模板具體化了,或者說實例化了。這時Compare<int>就至關於前面介紹的Compare_int類。

[例9.14] 聲明一個類模板,利用它分別實現兩個整數、浮點數和字符的比較,求出大數和小數。

#include <iostream>
using namespace std;
template <class numtype>
//定義類模板
class Compare
{
   public :
   Compare(numtype a,numtype b)
   {x=a;y=b;}
   numtype max( )
   {return (x>y)?x:y;}
   numtype min( )
   {return (x<y)?x:y;}
   private :
   numtype x,y;
};
int main( )
{
   Compare<int > cmp1(3,7);  //定義對象cmp1,用於兩個整數的比較
   cout<<cmp1.max( )<<" is the Maximum of two integer numbers."<<endl;
   cout<<cmp1.min( )<<" is the Minimum of two integer numbers."<<endl<<endl;
   Compare<float > cmp2(45.78,93.6);  //定義對象cmp2,用於兩個浮點數的比較
   cout<<cmp2.max( )<<" is the Maximum of two float numbers."<<endl;
   cout<<cmp2.min( )<<" is the Minimum of two float numbers."<<endl<<endl;
   Compare<char> cmp3(′a′,′A′);  //定義對象cmp3,用於兩個字符的比較
   cout<<cmp3.max( )<<" is the Maximum of two characters."<<endl;
   cout<<cmp3.min( )<<" is the Minimum of two characters."<<endl;
   return 0;
}

 

運行結果以下:
7 is the Maximum of two integers.
3 is the Minimum of two integers.

93.6 is the Maximum of two float numbers.
45.78 is the Minimum of two float numbers.

a is the Maximum of two characters.
A is the Minimum of two characters.

還有一個問題要說明: 上面列出的類模板中的成員函數是在類模板內定義的。若是改成在類模板外定義,不能用通常定義類成員函數的形式:
    numtype Compare::max( ) {…} //不能這樣定義類模板中的成員函數
而應當寫成類模板的形式:
    template <class numtype>
    numtype Compare<numtype>::max( )
    {
        return (x>y)?x:y;
    }
上面第一行表示是類模板,第二行左端的numtype是虛擬類型名,後面的Compare <numtype>是一個總體,是帶參的類。表示所定義的max函數是在類Compare <numtype>的做用域內的。在定義對象時,用戶固然要指定實際的類型(如int),進行編譯時就會將類模板中的虛擬類型名numtype所有用實際的類型代替。這樣Compare <numtype >就至關於一個實際的類。你們能夠將例9.14改寫爲在類模板外定義各成員 函數。

概括以上的介紹,能夠這樣聲明和使用類模板:
1) 先寫出一個實際的類。因爲其語義明確,含義清楚,通常不會出錯。

2) 將此類中準備改變的類型名(如int要改變爲float或char)改用一個本身指定的虛擬類型名(如上例中的numtype)。

3) 在類聲明前面加入一行,格式爲:
    template <class 虛擬類型參數>
如:
    template <class numtype> //注意本行末尾無分號
    class Compare
    {…}; //類體

4) 用類模板定義對象時用如下形式:
    類模板名<實際類型名> 對象名;
    類模板名<實際類型名> 對象名(實參表列);
如:
    Compare<int> cmp;
    Compare<int> cmp(3,7);

5) 若是在類模板外定義成員函數,應寫成類模板形式:
   template <class 虛擬類型參數>
   函數類型 類模板名<虛擬類型參數>::成員函數名(函數形參表列) {…}

關於類模板的幾點說明:
1) 類模板的類型參數能夠有一個或多個,每一個類型前面都必須加class,如:
    template <class T1,class T2>
    class someclass
    {…};
在定義對象時分別代入實際的類型名,如:
    someclass<int,double> obj;

2) 和使用類同樣,使用類模板時要注意其做用域,只能在其有效做用域內用它定義對象。

3) 模板能夠有層次,一個類模板能夠做爲基類,派生出派生模板類。有關這方面的知識實際應用較少。

 

九.運算符重載

1)什麼叫運算符重載

 如今要討論的問題是:用戶可否根據本身的須要對C++已提供的運算符進行重載,賦予它們新的含義,使之一名多用。譬如,可否用」+」號進行兩個複數的相加。在C++中不能在程序中直接用運算符」+」對複數進行相加運算。用戶必須本身設法實現複數相加。例如用戶能夠經過定義一個專門的函數來實現複數相加。見例10.1

#include <iostream>
using namespace std;
class Complex //定義Complex類
{
public:
   Complex( ){real=0;imag=0;}    //定義構造函數
   Complex(double r,double i){real=r;imag=i;}  //構造函數重載
   Complex complex_add(Complex &c2);  //聲明覆數相加函數
   void display( );  //聲明輸出函數
private:
   double real;  //實部
   double imag;  //虛部
};

Complex Complex::complex_add(Complex &c2)
{
   Complex c;
   c.real=real+c2.real;
   c.imag=imag+c2.imag;
   return c;
}

void Complex::display( ) //定義輸出函數
{
   cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main( )
{
   Complex c1(3,4),c2(5,-10),c3;//定義3個複數對象
   c3=c1.complex_add(c2); //調用複數相加函數
   cout<<"c1="; c1.display( );//輸出c1的值
   cout<<"c2="; c2.display( );//輸出c2的值
   cout<<"c1+c2="; c3.display( );//輸出c3的值
   return 0;
}

運行結果以下:
c1=(3+4i)
c2=(5-10i)
c1+c2=(8,-6i)

結果無疑是正確的,但調用方式不直觀、太煩瑣,令人感到很不方便。可否也和整數的加法運算同樣,直接用加號」+」來實現複數運算呢?
    c3=c1+c2;
編譯系統就會自動完成c1和c2兩個複數相加的運算。若是能作到,就爲對象的運算提供了很大的方便。這就須要對運算符」+「進行重載。

2)運算符重載方法

重載運算符的函數通常格式以下:
    函數類型 operator 運算符名稱 (形參表列)
    {
        // 對運算符的重載處理
    }

例如,想將」+」用於Complex類(複數)的加法運算,函數的原型能夠是這樣的:
    Complex operator+ (Complex& c1, Complex& c2);
在上面的通常格式中,operator是關鍵字,是專門用於定義重載運算符的函數的,運算符名稱就是C++提供給用戶的預約義運算符。注意,函數名是由operator和運算符組成,上面的operator+就是函數名,意思是「對運算符+重載」。只要掌握這點,就能夠發現,這 類函數和其餘函數在形式上沒有什麼區別。兩個形參是Complex類對象的引用,要求實參爲Complex類對象。

在定義了重載運算符的函數後,能夠說,函數operator +重載了運算符+。在執行復數相加的表達式c1 + c2時(假設c1和c2都已被定義爲Complex類對象),系統就會調用operator+函數,把c1和c2做爲實參,與形參進行虛實結合。

爲了說明在運算符重載後,執行表達式就是調用函數的過程,能夠把兩個整數相加也想像爲調用下面的函數:
int operator + (int a, int b)
{
    return (a+b);
}

若是有表達式5+8,就調用此函數,將5和8做爲調用函數時的實參,函數的返回值爲13。這就是用函數的方法理解運算符。能夠在例10.1程序的基礎上重載運算符「+」,使之用於複數相加。

[例10.2] 改寫例10.1,重載運算符「+」,使之能用於兩個複數相加。

#include <iostream>
using namespace std;
class Complex
{
public:
   Complex( ){real=0;imag=0;}
   Complex(double r,double i){real=r;imag=i;}
   Complex operator+(Complex &c2);//聲明重載運算符的函數   在對象中使用
   void display( );
private:
   double real;
   double imag;
};
Complex Complex::operator+(Complex &c2) //定義重載運算符的函數
{
   Complex c;
   c.real=real+c2.real;
   c.imag=imag+c2.imag;
   return c;
}

void Complex::display( )
{
   cout<<"("<<real<<","<<imag<<"i)"<<endl;
}

int main( )
{
   Complex c1(3,4),c2(5,-10),c3;
   c3=c1+c2; //運算符+用於複數運算
   cout<<"c1=";c1.display( );
   cout<<"c2=";c2.display( );
   cout<<"c1+c2=";c3.display( );
   return 0;
}

運行結果與例10.1相同:
c1=(3+4i)
c2=(5-10i)
c1+c2=(8,-6i)

請比較例10.1和例10.2,只有兩處不一樣:
1) 在例10.2中以operator+函數取代了例10.1中的complex_add函數,並且只是函數名不一樣,函數體和函數返回值的類型都是相同的。

2) 在main函數中,以「c3=c1+c2;」取代了例10.1中的「c3=c1.complex_add(c2);」。在將運算符+重載爲類的成員函數後,C++編譯系統將程序中的表達式c1+c2解釋爲
    c1.operator+(c2)  //其中c1和c2是Complex類的對象
即以c2爲實參調用c1的運算符重載函數operator+(Complex &c2),進行求值,獲得兩個複數之和。

能夠看到,兩個程序的結構和執行過程基本上是相同的,做用相同,運行結果也相同。重載運算符是由相應的函數實現的。有人可能說,既然這樣,何須對運算符重載呢?咱們要從用戶的角度來看問題,雖然重載運算符所實現的功能徹底能夠用函數實現,可是使用運算符重載能使用戶程序易於編寫、閱讀和維護。在實際工做中,類的聲明和類的使用每每是分離的。假如在聲明Complex類時,對運算符+, -, *, /都進行了重載,那麼使用這個類的用戶在編程時能夠徹底不考慮函數是怎麼實現的,放心大膽地直接使用+, -, *, /進行復數的運算便可,十分方便。

對上面的運算符重載函數operator+還能夠改寫得更簡練一些:
    Complex Complex::operator + (Complex &c2)
    {return Complex(real+c2.real, imag+c2.imag);}
return語句中的Complex( real+c2.real, imag+c2.imag)是創建一個臨時對象,它沒有對名,是一個無名對象。在創建臨時對象過程當中調用構造函數。return語句將此臨時對象做爲函數返回值。

請思考,在例10.2中可否將一個常量和一個複數對象相加?如
    c3=3+c2;  //錯誤,與形參類型不匹配
應寫成對象形式,如
    c3 = Complex (3,0) +c2;  //正確,類型均爲對象

須要說明的是,運算符被重載後,其原有的功能仍然保留,沒有喪失或改變。經過運算符重載,擴大了C++已有運算符的做用範圍,使之能用於類對象。

運算符重載對C++有重要的意義,把運算符重載和類結合起來,能夠在C++程序中定義出頗有實用意義而使用方便的新的數據類型。運算符重載使C++具備更強大的功能、更好的可擴充性和適應性,這是C++最吸引人的特色之一。

 

3)運算符重載規則

1) C++不容許用戶本身定義新的運算符,只能對已有的C++運算符進行重載。 例如,有人以爲BASIC中用「**「做爲冪運算符很方便,也想在C++中將」**「定義爲冪運算符,用」3**5「表示35,這樣是不行的。

2) 重載不能改變運算符運算對象(即搡做數)的個數。如關係運算符「>」和「 <」 等是雙目運算符,重載後仍爲雙目運算符,須要兩個參數。運算符「 +」,「-」,「*」,「&」等既能夠做爲單目運算符,也能夠做爲雙目運算符,能夠分別將它們重載爲單目運算符或雙目運算符。

3) 重載不能改變運算符的優先級別。例如「*」和「/」優先於「 +」和「-」,不論怎樣進行重載,各運算符之間的優先級別不會改變。有時在程序中但願改變某運算符的優先級,也只能使用加圓括號的辦法強制改變重載運算符的運算順序。

4) 重載不能改變運算符的結含性。如賦值運算符是右結合性(自右至左),重載後仍爲右結合性。

5) 重載運算符的函數不能有默認的參數,不然就改變了運算符參數的個數,與前面第(2)點矛盾。

6) 重載的運算符必須和用戶定義的自定義類型的對象一塊兒使用,其參數至少應有一個是類對象(或類對象的引用)。也就是說,參數不能所有是C++的標準類型,以防止用戶修改用於標準類型數據的運算符的性質,以下面這樣是不對的:
    int operator + (int a,int b)
    {
        retum(a-b);
    }
原來運算符+的做用是對兩個數相加,如今企圖經過重載使它的做用改成兩個數相減。 若是容許這樣重載的話,若是有表達式4+3,它的結果是7呢仍是1?顯然,這是絕對禁止的。

若是有兩個參數,這兩個參數能夠都是類對象,也能夠一個是類對象,一個是C ++標準類型的數據,如
    Complex operator + (int a,Complex&c)
    {
        return Complex(a +c.real, c.imag);
    }
它的做用是使一個整數和一個複數相加。

7) 用於類對象的運算符通常必須重載,但有兩個例外,運算符「=」和「&」沒必要重載

①賦值運算符( = )能夠用於每個類對象,能夠利用它在同類對象之間相互賦值。 咱們知道,能夠用賦值運算符對類的對象賦值,這是由於系統已爲每個新聲明的類重載了一個賦值運算符,它的做用是逐個複製類的數據成員。用戶能夠認爲它是系統提供的默認的對象賦值運算符,能夠直接用於對象間的賦值,沒必要本身進行重載。可是有時系統提供的默認的對象賦值運算符不能知足程序的要求,例如,數據成員中包含指向動態分配內存的指針成員時,在複製此成員時就可能出現危險。在這種狀況下, 就須要本身重載賦值運算符。

②地址運算符&也沒必要重載,它能返回類對象在內存中的起始地址。

8) 從理論上說,能夠將一個運算符重載爲執行任意的操做,如能夠將加法運算符重載爲輸出對象中的信息,將「>」運算符重載爲「小於」運算。但這樣違背了運算符重載的初衷,非但沒有提髙可讀性,反而令人莫名其妙,沒法理解程序。應當使重載運算符的功能相似於該運算符做用於標準類型數據時所實現的功能(如用「+」實現加法,用「>」實現「大於」的關係運算)一切都是爲了方便,不是爲了無理取鬧。

9) 運算符重載函數能夠是類的成員函數,也能夠是類的友元函數,還能夠是既非類的成員函數也不是友元函敝的普通函數。

以上這些規則是很容易理解的,沒必要死記。把它們集中在一塊兒介紹,只是爲了使讀者有一個總體的概念,也便於查閱。

 

4)容許重載和不容許重載的運算符

 C++中絕大部分的運算符容許重載,具體規定見表10.1。

表10.1 C++容許重載的運算符
雙目算術運算符 + (加),-(減),*(乘),/(除),% (取模)
關係運算符 ==(等於),!= (不等於),< (小於),> (大於>,<=(小於等於),>=(大於等於)
邏輯運算符 ||(邏輯或),&&(邏輯與),!(邏輯非)
單目運算符 + (正),-(負),*(指針),&(取地址)
自增自減運算符 ++(自增),--(自減)
位運算符 | (按位或),& (按位與),~(按位取反),^(按位異或),,<< (左移),>>(右移)
賦值運算符 =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>=
空間申請與釋放 new, delete, new[ ] , delete[]
其餘運算符 ()(函數調用),->(成員訪問),->*(成員指針訪問),,(逗號),[](下標)


不能重載的運算符只有5個:
.  (成員訪問運算符)
.*  (成員指針訪問運算符)
::  (域運算符)
sizeof  (長度運算符)
?:  (條件運算符)

前兩個運算符不能重載是爲了保證訪問成員的功能不能被改變,域運算符和sizeof 運算符的運算對象是類型而不是變量或通常表達式,不具有重載的特徵。

 

5)運算符重載函數做爲類成員函數和友元函數

 [例10.3] 將運算符「+」重載爲適用於複數加法,重載函數不做爲成員函數,而放在類外,做爲Complex類的友元函數。

#include <iostream>
using namespace std;
// 注意,該程序在VC 6.0中編譯出錯,將以上兩行替換爲 #include <iostream.h> 便可順利經過
class Complex
{
   public:
   Complex( ){real=0;imag=0;}
   Complex(double r,double i){real=r;imag=i;}
   friend Complex operator + (Complex &c1,Complex &c2); //重載函數做爲友元函數
   void display( );
   private:
   double real;
   double imag;
};

Complex operator + (Complex &c1,Complex &c2) //定義做爲友元函數的重載函數
{
   return Complex(c1.real+c2.real, c1.imag+c2.imag);
}

void Complex::display( )
{
   cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main( )
{
   Complex c1(3,4),c2(5,-10),c3;
   c3=c1+c2;
   cout<<"c1="; c1.display( );
   cout<<"c2="; c2.display( );
   cout<<"c1+c2 ="; c3.display( );
}

 

與例10.2相比較,只做了一處改動,將運算符函數不做爲成員函數,而把它放在類外,在Complex類中聲明它爲友元函數。同時將運算符函數改成有兩個參數。在將運算符「+」重載爲非成員函數後,C++編譯系統將程序中的表達式c1+c2解釋爲
    operator+(c1, c2)
即執行c1+c2至關於調用如下函數:
    Complex operator + (Complex &c1,Complex &c2)
    {
        return Complex(c1.real+c2.real, c1.imag+c2.imag);
    }
求出兩個複數之和。運行結果同例10.2。

爲何把運算符函數做爲友元函數呢?由於運算符函數要訪問Complex類對象中的成員。若是運算符函數不是Complex類的友元函數,而是一個普通的函數,它是沒有權利訪問Complex類的私有成員的。

在上節中曾提到過:運算符重載函數能夠是類的成員函數,也能夠是類的友元函數,還能夠是既非類的成員函數也不是友元函數的普通函數。如今分別討論這3種狀況。

首先,只有在極少的狀況下才使用既不是類的成員函數也不是友元函數的普通函數,緣由是上面提到的,普通函數不能直接訪問類的私有成員。

在剩下的兩種方式中,何時應該用成員函數方式,何時應該用友元函數方式?兩者有何區別呢?若是將運算符重載函數做爲成員函數,它能夠經過this指針自由地訪問本類的數據成員,所以能夠少寫一個函數的參數。但必需要求運算表達式第一個參數(即運算符左側的操做數)是一個類對象,並且與運算符函數的類型相同。由於必須經過類的對象去調用該類的成員函數,並且只有運算符重載函數返回值與該對象同類型,運算結果纔有意義。在例10.2中,表達式c1+c2中第一個參數c1是Complex類對象,運算符函數返回值的類型也是Complex,這是正確的。若是c1不是Complex類,它就沒法經過隱式this指針訪問Complex類的成員了。若是函數返回值不是Complex類複數,顯然這種運算是沒有實際意義的。

如想將一個複數和一個整數相加,如c1+i,能夠將運算符重載函數做爲成員函數,以下面的形式:
    Complex Complex∷operator+(int &i)  //運算符重載函數做爲Complex類的成員函數
    {
        return Complex(real+i,imag);
    }
注意在表達式中重載的運算符「+」左側應爲Complex類的對象(切記這點),如:
    c3=c2+i;
不能寫成
    c3=i+c2;  //運算符「+」的左側不是類對象,編譯出錯
若是出於某種考慮,要求在使用重載運算符時運算符左側的操做數是整型量(如表達式i+c2,運算符左側的操做數i是整數),這時是沒法利用前面定義的重載運算符的,由於沒法調用i.operator+函數。可想而知,若是運算符左側的操做數屬於C++標準類型(如int)或是一個其餘類的對象,則運算符重載函數不能做爲成員函數,只能做爲非成員函數。若是函數須要訪問類的私有成員,則必須聲明爲友元函數。能夠在Complex類中聲明:
    friend Complex operator+(int &i,Complex &c); //第一個參數能夠不是類對象
在類外定義友元函數:
    Complex operator+(int &i, Complex &c) //運算符重載函數不是成員函數
    {
        return Complex(i+c.real, c.imag);
    }
將雙目運算符重載爲友元函數時,在函數的形參表列中必須有兩個參數,不能省略,形參的順序任意,不要求第一個參數必須爲類對象。但在使用運算符的表達式中,要求運算符左側的操做數與函數第一個參數對應,運算符右側的操做數與函數的第二個參數對應。如:
    c3=i+c2;  //正確,類型匹配   說白了就是參數先後類型保存一致
    c3=c2+i;  //錯誤,類型不匹配

請注意,數學上的交換律在此不適用若是但願適用交換律,則應再重載一次運算符「+」。如
    Complex operator+(Complex &c, int &i) //此時第一個參數爲類對象
    {
        return Complex(i+c.real, c.imag);
    } (重載一次,變量先後適用)
這樣,使用表達式i+c2和c2+i都合法,編譯系統會根據表達式的形式選擇調用與之匹配的運算符重載函數。能夠將以上兩個運算符重載函數都做爲友元函數,也能夠將一個運算符重載函數(運算符左側爲對象名的) 做爲成員函數,另外一個(運算符左側不是對象名的)做爲友元函數。但不可能將兩個都做爲成員函數,緣由是顯然的。

C++規定,有的運算符(如賦值運算符、下標運算符、函數調用運算符)必須定義爲類的成員函數,有的運算符則不能定義爲類的成員函數(如流插入「<<」和流提取運算符「>>」、類型轉換運算符)。

因爲友元的使用會破壞類的封裝,所以從原則上說,要儘可能將運算符函數做爲成員函數。但考慮到各方面的因素,通常將單目運算符重載爲成員函數,將雙目運算符重載爲友元函數。在學習了本章第10.7節例10.9的討論後,讀者對此會有更深刻的認識。

說明:有的C++編譯系統(如Visual C++ 6.0)沒有徹底實現C++標準,它所提供不帶後綴.h的頭文件不支持把成員函數重載爲友元函數。上面例10.3程序在GCC中能正常運行,而在Visual C++ 6.0中會編譯出錯。可是Visual C++所提供的老形式的帶後綴.h的頭文件能夠支持此項功能,所以能夠將程序頭兩行修改以下,便可順利運行:
    #include <iostream.h>
之後如遇到相似狀況,亦可照此辦理。

 

6)重載流插入運算符和流提取運算符

C++的流插入運算符「<<」和流提取運算符「>>」是C++在類庫中提供的,全部C++編譯系統都在類庫中提供輸入流類istream和輸出流類ostream。cin和cout分別是istream類和ostream類的對象。在類庫提供的頭文件中已經對「<<」和「>>」進行了重載,使之做爲流插入運算符和流提取運算符,能用來輸出和輸入C++標準類型的數據。所以,凡是用「cout<<」和「cin>>」對標準類型數據進行輸入輸出的,都要用#include 把頭文件包含到本程序文件中。

用戶本身定義的類型的數據,是不能直接用「<<」和「>>」來輸出和輸入的。若是想用它們輸出和輸入本身聲明的類型的數據,必須對它們重載。

對「<<」和「>>」重載的函數形式以下:
    istream & operator >> (istream &, 自定義類 &);
    ostream & operator << (ostream &, 自定義類 &);
即重載運算符「>>」的函數的第一個參數和函數的類型都必須是istream&類型,第二個參數是要進行輸入操做的類。重載「<<」的函數的第一個參數和函數的類型都必須是ostream&類型,第二個參數是要進行輸出操做的類。所以,只能將重載「>>」和「<<」的函數做爲友元函數或普通的函數,而不能將它們定義爲成員函數。

重載流插入運算符「<<」

在程序中,人們但願能用插入運算符「<<」來輸出用戶本身聲明的類的對象的信息,這就須要重載流插入運算符「<<」。

[例10.7] 在例10.2的基礎上,用重載的「<<」輸出複數。

#include <iostream>
using namespace std;
class Complex
{
   public:  (調用該類就會執行)
   Complex( ){real=0;imag=0;}
   Complex(double r,double i){real=r;imag=i;}
   Complex operator + (Complex &c2);  //運算符「+」重載爲成員函數
   friend ostream& operator << (ostream&,Complex&);  //運算符「<<」重載爲友元函數
   private:
   double real;
   double imag;
};

Complex Complex::operator + (Complex &c2)//定義運算符「+」重載函數
{
   return Complex(real+c2.real,imag+c2.imag);
}
ostream& operator << (ostream& output,Complex& c) //定義運算符「<<」重載函數
{
   output<<"("<<c.real<<"+"<<c.imag<<"i)"<<endl;
   return output;
}

int main( )
{
   Complex c1(2,4),c2(6,10),c3;  //c1,c2,c3就叫對象了
   c3=c1+c2;
   cout<<c3;  
   return 0;
}

 

注意,在Visual C++ 6.0環境下運行時,需將第一行改成#include <iostream.h>,並刪去第2行,不然編譯不能經過。運行結果爲:
(8+14i)

能夠看到在對運算符「<<」重載後,在程序中用「<<」不只能輸出標準類型數據,並且能夠輸出用戶本身定義的類對象。用「cout<<c3」即能以複數形式輸出複數對象c3的值。形式直觀,可讀性好,易於使用。

下面對怎樣實現運算符重載做一些說明。程序中重載了運算符「<<」,運算符重載函數中的形參output是ostream類對象的引用,形參名output是用戶任意起的。分析main函數最後第二行:
    cout<<c3;
運算符「<<」的左面是cout,前面已提到cout是ostream類對象。「<<」的右面是c3,它是Complex類對象。因爲已將運算符「<<」的重載函數聲明爲Complex類的友元函數,編譯系統把「cout<<c3」解釋爲
    operator<<(cout, c3)
即以cout和c3做爲實參,調用下面的operator<<函數:
    ostream& operator<<(ostream& output,Complex& c)
    {
       output<<"("<<c.real<<"+"<<c.imag<<"i)"<<endl;
       return output;
    }
調用函數時,形參output成爲cout的引用,形參c成爲c3的引用。所以調用函數的過程至關於執行:
    cout<<″(″<<c3.real<<″+″<<c3.imag<<″i)″<<endl; return cout;
請注意,上一行中的「<<」是C++預約義的流插入符,由於它右側的操做數是字符串常量和double類型數據。執行cout語句輸出複數形式的信息。而後執行return語句。

請思考,return  output的做用是什麼?回答是能連續向輸出流插入信息。output是ostream類的對象,它是實參cout的引用,也就是cout經過傳送地址給output,使它們兩者共享同一段存儲單元,或者說output是cout的別名。所以,return output就是return cout,將輸出流cout的現狀返回,即保留輸出流的現狀。

請問返回到哪裏?剛纔是在執行
    cout<<c3;
在已知cout<<c3的返回值是cout的當前值。若是有如下輸出:
    cout<<c3<<c2;
先處理
    cout<<c3

   (cout<<c3)<<c2;
而執行(cout<<c3)獲得的結果就是具備新內容的流對象cout,所以,(cout<<c3)<<c2至關於cout(新值)<<c2。運算符「<<」左側是ostream類對象cout,右側是Complex類對象c2,則再次調用運算符「<<」重載函數,接着向輸出流插入c2的數據。如今能夠理解了爲何C++規定運算符「<<」重載函數的第一個參數和函數的類型都必須是ostream類型的引用,就是爲了返回cout的當前值以便連續輸出

請讀者注意區分什麼狀況下的「<<」是標準類型數據的流插入符,什麼狀況下的「<<」是重載的流插入符。如
    cout<<c3<<5<<endl;
有下劃線的是調用重載的流插入符,後面兩個「<<」不是重載的流插入符,由於它的右側不是Complex類對象而是標準類型的數據,是用預約義的流插入符處理的。

還有一點要說明,在本程序中,在Complex類中定義了運算符「<<」重載函數爲友元函數,所以只有在輸出Complex類對象時才能使用重載的運算符,對其餘類型的對象是無效的。如
    cout<<time1;  //time1是Time類對象(由於Time類中沒有定義該友元函數),不能使用用於Complex類的重載運算符

重載流提取運算符「>>」 

C++預約義的運算符「>>」的做用是從一個輸入流中提取數據,如「cin>>i;」表示從輸入流中提取一個整數賦給變量i(假設已定義i爲int型)。重載流提取運算符的目的是但願將「>>」用於輸入自定義類型的對象的信息。

[例10.8] 在例10.7的基礎上,增長重載流提取運算符「>>」,用「cin>>」輸入複數,用「cout<<」輸出複數。  (典  一個對象的輸入輸出)

#include <iostream>
using namespace std;
class Complex
{
   public:
   friend ostream& operator << (ostream&,Complex&); //聲明重載運算符「<<」
   friend istream& operator >> (istream&,Complex&); //聲明重載運算符「>>」
   private:
   double real;
   double imag;
};
ostream& operator << (ostream& output,Complex& c) //定義重載運算符「<<」
{
   output<<"("<<c.real<<"+"<<c.imag<<"i)";
   return output;
}
istream& operator >> (istream& input,Complex& c)  //定義重載運算符「>>」
{
   cout<<"input real part and imaginary part of complex number:";
   input>>c.real>>c.imag;
   return input;
}
int main( )
{
   Complex c1,c2;
   cin>>c1>>c2;
   cout<<"c1="<<c1<<endl;
   cout<<"c2="<<c2<<endl;
   return 0;
}

運行狀況以下:
input real part and imaginary part of complex number:3 6↙
input real part and imaginary part of complex number:4 10↙
c1=(3+6i)
c2=(4+10i)

以上運行結果無疑是正確的,但並不完善。在輸入複數的虛部爲正值時,輸出的結果是沒有問題的,可是虛部若是是負數,就不理想,請觀察輸出結果。
input real part and imaginary part of complex number:3 6↙
input real part and imaginary part of complex number:4 -10↙
c1=(3+6i)
c2=(4+-10i)

根據先調試經過,最後完善的原則,可對程序做必要的修改。將重載運算符「<<」函數修改以下:

ostream& operator << (ostream& output,Complex& c)
{
   output<<"("<<c.real;
   if(c.imag>=0) output<<"+";//虛部爲正數時,在虛部前加「+」號
   output<<c.imag<<"i)"<<endl;  //虛部爲負數時,在虛部前不加「+」號
   return output;
}

 

這樣,運行時輸出的最後一行爲c2=(4-10i) 。

能夠看到,在C++中,運算符重載是很重要的、頗有實用意義的。它使類的設計更加豐富多彩,擴大了類的功能和使用範圍,使程序易於理解,易於對對象進行操做,它體現了爲用戶着想、方便用戶使用的思想。有了運算符重載,在聲明瞭類以後,人們就能夠像使用標準類型同樣來使用本身聲明的類。類的聲明每每是一勞永逸的,有了好的類,用戶在程序中就沒必要定義許多成員函數去完成某些運算和輸入輸出的功能,使主函數更加簡單易讀。好的運算符重載能體現面向對象程序設計思想。

能夠看到,在運算符重載中使用引用(reference)的重要性。利用引用做爲函數的形參能夠在調用函數的過程當中不是用傳遞值的方式進行虛實結合,而是經過傳址方式使形參成爲實參的別名,所以不生成臨時變量(實參的副本),減小了時間和空間的開銷。此外,若是重載函數的返回值是對象的引用時,返回的不是常量,而是引用所表明的對象,它能夠出如今賦值號的左側而成爲左值(left value),能夠被賦值或參與其餘操做(如保留cout流的當前值以便能連續使用「<<」輸出)。但使用引用時要特別當心,由於修改了引用就等於修改了它所表明的對象。

 

7)數據類型轉換以及轉換構造函數

標準數據類型之間的轉換

在C++中,某些不一樣類型數據之間能夠自動轉換,例如
    int i = 6;
    i = 7.5 + i;
編譯系統對 7.5是做爲double型數處理的,在求解表達式時,先將6轉換成double型,而後與7.5相加,獲得和爲13.5,在向整型變量i賦值時,將13.5轉換爲整數13,而後賦給i。這種轉換是由C++編譯系統自動完成的,用戶不需干預。這種轉換稱爲隱式類型轉換。

C++還提供顯式類型轉換,程序人員在程序中指定將一種指定的數據轉換成另外一指定的類型,其形式爲:
    類型名(數據)

    int(89.5)
其做用是將89.5轉換爲整型數89。

之前咱們接觸的是標準類型之間的轉換,如今用戶本身定義了類,就提出了一個問題:一個自定義類的對象可否轉換成標準類型? 一個類的對象可否轉換成另一個類的對象?譬如,可否將一個複數類數據轉換成整數或雙精度數?可否將Date類的對象轉換成Time類的對象?

對於標準類型的轉換,編譯系統有章可循,知道怎樣進行轉換。而對於用戶本身聲明的類型,編譯系統並不知道怎樣進行轉換。解決這個問題的關鍵是讓編譯系統知道怎樣去進行這些轉換,須要定義專門的函數來處理。

轉換構造函數

轉換構造函數(conversion constructor function) 的做用是將一個其餘類型的數據轉換成一個類的對象。這裏回顧一下之前學習過的幾種構造函數:
1) 默認構造函數。以Complex類爲例,函數原型的形式爲:
    Complex( );  //沒有參數

2) 用於初始化的構造函數。函數原型的形式爲:
    Complex(double r, double i);  //形參表列中通常有兩個以上參數

3) 用於複製對象的複製構造函數。函數原型的形式爲:
    Complex (Complex &c);  //形參是本類對象的引用

如今介紹一種新的構造函數——轉換構造函數。

轉換構造函數只有一個形參,如
    Complex(double r) {real=r;imag=0;}
其做用是將double型的參數r轉換成Complex類的對象,將r做爲複數的實部,虛部爲0。用戶能夠根據須要定義轉換構造函數,在函數體中告訴編譯系統怎樣去進行轉換。

在類體中,能夠有轉換構造函數,也能夠沒有轉換構造函數,視須要而定。以上幾種構造函數能夠同時出如今同一個類中,它們是構造函數的重載。編譯系統會根據創建對象時給出的實參的個數與類型選擇形參與之匹配的構造函數。

假如在Complex類中定義了上面的構造函數,在Complex類的做用域中有如下聲明語句:
    Complex cl(3.5) ;  //創建對象cl,因爲只有一個參數,調用轉換構造函數
創建Comptex類對象cl,其real(實部)的值爲3.5,imag(虛部)的值爲0。它的做用就是將double型常數轉換成一個名爲cl的Complex類對象。也能夠用聲明語句創建一 個無名的Complex類對象。如
    Complex(3.6) ;   //用聲明語句創建一個無名的對象,合法,但沒法使用它

能夠在一個表達式中使用無名對象,如:
    cl =Complex(3.6);    //假設cl巳被定義爲Complex類對象
創建一個無名的Complex類對象,其值爲(3.6+0i),而後將此無名對象的值賦給cl,cl 在賦值後的值是(3.6+0i)。

若是已對運算符「+」進行了重載,使之能進行兩個Complex類對象的相加,若在程序中有如下表達式:
    c = cl +2.5;
編譯出錯,由於不能用運算符「+」將一個Comptex類對象和一個浮點數相加。能夠先將 2.5轉換爲Complex類無名對象,而後相加:
    c = cl + Complex (2.5);    //合法

請對比Complex(2.5)和int(2.5)。兩者形式相似,int(2.5)是強制類型轉換,將2.5轉換爲整數,int()是強制類型轉換運算符。能夠認爲Complex(2.5)的做用也是強制類型 轉換,將2.5轉換爲Complex類對象。

轉換構造函數也是一種構造函數,它遵循構造函數的通常規則。一般把有一個參數的構造函數用做類型轉換,因此,稱爲轉換構造函數。其實,有一個參數的構造函數也能夠不用做類型轉換,如
    Complex (double r){ cout<<r; }  //這種用法毫無心義,沒有人會這樣用

轉換構造函數的函數體是根據須要由用戶肯定的,務必使其有實際意義。例如也可 以這樣定義轉換構造函數:
    Complex(double r){ real =0; imag = r; }
即實部爲0,虛部爲r。這並不違反語法,但沒有人會這樣作。應該符合習慣,合乎情理。

注意:轉換構造函數只能有一個參數。若是有多個參數,就不是轉換構造函數。緣由是顯然的,若是有多個參數的話,到底是把哪一個參數轉換成Complex類的對象呢?

概括起來,使用轉換構造函數將一個指定的數據轉換爲類對象的方法以下:
1) 先聲明一個類。

2) 在這個類中定義一個只有一個參數的構造函數,參數的類型是須要轉換的類型,在函數體中指定轉換的方法。

3) 在該類的做用域內能夠用如下形式進行類型轉換:
    類名(指定類型的數據)
就能夠將指定類型的數據轉換爲此類的對象。

不只能夠將一個標準類型數據轉換成類對象,也能夠將另外一個類的對象轉換成轉換構造函數所在的類對象。如能夠將一個學生類對象轉換爲教師類對象,能夠在Teacher類中寫出下面的轉換構造函數:
    Teacher(Student& s){ num=s.num;strcpy(name, s.name);sex=s.sex; }
但應注意,對象s中的num,name,sex必須是公用成員,不然不能被類外引用。

 

8)類型轉換函數(類型轉換運算符函數)

用轉換構造函數 能夠將一個指定類型的數據轉換爲類的對象。可是不能反過來將一個類的對象轉換爲一個其餘類型的數據(例如將一個Complex類對象轉換成double類型數據)。

C++提供類型轉換函數(type conversion function)來解決這個問題。類型轉換函數的做用是將一個 類的對象轉換成另外一類型的數據。若是已聲明瞭一個Complex類,能夠在Complex類中這樣定義類型轉換函數:
    operator double( )
    {
        return real;  //返回的是double型
    }
函數返回double型變量real的值。它的做用是將一個Complex類對象轉換爲一個double型數據,其值是Complex類中的數據成員real的值。請注意,函數名是operator double,這點是和運算符重載時的規律一致的(在定義運算符「+」的重載函數時,函數名是operator +)。

類型轉換函數的通常形式爲:
    operator 類型名( )
    {
        實現轉換的語句
    }
在函數名前面不能指定函數類型,函數沒有參數。其返回值的類型是由函數名中指定的類型名來肯定的。類型轉換函數只能做爲成員函數,由於轉換的主體是本類的對象。不能做爲友元函數或普通函數。

從函數形式能夠看到,它與運算符重載函數類似,都是用關鍵字operator開頭,只是被重載的是類型名。double類型通過重載後,除了原有的含義外,還得到新的含義(將一個Complex類對象轉換爲double類型數據,並指定了轉換方法)。這樣,編譯系統不只能識別原有的double型數據,並且還會把Complex類對象做爲double型數據處理。

那麼程序中的Complex類對具備雙重身份,既是Complex類對象,又可做爲double類型數據。Complex類對象只有在須要時才進行轉換,要根據表達式的上下文來決定。 轉換構造函數和類型轉換運算符有一個共同的功能:當須要的時候,編譯系統會自動調用這些函數,創建一個無名的臨時對象(或臨時變量)。

[例10.9] 使用類型轉換函數的簡單例子。 
#include <iostream>
using namespace std;
class Complex
{
public:
   Complex( ){real=0;imag=0;}
   Complex(double r,double i){real=r;imag=i;}
   operator double( ) {return real;} //類型轉換函數
private:
   double real;
   double imag;
};

int main( )
{
   Complex c1(3,4),c2(5,-10),c3;
   double d;
   d=2.5+c1;//要求將一個double數據與Complex類數據相加
   cout<<d<<endl;
   return 0;
}

 

對程序的分析:
1) 若是在Complex類中沒有定義類型轉換函數operator double,程序編譯將出錯。由於不能實現double 型數據與Complex類對象的相加。如今,已定義了成員函數 operator double,就能夠利用它將Complex類對象轉換爲double型數據。請注意,程序中沒必要顯式地調用類型轉換函數,它是自動被調用的,即隱式調用。在什麼狀況下調用類型轉換函數呢?編譯系統在處理表達式 2.5 +cl 時,發現運算符「+」的左側是double型數據,而右側是Complex類對象,又無運算符「+」重載函數,不能直接相加,編譯系統發現有對double的重載函數,所以調用這個函數,返回一個double型數據,而後與2.5相加。

2) 若是在main函數中加一個語句:
    c3=c2;
請問此時編譯系統是把c2按Complex類對象處理呢,仍是按double型數據處理?因爲賦值號兩側都是同一類的數據,是能夠合法進行賦值的,沒有必要把c2轉換爲double型數據。

3) 若是在Complex類中聲明瞭重載運算符「+」函數做爲友元函數:
    Complex operator+ (Complex c1,Complex c2)//定義運算符「+」重載函數
    {
        return Complex(c1.real+c2.real, c1.imag+c2.imag);
    }
若在main函數中有語句
    c3=c1+c2;
因爲已對運算符「+」重載,使之能用於兩個Complex類對象的相加,所以將c1和c2按Complex類對象處理,相加後賦值給同類對象c3。若是改成
    d=c1+c2; //d爲double型變量
將c1與c2兩個類對象相加,獲得一個臨時的Complex類對象,因爲它不能賦值給double型變量,而又有對double的重載函數,因而調用此函數,把臨時類對象轉換爲double數據,而後賦給d。

從前面的介紹可知,對類型的重載和對運算符的重載的概念和方法都是類似的,重載函數都使用關鍵字 operator。所以,一般把類型轉換函數也稱爲類型轉換運算符函數,因爲它也是重載函數,所以也稱爲類型轉換運算符重載函數(或稱強制類型轉換運算符重載函數)。

假如程序中須要對一個Complex類對象和一個double型變量進行+,-,*,/等算術運算,以及關係運算和邏輯運算,若是不用類型轉換函數,就要對多種運算符進行重載,以便能進行各類運算。這樣,是十分麻煩的,工做量較大,程序顯得冗長。若是用類型轉換函數對double進行重載(使Complex類對象轉換爲double型數據),就沒必要對各類運算符進行重載,由於Complex類對象能夠被自動地轉換爲double型數據,而標準類型的數據的運算,是能夠使用系統提供的各類運算符的。

[例10.10] 包含轉換構造函數運算符重載函數類型轉換函數的程序。先閱讀如下程序,在這個程序中只包含轉換構造函數運算符重載函數

#include <iostream>
using namespace std;
class Complex
{
public:
   Complex( ){real=0;imag=0;}  //默認構造函數
   Complex(double r){real=r;imag=0;}//轉換構造函數
   Complex(double r,double i){real=r;imag=i;}//實現初始化的構造函數
   friend Complex operator + (Complex c1,Complex c2); //重載運算符「+」的友元函數
   void display( );
private:
   double real;
   double imag;
};
Complex operator + (Complex c1,Complex c2)//定義運算符「+」重載函數
{
   return Complex(c1.real+c2.real, c1.imag+c2.imag);
}
void Complex::display( )
{
   cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main( )
{
   Complex c1(3,4),c2(5,-10),c3;
   c3=c1+2.5; //複數與double數據相加
   c3.display( );
   return 0;
}

 

注意,在Visual C++ 6.0環境下運行時,需將第一行改成#include <iostream.h>,並刪去第2行,不然編譯不能經過。

對程序的分析:
1) 若是沒有定義轉換構造函數,則此程序編譯出錯。

2) 如今,在類Complex中定義了轉換構造函數,並具體規定了怎樣構成一個複數。因爲已重載了算符「+」,在處理表達式c1+2.5時,編譯系統把它解釋爲
    operator+(c1, 2.5)
因爲2.5不是Complex類對象,系統先調用轉換構造函數Complex(2.5),創建一個臨時的Complex類對象,其值爲(2.5+0i)。上面的函數調用至關於
    operator+(c1, Complex(2.5))
將c1與(2.5+0i) 相加,賦給c3。運行結果爲
    (5.5+4i)
3) 若是把「c3=c1+2.5;」改成c3=2.5+c1; 程序能夠經過編譯和正常運行。過程與前相同。

從中獲得一個重要結論,在已定義了相應的轉換構造函數狀況下,將運算符「+」函數重載爲友元函數,在進行兩個複數相加時,能夠用交換律

若是運算符函數重載爲成員函數,它的第一個參數必須是本類的對象。當第一個操做數不是類對象時,不能將運算符函數重載爲成員函數。若是將運算符「+」函數重載爲類的成員函數,交換律不適用。(這個問題是在定義時須要注意的)

因爲這個緣由,通常狀況下將雙目運算符函數重載爲友元函數。單目運算符則多重載爲成員函數。

(運算符重載函數爲類的成員函數時,第一個參數必須是本類的對象,當運算符重載函數是友元函數時,第一個參數沒有限制)

4) 若是必定要將運算符函數重載爲成員函數,而第一個操做數又不是類對象時,只有一個辦法可以解決,重載一個運算符「+」函數,其第一個參數爲double型。固然此函數只能是友元函數,函數原型爲
    friend operator+(double, Complex &);
顯然這樣作不太方便,仍是將雙目運算符函數重載爲友元函數方便些。

5) 在上面程序的基礎上增長類型轉換函數:
    operator double( ){return real;}
此時Complex類的公用部分爲:
   public:
   Complex( ){real=0;imag=0;}
   Complex(double r){real=r;imag=0;}  //轉換構造函數
   Complex(double r,double i){real=r;imag=i;} 
   operator double( ){return real;}//類型轉換函數
   friend Complex operator+ (Complex c1,Complex c2); //重載運算符「+」
   void display( );
其他部分不變。程序在編譯時出錯,緣由是出現二義性。

 

總結:

一)類型轉換函數(類型轉換運算符函數)
operator 類型名( )
{
實現轉換的語句
}
例如:
operator double( )
{
return real; //返回的是double型
}

 

二)轉換構造函數
如:
Complex(double r) {real=r;imag=0;}
特色:
1) 先聲明一個類。

2) 在這個類中定義一個只有一個參數的構造函數,參數的類型是須要轉換的類型,在函數體中指定轉換的方法。

3) 在該類的做用域內能夠用如下形式進行類型轉換:
類名(指定類型的數據)

 

三)重載流插入運算符和流提取運算符
對「<<」和「>>」重載的函數形式以下:
istream & operator >> (istream &, 自定義類 &);
ostream & operator << (ostream &, 自定義類 &);

 

四)運算符重載方法

函數類型 operator 運算符名稱 (形參表列)
{
// 對運算符的重載處理
}
特色:運算符重載函數爲類的成員函數時,第一個參數必須是本類的對象,當運算符重載函數是友元函數時,第一個參數沒有限制

 

 

十.繼承與派生詳解:C++派生類聲明和構成、繼承的意義

 1)繼承與派生的概念、什麼是繼承和派生

在C++中可重用性是經過繼承(inheritance)這一機制來實現的。所以,繼承是C++的一個重要組成部分。

前面介紹了類,一個類中包含了若干數據成員和成員函數。在不一樣的類中,數據成員和成員函數是不相同的。但有時兩個類的內容基本相同或有一部分相同,例如巳聲明瞭學生基本數據的類Student:

class Student
{
public:
   void display( )    //對成員函數display的定義
   {
      cout<<"num: " <<num<<endl;
      cout<<"name: "<< name <<endl;
      cout <<"sex: "<<sex<<endl;
   }
private:
   int num;
   string name;
   char sex;
};

 

若是學校的某一部門除了須要用到學號、姓名、性別之外,還須要用到年齡、地址等信息。固然能夠從新聲明另外一個類class Student1:

class Student1
{
public:
   void display( )  //此行原來已有
   {
      cout<<"num: " <<num<<endl;  //此行原來已有
      cout<<"name: "<< name <<endl;  //此行原來已有
      cout <<"sex: "<<sex<<endl;  //此行原來已有
      cout <<"age: "<<age<<endl;
      cout <<"address: "<<addr<<endl;
   }
private:
   int num;  //此行原來已有
   string name;  //此行原來已有
   char sex;  //此行原來已有
   int age;
   char addr[20];
};

能夠看到有至關一部分是原來已經有的,能夠利用原來聲明的類Student做爲基礎,再加上新的內容便可,以減小重複的工做量。C++提供的繼承機制就是爲了解決這個問題。

在C++中,所謂「繼承」就是在一個已存在的類的基礎上創建一個新的類。已存在的類稱爲「基類(base class)」或「父類(father class)」,新建的類稱爲「派生類(derived class)」或「子類(son class )」

一個新類從已有的類那裏得到其已有特性,這種現象稱爲類的繼承。經過繼承,一個新建子類從已有的父類那裏得到父類的特性。從另外一角度說,從已有的類(父類)產生一個新的子類,稱爲類的派生。類的繼承是用已有的類來創建專用類的編程技術。派生類繼承了基類的全部數據成員和成員函數,並能夠對成員做必要的增長或調整。一個基類能夠派生出多個派生類,每個派生類又能夠做爲基類再派生出新的派生類,所以基類和派生類是相對而言的。一代一代地派生下去,就造成類的繼承層次結構。至關於一個大的家族,有許多分支,全部的子孫後代都繼承了祖輩的基本特徵,同時又有區別和發展。與之相仿,類的每一次派生,都繼承了其基類的基本特徵,同時又根據須要調整和擴充原 有的特徵。

以上介紹的是最簡單的狀況:一個派生類只從一個基類派生,這稱爲單繼承(single inheritance),這種繼承關係所造成的層次是一個樹形結構,如圖11.3所示。


圖 11.3


一個派生類不只能夠從一個基類派生,也能夠從多個基類派生。也就是說,一個派生類能夠有一個或者多個基類。一個派生類有兩個或多個基類的稱爲多重繼承(multiple inheritance)。關於基類和派生類的關係,能夠表述爲派生類是基類的具體化,而基類則是派生類的抽象

 

2)派生類的聲明方式(定義方式)

假設已經聲明瞭一個基類Student(基類Student的定義見上節:C++繼承與派生的概念),在此基礎上經過單繼承創建一個派生類Student1:

class Student1: public Student  //聲明基類是Student
{
public:
   void display_1( ) //新增長的成員函數
   {
      cout<<"age: "<<age<<endl;
      cout<<"address: "<<addr<<endl;
   }
private:
   int age;  //新增長的數據成員
   string addr;  //新增長的數據成員
};

 

仔細觀察第一行:
    class Student1: public Student
在class後面的Student1是新建的類名,冒號後面的Student表示是已聲明的基類。在Student以前有一關鍵宇public,用來表示基類Student中的成員在派生類Studeml中的繼承方式。基類名前面有public的稱爲「公用繼承(public inheritance)」。

請你們仔細閱讀以上聲明的派生類Student1和基類Student,並將它們放在一塊兒進行分析。

聲明派生類的通常形式爲:
    class 派生類名:[繼承方式] 基類名
    {
        派生類新增長的成員
    };
繼承方式包括public (公用的)、private (私有的)和protected(受保護的),此項是可選的,若是不寫此項,則默認爲private(私有的)。

 

3)派生類的構成

派生類中的成員包括從基類繼承過來的成員和本身增長的成員兩大部分。從基類繼承的成員體現了派生類從基類繼承而得到的共性,而新增長的成員體現了派生類的個性。正是這些新增長的成員體現了派生類與基類的不一樣,體現了不一樣派生類之間的區別。

在基類中包括數據成員和成員函數 (或稱數據與方法)兩部分,派生類分爲兩大部分:一部分是從基類繼承來的成員,另外一部分是在聲明派生類時增長的部分。每一部分均分別包括數據成員和成員函數。

實際上,並非把基類的成員和派生類本身增長的成員簡單地加在一塊兒就成爲派生類。構造一個派生類包括如下3部分工做。

1) 從基類接收成員

派生類把基類所有的成員不包括構造函數和析構函數)接收過來,也就是說是沒有選擇的,不能選擇接收其中一部分紅員,而捨棄另外一部分紅員。 從定義派生類的通常形式中能夠看出是不可選擇的。

這樣就可能出現一種狀況:有些基類的成員,在派生類中是用不到的,可是也必須繼承過來。這就會形成數據的冗餘,尤爲是在屢次派生以後,會在許多派生類對象中存在大量無用的數據,不只浪費了大量的空間,並且在對象的創建、賦值、複製和參數的傳遞中, 花費了許多無謂的時間,從而下降了效率。這在目前的C++標準中是沒法解決的,要求咱們根據派生類的須要慎重選擇基類,使冗餘量最小。不要隨意地從已有的類中找一個做爲基類去構造派生類,應當考慮怎樣能使派生類有更合理的結構。事實上,有些類是專門做爲基類而設計的,在設計時充分考慮到派生類的要求。

2) 調整從基類接收的成員

接收基類成員是程序人員不能選擇的,可是程序人員能夠對這些成員做某些調整。例如能夠改變基類成員在派生類中的訪問屬性,這是經過指定繼承方式來實現的。如能夠經過繼承把基類的公用成員指定爲在派生類中的訪問屬性爲私有(派生類外不能訪問)。此外,能夠在派生類中聲明一個與基類成員同名的成員,則派生類中的新成員會覆蓋基類的同名成員。但應注意,若是是成員函數,不只應使函數名相同,並且函數的參數表(參數的個數和類型)也應相同,若是不相同,就成爲函數的重載而不是覆蓋了。用這樣的方法能夠用新成員取代基類的成員。

3) 在聲明派生類時增長的成員

這部份內容是很重要的,它體現了派生類對基類功能的擴展。要根據須要仔細考慮應當增長哪些成員,精心設計。例如在前面例子中(請查看:C++派生類的聲明方式),基類的display函數的做用是輸出學號、姓名和性別,在派生類中要求輸出學號、姓名、性別、年齡和地址,沒必要單獨另寫一個輸出這5個數據的函數,而要利用基類的display 函數輸出學號、姓名和性別,另外再定義一個display_1 函數輸出年齡和地址,前後執行這兩個函數。也能夠在 display_1 函數中調用基類的display函數,再輸出另外兩個數據,在主函數中只需調用一個display_1函數便可,這樣可能更清晰一些,易讀性更好。

此外,在聲明派生類時,通常還應當本身定義派生類的構造函數和析構函數,由於構造函數和析構函數是不能從基類繼承的

經過以上的介紹能夠看出:派生類是基類定義的延續。能夠先聲明一個基類,在此基類中只提供某些最基本的功能,而另外有些功能並未實現,而後在聲明派生類時加入某些具體的功能,造成適用於某一特定應用的派生類。經過對基類聲明的延續,將一個抽象的基類轉化成具體的派生類。所以,派生類是抽象基類的具體實現。

 

4)派生成員的訪問屬性

既然派生類中包含基類成員和派生類本身增長的成員,就產生了這兩部分紅員的關係和訪問屬性的問題。在創建派生類的時候,並非簡單地把基類的私有成員直接做爲派生類的私有成員,把基類的公用成員直接做爲派生類的公用成員。

實際上,對基類成員和派生類本身增長的成員是按不一樣的原則處理的。具體說,在討論訪問屬性時,要考慮如下幾種狀況:

  1. 基類的成員函數訪問基類成員。
  2. 派生類的成員函數訪問派生類本身增長的成員。
  3. 基類的成員函數訪問派生類的成員。(這樣不對吧)
  4. 派生類的成員函數訪問基類的成員。
  5. 在派生類外訪問派生類的成員。
  6. 在派生類外訪問基類的成員。


對於第(1)和第(2)種狀況,比較簡單,基類的成員函數能夠訪問基類成員,派生類的成員函數能夠訪問派生類成員。私有數據成員只能被同一類中的成員函數訪問,公用成員能夠被外界訪問。

第(3)種狀況也比較明確,基類的成員函數只能訪問基類的成員,而不能訪問派生類的成員。

第(5)種狀況也比較明確,在派生類外能夠訪問派生類的公用成員,而不能訪問派生類的私有成員。

對於第(4)和第(6)種狀況,就稍微複雜一些,也容易混淆。譬如,有人提出這樣的問題:

  • 基類中的成員函數是能夠訪問基類中的任一成員的,那麼派生類中新增長的成員是否能夠一樣地訪問基類中的私有成員;
  • 在派生類外,可否經過派生類的對象名訪問從基類繼承的公用成員。


這些牽涉到如何肯定基類的成員在派生類中的訪問屬性的問題,不只要考慮對基類成員所聲明的訪問屬性,還要考慮派生類所聲明的對基類的繼承方式,根據這兩個因素共同決定基類成員在派生類中的訪問屬性。

前面已提到,在派生類中,對基類的繼承方式能夠有public(公用的)、private (私有的)和protected(保護的)3種。不一樣的繼承方式決定了基類成員在派生類中的訪問屬性。簡單地說能夠總結爲如下幾點。

1) 公用繼承(public inheritance)
基類的公用成員和保護成員在派生類中保持原有訪問屬性,其私有成員仍爲基類私有。

2) 私有繼承(private inheritance)
基類的公用成員和保護成員在派生類中成了私有成員,其私有成員仍爲基類私有。

3) 受保護的繼承(protected inheritance)
基類的公用成員和保護成員在派生類中成了保護成員,其私有成員仍爲基類私有保護成員的意思是,不能被外界引用,但能夠被派生類的成員引用

 

5)公用繼承

在定義一個派生類時將基類的繼承方式指定爲public的,稱爲公用繼承,用公用繼承方式創建的派生類稱爲公用派生類(public derived class ),其基類稱爲公用基類(public base class )。

採用公用繼承方式時,基類的公用成員和保護成員在派生類中仍然保持其公用成員和保護成員的屬性,而基類的私有成員在派生類中並無成爲派生類的私有成員,它仍然是基類的私有成員,只有基類的成員函數能夠引用它,而不能被派生類的成員函數引用,所以就成爲派生類中的不可訪問的成員。公用基類的成員在派生類中的訪問屬性見表11.1。

表11.1 公用基類在派生類中的訪問屬性
公用基類的成員 私有成員 公用成員 保護成員
在公用派生類中的訪問屬性 不可訪問 公用 保護


有人問,既然是公用繼承,爲何不讓訪問基類的私有成員呢?要知道,這是C++中一個重要的軟件工程觀點。由於私有成員體現了數據的封裝性,隱藏私有成員有利於測試、調試和修改系統。若是把基類全部成員的訪問權限都原封不動地繼承到派生類,使基類的私有成員在派生類中仍保持其私有性質,派生類成員能訪問基類的私有成員,那麼豈非基類和派生類沒有界限了?這就破壞了基類的封裝性。若是派生類再繼續派生一個新的派生類,也能訪問基類的私有成員,那麼在這個基類的全部派生類的層次上都能訪問基類的私有成員,這就徹底丟棄了封裝性帶來的好處。保護私有成員是一條重要的原則。

[例11.1] 訪問公有基類的成員。下面寫出類的聲明部分:

Class Student//聲明基類
{
public: //基類公用成員
   void get_value( )
   {
      cin>>num>>name>>sex;
   }
   void display( )
   {
      cout<<" num: "<<num<<endl;
      cout<<" name: "<<name<<endl;
      cout<<" sex: "<<sex<<endl;
   }
private: //基類私有成員
   int num;
   string name;
   char sex;
};

class Student1: public Student //以public方式聲明派生類Student1
{
public:
   void display_1( )
   {
      cout<<" num: "<<num<<endl; //企圖引用基類的私有成員,錯誤
      cout<<" name: "<<name<<endl; //企圖引用基類的私有成員,錯誤
      cout<<" sex: "<<sex<<endl; //企圖引用基類的私有成員,錯誤
      cout<<" age: "<<age<<endl; //引用派生類的私有成員,正確
      cout<<" address: "<<addr<<endl;
   } //引用派生類的私有成員,正確
private:
   int age;
   string addr;
};

 

 因爲基類的私有成員對派生類來講是不可訪問的,所以在派生類中的display_1函數中直接引用基類的私有數據成員num,name和sex是不容許的。只能經過基類的公用成員函數來引用基類的私有數據成員。能夠將派生類Student1的聲明改成

class Student1: public Student  //以public方式聲明派生類Student1
{
public:
   void display_1( )
   {
      cout<<" age: "<<age<<endl; //引用派生類的私有成員,正確
      cout<<" address: "<<addr<<endl; //引用派生類的私有成員,正確
   }
private:
   int age; string addr;
};

 

 而後在main函數中分別調用基類的display函數和派生類中的display_1函數,前後輸出5個數據。

能夠這樣寫main函數(假設對象stud中已有數據):

int main( )
{
   Student1 stud;//定義派生類Student1的對象stud
   stud.display( ); //調用基類的公用成員函數,輸出基類中3個數據成員的值
   stud.display_1(); //調用派生類公用成員函數,輸出派生類中兩個數據成員的值
   return 0;
}

 

請根據上面的分析,寫出完整的程序,程序中應包括輸入數據的函數。

實際上,程序還能夠改進,在派生類的display_1函數中調用基類的display函數,在主函數中只要寫一行:
    stud.display_1();
便可輸出5個數據。

 

6)私有繼承

在聲明一個派生類時將基類的繼承方式指定爲private的,稱爲私有繼承,用私有繼承方式創建的派生類稱爲私有派生類(private derived class ), 其基類稱爲私有基類(private base class )。

私有基類的公用成員和保護成員派生類中的訪問屬性至關於派生類中的私有成員,即派生類的成員函數能訪問它們,而在派生類外不能訪問它們。私有基類的私有成員在派生類中成爲不可訪問的成員,只有基類的成員函數能夠引用它們。一個基類成員在基類中的訪問屬性和在派生類中的訪問屬性多是不一樣的。私有基類的成員在私有派生類中的訪問屬性見表11.2。

表11.2 私有基類在派生類中的訪問屬性
私有基類中的成員 在私有派生類中的訪問屬性
私有成員 不可訪問
公用成員 私有
保護成員 私有


上表沒必要死記硬背,只需理解:既然聲明爲私有繼承,就表示將原來能被外界引用的成員隱藏起來,不讓外界引用,所以私有基類的公用成員和保護成員理所固然地成爲派生類中的私有成員。

私有基類的私有成員按規定只能被基類的成員函數引用,在基類外固然不能訪問他們,所以它們在派生類中是隱蔽的,不可訪問的。

對於不須要再往下繼承的類的功能能夠用私有繼承方式把它隱蔽起來,這樣,下一層的派生類沒法訪問它的任何成員。能夠知道,一個成員在不一樣的派生層次中的訪問屬性多是不一樣的,它與繼承方式有關。

[例11.2] 將例11.1中的公用繼承方式改成用私有繼承方式(基類Student不改)。能夠寫出私有派生類以下:

class Student1: private Student//用私有繼承方式聲明派生類Student1
{
public:
   void display_1( ) //輸出兩個數據成員的值
   {
      cout<<"age: "<<age<<endl; //引用派生類的私有成員,正確
      cout<<"address: "<<addr<<endl;
   } //引用派生類的私有成員,正確
private:
   int age;
   string addr;
};

請分析下面的主函數:

int main( )
{
   Student1 stud1;//定義一個Student1類的對象stud1
   stud1.display(); //錯誤,私有基類的公用成員函數在派生類中是私有函數
   stud1.display_1( );//正確,Display_1函數是Student1類的公用函數
   stud1.age=18; //錯誤,外界不能引用派生類的私有成員
   return 0;
}

 

能夠看到:

  • 不能經過派生類對象(如stud1)引用從私有基類繼承過來的任何成員(如stud1.display()或stud1.num)。
  • 派生類的成員函數不能訪問私有基類的私有成員,但能夠訪問私有基類的公用成員(如stud1.display_1函數能夠調用基類的公用成員函數display,但不能引用基類的私有成員num)。


很多讀者提出這樣一個問題:私有基類的私有成員mun等數據成員只能被基類的成員函數引用,而私有基類的公用成員函數又不能被派生類外調用,那麼,有沒有辦法調用私有基類的公用成員函數,從而引用私有基類的私有成員呢?有。

應當注意到,雖然在派生類外不能經過派生類對象調用私有基類的公用成員函數,但能夠經過派生類的成員函數調用私有基類的公用成員函數(此時它是派生類中的私有成員函數,能夠被派生類的任何成員函數調用)。

可將上面的私有派生類的成員函數定義改寫爲:

void display_1( )//輸出5個數據成員的值
{
   display(): //調用基類的公用成員函數,輸出3個數據成員的值
   cout<<"age: "<<age<<endl; //輸出派生類的私有數據成員
   cout<<"address: "<<addr<<endl;
} //輸出派生類的私有數據成員

main函數可改寫爲:

int main( )
{
   Student1 stud1;
   stud1.display_1( );//display_1函數是派生類Student1類的公用函數
   return 0;
}

這樣就能正確地引用私有基類的私有成員。能夠看到,本例採用的方法是:

  1. 在main函數中調用派生類中的公用成員函數stud1.display_1;
  2. 經過該公用成員函數調用基類的公用成員函數display(它在派生類中是私有函數,能夠被派生類中的任何成員函數調用);
  3. 經過基類的公用成員函數display引用基類中的數據成員。


請根據上面的要求,補充和完善上面的程序,使之成爲完整、正確的程序,程序中應包括輸入數據的函數。

因爲私有派生類限制太多,使用不方便,通常不常使用。

 

7)保護成員和保護繼承

protected 與 public 和 private 同樣是用來聲明成員的訪問權限的。由protected聲明的成員稱爲「受保護的成員」,或簡稱「保護成員」。從類的用戶角度來看,保護成員等價於私有成員。但有一點與私有成員不一樣,保護成員能夠被派生類的成員函數引用

若是基類聲明瞭私有成員,那麼任何派生類都是不能訪問它們的,若但願在派生類中能訪問它們,應當把它們聲明爲保護成員。若是在一個類中聲明瞭保護成員,就意味着該類可能要用做基類,在它的派生類中會訪問這些成員。

在定義一個派生類時將基類的繼承方式指定爲protected的,稱爲保護繼承,用保護繼承方式創建的派生類稱爲保護派生類(protected derived class ), 其基類稱爲受保護的基類(protected base class ),簡稱保護基類。

保護繼承的特色是:保護基類的公用成員和保護成員在派生類中都成了保護成員,其私有成員仍爲基類私有。也就是把基類原有的公用成員也保護起來,不讓類外任意訪問。

表11.3 基類成員在派生類中的的訪問屬性
基類中的成員 在公用派生類中的訪問屬性 在私有派生類中的訪問屬性 在保護派生類中的訪問屬性
私有成員 不可訪問 不可訪問 不可訪問
公用成員 公用 私有 保護
保護成員 保護 私有 保護

                                  ☆☆☆


保護基類的全部成員在派生類中都被保護起來,類外不能訪問,其公用成員和保護成 員能夠被其派生類的成員函數訪問。

保護基類的全部成員在派生類中都被保護起來,類外不能訪問,其公用成員和保護成員能夠被其派生類的成員函數訪問。

比較一下私有繼承和保護繼承(也就是比較在私有派生類中和在保護派生類中的訪問屬性), 能夠發現,在直接派生類中,以上兩種繼承方式的做用其實是相同的:在類外不能訪問任何成員,而在派生類中能夠經過成員函數訪問基類中的公用成員和保護成員。可是若是繼續派生,在新的派生類中,兩種繼承方式的做用就不一樣了。

例如,若是以公用繼承方式派生出一個新派生類,原來私有基類中的成員在新派生類中都成爲不可訪問的成員,不管在派生類內或外都不能訪問,而原來保護基類中的公用成員和保護成員在新派生類中爲保護成員,能夠被新派生類的成員函數訪問。

你們須要記住:基類的私有成員被派生類繼承(無論是私有繼承、公有繼承仍是保護繼承)後變爲不可訪問的成員,派生類中的一切成員均沒法訪問它們。若是須要在派生類中引用基類的某些成員,應當將基類的這些成員聲明爲protected,而不要聲明爲private。

若是善於利用保護成員,能夠在類的層次結構中找到數據共享與成員隱蔽之間的結合點。既可實現某些成員的隱蔽,又可方便地繼承,能實現代碼重用與擴充。

經過以上的介紹,能夠知道如下幾點。

1) 在派生類中,成員有4種不一樣的訪問屬性:

  • 公用的,派生類內和派生類外均可以訪問。
  • 受保護的,派生類內能夠訪問,派生類外不能訪問,其下一層的派生類能夠訪問。
  • 私有的,派生類內能夠訪問,派生類外不能訪問。
  • 不可訪問的,派生類內和派生類外都不能訪問。

 

表11.4 派生類中的成員的訪問屬性
派生類中的成員 在派生類中 在派生類外部 在下層公用派生類中
派生類中訪問屬性爲公用的成員 能夠 能夠 能夠
派生類中訪問屬性爲受保護的成員 能夠 不能夠 能夠
派生類中訪問屬性爲私有的成員 能夠 不能夠 不能夠
在派生類中不可訪問的成員 不能夠 不能夠 不能夠

                                                                    ☆☆☆

 


須要說明的是:

  • 這裏所列出的成員的訪問屬性是指在派生類中所得到的訪問屬性。
  • 所謂在派生類外部,是指在創建派生類對象的模塊中,在派生類範圍以外。
  • 若是本派生類繼續派生,則在不一樣的繼承方式下,成員所得到的訪問屬性是不一樣的,在本表中只列出在下一層公用派生類中的狀況,若是是私有繼承或保護繼承,你們能夠從表11.3中找到答案。


2) 類的成員在不一樣做用域中有不一樣的訪問屬性,對這一點要十分清楚。一個成員的訪問屬性是有前提的,要看它在哪個做用域中。有的讀者問:「一個基類的公用成 員,在派生類中變成保護的,究竟它自己是公用的仍是保護的?」應當說:這是同一個成員在不一樣的做用域中所表現出的不一樣特徵。例如,學校人事部門掌握了全校師生員工的資 料,學校的領導能夠查閱任何人的材料,學校下屬的系只能從全校的資料中獲得本系師生員工的資料,而不能查閱其餘部門任何人的材料。若是你要問:可否查閱張某某的材料, 沒法一律而論,必須查明你的身份,才能決定該人的材料可否被你「訪問」。

在未介紹派生類以前,類的成員只屬於其所屬的類,不涉及其餘類,不會引發歧義。 在介紹派生類後,就存在一個問題:在哪一個範圍內討論成員的特徵,同一個成員在不一樣 的繼承層次中有不一樣的特徵。爲了說明這個概念,能夠打個比方,汽車駕駛證是按地區核發的,北京的駕駛證在北京市範圍內暢通無阻,若是到了外地,可能會受到某些限制,到了外國就無效了。同一個駕駛員在不一樣地區的權利是不一樣的。又譬如,到醫院探視病人,如 果容許你進人病房近距離地看望病人並與之交談,則可對病人瞭解比較深人;若是隻容許你在玻璃門窗外探視,在必定距離外看到病人,只能對病人情況有粗略的印象;若是隻容許在病區的走廊裏經過電視看病人活動的片斷鏡頭,那就更間接了。人們在不一樣的場合下對同一個病人,獲得不一樣的信息,或者說,這個病人在不一樣的場合下的「可見性」不一樣。

日常,人們常習慣說某類的公用成員如何如何,這在通常不致引發誤解的狀況下是能夠的。可是決不要誤認爲該成員的訪問屬性只能是公用的而不能改變。在討論成員的訪問屬性時,必定要說明是對什麼範圍而言的,如基類的成員a,在基類中的訪問屬性是公用的,在私有派生類中的訪問屬性是私有的。

下面經過一個例子說明怎樣訪問保護成員。

[例11.3] 在派生類中引用保護成員。

 

#include <iostream>
#include <string>
using namespace std;
class Student//聲明基類
{
public:
   //基類公用成員
   void display( );
protected:
   //基類保護成員
   int num;
   string name;
   char sex;
};
//定義基類成員函數
void Student::display( )
{
   cout<<"num: "<<num<<endl;
   cout<<"name: "<<name<<endl;
   cout<<"sex: "<<sex<<endl;
}
class Student1: protected
Student //用protected方式聲明派生類Student1
{
public:
   void display1( );//派生類公用成員函數
private:
   int age;//派生類私有數據成員
   string addr;//派生類私有數據成員
};
void Student1::display1( )//定義派生類公用成員函數
{
   cout<<"num: "<<num<<endl;//引用基類的保護成員,合法
   cout<<"name: "<<name<<endl;//引用基類的保護成員,合法
   cout<<"sex: "<<sex<<endl;//引用基類的保護成員,合法
   cout<<"age: "<<age<<endl;//引用派生類的私有成員,合法
   cout<<"address: "<<addr<<endl; //引用派生類的私有成員,合法
}
int main( )
{
   Student1 stud1; //stud1是派生類Student1類的對象
   stud1.display1( ); //合法,display1是派生類中的公用成員函數
   stud1.num=10023; //錯誤,外界不能訪問保護成員
   return 0;
}

總結:

①類的繼承中,私有部分永遠只能被自身類使用

共用繼承:父類中的公用成員在子類爲公用成員

私有繼承:父類中的公用成員在子類爲私有成員

受保護繼承: 父類的保護成員在子類中任爲保護成員,只能在類中引用,不可對外使用。

 

8)類多級派生時的訪問屬性

 在實際項目開發中,常常會有多級派生的狀況。如圖11.9所示的派生關係:類A爲基類,類B是類A 的派生類,類C是類B的派生類,則類C也是類A的派生類;類B稱爲類A 的直接派生類,類C稱爲類A的間接派生類;類A是類B的直接基類,是類 C的間接基類。


圖 11.9


在多級派生的狀況下,各成員的訪問屬性仍按以上原則肯定。

爲了把多重繼承說的更加詳細,請你們先看下面的幾個繼承的類。

[例11.4] 若是聲明瞭如下的類:

class A  //基類
{
public:
   int i;
protected:
   void f2( );
   int j;
private:
   int k;
};
class B: public A  //public方式
{
public:
   void f3( );
protected:
   void f4( );
private:
   int m;
};
class C: protected B  //protected方式
{
public:
   void f5( );
private:
   int n;
};

 

 類A是類B的公用基類,類B是類C的保護基類。各成員在不一樣類中的訪問屬性以下:

  i f2 j k f3 f4 m f5 n
基類A 公用 保護 保護 私有          
公用派生類B 公用 保護 保護 不可訪問 公用 保護 私有    
保護派生類C 保護 保護 保護 不可訪問 保護 保護 不可訪問 公用 私有


9)派生類的構造函數

 基類的構造函數不能被繼承,在聲明派生類時,對繼承過來的成員變量的初始化工做也要由派生類的構造函數來完成。因此在設計派生類的構造函數時,不只要考慮派生類新增的成員變量,還要考慮基類的成員變量,要讓它們都被初始化。

解決這個問題的思路是:在執行派生類的構造函數時,調用基類的構造函數。

下面的例子展現瞭如何在派生類的構造函數中調用基類的構造函數。

#include<iostream>
using namespace std;

//基類
class People{
protected:
    char *name;
    int age;
public:
    People(char*, int);
};
People::People(char *name, int age): name(name), age(age){}

//派生類
class Student: public People{
private:
    float score;
public:
    Student(char*, int, float);
    void display();
};
//調用了基類的構造函數
Student::Student(char *name, int age, float score): People(name, age){   //People(name,age)是基類中的公共函數,此處也就是對它的初始化了,而且也是一次調用。形參能夠直接給值
    this->score = score;
}
void Student::display(){
    cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}

int main(){
    Student stu("小明", 16, 90.5);
    stu.display();

    return 0;
}

 

 運行結果爲:
小明的年齡是16,成績是90.5

請注意代碼第23行:

Student::Student(char *name, int age, float score): People(name, age)

 

這是派生類 Student 的構造函數的寫法。冒號前面是派生類構造函數的頭部,這和咱們之前介紹的構造函數的形式同樣,但它的形參列表包括了初始化基類和派生類的成員變量所需的數據;冒號後面是對基類構造函數的調用,這和普通構造函數的參數初始化表很是相似。

實際上,你能夠將對基類構造函數的調用參數初始化表放在一塊兒,以下所示:

Student::Student(char *name, int age, float score): People(name, age), score(score){}

 

基類構造函數和初始化表用逗號隔開。

須要注意的是:冒號後面是對基類構造函數的調用,而不是聲明,因此括號裏的參數是實參,它們不但能夠是派生類構造函數總參數表中的參數,還能夠是局部變量、常量等。以下所示:

Student::Student(char *name, int age, float score): People("李磊", 20)  //是直接調用

 

 

基類構造函數調用規則

事實上,經過派生類建立對象時必需要調用基類的構造函數,這是語法規定。也就是說,定義派生類構造函數時最好指明基類構造函數;若是不指明,就調用基類的默認構造函數(不帶參數的構造函數);若是沒有默認構造函數,那麼編譯失敗。

請看下面的例子:

#include<iostream>
using namespace std;

//基類
class People{
protected:
    char *name;
    int age;
public:
    People();
    People(char*, int);
};
People::People(){
    this->name = "xxx";
    this->age = 0;
}
People::People(char *name, int age): name(name), age(age){}

//派生類
class Student: public People{
private:
    float score;
public:
    Student();
    Student(char*, int, float);
    void display();
};
Student::Student(){
    this->score = 0.0;
}
Student::Student(char *name, int age, float score): People(name, age){  
    this->score = score;
}
void Student::display(){
    cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}

int main(){
    Student stu1;
    stu1.display();

    Student stu2("小明", 16, 90.5);
    stu2.display();

    return 0;
}

 

運行結果:
xxx的年齡是0,成績是0
小明的年齡是16,成績是90.5

建立對象 stu1 時,執行派生類的構造函數 Student::Student(),它並無指明要調用基類的哪個構造函數,從運行結果能夠很明顯地看出來,系統默認調用了不帶參數的構造函數,也就是 People::People()。

建立對象 stu2 時,執行派生類的構造函數 Student::Student(char *name, int age, float score),它指明瞭基類的構造函數。

在第31行代碼中,若是將 People(name, age) 去掉,也會調用默認構造函數,stu2.display() 的輸出結果將變爲:
xxx的年齡是0,成績是90.5

若是將基類 People 中不帶參數的構造函數刪除,那麼會發生編譯錯誤,由於建立對象 stu1 時沒有調用基類構造函數。

總結:若是基類有默認構造函數,那麼在派生類構造函數中能夠不指明,系統會默認調用;若是沒有,那麼必需要指明,不然系統不知道如何調用基類的構造函數。

構造函數的調用順序

爲了搞清這個問題,咱們不妨先來看一個例子:

#include<iostream>
using namespace std;

//基類
class People{
protected:
    char *name;
    int age;
public:
    People();
    People(char*, int);
};
People::People(): name("xxx"), age(0){
    cout<<"PeoPle::People()"<<endl;
}
People::People(char *name, int age): name(name), age(age){
    cout<<"PeoPle::People(char *, int)"<<endl;
}

//派生類
class Student: public People{
private:
    float score;
public:
    Student();
    Student(char*, int, float);
};
Student::Student(): score(0.0){
    cout<<"Student::Student()"<<endl;
}
Student::Student(char *name, int age, float score): People(name, age), score(score){
    cout<<"Student::Student(char*, int, float)"<<endl;
}

int main(){
    Student stu1;
    cout<<"--------------------"<<endl;
    Student stu2("小明", 16, 90.5);

    return 0;
}

運行結果:
PeoPle::People()
Student::Student()      //這是第一次調用生成的,創建對象時只執行一個構造函數
--------------------
PeoPle::People(char *, int)
Student::Student(char*, int, float)

從運行結果能夠清楚地看到,當建立派生類對象時,先調用基類構造函數,再調用派生類構造函數。若是繼承關係有好幾層的話,例如:

A --> B --> C

 

那麼則建立C類對象時,構造函數的執行順序爲:

A類構造函數 --> B類構造函數 --> C類構造函數

 

構造函數的調用順序是按照繼承的層次自頂向下、從基類再到派生類的。

 

10)有子對象的派生類的構造函數

類的數據成員不但能夠是標準型(如int、char)或系統提供的類型(如string),還能夠包含類對象,如能夠在聲明一個類時包含這樣的數據成員:
    Student s1;  //Student是已聲明的類名,s1是Student類的對象
這時,s1就是類對象中的內嵌對象,稱爲子對象(subobject),即對象中的對象。

經過例子來講明問題。在例11.5(具體代碼請查看:C++派生類的構造函數)中的派生類Studentl中,除了能夠在派生類中要增長數據成員age和address外,還能夠增長「班長」一項,即學生數據中包含他們的班長的姓名和其餘基本狀況,而班長自己也是學生,他也屬於Student類型,有學號和姓名等基本數據,這樣班長項就是派生類Student1中的子對象。在下面程序的派生類的數據成員中, 有一項monitor(班長),它是基類Student的對象,也就是派生類Student1的子對象。

那麼,在對數據成員初始化時怎樣對子對象初始化呢?請仔細分析下面程序,特別注意派生類構造函數的寫法。

[例11.6] 包含子對象的派生類構造函數。爲了簡化程序以易於閱讀,這裏設基類Student的數據成員只有兩個,即num和name。

#include <iostream>
#include <string>
using namespace std;
class Student//聲明基類
{
public: //公用部分
   Student(int n, string nam ) //基類構造函數,與例11.5相同
   {
      num=n;
      name=nam;
   }
   void display( ) //成員函數,輸出基類數據成員
   {
      cout<<"num:"<<num<<endl<<"name:"<<name<<endl;
   }
protected: //保護部分
   int num;
   string name;
};
class Student1: public Student //聲明公用派生類Student1
{
public:
   Student1(int n, string nam,int n1, string nam1,int a, string ad):Student(n,nam),monitor(n1,nam1) //派生類構造函數
   {
      age=a;
      addr=ad;
   }
   void show( )
   {
      cout<<"This student is:"<<endl;
      display(); //輸出num和name
      cout<<"age: "<<age<<endl; //輸出age
      cout<<"address: "<<addr<<endl<<endl; //輸出addr
   }
   void show_monitor( ) //成員函數,輸出子對象
   {
      cout<<endl<<"Class monitor is:"<<endl;
      monitor.display( ); //調用基類成員函數
   }
private: //派生類的私有數據
   Student monitor; //定義子對象(班長) 仍是個對象而已!是個對象就行,無所謂語法而已
   int age;
   string addr;
};
int main( )
{
   Student1 stud1(10010,"Wang-li",10001,"Li-sun",19,"115 Beijing Road,Shanghai");
   stud1.show( ); //輸出學生的數據
   stud1.show_monitor(); //輸出子對象的數據
   return 0;
}

 

運行時的輸出以下:
This student is:
num: 10010
name: Wang-li
age: 19
address:115 Beijing Road,Shanghai
Class monitor is:
num:10001
name:Li-sun

請注意在派生類Student1中有一個數據成員:
    Student monitor;   //定義子對象 monitor(班長)

「班長」的類型不是簡單類型(如int、char、float等),它是Student類的對象。咱們知道, 應當在創建對象時對它的數據成員初始化。那麼怎樣對子對象初始化呢?顯然不能在聲明派生類時對它初始化(如Student monitor(10001, "Li-fun");),由於類是抽象類型,只是一個模型,是不能有具體的數據的,並且每個派生類對象的子對象通常是不相同的(例如學生A、B、C的班長是A,而學生D、E、F的班長是F)。所以子對象的初始化是在創建派生類時經過調用派生類構造函數來實現的。

派生類構造函數的任務應該包括3個部分:

  • 對基類數據成員初始化;
  • 對子對象數據成員初始化;
  • 對派生類數據成員初始化。


程序中派生類構造函數首部以下:
    Student1(int n, string nam,int n1, string nam1,int a, string ad):
        Student(n,nam),monitor(n1,nam1)
在上面的構造函數中有6個形參,前兩個做爲基類構造函數的參數,第三、第4個做爲子對象構造函數的參數,第五、第6個是用做派生類數據成員初始化的。

概括起來,定義派生類構造函數的通常形式爲:
    派生類構造函數名(總參數表列): 基類構造函數名(參數表列), 子對象名(參數表列)
    {
        派生類中新增數成員據成員初始化語句
    }

執行派生類構造函數的順序是:

  1. 調用基類構造函數,對基類數據成員初始化;
  2. 調用子對象構造函數,對子對象數據成員初始化;
  3. 再執行派生類構造函數自己,對派生類數據成員初始化。


派生類構造函數的總參數表列中的參數,應當包括基類構造函數和子對象的參數表列中的參數。基類構造函數和子對象的次序能夠是任意的,如上面的派生類構造函數首部能夠寫成
    Student1(int n, string nam,int n1, string nam1,int a, string ad): monitor(n1,nam1),Student(n,nam)
編譯系統是根據相同的參數名(而不是根據參數的順序)來確立它們的傳遞關係的。可是習慣上通常先寫基類構造函數。

若是有多個子對象,派生類構造函數的寫法依此類推,應列出每個子對象名及其參數表列。

 

11)多層派生時的構造函數

一個類不只能夠派生出一個派生類,派生類還能夠繼續派生,造成派生的層次結構。在上面敘述的基礎上,不難寫出在多級派生狀況下派生類的構造函數。

經過例下面的程序,讀者能夠了解在多級派生狀況下怎樣定義派生類的構造函數。相信你們徹底能夠本身看懂這個程序。

#include <iostream>
#include<string>
using namespace std;
class Student//聲明基類
{
public://公用部分
   Student(int n, string nam)//基類構造函數
   {
      num=n;
      name=nam;
   }
   void display( )//輸出基類數據成員
   {
      cout<<"num:"<<num<<endl;
      cout<<"name:"<<name<<endl;
   }
protected://保護部分
   int num;//基類有兩個數據成員
   string name;
};
class Student1: public Student//聲明公用派生類Student1
{
public:
   Student1(int n,char nam[10],int a):Student(n,nam)//派生類構造函數
   {age=a;}//在此處只對派生類新增的數據成員初始化
   void show( ) //輸出num,name和age
   {
      display( ); //輸出num和name
      cout<<"age: "<<age<<endl;
   }
private://派生類的私有數據
   int age; //增長一個數據成員
};
class Student2:public Student1 //聲明間接公用派生類Student2
{
public://下面是間接派生類構造函數
   Student2(int n, string nam,int a,int s):Student1(n,nam,a) {score=s;}
   void show_all( ) //輸出所有數據成員
   {
      show( ); //輸出num和name
      cout<<"score:"<<score<<endl; //輸出age
   }
private:
   int score; //增長一個數據成員
};
int main( )
{
   Student2 stud(10010,"Li",17,89);
   stud.show_all( ); //輸出學生的所有數據
   return 0;
}

 

運行時的輸出以下:
num:10010
name:Li
age:17
score:89

請注意基類和兩個派生類的構造函數的寫法。

基類的構造函數首部:
    Student(int n, string nam)
派生類Student1的構造函數首部:
    Student1(int n, string nam],int a):Student(n,nam)
派生類Student2的構造函數首部:
    Student2(int n, string nam,int a,int s):Student1(n,nam,a)
注意不要寫成:
    Student2(int n, string nam,int a,int s):Student1(n,nam),student1(n, nam, a)

不要列出每一層派生類的構造函數,只需寫出其上一層派生類(即它的直接基類)的構造函數便可。在聲明Student2類對象時,調用Student2構造函數;在執行Student2構造函數時,先調用Student1構造函數;在執行Student1構造函數時,先調用基類Student構造函數。初始化的順序是:

    • 先初始化基類的數據成員num和name。
    • 再初始化Student1的數據成員age。
    • 最後再初始化Student2的數據成員score。

 

12)派生類構造函數的特殊形式

在使用派生類構造函數時,有如下特殊的形式。

1) 當不須要對派生類新增的成員進行任何初始化操做時,派生類構造函數的函數體能夠爲空,即構造函數是空函數,如例11.6(具體代碼請查看:C++有子對象的派生類的構造函數)程序中派生類Student1構造函數能夠改寫爲:
    Student1(int n, strin nam,int n1, strin nam1): Student(n,nam),monitor(n1,nam1) { }

能夠看到,函數體爲空。此時,派生類構造函數的參數個數等於基類構造函數和子對象的參數個數之和,派生類構造函數的所有參數都傳遞給基類構造函數和子對象,在調用派生類構造函數時不對派生類的數據成員初始化。此派生類構造函數的做用只是爲了將參數傳遞給基類構造函數和子對象,並在執行派生類構造函數時調用基類構造函數和子對象構造函數。在實際工做中常見這種用法。

2) 若是在基類中沒有定義構造函數,或定義了沒有參數的構造函數,那麼在定義派生類構造函數時可不寫基類構造函數。由於此時派生類構造函數沒有向基類構造函數傳遞參數的任務。調用派生類構造函數時系統會自動首先調用基類的默認構造函數。

若是在基類和子對象類型的聲明中都沒有定義帶參數的構造函數,並且也不需對派生類本身的數據成員初始化,則能夠沒必要顯式地定義派生類構造函數。由於此時派生類構造函數既沒有向基類構造函數和子對象構造函數傳遞參數的任務,也沒有對派生類數據成員初始化的任務。

在創建派生類對象時,系統會自動調用系統提供的派生類的默認構造函數,並在執行派生類默認構造函數的過程當中,調用基類的默認構造函數和子對象類型默認構造函數。

若是在基類或子對象類型的聲明中定義了帶參數的構造函數,那麼就必須顯式地定義派生類構造函數,並在派生類構造函數中寫出基類或子對象類型的構造函數及其參數表。

若是在基類中既定義無參的構造函數,又定義了有參的構造函數(構造函數重載),則在定義派生類構造函數時,既能夠包含基類構造函數及其參數,也能夠不包含基類構造函數。

在調用派生類構造函數時,根據構造函數的內容決定調用基類的有參的構造函數仍是無參的構造函數。編程者能夠根據派生類的須要決定採用哪種方式。

 

13)派生類的析構函數

和構造函數相似,析構函數也是不能被繼承的。

建立派生類對象時,構造函數的調用順序和繼承順序相同,先執行基類構造函數,而後再執行派生類的構造函數。可是對於析構函數,調用順序剛好相反,即先執行派生類的析構函數,而後再執行基類的析構函數(但會有必定的語法關係,致使不是徹底倒序)。

請看下面的例子:

#include <iostream>
using namespace std;

class A{
public:
    A(){cout<<"A constructor"<<endl;}
    ~A(){cout<<"A destructor"<<endl;}
};

class B: public A{
public:
    B(){cout<<"B constructor"<<endl;}
    ~B(){cout<<"B destructor"<<endl;}
};

class C: public B{
public:
    C(){cout<<"C constructor"<<endl;}
    ~C(){cout<<"C destructor"<<endl;}
};

int main(){
    C test;
    return 0;
}

 

運行結果:
A constructor
B constructor
C constructor
C destructor
B destructor
A destructor

從運行結果能夠很明顯地看出來,構造函數和析構函數的執行順序是相反的。

須要注意的是,一個類只能有一個析構函數,調用時不會出現二義性,因此析構函數不須要顯式地調用。

 

14)類的多繼承

在前面的例子中,派生類都只有一個基類,稱爲單繼承。除此以外,C++也支持多繼承,即一個派生類能夠有兩個或多個基類

多繼承容易讓代碼邏輯複雜、思路混亂,一直備受爭議,中小型項目中較少使用,後來的 Java、C#、PHP 等乾脆取消了多繼承。想快速學習C++的讀者能夠沒必要細讀。

多繼承的語法也很簡單,將多個基類用逗號隔開便可。例如已聲明瞭類A、類B和類C,那麼能夠這樣來聲明派生類D:

class D: public A, private B, protected C{
    //類D新增長的成員
}

 

D是多繼承的派生類,它以共有的方式繼承A類,以私有的方式繼承B類,以保護的方式繼承C類。D根據不一樣的繼承方式獲取A、B、C中的成員,肯定各基類的成員在派生類中的訪問權限。

D獲取了 A、B、C不一樣的資源,能夠叫作吸心大法

多繼承下的構造函數

多繼承派生類的構造函數和單繼承類基本相同,只是要包含多個基類構造函數。如:

D類構造函數名(總參數表列): A構造函數(實參表列), B類構造函數(實參表列), C類構造函數(實參表列){
    新增成員初始化語句
}

 

各基類的排列順序任意。

派生類構造函數的執行順序一樣爲:先調用基類的構造函數,再調用派生類構造函數。基類構造函數的調用順序是按照聲明派生類時基類出現的順序。

下面的定義了兩個基類,BaseA類和BaseB類,而後用多繼承的方式派生出Sub類。

#include <iostream>
using namespace std;

//基類
class BaseA{
protected:
    int a;
    int b;
public:
    BaseA(int, int);
};
BaseA::BaseA(int a, int b): a(a), b(b){}

//基類
class BaseB{
protected:
    int c;
    int d;
public:
    BaseB(int, int);
};
BaseB::BaseB(int c, int d): c(c), d(d){}

//派生類
class Sub: public BaseA, public BaseB{
private:
    int e;
public:
    Sub(int, int, int, int, int);
    void display();
};
Sub::Sub(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), e(e){}
void Sub::display(){
    cout<<"a="<<a<<endl;
    cout<<"b="<<b<<endl;
    cout<<"c="<<c<<endl;
    cout<<"d="<<d<<endl;
    cout<<"e="<<e<<endl;
}

int main(){
    (new Sub(1, 2, 3, 4, 5)) -> display();
    return 0;
}

運行結果:
a=1
b=2
c=3
d=4
e=5

從基類BaseA和BaseB繼承來的成員變量,在 Sub::display() 中均可以訪問。

命名衝突

當兩個基類中有同名的成員時,就會產生命名衝突,這時不能直接訪問該成員,須要加上類名和域解析符

假如在基類BaseA和BaseB中都有成員函數 display(),那麼下面的語句是錯誤的:

Sub obj;
obj.display();

因爲BaseA和BaseB中都有display(),系統將沒法斷定到底要調用哪個類的函數,因此報錯。

應該像下面這樣加上類名和域解析符:

Sub obj;
obj.BaseA::display();
obj.BaseB::display();

 

 經過這個舉例能夠發現:在多重繼承時,從不一樣的基類中會繼承一些重複的數據。若是有多個基類,問題會更突出,因此在設計派生類時要細緻考慮其數據成員,儘可能減小數據冗餘

 

15)多重繼承的二義性問題

多重繼承能夠反映現實生活中的狀況,可以有效地處理一些較複雜的問題,使編寫程序具備靈活性,可是多重繼承也引發了一些值得注意的問題,它增長了程序的複雜度,使 程序的編寫和維護變得相對困難,容易出錯。其中最多見的問題就是繼承的成員同名而產生的二義性(ambiguous)問題。

若是類A和類B中都有成員函數display和數據成員a,類C是類A和類B的直接派生類。分別討論下列3種狀況。

1) 兩個基類有同名成員

代碼以下所示:

class A
{
public:
   int a;
   void display();
};

class B
{
public:
   int a;
   void display ();
};

class C: public A, public B
{
public:
   int b;
   void show();
};

 

若是在main函數中定義C類對象cl,並調用數據成員a和成員函數display :
    C cl;
    cl.a=3;
    cl.display();
因爲基類A和基類B都有數據成員a和成員函數display,編譯系統沒法判別要訪問的是哪個基類的成員,所以程序編譯出錯。那麼,應該怎樣解決這個問題呢?能夠用基類名來限定:
    cl.A::a=3;  //引用cl對象中的基類A的數據成員a
    cl.A::display();  //調用cl對象中的基類A的成員函數display

若是是在派生類C中經過派生類成員函數show訪問基類A的display和a,能夠不 必寫對象名而直接寫
    A::a = 3;  //指當前對象
    A::display();

2) 兩個基類和派生類三者都有同名成員

將上面的C類聲明改成:
    class C: public A, public B
    {
        int a;
        void display();
    };
若是在main函數中定義C類對象cl,並調用數據成員a和成員函數display:
    C cl;
    cl.a = 3;
    cl.display();
此時,程序能經過編譯,也能夠正常運行。請問:執行時訪問的是哪個類中的成員?答案是:訪問的是派生類C中的成員。規則是:基類的同名成員在派生類中被屏蔽,成爲「不可見」的,或者說,派生類新增長的同名成員覆蓋了基類中的同名成員。所以若是在定義派生類對象的模塊中經過對象名訪問同名的成員,則訪問的是派生類的成員。請注意:不一樣的成員函數,只有在函數名和參數個數相同、類型相匹配的狀況下才發生同名覆蓋,若是隻有函數名相同而參數不一樣,不會發生同名覆蓋,而屬於函數重載。

有些讀者可能對同名覆蓋感到不大好理解。爲了說明問題,舉個例子,例如把中國做爲基類,四川則是中國的派生類,成都則是四川的派生類。基類是相對抽象的,派生類是相對具體的,基類處於外層,具備較普遍的做用域,派生類處於內層,具備局部的做用域。若「中國」類中有平均溫度這一屬性,四川和成都也都有平均溫度這一屬性,若是沒有四川和成都這兩個派生類,談平均溫度顯然是指全國平均溫度。若是在四川,談論當地的平均溫度顯然是指四川的平均溫度;若是在成都,談論當地的平均溫度顯然是指成都的平均溫度。這就是說,全國的「平均溫度」在四川省被四川的「平均溫度」屏蔽了,或者說,四川的「平均溫度」在當地屏蔽了全國的「平均溫度」。四川人最關心的是四川的溫度,固然不但願用全國溫度覆蓋四川的平均溫度。

若是在四川要查全國平均溫度,必定要聲明:我要查的是全國的平均溫度。一樣,要在派生類外訪問基類A中的成員,應指明做用域A,寫成如下形式:
    cl.A::a=3;  //表示是派生類對象cl中的基類A中的數據成員a
    cl.A::display();  //表示是派生類對象cl中的基類A中的成員函數display

3) 類A和類B是從同一個基類派生的

代碼以下所示:

class N
{
public:
   int a;
   void display(){ cout<<"A::a="<<a<<endl; }
};

class A: public N
{
public:
   int al;
};

class B: public N
{
public:
   int a2;
};

class C: public A, public B
{
public:
   int a3;
   void show(){ cout<<"a3="<<a3<<endl; }
}

int main()
{
   C cl;  //定義C類對象cl
   // 其餘代碼
}

 

在類A和類B中雖然沒有定義數據成員a和成員函數display,可是它們分別從類N繼承了數據成員a和成員函數display,這樣在類A和類B中同時存在着兩個同名的數據成員a和成員函數display。它們是N類成員的拷貝。類A和類B中的數據成員a表明兩個不一樣的存儲單元,能夠分別存放不一樣的數據。在程序中能夠經過類A和類B的構造函數去調用基類N的構造函數,分別對類A和類B的數據成員a初始化。

怎樣才能訪問類A中從基類N繼承下來的成員呢?顯然不能用
    cl.a = 3;  cl.display();

    cl.N::a = 3;  cl. N::display();  沒法獲得獲取途徑
由於這樣依然沒法區別是類A中從基類N繼承下來的成員,仍是類B中從基類N繼承下來的成員。應當經過類N的直接派生類名來指出要訪問的是類N的哪個派生類中的基類成員。如
    cl.A::a=3;   cl.A::display();  //要訪問的是類N的派生類A中的基類成員

 

16)虛基類

多繼承時很容易產生命名衝突,即便咱們很當心地將全部類中的成員變量和成員函數都命名爲不一樣的名字,命名衝突依然有可能發生,好比很是經典的菱形繼承層次。以下圖所示:


類A派生出類B和類C,類D繼承自類B和類C,這個時候類A中的成員變量和成員函數繼承到類D中變成了兩份,一份來自 A-->B-->D 這一路,另外一份來自 A-->C-->D 這一條路。

在一個派生類中保留間接基類的多份同名成員,雖然能夠在不一樣的成員變量中分別存放不一樣的數據,但大多數狀況下這是多餘的:由於保留多份成員變量不只佔用較多的存儲空間,還容易產生命名衝突,並且不多有這樣的需求

爲了解決這個問題,C++提供了虛基類,使得在派生類中只保留間接基類的一份成員

聲明虛基類只須要在繼承方式前面加上 virtual 關鍵字,請看下面的例子:

#include <iostream>
using namespace std;

class A{
protected:
    int a;
public:
    A(int a):a(a){}
};

class B: virtual public A{  //聲明虛基類
protected:
    int b;
public:
    B(int a, int b):A(a),b(b){}
};

class C: virtual public A{  //聲明虛基類
protected:
    int c;
public:
    C(int a, int c):A(a),c(c){}
};

class D: virtual public B, virtual public C{  //聲明虛基類
private:
    int d;
public:
    D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){}
    void display();
};
void D::display(){
    cout<<"a="<<a<<endl;
    cout<<"b="<<b<<endl;
    cout<<"c="<<c<<endl;
    cout<<"d="<<d<<endl;
}

int main(){
    (new D(1, 2, 3, 4)) -> display();
    return 0;
}

 

運行結果:
a=1
b=2
c=3
d=4

本例中咱們使用了虛基類,在派生類D中只有一份成員變量 a 的拷貝,因此在 display() 函數中能夠直接訪問 a,而不用加類名和域解析符。
請注意派生類D的構造函數,與以往的用法有所不一樣。以往,在派生類的構造函數中只需負責對其直接基類初始化,再由其直接基類負責對間接基類初始化。如今,因爲虛基類在派生類中只有一份成員變量,因此對這份成員變量的初始化必須由派生類直接給出。若是不禁最後的派生類直接對虛基類初始化,而由虛基類的直接派生類(如類B和類C)對虛基類初始化,就有可能因爲在類B和類C的構造函數中對虛基類給出不一樣的初始化參數而產生矛盾。因此規定:在最後的派生類中不只要負責對其直接基類進行初始化,還要負責對虛基類初始化
有的讀者會提出:類D的構造函數經過初始化表調了虛基類的構造函數A,而類B和類C的構造函數也經過初始化表調用了虛基類的構造函數A,這樣虛基類的構造函數豈非被調用了3次?你們沒必要過慮,C++編譯系統只執行最後的派生類對虛基類的構造函數的調用,而忽略虛基類的其餘派生類(如類B和類C)對虛基類的構造函數的調用,這就保證了虛基類的數據成員不會被屢次初始化。
最後請注意:爲了保證虛基類在派生類中只繼承一次,應當在該基類的全部直接派生類中聲明爲虛基類,不然仍然會出現對基類的屢次繼承
能夠看到:使用多重繼承時要十分當心,常常會出現二義性問題。上面的例子是簡單的,若是派生的層次再多一些,多重繼承更復雜一些,程序員就很容易陷人迷 魂陣,程序的編寫、調試和維護工做都會變得更加困難。所以不少程序員不提倡在程序中使用多重繼承,只有在比較簡單和不易出現二義性的狀況或實在必要時才使用多重繼承,能用單一繼承解決的問題就不要使用多重繼承。也正因爲這個緣由,C++以後的不少面向對象的編程語言(如Java、Smalltalk、C#、PHP等)並不支持多重繼承

虛基類並非不佔地址,只是保證了相似於菱形繼承數據的獨立性;

 

17)基類與派生類的轉換

在公用繼承、私有繼承和保護繼承中,只有公用繼承能較好地保留基類的特徵,它保留了除構造函數和析構函數之外的基類全部成員,基類的公用或保護成員的訪問權限在派生類中所有都按原樣保留下來了,在派生類外能夠調用基類的公用成員函數訪問基類的私有成員。所以,公用派生類具備基類的所有功能,全部基類可以實現的功能, 公用派生類都能實現。而非公用派生類(私有或保護派生類)不能實現基類的所有功能(例如在派生類外不能調用基類的公用成員函數訪問基類的私有成員)。所以,只有公用派生類纔是基類真正的子類型,它完整地繼承了基類的功能。

不一樣類型數據之間在必定條件下能夠進行類型的轉換,例如整型數據能夠賦給雙精度型變量,在賦值以前,把整型數據先轉換成爲雙精度型數據,可是不能把一個整型數據賦給指針變量。這種不一樣類型數據之間的自動轉換和賦值,稱爲賦值兼容。如今要討論 的問題是:基類與派生類對象之間是否也有賦值兼容的關係,能否進行類型間的轉換?

回答是能夠的。基類與派生類對象之間有賦值兼容關係,因爲派生類中包含從基類繼承的成員,所以能夠將派生類的值賦給基類對象,在用到基類對象的時候能夠用其子類對象代替。具體表如今如下幾個方面。

1) 派生類對象能夠向基類對象賦值

能夠用子類(即公用派生類)對象對其基類對象賦值。如
    A a1;  //定義基類A對象a1
    B b1;  //定義類A的公用派生類B的對象b1
    a1=b1;  //用派生類B對象b1對基類對象a1賦值
在賦值時捨棄派生類本身的成員。也就是「大材小用」,如圖11.26所示。


圖 11.26


實際上,所謂賦值只是對數據成員賦值,對成員函數不存在賦值問題。

請注意,賦值後不能企圖經過對象a1去訪問派生類對象b1的成員,由於b1的成員與a1的成員是不一樣的。假設age是派生類B中增長的公用數據成員,分析下面的用法:
    a1.age=23;  //錯誤,a1中不包含派生類中增長的成員
    b1.age=21;  //正確,b1中包含派生類中增長的成員

應當注意,子類型關係是單向的、不可逆的。B是A的子類型,不能說A是B的子類型。只能用子類對象對其基類對象賦值,而不能用基類對象對其子類對象賦值,理由是顯然的,由於基類對象不包含派生類的成員,沒法對派生類的成員賦值。同理,同一基類的不一樣派生類對象之間也不能賦值。

2) 派生類對象能夠替代基類對象向基類對象的引用進行賦值或初始化

如已定義了基類A對象a1,能夠定義a1的引用變量:
    A a1; //定義基類A對象a1
    B b1; //定義公用派生類B對象b1
    A& r=a1; //定義基類A對象的引用變量r,並用a1對其初始化
這時,引用變量r是a1的別名,r和a1共享同一段存儲單元。也能夠用子類對象初始化引用變量r,將上面最後一行改成
    A& r=b1;  //定義基類A對象的引用變量r,並用派生類B對象b1對其初始化
或者保留上面第3行「A& r=a1;」,而對r從新賦值:
    r=b1;  //用派生類B對象b1對a1的引用變量r賦值

注意,此時r並非b1的別名,也不與b1共享同一段存儲單元。它只是b1中基類部分的別名,r與b1中基類部分共享同一段存儲單元,r與b1具備相同的起始地址。

3) 若是函數的參數是基類對象或基類對象的引用,相應的實參能夠用子類對象

若有一函數:
    fun: void fun(A& r)  //形參是類A的對象的引用變量
    {
        cout<<r.num<<endl;
    }  //輸出該引用變量的數據成員num
函數的形參是類A的對象的引用變量,原本實參應該爲A類的對象。因爲子類對象與派生類對象賦值兼容,派生類對象能自動轉換類型,在調用fun函數時能夠用派生類B的對象b1做實參:
     fun(b1);
輸出類B的對象b1的基類數據成員num的值。

與前相同,在fun函數中只能輸出派生類中基類成員的值。

4) 派生類對象的地址能夠賦給指向基類對象的指針變量,也就是說,指向基類對象的指針變量也能夠指向派生類對象。

[例11.10] 定義一個基類Student(學生),再定義Student類的公用派生類Graduate(研究生), 用指向基類對象的指針輸出數據。本例主要是說明用指向基類對象的指針指向派生類對象,爲了減小程序長度,在每一個類中只設不多成員。學生類只設num(學號),name(名字)和score(成績)3個數據成員,Graduate類只增長一個數據成員pay(工資)。程序以下:

#include <iostream>
#include <string>
using namespace std;
class Student//聲明Student類
{
public:
   Student(int, string,float);  //聲明構造函數
   void display( );  //聲明輸出函數
private:
   int num;
   string name;
   float score;
};

Student::Student(int n, string nam,float s)  //定義構造函數
{
   num=n;
   name=nam;
   score=s;
}

void Student::display( )  //定義輸出函數
{
   cout<<endl<<"num:"<<num<<endl;
   cout<<"name:"<<name<<endl;
   cout<<"score:"<<score<<endl;
}

class Graduate:public Student  //聲明公用派生類Graduate
{
public:
  Graduate(int, string ,float,float);  //聲明構造函數
  void display( );  //聲明輸出函數
private:
  float pay;  //工資
};

//定義構造函數
Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){ }
void Graduate::display()  //定義輸出函數
{
   Student::display();  //調用Student類的display函數
   cout<<"pay="<<pay<<endl;
}

int main()
{
   Student stud1(1001,"Li",87.5);  //定義Student類對象stud1
   Graduate grad1(2001,"Wang",98.5,563.5);  //定義Graduate類對象grad1
   Student *pt=&stud1; //定義指向Student類對象的指針並指向stud1
   pt->display( );  //調用stud1.display函數
   pt=&grad1; //指針指向grad1
   pt->display( );  //調用grad1.display函數
}

下面對程序的分析很重要,請你們仔細閱讀和思考。

不少讀者會認爲,在派生類中有兩個同名的display成員函數,根據同名覆蓋的規則,被調用的應當是派生類Graduate對象的display函數,在執行Graduate::display函數過程當中調用Student::display函數,輸出num,name,score,而後再輸出pay的值。

事實上這種推論是錯誤的,先看看程序的輸出結果:

num:1001
name:Li
score:87.5

num:2001
name:wang
score:98.5

前3行是學生stud1的數據,後3行是研究生grad1的數據,並無輸出pay的值。

問題在於pt是指向Student類對象的指針變量,即便讓它指向了grad1,但實際上pt指向的是grad1中從基類繼承的部分。(多是空間長度不夠)

經過指向基類對象的指針,只能訪問派生類中的基類成員,而不能訪問派生類增長的成員。因此pt->display()調用的不是派生類Graduate對象所增長的display函數,而是基類的display函數,因此只輸出研究生grad1的num,name,score3個數據。

若是想經過指針輸出研究生grad1的pay,能夠另設一個指向派生類對象的指針變量ptr,使它指向grad1,而後用ptr->display()調用派生類對象的display函數。但這不大方便。

經過本例能夠看到,用指向基類對象的指針變量指向子類對象是合法的、安全的,不會出現編譯上的錯誤。但在應用上卻不能徹底知足人們的但願,人們有時但願經過使用基類指針可以調用基類和子類對象的成員。若是能作到這點,程序人員會感到方便。後續章節將會解決這個問題。辦法是使用虛函數和多態性

 18)繼承與組合詳解

 咱們知道,在一個類中能夠用類對象做爲數據成員,即子對象(詳情請查看:C++有子對象的派生類的構造函數)。實際上,對象成員的類型能夠是本派生類的基類,也能夠是另一個已定義的類。在一個類中以另外一個類的對象做爲數據成員的,稱爲類的組合(composition)。

例如,聲明Professor(教授)類是Teacher(教師)類的派生類,另有一個類BirthDate(生日),包含year,month,day等數據成員。能夠將教授生日的信息加入到Professor類的聲明中。如:

class Teacher  //教師類
{
public:
   // Some Code
private:
   int num;
   string name;
   char sex;
};
class BirthDate  //生日類
{
public:
   // Some Code
private:
   int year;
   int month;
   int day;
};
class Professor:public Teacher  //教授類
{
public:
   // Some Code
private:
 BirthDate birthday; //BirthDate類的對象做爲數據成員    前面的寫法是用過的
};

 

 類的組合和繼承同樣,是軟件重用的重要方式。組合和繼承都是有效地利用已有類的資源。但兩者的概念和用法不一樣。經過繼承創建了派生類與基類的關係,它是一種 「是」的關係,如「白貓是貓」,「黑人是人」,派生類是基類的具體化實現,是基類中的一 種。經過組合創建了成員類與組合類(或稱複合類)的關係,在本例中BirthDate是成員類,Professor是組合類(在一個類中又包含另外一個類的對象成員)。它們之間不是‘‘是」的 關係,而是「有」的關係。不能說教授(Professor)是一個生日(BirthDate),只能說教授(Professor)有一個生日(BirthDate)的屬性。

Professor類經過繼承,從Teacher類獲得了num,name,age,sex等數據成員,經過組合,從BirthDate類獲得了year,month,day等數據成員。繼承是縱向的,組合是橫向的。

若是定義了Professor對象prof1,顯然prof1包含了生日的信息。經過這種方法有效地組織和利用現有的類,大大減小了工做量。若是有
    void fun1(Teacher &);
    void fun2(BirthDate &);
在main函數中調用這兩個函數:
    fun1(prof1);  //正確,形參爲Teacher類對象的引用,實參爲Teacher類的子類對象,與之賦值兼容
    fun2(prof1.birthday);  //正確,實參與形參類型相同,都是BirthDate類對象
    fun2(prof1);  //錯誤,形參要求是BirthDate類對象,而prof1是Professor類型,不匹配

若是修改了成員類的部份內容,只要成員類的公用接口(如頭文件名)不變,如無必要,組合類能夠不修改。但組合類須要從新編譯。

 

十一.多態性的概念和純虛數的定義

 1)多態性的概念

多態性(polymorphism)是面向對象程序設計的一個重要特徵。若是一種語言只支持類而不支持多態,是不能被稱爲面嚮對象語言的,只能說是基於對象的,如Ada、VB就屬此類。C++支持多態性,在C++程序設計中可以實現多態性。利用多態性能夠設計和實現一個易於擴展的系統

顧名思義,多態的意思是一個事物有多種形態。多態性的英文單詞polymorphism來源於希臘詞根poly(意爲「不少」)和morph(意爲「形態」)。在C ++程序設計中,多態性是指具備不一樣功能的函數能夠用同一個函數名,這樣就能夠用一個函數名調用不一樣內容的函數。在面向對象方法中通常是這樣表述多態性的:向不一樣的對象發送同一個消息, 不一樣的對象在接收時會產生不一樣的行爲(即方法)。也就是說,每一個對象能夠用本身的方式去響應共同的消息。所謂消息,就是調用函數,不一樣的行爲就是指不一樣的實現,即執行不一樣的函數。

其實,咱們已經屢次接觸過多態性的現象,例如函數的重載、運算符重載都是多態現象。只是那時沒有用到多態性這一專門術語而已。例如,使用運算符「+」使兩個數值相加,就是發送一個消息,它要調用operator +函數。實際上,整型、單精度型、雙精度型的加法操做過程是互不相同的,是由不一樣內容的函數實現的。顯然,它們以不一樣的行爲或方法來響應同一消息。

在現實生活中能夠看到許多多態性的例子。如學校校長向社會發佈一個消息:9月1日新學年開學。不一樣的對象會做出不一樣的響應:學生要準備好課本準時到校上課;家長要籌集學費;教師要備好課;後勤部門要準備好教室、宿舍和食堂……因爲事先對各類人的任務已做了規定,所以,在獲得同一個消息時,各類人都知道本身應當怎麼作,這就是 多態性。能夠設想,若是不利用多態性,那麼校長就要分別給學生、家長、教師、後勤部門等許多不一樣的對象分別發通知,分別具體規定每一種人接到通知後應該怎麼作。顯然這是一件十分複雜而細緻的工做。一人包攬一切,吃力還不討好。如今,利用了多態性機制,校長在發佈消息時,沒必要一一具體考慮不一樣類型人員是怎樣執行的。至於各種人員在接到消息後應氣作什麼,並非臨時決定的,而是學校的工做機制事先安排決定好的。校長只需不斷髮布各類消息,各類人員就會按預約方案有條不紊地工做。

一樣,在C++程序設計中,在不一樣的類中定義了其響應消息的方法,那麼使用這些類 時,沒必要考慮它們是什麼類型,只要發佈消息便可。正如在使用運算符「 」時沒必要考慮相加的數值是整型、單精度型仍是雙精度型,直接使用「+」,不論哪類數值都能實現相加。能夠說這是以不變應萬變的方法,不論對象變幻無窮,用戶都是用同一形式的信息去調用它們,使它們根據事先的安排做出反應。

從系統實現的角度看,多態性分爲兩類:靜態多態性和動態多態性。之前學過的函數重載和運算符重載實現的多態性屬於靜態多態性,在程序編譯時系統就能決定調用的是哪一個函數,所以靜態多態性又稱編譯時的多態性。靜態多態性是經過函數的重載實現的(運算符重載實質上也是函數重載)。動態多態性是在程序運行過程當中才動態地肯定操做所針對的對象。它又稱運行時的多態性。動態多態性是經過虛函數(Virtual fiinction)實現的。

有關靜態多態性的應用,即函數的重載(請查看:C++函數重載)和運算符重載(請查看:C++運算符重載),已經介紹過了,這裏主要介紹動態多態性和虛函數。要研究的問題是:當一個基類被繼承爲不一樣的派生類時,各派生類能夠使用與基類成員相同的成員名,若是在運行時用同一個成員名調用類對象的成員,會調用哪一個對象的成員?也就是說,經過繼承而產生了相關的不一樣的派生類,與基類成員同名的成員在不一樣的派生類中有不一樣的含義。也能夠說,多態性是「一個接口,多種 方法」。

例子:

下面是一個承上啓下的例子。一方面它是有關繼承和運算符重載內容的綜合應用的例子,經過這個例子能夠進一步融會貫通前面所學的內容,另外一方面又是做爲討論多態性的一個基礎用例。

但願你們耐心、深刻地閱讀和消化這個程序,弄清其中的每個細節。

[例12.1] 先創建一個Point(點)類,包含數據成員x,y(座標點)。以它爲基類,派生出一個Circle(圓)類,增長數據成員r(半徑),再以Circle類爲直接基類,派生出一個Cylinder(圓柱體)類,再增長數據成員h(高)。要求編寫程序,重載運算符「<<」和「>>」,使之能用於輸出以上類對象。

這個例題難度不大,但程序很長。對於一個比較大的程序,應當分紅若干步驟進行。先聲明基類,再聲明派生類,逐級進行,分步調試。

1) 聲明基類Point

類可寫出聲明基類Point的部分以下:

#include <iostream>
//聲明類Point
class Point
{
public:
   Point(float x=0,float y=0);  //有默認參數的構造函數
   void setPoint(float ,float);  //設置座標值
   float getX( )const {return x;}  //讀x座標
   float getY( )const {return y;}  //讀y座標
   friend ostream & operator <<(ostream &,const Point &);  //重載運算符「<<」
protected:  //受保護成員
   float x, y;
};
//下面定義Point類的成員函數
Point::Point(float a,float b) //Point的構造函數
{  //對x,y初始化
   x=a;
   y=b;
}
void Point::setPoint(float a,float b) //設置x和y的座標值
{  //爲x,y賦新值
   x=a;
   y=b;
}
//重載運算符「<<」,使之能輸出點的座標
ostream & operator <<(ostream &output, const Point &p)
{
   output<<"["<<p.x<<","<<p.y<<"]"<<endl;
   return output;
}

以上完成了基類Point類的聲明。

爲了提升程序調試的效率,提倡對程序分步調試,不要將一個長的程序都寫完之後才統一調試,那樣在編譯時可能會同時出現大量的編譯錯誤,面對一個長的程序,程序人員每每難以迅速準確地找到出錯位置。要善於將一個大的程序分解爲若干個文件,分別編譯,或者分步調試,先經過最基本的部分,再逐步擴充。

如今要對上面寫的基類聲明進行調試,檢查它是否有錯,爲此要寫出main函數。實際上它是一個測試程序。

int main( )
{
   Point p(3.5,6.4);  //創建Point類對象p
   cout<<"x="<<p.getX( )<<",y="<<p.getY( )<<endl;  //輸出p的座標值
   p.setPoint(8.5,6.8);  //從新設置p的座標值
   cout<<"p(new):"<<p<<endl;  //用重載運算符「<<」輸出p點座標
   return 0;
}

getX和getY函數聲明爲常成員函數,做用是隻容許函數引用類中的數據,而不容許修改它們,以保證類中數據的安全。數據成員x和y聲明爲protected,這樣能夠被派生類訪問(若是聲明爲private,派生類是不能訪問的)。

程序編譯經過,運行結果爲:
x=3.5,y=6.4
p(new):[8.5,6.8]

測試程序檢查了基類中各函數的功能,以及運算符重載的做用,證實程序是正確的。

2)聲明派生類Circle

在上面的基礎上,再寫出聲明派生類Circle的部分:

class Circle:public Point  //circle是Point類的公用派生類
{
public:
   Circle(float x=0,float y=0,float r=0);  //構造函數
   void setRadius(float );  //設置半徑值
   float getRadius( )const;  //讀取半徑值
   float area ( )const;  //計算圓面積
   friend ostream &operator <<(ostream &,const Circle &);  //重載運算符「<<」
private:
   float radius;
};
//定義構造函數,對圓心座標和半徑初始化
Circle::Circle(float a,float b,float r):Point(a,b),radius(r){}
//設置半徑值
void Circle::setRadius(float r){radius=r;}
//讀取半徑值
float Circle::getRadius( )const {return radius;}
//計算圓面積
float Circle::area( )const
{
   return 3.14159*radius*radius;
}
//重載運算符「<<」,使之按規定的形式輸出圓的信息
ostream &operator <<(ostream &output,const Circle &c)
{
   output<<"Center=["<<c.x<<","<<c.y<<"],r="<<c.radius<<",area="<<c.area( )<<endl;
   return output;
}

爲了測試以上Circle類的定義,能夠寫出下面的主函數:

int main( )
{
   Circle c(3.5,6.4,5.2);  //創建Circle類對象c,並給定圓心座標和半徑
   cout<<"original circle:\\nx="<<c.getX()<<", y="<<c.getY()<<", r="<<c.getRadius( )<<", area="<<c.area( )<<endl;  //輸出圓心座標、半徑和麪積
   c.setRadius(7.5);  //設置半徑值
   c.setPoint(5,5);  //設置圓心座標值x,y
   cout<<"new circle:\\n"<<c;  //用重載運算符「<<」輸出圓對象的信息
   Point &pRef=c;  //pRef是Point類的引用變量,被c初始化
   cout<<"pRef:"<<pRef;  //輸出pRef的信息
   return 0;
}

程序編譯經過,運行結果爲:
original circle:(輸出原來的圓的數據)
x=3.5, y=6.4, r=5.2, area=84.9486
new circle:(輸出修改後的圓的數據)
Center=[5,5], r=7.5, area=176.714
pRef:[5,5] (輸出圓的圓心「點」的數據)

能夠看到,在Point類中聲明瞭一次運算符「 <<」重載函數,在Circle類中又聲明瞭一次運算符「 <<」,兩次重載的運算符「<<」內容是不一樣的,在編譯時編譯系統會根據輸出項的類型肯定調用哪個運算符重載函數。main函數第7行用「cout<< 」輸出c,調用的是在Circle類中聲明的運算符重載函數。

請注意main函數第8行:
    Point & pRef = c;

定義了 Point類的引用變量pRef,並用派生類Circle對象c對其初始化。前面咱們已經講過,派生類對象能夠替代基類對象爲基類對象的引用初始化或賦值(詳情請查看:C++基類與派生類的轉換)。如今 Circle是Point的公用派生類,所以,pRef不能認爲是c的別名,它獲得了c的起始地址, 它只是c中基類部分的別名,與c中基類部分共享同一段存儲單元。因此用「cout<<pRef」輸出時,調用的不是在Circle中聲明的運算符重載函數,而是在Point中聲明的運算符重載函數,輸出的是「點」的信息,而不是「圓」的信息。

3) 聲明Circle的派生類Cylinder

前面已從基類Point派生出Circle類,如今再從Circle派生出Cylinder類。

class Cylinder:public Circle// Cylinder是Circle的公用派生類
{
public:
   Cylinder (float x=0,float y=0,float r=0,float h=0);  //構造函數
   void setHeight(float );  //設置圓柱高
   float getHeight( )const;  //讀取圓柱高
   loat area( )const;  //計算圓表面積
   float volume( )const;  //計算圓柱體積
   friend ostream& operator <<(ostream&,const Cylinder&);  //重載運算符<<
protected:
   float height;//圓柱高
};
//定義構造函數
Cylinder::Cylinder(float a,float b,float r,float h):Circle(a,b,r),height(h){}
//設置圓柱高
void Cylinder::setHeight(float h){height=h;}
//讀取圓柱高
float Cylinder::getHeight( )const {return height;}
//計算圓表面積
float Cylinder::area( )const { return 2*Circle::area( )+2*3.14159*radius*height;}
//計算圓柱體積
float Cylinder::volume()const {return Circle::area()*height;}
ostream &operator <<(ostream &output,const Cylinder& cy)
{
   output<<"Center=["<<cy.x<<","<<cy.y<<"],r="<<cy.radius<<",h="<<cy.height <<"\\narea="<<cy.area( )<<", volume="<<cy.volume( )<<endl;
   return output;
} //重載運算符「<<」


能夠寫出下面的主函數:

int main( )
{
   Cylinder cy1(3.5,6.4,5.2,10);//定義Cylinder類對象cy1
   cout<<"\\noriginal cylinder:\\nx="<<cy1.getX( )<<", y="<<cy1.getY( )<<", r="
      <<cy1.getRadius( )<<", h="<<cy1.getHeight( )<<"\\narea="<<cy1.area()
      <<",volume="<<cy1.volume()<<endl;//用系統定義的運算符「<<」輸出cy1的數據
   cy1.setHeight(15);//設置圓柱高
   cy1.setRadius(7.5);//設置圓半徑
   cy1.setPoint(5,5);//設置圓心座標值x,y
   cout<<"\\nnew cylinder:\\n"<<cy1;//用重載運算符「<<」輸出cy1的數據
   Point &pRef=cy1;//pRef是Point類對象的引用變量
   cout<<"\\npRef as a Point:"<<pRef;//pRef做爲一個「點」輸出
   Circle &cRef=cy1;//cRef是Circle類對象的引用變量
   cout<<"\\ncRef as a Circle:"<<cRef;//cRef做爲一個「圓」輸出
   return 0;
}

運行結果以下:
original cylinder:(輸出cy1的初始值)
x=3.5, y=6.4, r=5.2, h=10 (圓心座標x,y。半徑r,高h)
area=496.623, volume=849.486 (圓柱表面積area和體積volume)
new cylinder: (輸出cy1的新值)
Center=[5,5], r=7.5, h=15 (以[5,5]形式輸出圓心座標)
area=1060.29, volume=2650.72(圓柱表面積area和體積volume)
pRef as a Point:[5,5] (pRef做爲一個「點」輸出)
cRef as a Circle:Center=[5,5], r=7.5, area=176.714(cRef做爲一個「圓」輸出)

說明:在Cylinder類中定義了 area函數,它與Circle類中的area函數同名,根據前面咱們講解的同名覆蓋的原則(詳情請查看:C++多重繼承的二義性問題),cy1.area( ) 調用的是Cylinder類的area函數(求圓柱表面積),而不是Circle類的area函數(圓面積)。請注意,這兩個area函數不是重載函數,它們不只函數名相同,並且函數類型和參數個數都相同,兩個同名函數不在同 —個類中,而是分別在基類和派生類中,屬於同名覆蓋。重載函數的參數個數和參數類型必須至少有一者不一樣,不然系統沒法肯定調用哪個函數。

main函數第9行用「cout<<cy1」來輸出cy1,此時調用的是在Cylinder類中聲明的重載運算符「<<」,按在重載時規定的方式輸出圓柱體cy1的有關數據。

main函數中最後4行的含義與在定義Circle類時的狀況相似。pRef是Point類的引用變量,用cy1對其初始化,但它不是cy1的別名,只是cy1中基類Point部分的別名,在輸出pRef時是做爲一個Point類對象輸出的,也就是說,它是一個「點」。一樣,cRef是Circle類的引用變量,用cy1對其初始化,但它只是cy1中的直接基類Circle部分的別名, 在輸出 cRef 時是做爲Circle類對象輸出的,它是一個"圓」,而不是一個「圓柱體」。從輸 出的結果能夠看出調用的是哪一個運算符函數。

在本例中存在靜態多態性,這是運算符重載引發的(注意3個運算符函數是重載而不是同名覆蓋,由於有一個形參類型不一樣)。能夠看到,在編譯時編譯系統便可以斷定應調用哪一個重載運算符函數。

 

2)虛函數、虛函數的做用和使用方法

 咱們知道,在同一類中是不能定義兩個名字相同、參數個數和類型都相同的函數的,不然就是「重複定義」。可是在類的繼承層次結構中,在不一樣的層次中能夠出現名字相同、參數個數和類型都相同而功能不一樣的函數。例如在例12.1(具體代碼請查看:C++多態性的一個典型例子)程序中,在Circle類中定義了 area函數,在Circle類的派生類Cylinder中也定義了一個area函數。這兩個函數不只名字相同,並且參數個數相同(均爲0),但功能不一樣,函數體是不一樣的。前者的做用是求圓面積,後者的做用是求圓柱體的表面積。這是合法的,由於它們不在同一個類中。 編譯系統按照同名覆蓋的原則決定調用的對象。在例12.1程序中用cy1.area( ) 調用的是派生類Cylinder中的成員函數area。若是想調用cy1 中的直接基類Circle的area函數,應當表示爲 cy1.Circle::area()。用這種方法來區分兩個同名的函數。可是這樣作 很不方便。

人們提出這樣的設想,可否用同一個調用形式,既能調用派生類又能調用基類的同名函數。在程序中不是經過不一樣的對象名去調用不一樣派生層次中的同名函數,而是經過指針調用它們。例如,用同一個語句「pt->display( );」能夠調用不一樣派生層次中的display函數,只需在調用前給指針變量 pt 賦以不一樣的值(使之指向不一樣的類對象)便可。

打個比方,你要去某一地方辦事,若是乘坐公交車,必須事先肯定目的地,而後乘坐可以到達目的地的公交車線路。若是改成乘出租車,就簡單多了,沒必要查行車路線,由於出租車什麼地方都能去,只要在上車後臨時告訴司機要到哪裏便可。若是想訪問多個目的地,只要在到達一個目的地後再告訴司機下一個目的地便可,顯然,「打的」要比乘公交車 方便。不管到什麼地方去均可以乘同—輛出租車。這就是經過同一種形式能達到不一樣目的的例子。

C++中的虛函數就是用來解決這個問題的。虛函數的做用是容許在派生類中從新定義與基類同名的函數,而且能夠經過基類指針或引用來訪問基類和派生類中的同名函數

請分析例12.2。這個例子開始時沒有使用虛函數,而後再討論使用虛函數的狀況。

[例12.2] 基類與派生類中有同名函數。在下面的程序中Student是基類,Graduate是派生類,它們都有display這個同名的函數。

#include <iostream>
#include <string>
using namespace std;
//聲明基類Student
class Student
{
public:
   Student(int, string,float);  //聲明構造函數
   void display( );//聲明輸出函數
protected:  //受保護成員,派生類能夠訪問
   int num;
   string name;
   float score;
};
//Student類成員函數的實現
Student::Student(int n, string nam,float s)//定義構造函數
{
   num=n;
   name=nam;
   score=s;
}
void Student::display( )//定義輸出函數
{
   cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\n\n";
}
//聲明公用派生類Graduate
class Graduate:public Student
{
public:
   Graduate(int, string, float, float);//聲明構造函數
   void display( );//聲明輸出函數
private:float pay;
};
// Graduate類成員函數的實現
void Graduate::display( )//定義輸出函數
{
   cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl;
}
Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){}
//主函數
int main()
{
   Student stud1(1001,"Li",87.5);//定義Student類對象stud1
   Graduate grad1(2001,"Wang",98.5,563.5);//定義Graduate類對象grad1
   Student *pt=&stud1;//定義指向基類對象的指針變量pt
   pt->display( );
   pt=&grad1;
   pt->display( );
   return 0;
}

運行結果以下:
num:1001(stud1的數據)
name:Li
score:87.5

num:2001 (grad1中基類部分的數據)
name:wang
score:98.5

假如想輸出grad1的所有數據成員,固然也能夠採用這樣的方法:經過對象名調用display函數,如grad1.display(),或者定義一個指向Graduate類對象的指針變量ptr,而後使ptr指向gradl,再用ptr->display()調用。這固然是能夠的,可是若是該基類有多個派生類,每一個派生類又產生新的派生類,造成了同一基類的類族。每一個派生類都有同名函數display,在程序中要調用同一類族中不一樣類的同名函數,就要定義多個指向各派生類的指針變量。這兩種辦法都不方便,它要求在調用不一樣派生類的同名函數時採用不一樣的調用方式,正如同前面所說的那樣,到不一樣的目的地要乘坐指定的不一樣的公交車,一一 對應,不能搞錯。若是可以用同一種方式去調用同一類族中不一樣類的全部的同名函數,那就行了。

用虛函數就能順利地解決這個問題。下面對程序做一點修改,在Student類中聲明display函數時,在最左面加一個關鍵字virtual,即
    virtual void display( );
這樣就把Student類的display函數聲明爲虛函數。程序其餘部分都不改動。再編譯和運行程序,請注意分析運行結果:
num:1001(stud1的數據)
name:Li
score:87.5

num:2001 (grad1中基類部分的數據)
name:wang
score:98.5
pay=1200 (這一項之前是沒有的)

看!這就是虛函數的奇妙做用。如今用同一個指針變量(指向基類對象的指針變量),不但輸出了學生stud1的所有數據,並且還輸出了研究生grad1的所有數據,說明已調用了grad1的display函數。用同一種調用形式「pt->display()」,並且pt是同一個基類指針,能夠調用同一類族中不一樣類的虛函數。這就是多態性,對同一消息,不一樣對象有 不一樣的響應方式。

說明:原本基類指針是用來指向基類對象的,若是用它指向派生類對象,則進行指針類型轉換,將派生類對象的指針先轉換爲基類的指針,因此基類指針指向的是派生類對象中的基類部分。在程序修改前,是沒法經過基類指針去調用派生類對象中的成員函數的。虛函數突破了這一限制,在派生類的基類部分中,派生類的虛函數取代了基類原來的虛函數,所以在使基類指針指向派生類對象後,調用虛函數時就調用了派生類的虛函數。 要注意的是,只有用virtual聲明瞭虛函數後才具備以上做用。若是不聲明爲虛函數,企圖經過基類指針調用派生類的非虛函數是不行的。

虛函數的以上功能是頗有實用意義的。在面向對象的程序設計中,常常會用到類的繼承,目的是保留基類的特性,以減小新類開發的時間。可是,從基類繼承來的某些成員函數不徹底適應派生類的須要,例如在例12.2中,基類的display函數只輸出基類的數據,而派生類的display函數須要輸出派生類的數據。過去咱們曾經使派生類的輸出函數與基類的輸出函數不一樣名(如display和display1),但若是派生的層次多,就要起許多不一樣的函數名,很不方便。若是採用同名函數,又會發生同名覆蓋。

利用虛函數就很好地解決了這個問題。能夠看到:當把基類的某個成員函數聲明爲虛函數後,容許在其派生類中對該函數從新定義,賦予它新的功能,而且能夠經過指向基類的指針指向同一類族中不一樣類的對象,從而調用其中的同名函數。由虛函數實現的動態多態性就是:同一類族中不一樣類的對象,對同一函數調用做出不一樣的響應。

虛函數的使用方法是:

  1. 在基類用virtual聲明成員函數爲虛函數
    這樣就能夠在派生類中從新定義此函數,爲它賦予新的功能,並能方便地被調用。在類外定義虛函數時,沒必要再加virtual。
  2. 在派生類中從新定義此函數,要求函數名、函數類型、函數參數個數和類型所有與基類的虛函數相同,並根據派生類的須要從新定義函數體。
    C++規定,當一個成員函數被聲明爲虛函數後,其派生類中的同名函數都自動成爲虛函數。所以在派生類從新聲明該虛函數時,能夠加virtual,也能夠不加,但習慣上通常在每一層聲明該函數時都加virtual,使程序更加清晰。若是在派生類中沒有對基類的虛函數從新定義,則派生類簡單地繼承其直接基類的虛函數。
  3. 定義一個指向基類對象的指針變量,並使它指向同一類族中須要調用該函數的對象。
  4. 經過該指針變量調用此虛函數此時調用的就是指針變量指向的對象的同名函數
    經過虛函數與指向基類對象的指針變量的配合使用,就能方便地調用同一類族中不一樣類的同名函數,只要先用基類指針指向便可。若是指針不斷地指向同一類族中不一樣類的對象,就能不斷地調用這些對象中的同名函數。這就如同前面說的,不斷地告訴出租車司機要去的目的地,而後司機把你送到你要去的地方。


須要說明;有時在基類中定義的非虛函數會在派生類中被從新定義(如例12.1中的area函數),若是用基類指針調用該成員函數,則系統會調用對象中基類部分的成員函數;若是用派生類指針調用該成員函數,則系統會調用派生類對象中的成員函數,這並非多態性行爲(使用的是不一樣類型的指針),沒有用到虛函數的功能。

之前介紹的函數重載處理的是同一層次上的同名函數問題,而虛函數處理的是不一樣派生層次上的同名函數問題,前者是橫向重載,後者能夠理解爲縱向重載。但與重載不一樣的是:同一類族的虛函數的首部是相同的,而函數重載時函數的首部是不一樣的(參數個數或類型不一樣)。

 

使用虛函數時,有兩點要注意:

  1. 只能用virtual聲明類的成員函數,使它成爲虛函數,而不能將類外的普通函數聲明爲虛函數。由於虛函數的做用是容許在派生類中對基類的虛函數從新定義。顯然,它只能用於類的繼承層次結構中。(我的理解:虛構函數說白了是把對應地址虛構,在後續工做中能夠多該地址經行從新整合,聲明成虛構函數後,該操做更消耗硬件資源)
  2. 一個成員函數被聲明爲虛函數後,在同一類族中的類就不能再定義一個非virtual的但與該虛函數具備相同的參數(包括個數和類型)和函數返回值類型的同名函數。


根據什麼考慮是否把一個成員函數聲明爲虛函數呢?主要考慮如下幾點:

  1. 首先當作員函數所在的類是否會做爲基類。而後當作員函數在類的繼承後有無可能被更改功能,若是但願更改其功能的,通常應該將它聲明爲虛函數
  2. 若是成員函數在類被繼承後功能不需修改,或派生類用不到該函數,則不要把它聲明爲虛函數。不要僅僅考慮到要做爲基類而把類中的全部成員函數都聲明爲虛函數。
  3. 應考慮對成員函數的調用是經過對象名仍是經過基類指針或引用去訪問,若是是經過基類指針或引用去訪問的,則應當聲明爲虛函數。
  4. 有時,在定義虛函數時,並不定義其函數體,即函數體是空的。它的做用只是定義了一個虛函數名,具體功能留給派生類去添加。(這種理解卻是更夠味)


須要說明的是:使用虛函數,系統要有必定的空間開銷。當一個類帶有虛函數時,編譯系統會爲該類構造一個虛函數表(virtual function table,簡稱vtable),它是一個指針數組,存放每一個虛函數的入口地址。系統在進行動態關聯時的時間開銷是不多的,所以,多態性是高效的。

 

 3)靜態關聯與動態關聯、C++是怎樣實現多態性的

這一節將探討C++是怎樣實現多態性的。

從例12.2(具體代碼請查看:什麼是C++虛函數)中修改後的程序能夠看到, 同一個display函數在不一樣對象中有不一樣的做用,呈現了多態。計算機系統應該能正確地選擇調用對象。

在現實生活中,多態性的例子是不少的。咱們分析一下人是怎樣處理多 態性的。例如,新生被錄取人大學,在人學報到時,先有一名工做人員審查材料,他的職責是甄別資格,而後根據錄取通知書上註明的錄取的系和專業,將材料轉到有關的系和專業,辦理具體的註冊人學手續,也能夠看做調用不一樣部門的處理程序辦理入學手續。在學 生眼裏,這名工做人員是總的人口,全部新生辦入學手續都要通過他。學生拿的是統一的錄取通知書,但實際上分屬不一樣的系,要進行不一樣的註冊手續,這就是多態。那麼,這名工 做人員怎麼處理多態呢?憑什麼把它分發到哪一個系呢?就是根據錄取通知書上的一個信 息(你被錄取入本校某某專業)。可見,要區分就必需要有相關的信息,不然是沒法判別的。

一樣,編譯系統要根據已有的信息,對同名函數的調用做出判斷。例如函數的重載, 系統是根據參數的個數和類型的不一樣去找與之匹配的函數的。對於調用同一類族中的虛函數,應當在調用時用必定的方式告訴編譯系統,你要調用的是哪一個類對象中的函數。例如能夠直接提供對象名,如studl.display()或grad1.display()。這樣編譯系統在對程序進行編譯時,即能肯定調用的是哪一個類對象中的函數。

肯定調用的具體對象的過程稱爲關聯(binding)。binding原意是捆綁或鏈接,即把兩樣東西捆綁(或鏈接)在一塊兒。在這裏是指把一個函數名與一個類對象捆綁在一塊兒,創建關聯。通常地說,關聯指把一個標識符和一個存儲地址聯繫起來。在計算機字典中能夠査到,所謂關聯,是指計算機程序中不一樣的部分互相鏈接的過程。有些書中把binding譯爲聯編、編聯、束定、或兼顧音和意,稱之爲綁定。做者認爲:從意思上說,關聯比較確切, 也好理解。可是有些教程中用了聯編這個術語。 你們在看到這個名詞時,應當知道指的就是本節介紹的關聯。

順便說一句題外話,計算機領域中大部分術語是從外文翻譯過來的,有許多譯名是譯得比較好的,能見名知意的。但也有一些則使人費解,甚至不大確切。例如在某些介紹計算機語言的書籍中,把project譯爲「工程」,令人難以理解,其實譯爲「項目」比較確切。 有些介紹計算機應用的書中充斥大量的術語,初聽起來好像很唬人、很難懂,許多學習 C++的人每每被大量的專門術語嚇住了,又難以理解其真正含義,很多人「見難而退」。 這個問題成爲許多人學習C++的攔路虎。所以,應當提倡用通俗易懂的方法去闡明覆雜的概念。其實,有許多看起來深奧難懂的概念和術語,捅破窗戶紙後是很簡單的。建議讀者在初學時千萬不要糾纏於名詞術語的字面解釋上,而要掌握其精神實質和應用方法。

說明:與其餘編程語言相比,例如Java、C#等,C++的語法是最豐富最靈活的,一樣也是最難掌握的,你們要按部就班,莫求速成,在編程實踐中不斷翻閱和記憶。

前面所提到的函數重載和經過對象名調用的虛函數,在編譯時便可肯定其調用的虛函數屬於哪個類,其過程稱爲靜態關聯(static binding),因爲是在運行前進行關聯的, 故又稱爲早期關聯(early binding)。函數重載屬靜態關聯。

從例12.2咱們中看到了怎樣使用虛函數,在調用虛函數時並無指定對象名,那麼系統是怎樣肯定關聯的呢?讀者能夠看到,是經過基類指針與虛函數的結合來實現多態性的。先定義了一個指向基類的指針變量,並使它指向相應的類對象,而後經過這個基類指針去調用虛函數(例如「pt->display()」)。顯然,對這樣的調用方式,編譯系統在編譯該行時是沒法肯定調用哪個類對象的虛函數的。由於編譯只做靜態的語法檢査,光從語句形式(例如「pt->display();」)是沒法肯定調用對象的。

在這樣的狀況下,編譯系統把它放到運行階段處理,在運行階段肯定關聯關係。在運行階段,基類指針變量先指向了某一個類對象,而後經過此指針變量調用該對象中的函數。此時調用哪個對象的函數無疑是肯定的。例如,先使pt指向grad1,再執行「pt->display()」,固然是調用grad1中的display函數。因爲是在運行階段把虛函數和類對象「綁定」在一塊兒的,所以,此過程稱爲動態關聯(dynamic binding)。這種多態性是動態的多態性,即運行階段的多態性。

在運行階段,指針能夠前後指向不一樣的類對象,從而調用同一類族中不一樣類的虛函數。因爲動態關聯是在編譯之後的運行階段進行的,所以也稱爲滯後關聯(late binding) 。

 

4)虛析構函數詳解

 咱們已經介紹過析構函數(詳情請查看:C++析構函數),它的做用是在對象撤銷以前作必要的「清理現場」的工做。

當派生類的對象從內存中撤銷時通常先調用派生類的析構函數,而後再調用基類的析構函數。可是,若是用new運算符創建了臨時對象,若基類中有析構函數,而且定義了一個指向該基類的指針變量。在程序用帶指針參數的delete運算符撤銷對象時,會發生一個狀況:系統會只執行基類的析構函數,而不執行派生類的析構函數。

[例12.3] 基類中有非虛析構函數時的執行狀況。爲簡化程序,只列出最必要的部分。

#include <iostream>
using namespace std;
class Point  //定義基類Point類
{
public:
   Point( ){}  //Point類構造函數
   ~Point(){cout<<"executing Point destructor"<<endl;}  //Point類析構函數
};
class Circle:public Point  //定義派生類Circle類
{
public:
   Circle( ){}  //Circle類構造函數
   ~Circle( ){cout<<"executing Circle destructor"<<endl;}  //Circle類析構函數
private:
   int radius;
};
int main( )
{
   Point *p=new Circle;  //用new開闢動態存儲空間
   delete p;  //用delete釋放動態存儲空間
   return 0;
}

這只是一個示意的程序。p是指向基類的指針變量,指向new開闢的動態存儲空間,但願用detele釋放p所指向的空間。但運行結果爲:
executing Point destructor

表示只執行了基類Point的析構函數,而沒有執行派生類Circle的析構函數。

若是但願能執行派生類Circle的析構函數,能夠將基類的析構函數聲明爲虛析構函數,如:
    virtual ~Point(){cout<<″executing Point destructor″<<endl;}

程序其餘部分不改動,再運行程序,結果爲:
executing Circle destructor
executing Point destructor

先調用了派生類的析構函數,再調用了基類的析構函數,符合人們的願望。

當基類的析構函數爲虛函數時,不管指針指的是同一類族中的哪個類對象,系統會採用動態關聯,調用相應的析構函數,對該對象進行清理工做。

若是將基類的析構函數聲明爲虛函數時,由該基類所派生的全部派生類的析構函數也都自動成爲虛函數,即便派生類的析構函數與基類的析構函數名字不相同

最好把基類的析構函數聲明爲虛函數。這將使全部派生類的析構函數自動成爲虛函數。這樣,若是程序中顯式地用了delete運算符準備刪除一個對象,而delete運算符的操做對象用了指向派生類對象的基類指針,則系統會調用相應類的析構函數。

虛析構函數的概念和用法很簡單,但它在面向對象程序設計中倒是很重要的技巧。

專業人員通常都習慣聲明虛析構函數,即便基類並不須要析構函數,也顯式地定義一個函數體爲空的虛析構函數,以保證在撤銷動態分配空間時能獲得正確的處理。

構造函數不能聲明爲虛函數。這是由於在執行構造函數時類對象還未完成創建過程,固然談不上函數與類對象的綁定。

 

 5)純虛函數詳解 (做用:類中函數延展做用)

 有時在基類中將某一成員函數定爲虛函數,並非基類自己的要求,而是考慮到派生類的須要,在基類中預留了一個函數名,具體功能留給派生類根據須要去定義。

例如在前邊的例12.1(詳情請查看:什麼是C++虛函數)程序中,基類Point中沒有求面積的area函數,由於「點」是沒有面積的,也就是說,基類自己不須要這個函數,因此在例12.1程序中的Point類中沒有定義area函數。

可是,在其直接派生類Circle和間接派生類Cylinder中都須要有area函數,並且這兩個area函數的功能不一樣,一個是求圓面積,一個是求圓柱體表面積。

有的讀者天然會想到,在這種狀況下應當將area聲明爲虛函數。能夠在基類Point中加一個area函數,並聲明爲虛函數:
    virtual float area( )const {return 0;}
其返回值爲0,表示「點」是沒有面積的。

其實,在基類中並不使用這個函數,其返回值也是沒有意義的。爲簡化,能夠不寫出這種無心義的函數體,只給出函數的原型,並在後面加上「=0」,如:
    virtual float area( )const =0;  //純虛函數
這就將area聲明爲一個純虛函數(pure virtual function)。

純虛函數是在聲明虛函數時被「初始化」爲0的函數。聲明純虛函數的通常形式是
    virtual 函數類型 函數名 (參數表列) = 0;

關於純虛函數須要注意的幾點:

  1. 純虛函數沒有函數體;
  2. 最後面的「=0」並不表示函數返回值爲0,它只起形式上的做用,告訴編譯系統「這是純虛函數」;
  3. 這是一個聲明語句,最後應有分號。


純虛函數只有函數的名字而不具有函數的功能,不能被調用。它只是通知編譯系統:「在這裏聲明一個虛函數,留待派生類中定義」。在派生類中對此函數提供定義後,它才能具有函數的功能,可被調用。

純虛函數的做用是在基類中爲其派生類保留一個函數的名字,以便派生類根據須要對它進行定義。

若是在基類中沒有保留函數名字,則沒法實現多態性。若是在一個類中聲明瞭純虛函數,而在其派生類中沒有對該函數定義,則該虛函數在派生類中仍然爲純虛函數

 

 6)抽象類

若是聲明瞭一個類,通常能夠用它定義對象。可是在面向對象程序設計中,每每有一些類,它們不用來生成對象。定義這些類的唯一目的是用它做爲基類去創建派生類。它們做爲一種基本類型提供給用戶,用戶在這個基礎上根據本身的須要定義出功能各異的派生類。用這些派生類去創建對象。

打個比方,汽車製造廠每每向客戶提供卡車的底盤(包括髮動機、傳動部分、車輪等),組裝廠能夠把它組裝成貨車、公共汽車、工程車或客車等不一樣功能的車輛。底盤自己不是車輛,要通過加工才能成爲車輛,但它是車輛的基本組成部分。它至關於基類。在現代化的生產中,大多采用專業化的生產方式,充分利用專業化工廠生產的部件,加工集成爲新品種的產品。生產公共汽車的廠家決不會從製造發動機到生產輪胎、製造車箱都由本廠完成。其實,不一樣品牌的電腦裏面的基本部件是同樣的或類似的。這種觀念對軟件開發是十分重要的。一個優秀的軟件工做者在開發一個大的軟件時,決不會從頭至尾都由本身編寫程序代碼,他會充分利用已有資源(例如類庫)做爲本身工做的基礎。

這種不用來定義對象而只做爲一種基本類型用做繼承的類,稱爲抽象類(abstract class ),因爲它經常使用做基類,一般稱爲抽象基類(abstract base class )。凡是包含純虛函數的類都是抽象類。由於純虛函數是不能被調用的,包含純虛函數的類是沒法創建對象的

抽象類的做用是做爲一個類族的共同基類,或者說,爲一個類族提供一個公共接口。一個類層次結構中固然也可不包含任何抽象類,每一層次的類都是實際可用的,能夠用來創建對象的。

可是,許多好的面向對象的系統,其層次結構的頂部是一個抽象類,甚至頂部有好幾層都是抽象類。

若是在抽象類所派生出的新類中對基類的全部純虛函數進行了定義,那麼這些函數就被賦予了功能,能夠被調用。這個派生類就不是抽象類,而是能夠用來定義對象的具體類(concrete class )。

若是在派生類中沒有對全部純虛函數進行定義,則此派生類仍然是抽象類,不能用來定義對象。雖然抽象類不能定義對象(或者說抽象類不能實例化),可是能夠定義指向抽象類數據的指針變量。當派生類成爲具體類以後,就能夠用這種指針指向派生類對象,而後經過該指針調用虛函數,實現多態性的操做。

 實例:

咱們在例12.1(具體代碼請查看:C++多態性的一個典型例子)介紹了以Point爲基類的點—圓—圓柱體類的層次結構。如今要對它進行改寫,在程序中使用虛函數和抽象基類。類的層次結構的頂層是抽象基類Shape(形狀)。Point(點), Circle(圓), Cylinder(圓柱體)都是Shape類的直接派生類和間接派生類。

下面是一個完整的程序,爲了便於閱讀,分段插入了一些文字說明。程序以下:

第(1)部分

#include <iostream>
using namespace std;
//聲明抽象基類Shape
class Shape
{
public:
   virtual float area( )const {return 0.0;}  //虛函數
   virtual float volume()const {return 0.0;}  //虛函數 體積
   virtual void shapeName()const =0;  //純虛函數
};

Shape類有3個成員函數,沒有數據成員。3個成員函數都聲明爲虛函數,其中shapeName聲明爲純虛函數,所以Shape是一個抽象基類。shapeName函數的做用是輸出具體的形狀(如點、圓、圓柱體)的名字,這個信息是與相應的派生類密切相關的,顯然這不該當在基類中定義,而應在派生類中定義。因此把它聲明爲純虛函數。Shape雖然是抽象基類,可是也能夠包括某些成員的定義部分。類中兩個函數area(面積)和volume (體積)包括函數體,使其返回值爲0(由於能夠認爲點的面積和體積都爲0)。因爲考慮到在Point類中再也不對area和volume函數從新定義,所以沒有把area和volume函數也聲明爲純虛函數。在Point類中繼承了Shape類的area和volume函數。這3個函數在各派生類中都要用到。

第(2)部分

//聲明Point類
class Point:public Shape//Point是Shape的公用派生類
{
public:
   Point(float=0,float=0);
   void setPoint(float ,float );
   float getX( )const {return x;}
   float getY( )const {return y;}
   virtual void shapeName( )const {cout<<"Point:";}//對虛函數進行再定義
   friend ostream & operator <<(ostream &,const Point &);
protected:
   float x,y;
};

//定義Point類成員函數
Point::Point(float a,float b)
{x=a;y=b;}
void Point::setPoint(float a,float b)
{x=a;y=b;}
ostream & operator <<(ostream &output,const Point &p)
{
   output<<"["<<p.x<<","<<p.y<<"]";
   return output;
}

Point從Shape繼承了3個成員函數,因爲「點」是沒有面積和體積的,所以沒必要從新定義area和volume。雖然在Point類中用不到這兩個函數,可是Point類仍然從Shape類繼承了這兩個函數,以便其派生類繼承它們。shapeName函數在Shape類中是純虛函數, 在Point類中要進行定義。Point類還有本身的成員函數( setPoint, getX, getY)和數據成 員(x和y)。

第(3)部分

//聲明Circle類
class Circle:public Point
{
public:
   Circle(float x=0,float y=0,float r=0);
   void setRadius(float );
   float getRadius( )const;
   virtual float area( )const;
   virtual void shapeName( )const {cout<<"Circle:";}//對虛函數進行再定義
   friend ostream &operator <<(ostream &,const Circle &);
protected:
   float radius;
};
//聲明Circle類成員函數
Circle::Circle(float a,float b,float r):Point(a,b),radius(r){}
void Circle::setRadius(float r):radius(r){}
float Circle::getRadius( )const {return radius;}
float Circle::area( )const {return 3.14159*radius*radius;}
ostream &operator <<(ostream &output,const Circle &c)
{
   output<<"["<<c.x<<","<<c.y<<"], r="<<c.radius;
   return output;
}

 

 在Circle類中要從新定義area函數,由於須要指定求圓面積的公式。因爲圓沒有體積,所以沒必要從新定義volume函數,而是從Point類繼承volume函數。shapeName函數是虛函數,須要從新定義,賦予新的內容(若是不從新定義,就會繼承Point類中的 shapeName函數)。此外,Circle類還有本身新增長的成員函數(setRadius, getRadius)和數據成員(radius)。

第(4)部分

//聲明Cylinder類
class Cylinder:public Circle
{
public:
   Cylinder (float x=0,float y=0,float r=0,float h=0);
   void setHeight(float );
   virtual float area( )const;
   virtual float volume( )const;
   virtual void shapeName( )const {
      cout<<"Cylinder:";
   }//對虛函數進行再定義
   friend ostream& operator <<(ostream&,const Cylinder&);
protected:
   float height;
};
//定義Cylinder類成員函數
Cylinder::Cylinder(float a,float b,float r,float h):Circle(a,b,r),height(h){}
void Cylinder::setHeight(float h){height=h;}
float Cylinder::area( )const{
   return 2*Circle::area( )+2*3.14159*radius*height;
}
float Cylinder::volume( )const{
   return Circle::area( )*height;
}
ostream &operator <<(ostream &output,const Cylinder& cy){
   output<<"["<<cy.x<<","<<cy.y<<"], r="<<cy.radius<<", h="<<cy.height;
   return output;
}

Cylinder類是從Circle類派生的。因爲圓柱體有表面積和體積,因此要對area和 volume函數從新定義。虛函數shapeName也須要從新定義。此外,Cylinder類還有自已 的成員函數setHeight和數據成員radius。

第(5)部分

//main函數
int main( )
{
   Point point(3.2,4.5);  //創建Point類對象point
   Circle circle(2.4,1.2,5.6);
   //創建Circle類對象circle
   Cylinder cylinder(3.5,6.4,5.2,10.5);
   //創建Cylinder類對象cylinder
   point.shapeName();
   //靜態關聯
   cout<<point<<endl;
   circle.shapeName();  //靜態關聯
   cout<<circle<<endl;
   cylinder.shapeName();  //靜態關聯
   cout<<cylinder<<endl<<endl;
   Shape *pt;  //定義基類指針
   pt=&point;  //指針指向Point類對象
   pt->shapeName( );  //動態關聯
   cout<<"x="<<point.getX( )<<",y="<<point.getY( )<<"\narea="<<pt->area( )
      <<"\nvolume="<<pt->volume()<<"\n\n";
   pt=&circle;  //指針指向Circle類對象
   pt->shapeName( );  //動態關聯
   cout<<"x="<<circle.getX( )<<",y="<<circle.getY( )<<"\narea="<<pt->area( )
      <<"\nvolume="<<pt->volume( )<<"\n\n";
   pt=&cylinder;  //指針指向Cylinder類對象
   pt->shapeName( );  //動態關聯
   cout<<"x="<<cylinder.getX( )<<",y="<<cylinder.getY( )<<"\narea="<<pt->area( )
      <<"\nvolume="<<pt->volume( )<<"\n\n";
   return 0;
}

在主函數中調用有關函數並輸出結果。先分別定義了 Point類對象point,Circle類對象circle和Cylinder類對象cylinder。而後分別經過對象名point, circle和cylinder調用 了shapeNanme函數,這是屬於靜態關聯,在編譯階段就能肯定應調用哪個類的 shapeName函數。同時用重載的運箅符「<<」來輸出各對象的信息,能夠驗證對象初始化是否正確。

再定義一個指向基類Shape對象的指針變量pt,使它前後指向3個派生類對象 point, Circle和cylinder,而後經過指針調用各函數,如 pt->shapeName( ),pt ->area(), pt->volume( )。這時是經過動態關聯分別肯定應該調用哪一個函數。分別輸出不一樣類對象的信息。

程序運行結果以下:
Point:[3.2,4.5](Point類對象point的數據:點的座標)
Circle:[2.4,1.2], r=5.6 (Circle類對象circle的數據:圓心和半徑)
Cylinder:[3.5,6.4], r=5.5, h=10.5 (Cylinder類對象cylinder的數據: 圓心、半徑和高)

Point:x=3.2,y=4.5 (輸出Point類對象point的數據:點的座標)
area=0 (點的面積)
volume=0 (點的體積)

Circle:x=2.4,y=1.2 (輸出Circle類對象circle的數據:圓心座標)
area=98.5203 (圓的面積)
volume=0 (圓的體積)
Cylinder:x=3.5,y=6.4 (輸出Cylinder類對象cylinder的數據:圓心座標)
area=512.595 (圓的面積)
volume=891.96 (圓柱的體積)

從本例能夠進一步明確如下結論:

  1. 一個基類若是包含一個或一個以上純虛函數,就是抽象基類。抽象基類不能也沒必要要定義對象。
  2. 抽象基類與普通基類不一樣,它通常並非現實存在的對象的抽象(例如圓形(Circle)就是千千萬萬個實際的圓的抽象),它能夠沒有任何物理上的或其餘實際意義方面的含義。
  3. 在類的層次結構中,頂層或最上面的幾層能夠是抽象基類。抽象基類體現了本類族中各種的共性,把各種中共有的成員函數集中在抽象基類中聲明。
  4. 抽象基類是本類族的公共接口。或者說,從同一基類派生出的多個類有同一接口。
  5. 區別靜態關聯和動態關聯。若是是經過對象名調用虛函數(如point.shapeName()),在編譯階段就能肯定調用的是哪個類的虛函數,因此屬於靜態關聯 若是是經過基類指針調用虛函數(如pt ->shapeName()),在編譯階段沒法從語句自己肯定調用哪個類的虛函數,只有在運行時,pt指向某一類對象後,才能肯定調用的是哪 一個類的虛函數,故爲動態關聯。
  6. 若是在基類聲明瞭虛函數,則在派生類中凡是與該函數有相同的函數名、函數類型、參數個數和類型的函數,均爲虛函數(不論在派生類中是否用virtual聲明)。
  7. 使用虛函數提升了程序的可擴充性。把類的聲明與類的使用分離。這對於設計類庫的軟件開發商來講尤其重要。


開發商設計了各類各樣的類,但不向用戶提供源代碼,用戶能夠不知道類是怎樣聲明的,可是能夠使用這些類來派生出本身的類。利用虛函數和多態性,程序員的注意力集中在處理廣泛性,而讓執行環境處理特殊性。

多態性把操做的細節留給類的設計者(他們多爲專業人員)去完成,而讓程序人員(類的使用者)只須要作一些宏觀性的工做,告訴系統作什麼,而沒必要考慮怎麼作,極大地簡化了應用程序的編碼工做,大大減輕了程序員的負擔,也下降了學習和使用C++編程的難度,使更多的人能更快地進入C++程序設計的大門。

 

 

 

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

相關文章
相關標籤/搜索