C++語言概述
C++語言是一種應用較廣的面向對象的程序設計語言,使用它可以實現面向對象的程序設計。面向對象的設計與面向過程的設計是有很大區別的,面向對象的程序設計是在面向過程的程序設計的基礎上一個質的飛躍。要學會面向對象的程序設計,首先要學會一種面向對象的語言,即要學會用VC編程,就要先有C++的基礎,而學習C++語言首先要認識它面向對象的特性和實現面向對象的方法。
C++是一種面向對象的程序設計語言
當你首次學習C++時,總會碰到一些在C語言從未見過的概念,如:類、對象、抽象、封裝、繼承、多態性、虛函數等等。這些概念是C++所具有,下面簡單的介紹一下C++對面向對象程序設計方法的支持和實現。
1、C++支持數據封裝
支持數據封裝就是支持數據抽象。在C++中,類是支持數據封裝的工具,對象則是數據封裝的實現。面向過程的程序設計方法與面向對象的程序設計方法在對待數據和函數關系上是不同的,在面向對象的程序設計中,將數據和對該數據進行合法操作的函數封裝在一起作為一個類的定義,數據將被隱藏在封裝體中,該封裝體通過操作接口與外界交換信息。對象被說明具有一個給定類的變量,類類似于C語言中的結構,在C語言中可以定義結構,但這種結構中包含數據,而不包含函數。C++中的類是數據和函數的封裝體。在C++中,結構可作為一種特殊的類,它雖然可以包含函數,但是它沒有私有或保護的成員。
2、C++類中包含私有、公有和保護成員
C++類中可定義三種不同訪控制權限的成員。一種是私有(Private)成員,只有在類中說明的函數才能訪問該類的私有成員,而在該類外的函數不可以訪問私有成員;另一種是公有(Public)成員,類外面也可訪問公有成員,成為該類的接口;還有一種是保護(Protected)成員,這種成員只有該類的派生類可以訪問,其余的在這個類外不能訪問。
3、C++中通過發關消息來處理對象
C++中是通過向對象發關消息來處理對象的,每個對象根據所接收到的消息的性質來決定需要采取的行動,以響應這個消息。響應這些消息是一系列的方法,方法是在類定義中使用函數來定義的,使用一種類似于函數調用的機制把消息發送到一個對象上。
4、C++中允許友元破壞封裝性
類中的私有成員一般是不允許該類外面的任何函數訪問的,但是友元便可打破這條禁令,它可以訪問該類的私有成員(包含數據成員和成員函數)。友元可以是在類外定義的函數,也可以是在類外定義的整個類,前者稱友元函數,后者稱為友元類。友元打破了類的封裝性,它是C++另一個面向對象的重要牲。
5、C++允許函數名和運算符重載
C++支持多態性,C++允許一個相同的標識符或運算符代表多個不同實現的函數,這就稱標識符或運算符的重載,用戶可以根據需要定義標識符重載或運算符重載。
6、C++支持繼承性
C++中可以允許單繼承和多繼承。一個類可以根據需要生成派生類。派生類繼承了基類的所有方法,另外派生類自身還可以定義所需要的不包含在父類中的新方法。一個子類的每個對象包含有從父類那里繼承來的數據成員以及自己所特有的數據成員。
7、C++支持動態聯編
C++中可以定義虛函數,通過定義虛函數來支持動態聯編。
以上是所講的是C++對面向對象程序設計中的一些主要特征的支持。
C++的詞法及詞法規則
1、C++的字符集
字符是一些可以區分的最小符號。C++的字符集由大小寫英文字母(a-z和A-Z)、數據字符(0-9)、特殊字符(空格,!,#,%,^,&,*,_,<,>,?,\,,)組成。
2、單詞及詞法規則
單詞又稱詞法記號,它是由若干個字符組成的具有一定意義的最小詞法單元。C++共有6種單詞,分別是:標識符、關鍵字、運算符、分隔符、常量、注釋符,在編碼時要特別注意這些單詞的詞法規則。要注意的是C++中的空白符:C++中經常使用空白符,實際上,空白符不是一個字符,它是空格符、換行符和水平制表符的統稱。注意,空白符不等于空格符,只是空白符包含空格符。還有一個空字符,要把它與空白符分開。空字符是指ASCII碼值為0的那個字符。空字符在C++中有特殊用途,用它來作為字符串的結束符。存放在內存中的字符串常量都在最后有一個結束符,即用空字符,它用轉義序列方法表示為’\0’。
C++程序結構的組成
C++程序結構的基本組成部分
1 預處理命令,C++提供了三類預處理命令:宏定義命令、文件包含命令、條件編譯命令。
2 輸入輸出,C++程序中總是少不了輸入和輸出的語句,實現與程序內部的信息交流。特別是屏幕輸出的功能,幾乎每個程序都要用到,使用它把計算機的結果顯示在屏幕上。
3 函數,C++的程序是由若干個文件組成的,每個文件又是由若干個函數組成,因此,可以認為C++的程序就是函數串,即由若干個函數組成,函數與函數之間是相對的,并且是并行的,函數之間可以調用。在組成一個程序的若干個函中,必須有一個main()。
4 語句,語句是組成程序的基本單元。函數是由若干條語句組成的。但是,空函數是沒有語句的。語句是由單詞組成,單詞間用空格符分隔,C++程序中的語句又是以以分號結束。語句除了有表達式語句和空語句之外,還有復合語句、分支語句、循環語句和轉向語句等若干類。
5 變量,多數程序都需要說明和使用變量。廣義講,對象包含了變量,即將變量也稱為一種對象,狹義講,將對象看作是類的實例,對象是指某個類的對象。
6 其他,除了以上講述的5個部分以外,還有其他組成部分。例如,符號常量和注釋信息也是程序的一部分。C++中都盡量把常量定義為符號常量,在C++的程序中出現的是符號常量,該符號常量代表著某個確定的常量值。
C++程序的書寫格式
在編程時應該注意C++的書寫格式,基本原則是:一行一般寫一條語句。短語句可以一行寫多個。長語句可以一條寫多行。分行原則是不能將一個單詞分開。用雙引號引用的一個字符串也最好不分開,如果一定要分開,有的編譯系統要求在行尾加續行符(“\”)
C++程序的實現
C++源程序的實現與其他高級語言源程序實現的原理是一樣的。一般都要經過編輯、編譯、運行。其中最要的是編譯過程,C++是以編譯方式實現的高級語言。C++程序的實現,必須要使用某種C++語言的編譯器對程序進行編譯。編譯器的功能是將程序的源代碼轉換成為機器代碼的形式,稱為目標代碼;然后,再使目標代碼進行連接,生成可執行文件。該過程可分為三個子過程:預處理過程、編譯過程(詞法分析、語法分析、符號表、錯誤處理程序、生成目標代碼)、連接過程。
C++常類型(const)
常類型是指使用類型修飾符const說明的類型,常類型的變量或對象的值是不能被更新的。因此,定義或說明常類型時必須進行初始化。
一般常量和對象常量
1. 一般常量
一般常量是指簡單類型的常量。這種常量在定義時,修飾符const可以用在類型說明符前,也可以用在類型說明符后。如:
int const x=2;
或
const int x=2;
定義或說明一個常數組可采用如下格式:
<類型說明符> const <數組名>[<大小>]…
或者
const <類型說明符> <數組名>[<大小>]…
例如:
int const a[5]={1, 2, 3, 4, 5};
2. 常對象
常對象是指對象常量,定義格式如下:
<類名> const <對象名>
或者
const <類名> <對象名>
定義常對象時,同樣要進行初始化,并且該對象不能再被更新,修飾符const可以放在類名后面,也可以放在類名前面。
常指針和常引用
1. 常指針
使用const修飾指針時,由于const的位置不同,而含意不同。下面舉兩個例子,說明它們的區別。
下面定義的一個指向字符串的常量指針:
char * const prt1 = stringprt1;
其中,ptr1是一個常量指針。因此,下面賦值是非法的。
ptr1 = stringprt2;
而下面的賦值是合法的:
*ptr1 = "m";
因為指針ptr1所指向的變量是可以更新的,不可更新的是常量指針ptr1所指的方向(別的字符串)。
下面定義了一個指向字符串常量的指針:
const * ptr2 = stringprt1;
其中,ptr2是一個指向字符串常量的指針。ptr2所指向的字符串不能更新的,而ptr2是可以更新的。因此,
*ptr2 = "x";
是非法的,而:
ptr2 = stringptr2;
是合法的。
所以,在使用const修飾指針時,應該注意const的位置。定義一個指向字符串的指針常量和定義一個指向字符串常量的指針時,const修飾符的位置不同,前者const放在*和指針名之間,后者const放在類型說明符前。
2. 常引用
使用const修飾符也可以說明引用,被說明的引用為常引用,該引用所引用的對象不能被更新。其定義格式如下:
const <類型說明符> & <引用名>
例如:
const double & v;
在實際應用中,常指針和常引用往往用來作函數的形參,這樣的參數稱為常參數。
在C++面向對象的程序設計中,指針和引用使用得較多,其中使用const修飾的常指針和常引用用得更多。使用常參數則表明該函數不會更新某個參數所指向或所引用的對象,這樣,在參數傳遞過程中就不需要執行拷貝初始化構造函數,這將會改善程序的運行效率。
下面舉一例子說明常指針作函數參數的作法。
#include
const int N = 6;
void print(const int *p, int n);
void main()
{
int array[N];
for (int i=0; i cin>>array[i];
print(array, N);
}
void print(const int *p, int n)
{
cout<<"{"<<*p;
for (int i=1; i cout<<","<<*(p+i);
cout<<"}"< }
常成員函數
使用const關鍵字進行說明的成員函數,稱為常成員函數。只有常成員函數才有資格操作常量或常對象,沒有使用const關鍵字說明的成員函數不能用來操作常對象。常成員函數說明格式如下:
<類型說明符> <函數名> (<參數表>) const;
其中,const是加在函數說明后面的類型修飾符,它是函數類型的一個組成部分,因此,在函數實現部分也要帶const關鍵字。下面舉一例子說明常成員函數的特征。
#include
class R
{
public:
R(int r1, int r2) { R1=r1; R2=r2; }
void print();
void print() const;
private:
int R1, R2;
};
void R::print()
{
cout< }
void R::print() const
{
cout< }
void main()
{
R a(5, 4);
a.print();
const R b(20, 52);
b.print();
}
該例子的輸出結果為:
5,4
20;52
該程序的類聲明了兩個成員函數,其類型是不同的(其實就是重載成員函數)。有帶const修飾符的成員函數處理const常量,這也體現出函數重載的特點。
常數據成員
類型修飾符const不僅可以說明成員函數,也可以說明數據成員。
由于const類型對象必須被初始化,并且不能更新,因此,在類中說明了const數據成員時,只能通過成員初始化列表的方式來生成構造函數對數據成員初始化。
下面通過一個例子講述使用成員初始化列表來生成構造函數。
#include
class A
{
public:
A(int i);
void print();
const int &r;
private:
const int a;
static const int b;
};
const int A::b=10;
A::A(int i):a(i), r(a)
{
}
void A::print()
{
cout< }
void main()
{
A a1(100), a2(0);
a1.print();
a2.print();
}
該程序的運行結果為:
100:10:100
0:10:0
在該程序中,說明了如下三個常類型數據成員:
const int & r;
const int a;
static const int b;
其中,r是常int型引用,a是常int型變量,b是靜態常int型變量。
程序中對靜態數據成員b進行初始化。
值得注意的是構造函數的格式如下所示:
A(int i):a(i),r(a)
{
}
其中,冒號后邊是一個數據成員初始化列表,它包含兩個初始化項,用逗號進行了分隔,因為數據成員a和r都是常類型的,需要采用初始化格式。
淺談C++函數的參數
函數參數的求值順序
當一個函數帶有多個參數時,C++語言沒有規定在函數調用時實參的求值順序。而編譯器根據對代碼進行優化的需要自行規定對實參的求值順序。有的編譯器規定自左至右,有的編譯器規定自右至左,這種對求值順序的不同規定,對一般參數來講沒有影響。但是,如果實參表達式中帶有副作用的運算符時,就有可能產生由于求值順序不同而造成了二義性。例如:int z = add_int(++x, x+y);,這樣,在不同的編譯器就有可能生產不同的結果。
設置參數的默認值
在C++語言中,允許在函數的說明或定義時給一個或多個參數指定默認值。但是,要求在一個指定了默認值的參數的右邊,不能出現沒有指定默認值的參數。例如:
int add_int(int x, int 10);
在上述對函數add_int()的說明中,對該函數的最右邊的一個參數指定了默認值。
在函數調用時,編譯器按從左至右的順序將實參與形參結合,當實參的數目不足時,編譯器將按同樣的順序用說明中或定義中的默認值來補足所缺少的實參。例如,如有下列的函數調用表達式:
add_int(15)
它將與下列調用表達式:
add_int(15, 10)
是等價的。
在給某個參數指定默認值是,不僅可以是一個數值,而且還可以是任意復雜的表達式。
使用數組作函數參數
數組作函數參數可以分為如下三種情況:(這三種情況的結果相同,只是所采用的調用機制不同)
1. 形參和實參都用數組
調用函數的實參用數組名,被調用函數的形參用數組,這種調用的機制是形參和實參共用內存中的同一個數組。因此,在被調用函數中改變了數組中某個無素的值,對調用函數該數組的該元素值也被改變,因為它們是共用同一個數組。
2. 形參和實參都用對應數組的指針
在C++中,數組名被規定為是一個指針,該指針便是指向該數組的首元素的指針,國為它的值是該數組首元素的地址值,因此,數組名是一個常量指針。
實際中,形參和實參一個用指針,另一個用數組也是可以的。在使用指針時可以用數組名,也可以用另外定義的指向數組的指針。
3. 實參用數組名形參用引用
如何對數組類型使用引用方式,這里先做如下說明:先用類型定義語句定義一個int型的數組類型,如下所示:
typedef int array[8];
然后,使用array來定義數組和引用。
示例:
#include
typedef int array[8];
int a[8] = {1, 3, 5, 7, 9, 11, 13};
void fun(array &b, int n)
{
for(int i=0; i b[7]+=b[i];
}
void main()
{
int m=8;
fun(a, m);
cout< }
該程序中,在fun()函數中,使用了引用作形參,調用時所對應的實參應該是一個數組名,這里的引用是給數組起個別名。在fun()函數中對數組b的操作,就相當于b所引用數組a的操作。在C++中,常用這種調用方式。
C++語法之函數重載
所謂函數重載是指同一個函數名可以對應著多個函數的實現。例如,可以給函數名add()定義多個函數實現,該函數的功能是求和,即求兩個操作數的和。其中,一個函數實現是求兩個int型數之和,另一個實現是求兩個浮點型數之和,再一個實現是求兩個復數的和。每種實現對應著一個函數體,這些函數的名字相同,但是函數的參數的類型不同。這就是函數重載的概念。函數重載在類和對象的應用尤其重要。
函數重載要求編譯器能夠唯一地確定調用一個函數時應執行哪個函數代碼,即采用哪個函數實現。確定函數實現時,要求從函數參數的個數和類型上來區分。這就是說,進行函數重載時,要求同名函數在參數個數上不同,或者參數類型上不同。否則,將無法實現重載。
參數類型上不同的重載函數
下面舉一個在參數類型不同的重載函數的例子:
#include
int add(int, int);
double add(double, double);
void main()
{
cout< cout< }
int add(int x, int y)
{
return x+y;
}
double add(double a, double b)
{
return a+b;
}
該程序中,main()函數中調用相同名字add的兩個函數,前邊一個add()函數對應的是兩個int型數求和的函數實現,而后邊一個add()函數對應的是兩個double型數求和的函數實現。這便是函數的重載。
以上程序輸出結果為:
15
15.5
參數個數上不同的重載函數
下面舉一個在參數個數上不相同的重載函數的例子:
#include
int min(int a, int b);
int min(int a, int b, int c);
int min(int a, int b, int c, int d);
void main()
{
cout< cout< }
int min(int a, int b)
{
return a }
int min(int a, int b, int c)
{
int t = min(a, b);
return min(t,c);
}
int min(int a, int b, int c, int d)
{
int t1 = min(a, b);
int t2 = min(c, d);
return min(t1, t2);
}
該程序中出現了函數重載,函數名min對應有三個不同的實現,函數的區分依據參數個數不同,這里的三個函數實現中,參數個數分別為2,3和4,在調用函數時根據實參的個數來選取不同的函數實現。
函數重載在類和對象應用比較多,尤其是在類的多態性中。在以后我們將碰到更多的在類型不同的函數重載,尤其是在結合類的繼承性和指針類型的不同,而這些都是我們以后用VC編程中經常要用到的。
C++子對象和堆對象
子對象
當一個類的成員是某一個類的對象時,該對象就為子對象。子對象實際就是對象成員。如:
class A
{
public:
…
private:
…
};
class B
{
public:
…
private:
A a;
…
};
其中,B類中成員a就是子對象,它是A類的對象作為B類的成員。
在類中出現了子對象或稱對象成員時,該類的構造函數要包含對子對象的初始化,通常采用成員初始化表的方法來初始化子對象。在成員初始化表中包含對子對象的初始化和對類中其他成員的初始化。下面舉一例子說明成員初始化的構造。
#include
class A
{
public:
A(int i, int j) { A1=i; A2=j; }
void print() { cout< private:
int A1, A2;
};
class B
{
public:
B(int i, int j, int k):a(i, j), b(k)
{
}
void print();
private:
A a; file://子對象
int b;
};
void B::print()
{
a.print();
cout< }
void main()
{
B b(6, 7, 8);
b.print();
}
該程序的輸出結果為:
6,7
8
其中,a(i, j), b(k)是成員初始化表,它有二項,前一項是給子對象a初始化,其格式如下:
<子對象名> (<參數表>)
后一項是給類B的數據成員b初始化。這一項也可以寫在構造函數的函數體內,使用賦值表達式語句
b = k;
給類B的數據成員初始化。
堆對象
所謂堆對象是指在程序運行過程中根據需要隨時可以建立或刪除的對象。這種堆對象被創建在內存一些空閑的存儲單元中,這些存儲單元被稱為堆。它們可以被創建的堆對象占有,也可以通過刪除堆對象而獲得釋放。
創建或刪除堆對象時,需要如下兩個運算符:
new
delete
這兩個運算符又稱為動態分配內存空間運算符。new相當于C語言中malloc()函數,而delete相當于C語言中free()函數。
1. 運算符new的用法
該運算符的功能是用來創建堆對象,或者說,它是用來動態地創建對象。
new運算符使用格式如下:
new <類型說明符> (<初始值列表>)
它表明在堆中建立一個由<類型說明符>給定的類型的對象,并且由括號中的<初始值列表>給出被創建對象的初始值。如果省去括號和括號中的初始值,則被創建的對象選用缺省值。
使用new運算符創建對象時,它可以根據其參數來選擇適當的構造函數,它不用sizeof來計算對象所占的字節數,而可以計算其大小。
new運算符返回一個指針,指針類型將與new所分配對象相匹配,如果不匹配可以通過強制類型的方法,否則將出現編譯錯。
如果new運算符不能分配到所需要的內存,它將返回0,這時的指針為空指針。
運算符new也可以用來創建數組類型的對象,即對象數組。其格式如下:
new <類名> [<算術表達式>]
其中,<算術表達式>的值為所創建的對象數組的大小。如:
A *ptr;
ptr = new A[5];
new還可用來創建一般類型的數組。如:
int *p;
p = new int[10];
使用new[]創建的對象數組或一般數組時,不能為該數組指定初始值,其初始值為缺省值。
2. 運算符delete的用法
該運算符的功能是用來刪除使用new創建的對象或一般類型的指針。其格式如下:
delete <指針名>
例如:
A *ptr;
ptr = new A(5, 6);
delete ptr;
運算符delete也可用來刪除使用new創建對象數組,其使用格式如下:
delete[] <指針名>
同樣,delete也可以刪除由new創建的一般類型的數組。如:
int *p;
p = new int[10];
delete[] p;
使用運算符delete時,應注意如下幾點:
(1) 它必須使用于由運算符new返回的指針;
(2) 該運算符也適用于空指針(即其值為0的指針);
(3) 指針名前只用一對方括號符,并且不管所刪除數組的維數,忽略方括號內的任何數字。
下面舉一例子說明new運算符和delete運算符的使用方法。
#include
class AA
{
public:
AA(int i, int j)
{
A=i; B=j;
cout<<"構造函數.\n";
}
~AA() { cout<<"析構函數.\n"; }
void print();
private:
int A, B;
};
void AA::print()
{
cout< }
void main()
{
AA *a1, *a2;
a1 = new AA(1, 2);
a2 = new AA(5, 6);
a1->print();
a2->print();
delete a1;
delete a2;
}
該程序的輸出結果為:
構造函數.
構造函數.
1, 2
5, 6
構造函數.
構造函數.
從程序中可以看到:用new創建對象時,要調用構造函數,用delete刪除對象時,要調用析構函數。如果創建或刪除的時對象數組,對象數組有多少,就調用多少次構造函數或構造函數。
在實際應用中,經常對于new運算符返回的指針進行檢驗,看是否分配了有效的內存空間。結合本例給出檢驗方法如下:
if (!a1)
{
cout<<"Heap erroe!\n";
exit(1);
}
下面再舉一個使用new和delete運算符對一般指針和數組的例子。
#include
#include
void fun()
{
int *p;
if (p = new int)
{
*p = 5;
cout<<*p< delete p;
}
else
cout<<"Heap error!\n";
}
void main()
{
fun();
int *pa;
pa = new int[5];
if (!pa)
{
cout<<"Heap error!\n";
exit(1);
}
for (int i=0; i<5; i++)
pa[i] = i+1;
for (i=0; i<5; i++)
cout<
(<派生類構造函數總參數表>):<基類構造函數>(參數表1),<子對象名>(<參數表2>)
{
<派生類中數據成員初始化>
};
派生類構造函數的調用順序如下:
· 基類的構造函數
· 子對象類的構造函數(如果有的話)
· 派生類構造函數
在前面的例子中,B::B(int i, int j, int k):A(i), bb(j), bbb(k)就是派生類構造函數的定義,下面再舉一個構造派生類構造函數的例子。
#include
class A
{
public:
A() { a=0; cout<<"類A的缺省構造函數.\n"; }
A(int i) { a=i; cout<<"類A的構造函數.\n"; }
~A() { cout<<"類A的析構函數.\n"; }
void Print() const { cout< int Geta() { reutrn a; }
private:
int a;
}
class B : public A
{
public:
B() { b=0; cout<<"類B的缺省構造函數.\n"; }
B(int i, int j, int k);
~B() { cout<<"類B的析構函數.\n"; }
void Print();
private:
int b;
A aa;
}
B::B(int i, int j, int k):A(i), aa(j)
{
b=k;
cout<<"類B的構造函數.\n";
}
void B::Print()
{
A::Print();
cout< }
void main()
{
B bb[2];
bb[0] = B(1, 2, 5);
bb[1] = B(3, 4, 7);
for(int i=0; i<2; i++)
bb[i].Print();
}
2. 構造函數
當對象被刪除時,派生類的析構函數被執行。由于析構函數也不能被繼承,因此在執行派生類的析構函數時,基類的析構函數也將被調用。執行順序是先執行派生類的構造函數,再執行基類的析構函數,其順序與執行構造函數時的順序正好相反。這一點從前面講過的例子可以看出,請讀者自行分析。
3. 派生類構造函數使用中應注意的問題
(1) 派生類構造函數的定義中可以省略對基類構造函數的調用,其條件是在基類中必須有缺省的構造函數或者根本沒有定義構造函數。當然,基類中沒有定義構造函數,派生類根本不必負責調用基類的析構函數。
(2) 當基類的構造函數使用一個或多個參數時,則派生類必須定義構造函數,提供將參數傳遞給基類構造函數途徑。在有的情況下,派生類構造函數的函數體可能為空,僅起到參數傳遞作用。如本講第一個例子就屬此種情況。
子類型化和類型適應
1. 子類型化
子類型的概念涉及到行為共享,它與繼承有著密切關系。
有一個特定的類型S,當且僅當它至少提供了類型T的行為,由稱類型S是類型T的子類型。子類型是類型之間的一般和特殊的關系。
在繼承中,公有繼承可以實現子類型。例如:
class A
{
public:
void Print() const { cout<<"A::print() called.\n"; }
};
class B : public A
{
public:
void f() {}
};
類B繼承了類A,并且是公有繼承方式。因此,可以說類B是類A的一個子類型。類A還可以有其他的子類型。類B是類A的子類型,類B具備類A中的操作,或者說類A中的操作可被用于操作類B的對象。
子類型關系是不可逆的。這就是說,已知B是A的子類型,而認為A也是B的子類型是錯誤的,或者說,子類型關系是不對稱不。
因此,可以說公有繼承可以實現子類型化。
2. 類型適應
類型適應是指兩種類型之間的關系。例如,B類型適應A類型是指B類型的對象能夠用于A類型的對象所能使用的場合。
前面講過的派生類的對象可以用于基類對象所能使用的場合,我們說派生類適應于基類。
同樣道理,派生類對象的指針和引用也適應于基類對象的指針和引用。
子類型化與類型適應是致的。A類型是B類型的子類型,那么A類型必將適應于B類型。
子類型的重要性就在于減輕程序人員編寫程序代碼的負擔。因為一個函數可以用于某類型的對象,則它也可以用于該類型的各個子類型的對象,這樣就不必為處理這些子類型的對象去重載該函數。
C++多繼承
多繼承可以看作是單繼承的擴展。所謂多繼承是指派生類具有多個基類,派生類與每個基類之間的關系仍可看作是一個單繼承。
多繼承下派生類的定義格式如下:
class <派生類名>:<繼承方式1><基類名1>,<繼承方式2><基類名2>,…
{
<派生類類體>
};
其中,<繼承方式1>,<繼承方式2>,…是三種繼承方式:public、private、protected之一。例如:
class A
{
…
};
class B
{
…
};
class C : public A, public, B
{
…
};
其中,派生類C具有兩個基類(類A和類B),因此,類C是多繼承的。按照繼承的規定,派生類C的成員包含了基類B中成員以及該類本身的成員。
多繼承的構造函數
在多繼承的情況下,派生類的構造函數格式如下:
<派生類名>(<總參數表>):<基類名1>(<參數表1>),<基類名2>(<參數表2>),…
<子對象名>(<參數表n+1>),…
{
<派生類構造函數體>
}
其中,<總參數表>中各個參數包含了其后的各個分參數表。
多繼承下派生類的構造函數與單繼承下派生類構造函數相似,它必須同時負責該派生類所有基類構造函數的調用。同時,派生類的參數個數必須包含完成所有基類初始化所需的參數個數。
派生類構造函數執行順序是先執行所胡基類的構造函數,再執行派生類本身構造函數,處于同一層次的各基類構造函數的執行順序取決于定義派生類時所指定的各基類順序,與派生類構造函數中所定義的成員初始化列表的各項順序無關。也就是說,執行基類構造函數的順序取決于定義派生類時基類的順序。可見,派生類構造函數的成員初始化列表中各項順序可以任意地排列。
下面通過一個例子來說明派生類構造函數的構成及其執行順序。
#include
class B1
{
public:
B1(int i)
{
b1 = i;
cout<<"構造函數 B1."< }
void print() { cout< private:
int b1;
};
class B2
{
public:
B2(int i)
{
b2 = i;
cout<<"構造函數 B2."< }
void print() { cout< private:
int b2;
};
class B3
{
public:
B3(int i)
{
b3 = i;
cout<<"構造函數 B3."< }
int getb3() { return b3; }
private:
int b3;
};
class A : public B2, public B1
{
public:
A(int i, int j, int k, int l):B1(i), B2(j), bb(k)
{
a = l;
cout<<"構造函數 A."< }
void print()
{
B1::print();
B2::print();
cout< }
private:
int a;
B3 bb;
};
void main()
{
A aa(1, 2, 3, 4);
aa.print();
}
該程序的輸出結果為:
構造函數 B2.2
構造函數 B1.1
構造函數 B3.3
構造函數 A.4
1
2
4, 3
在該程序中,作用域運算符::用于解決作用域沖突的問題。在派生類A中的print()函數的定義中,使用了B1::print;和B2::print();語句分別指明調用哪一個類中的print()函數,這種用法應該學會。
C++多繼承
8/27/2001 8:37:55· ·--··pcvc
上一頁 1 2
二義性問題
一般說來,在派生類中對基類成員的訪問應該是唯一的,但是,由于多繼承情況下,可能造成對基類中某成員的訪問出現了不唯一的情況,則稱為對基類成員訪問的二義性問題。
實際上,在上例已經出現過這一問題,回憶一下上例中,派生類A的兩基類B1和B2中都有一個成員函數print()。如果在派生類中訪問print()函數,到底是哪一個基類的呢?于是出現了二義性。但是在上例中解決了這個問題,其辦法是通過作用域運算符::進行了限定。如果不加以限定,則會出現二義性問題。
下面再舉一個簡單的例子,對二義性問題進行深入討論。例如:
class A
{
public:
void f();
};
class B
{
public:
void f();
void g();
};
class C : public A, public B
{
public:
void g();
void h();
};
如果定義一個類C的對象c1:
C c1;
則對函數f()的訪問
c1.f();
便具有二義性:是訪問類A中的f(),還是訪問類B中的f()呢?
解決的方法可用前面用過的成員名限定法來消除二義性,例如:
c1.A::f();
或者
c1.B::f();
但是,最好的解決辦法是在類C中定義一個同名成員f(),類C中的f()再根據需要來決定調用A::f(),還是B::f(),還是兩者皆有,這樣,c1.f()將調用C::f()。
同樣地,類C中成員函數調用f()也會出現二義性問題。例如:
viod C::h()
{
f();
}
這里有二義性問題,該函數應修改為:
void C::h()
{
A::f();
}
或者
void C::h()
{
B::f();
}
或者
void C::f()
{
A::f();
B::f();
}
另外,在前例中,類B中有一個成員函數g(),類C中也有一個成員函數g()。這時,
c1.g();
不存在二義性,它是指C::g(),而不是指B::g()。因為這兩個g()函數,一個出現在基類B,一個出現在派生類C,規定派生類的成員將支配基類中的同名成員。因此,上例中類C中的g()支配類B中的g(),不存在二義性,可選擇支配者的那個名字。
當一個派生類從多個基類派生類,而這些基類又有一個共同的基類,則對該基類中說明的成員進行訪問時,也可能會出現二義性。例如:
class A
{
public:
int a;
};
class B1 : public A
{
private:
int b1;
};
class B2 : public A
{
private:
int b2;
};
class C : public B1, public B2
{
public:
int f();
private:
int c;
};
已知:C c1;
下面的兩個訪問都有二義性:
c1.a;
c1.A::a;
而下面的兩個訪問是正確的:
c1.B1::a;
c1.B2::a;
類C的成員函數f()用如下定義可以消除二義性:
int C::f()
{
retrun B1::a + B2::a;
}
由于二義性的原因,一個類不可以從同一個類中直接繼承一次以上,例如:
class A : public B, public B
{
…
}
這是錯誤的。
C++ 對象與數組
對象數組是指數組元素為對象的數組。該數組中若干個元素必須是同一個類的若干個對象。對象數組的定義、賦值和引用與普通數組一樣,只是數組的元素與普通數組不同,它是同類的若干個對象。
1. 對象數組的定義
對象數組定義格式如下:
<類名><數組名>[<大小>]...
其中,<類名>指出該數組元素是屬于該類的對象,方括號內的<大小>給出某一維的元素個數。一維對象數組只有一個方括號,二維對象數組要有兩個方括號,等等,例如:
DATE dates[7];
表明dates是一維對象數組名,該數組有7個元素,每個元素都是類DATE的對象。
2. 對象數組的賦值
對象數組可以被賦初值,也可以被賦值。例如:
class DATE
{
public:
DATE(int m, int d, int y);
void printf();
private:
int month, day, year;
};
下面是定義對象數組并賦初值和賦值:
DATE dates[4]={ DATE(7, 7, 2001), DATE(7, 8, 2001), DATE(7, 9, 2001), DATE(7, 10, 2001) }
或者
dates[0] = DATE(7, 7, 2001);
dates[1] = DATE(7, 8, 2001);
dates[2] = DATE(7, 9, 2001);
dates[3] = DATE(7, 10, 2001);
指向數組的指針和指針數組
指向數組的指針和指針數組是兩個完全不同的概念,現放在一起介紹是中為兩者在定義格式相似,千萬不要把它們搞混了。
1. 指向數組的指針
指向一般數組的指針定義格式如下:
<類型說明符>(*<指針名>)[<大小>]...
其中,用來說明指針的 * 要與<指針名>括在一起。后面用一個方括號表示該指針指向一維數組,后面用二個方括號表示該指針指向二維數組。<類型說明符>用來說明指針所指向的數組的元素的類型。例如:
int (*P)[3];
P是一個指向一維數組的指針,該數組有3個int型元素。
而指向對象數組的指針,則把<類型說明符>改為<類名>即可:
<類名>(*<指針名>)[<大小>]...
指向數組的指針的主要應用思想是:將數組的首地址(二維數組的某個行地址)賦給指針,然后通過循環(for)改變指針指向的地址,從而動態的訪問數組中各個元素。
2. 指針數組
所謂指針數組指的是數組元素為指針的那類數組。一個數組的元素可以是指向同一類型的一般指針,也可以是指向同一類類型的對象。
一般指針數組的定義格式如下:
<類型名>*<數組名>[<大小>]...
其中,*加在<數組名>前面表示該數組為指針數組。[<大小>]表示某一維的大小,即該維的元素個數,…表示可以是多維指針數組,每一個[<大小>]表示一維。例如:
int * pa[3];
char * pc[2][5];
在C++編程中,經常使用char型的指針數組用來存放若干個字符串。下面是一個一維指針數組的例子。
#include
#include
const int N = 5;
void main()
{
char *strings[N]; file://定義一個一維指針數組strings
char str[80];
cout<<"At each prompt, enter a string:\n";
for (int i=0; i {
cout<<"Enter a string #"< cin.getline(str, sizeof(str));
strings[i] = new char[strlen(str) + 1];
strcpy(strings[i], str);
}
cout< for (i=0; i cout<<"String #"< }
對象指針數組的定義如下:
對象指針數組是指該數組的元素是指向對象的指針,它要求所有數組元素都是指向同一個類類型的對象的指針。格式如下:
<類名>*<數組名>[<大小>]...
它與前面講過的一般的指針數組所不同的地方僅在于該數組一定是指向對象的指針。即指向對象的指針用來作該數組的元素。下面通過一個例子看一下對象指針數組的用法。
#include
class A
{
public:
A(int i=0, int j=0) { a=i; b=j; }
void print();
private:
int a, b;
};
void A::print()
{
cout< }
void main()
{
A a1(7, 8), a2, a3(5, 7);
A *b[3] = { &a3, &a2, &a1 };
for (int i=0; i<3; i++)
b[i]->print();
}
帶參數的main()參數
前面講過的main()函數都是不帶參數的。在實際編程中,有時需要main()帶參數。通過main()函數的參數給程序增加一些處理信息。一般地說,當使用C++編寫的源程序經過編譯連接生成的可執行文件在執行時,需要還命令行參數,由該源程序的主函數main()就需要帶參數。使用所還有的參數來存放命令行中的參數,以便在程序中對命令行參數進行處理。
帶有參數的main()函數頭格式如下:
void main(int argc, char * argv[])
其中,第一個參數argc是int型的,它用來存放命令行參數的個數,實際上argc所存放的數值比命令行參數的個數多1,即將命令字(可執行文件名)也計算在內。第二個參數argv是一個一維的一級指針數組,它是用來存放命令行中各個參數和命令字的字符串的,并且規定:
argv[0]存放命令字
argv[1]存放命令行中第一個參數
argv[2]存放命令行中第二個參數
…
這里,argc的值和argv[]各元素的值都是系統自動組賦值的。
在這里講述帶參數的main()函數實際上是對指針數組應用的一個具體實例。
#include
void main(int argc, char *argv[])
{
cout<<"The number of command line arguments is:"< cout<<"The program name is:"< if (argc>1)
{
cout<<"The command line arguments:\n";
for (int i=1; i cout< }
}
上述編譯連接后的EXE文件,可在DOS命令行下調試。
關于命令行參數的使用,其基本方法是直接引用指針數組argv[]中某個元素所存放的字符串,可用下標方式,也可用指針方式。
C++ 類的靜態成員(static)
靜態成員的提出是為了解決數據共享的問題。實現共享有許多方法,如:設置全局性的變量或對象是一種方法。但是,全局變量或對象是有局限性的。這一章里,我們主要講述類的靜態成員來實現數據的共享。
靜態數據成員
在類中,靜態成員可以實現多個對象之間的數據共享,并且使用靜態數據成員還不會破壞隱藏的原則,即保證了安全性。因此,靜態成員是類的所有對象中共享的成員,而不是某個對象的成員。
使用靜態數據成員可以節省內存,因為它是所有對象所公有的,因此,對多個對象來說,靜態數據成員只存儲一處,供所有對象共用。靜態數據成員的值對每個對象都是一樣,但它的值是可以更新的。只要對靜態數據成員的值更新一次,保證所有對象存取更新后的相同的值,這樣可以提高時間效率。
靜態數據成員的使用方法和注意事項如下:
1、靜態數據成員在定義或說明時前面加關鍵字static。
2、靜態成員初始化與一般數據成員初始化不同。靜態數據成員初始化的格式如下:
<數據類型><類名>::<靜態數據成員名>=<值>
這表明:
(1) 初始化在類體外進行,而前面不加static,以免與一般靜態變量或對象相混淆。
(2) 初始化時不加該成員的訪問權限控制符private,public等。
(3) 初始化時使用作用域運算符來標明它所屬類,因此,靜態數據成員是類的成員,而不是對象的成員。
3、靜態數據成員是靜態存儲的,它是靜態生存期,必須對它進行初始化。
4、引用靜態數據成員時,采用如下格式:
<類名>::<靜態成員名>
如果靜態數據成員的訪問權限允許的話(即public的成員),可在程序中,按上述格式來引用靜態數據成員。
下面舉一例子,說明靜態數據成員的應用:
#include
class Myclass
{
public:
Myclass(int a, int b, int c);
void GetNumber();
void GetSum();
private:
int A, B, C;
static int Sum;
};
int Myclass::Sum = 0;
Myclass::Myclass(int a, int b, int c)
{
A = a;
B = b;
C = c;
Sum += A+B+C;
}
void Myclass::GetNumber()
{
cout<<"Number="< }
void Myclass::GetSum()
{
cout<<"Sum="< }
void main()
{
Myclass M(3, 7, 10),N(14, 9, 11);
M.GetNumber();
N.GetNumber();
M.GetSum();
N.GetSum();
}
從輸出結果可以看到Sum的值對M對象和對N對象都是相等的。這是因為在初始化M對象時,將M對象的三個int型數據成員的值求和后賦給了Sum,于是Sum保存了該值。在初始化N對象時,對將N對象的三個int型數據成員的值求和后又加到Sum已有的值上,于是Sum將保存另后的值。所以,不論是通過對象M還是通過對象N來引用的值都是一樣的,即為54。
靜態成員函數
靜態成員函數和靜態數據成員一樣,它們都屬于類的靜態成員,它們都不是對象成員。因此,對靜態成員的引用不需要用對象名。
在靜態成員函數的實現中不能直接引用類中說明的非靜態成員,可以引用類中說明的靜態成員。如果靜態成員函數中要引用非靜態成員時,可通過對象來引用。下面通過例子來說明這一點。
#include
class M
{
public:
M(int a) { A=a; B+=a;}
static void f1(M m);
private:
int A;
static int B;
};
void M::f1(M m)
{
cout<<"A="< cout<<"B="< }
int M::B=0;
void main()
{
M P(5),Q(10);
M::f1(P); file://調用時不用對象名
M::f1(Q);
}
讀者可以自行分析其結果。從中可看出,調用靜態成員函數使用如下格式:
<類名>::<靜態成員函數名>(<參數表>);
C++ 虛基類
在《多繼承》中講過的例子中,由類A,類B1和類B2以及類C組成了類繼承的層次結構。在該結構中,類C的對象將包含兩個類A的子對象。由于類A是派生類C兩條繼承路徑上的一個公共基類,那么這個公共基類將在派生類的對象中產生多個基類子對象。如果要想使這個公共基類在派生類中只產生一個基類子對象,則必須將這個基類設定為虛基類。
虛基類的引入和說明
前面簡單地介紹了要引進虛基類的原因。實際上,引進虛基類的真正目的是為了解決二義性問題。
虛基類說明格式如下:
virtual <繼承方式><基類名>
其中,virtual是虛類的關鍵字。虛基類的說明是用在定義派生類時,寫在派生類名的后面。例如:
class A
{
public:
void f();
protected:
int a;
};
class B : virtual public A
{
protected:
int b;
};
class C : virtual public A
{
protected:
int c:
};
class D : public B, public C
{
public:
int g();
private:
int d;
};
由于使用了虛基類,使得類A,類B,類C和類D之間關系用DAG圖示法表示如下:
A{ f(), a }
/ \
B{b} C{c}
\ /
D{g(),d}
從該圖中可見不同繼承路徑的虛基類子對象被合并成為一個對象。這便是虛基類的作用,這樣將消除了合并之前可能出現的二義性。這時,在類D的對象中只存在一個類A的對象。因此,下面的引用都是正確的:
D n;
n.f(); file://對f()引用是正確的。
void D::g()
{
f(); file://對f()引用是正確的。
}
下面程序段是正確的。
D n;
A *pa;
pa = &n;
其中,pa是指向類A對象的指針,n是類D的一個對象,&n是n對象的地址。pa=&n是讓pa指針指向類D的對象,這是正確的,并且也無二義性。
C++ 虛基類
9/3/2001 8:22:51· ·--··pcvc
上一頁 1 2
虛基類的構造函數
前面講過,為了初始化基類的子對象,派生類的構造函數要調用基類的構造函數。對于虛基類來講,由于派生類的對象中只有一個虛基類子對象。為保證虛基類子對象只被初始化一次,這個虛基類構造函數必須只被調用一次。由于繼承結構的層次可能很深,規定將在建立對象時所指定的類稱為最派生類。C++規定,虛基類子對象是由最派生類的構造函數通過調用虛基類的構造函數進行初始化的。如果一個派生類有一個直接或間接的虛基類,那么派生類的構造函數的成員初始列表中必須列出對虛基類構造函數的調用。如果未被列出,則表示使用該虛基類的缺省構造函數來初始化派生類對象中的虛基類子對象。
從虛基類直接或間接繼承的派生類中的構造函數的成員初始化列表中都要列出這個虛基類構造函數的調用。但是,只有用于建立對象的那個最派生類的構造函數調用虛基類的構造函數,而該派生類的基類中所列出的對這個虛基類的構造函數調用在執行中被忽略,這樣便保證了對虛基類的對象只初始化一次。
C++又規定,在一個成員初始化列表中出現對虛基類和非虛基類構造函數的調用,則虛基類的構造函數先于非虛基類的構造函數的執行。
下面舉一例子說明具有虛基類的派生類的構造函數的用法。
#include
class A
{
public:
A(const char *s) { cout< ~A() {}
};
class B : virtual public A
{
public:
B(const char *s1, const char *s2):A(s1)
{
cout< }
};
class C : virtual public A
{
public:
C(const char *s1, const char *s2):A(s1)
{
cout< }
};
class D : public B, public C
{
public:
D(const char *s1, const char *s2, const char *s3, const char *s4)
:B(s1, s2), C(s1, s3), A(s1)
{
cout< }
};
void main()
{
D *ptr = new D("class A", "class B", "class C", "class D");
delete ptr;
}
該程序的輸出結果為:
class A
class B
class C
class D
在派生類B和C中使用了虛基類,使得建立的D類對象只有一個虛基類子對象。
在派生類B,C,D的構造函數的成員初始化列表中都包含了對虛基類A的構造函數。
在建立類D對象時,只有類D的構造函數的成員初始化列表中列出的虛基類構造函數被調用,并且僅調用一次,而類D基類的構造函數的成員初始化列表中列出的虛基類構造函數不被執行。這一點將從該程序的輸出結果可以看出。
posted on 2006-01-17 19:41
EricWong 閱讀(227)
評論(0) 編輯 收藏 所屬分類:
C&C++