PKU C++程式設計實習 學習筆記1
第一章 從C走進C++
1.7 行內函數和過載函式
行內函數:函式呼叫是有時間開銷的。如果函式本身只有幾條語句,執行非常快,而且函式被反覆執行很多次,相比之下呼叫函式所產生的這個開銷就會顯得比較大。
為了減少函式呼叫的開銷,引入了行內函數機制。編譯器處理對行內函數的呼叫語句時,是將整個函式的程式碼插入到呼叫語句處,而不會產生呼叫函式的語句。
過載函式:一個或多個函式,名字相同,然而引數個數或引數型別不相同,這叫做函式的過載。編譯器根據呼叫語句的中的實參的個數和型別判斷應該呼叫哪個函式。
(1) int Max(double f1,double f2) { } (2) int Max(int n1,int n2) { } (3) int Max(int n1,int n2,int n3) { } Max(3.4,2.5); //呼叫 (1) Max(2,4); //呼叫 (2) Max(1,2,3); //呼叫 (3) Max(3,2.4); //error,二義性,既可以型別轉換後呼叫(1),也可以型別轉換後呼叫(2)
1.8 函式預設引數
C++中,定義函式的時候可以讓最右邊的連續若干個引數有預設值,那麼呼叫函式的時候,若相應位置不寫引數,引數就是預設值。
函式引數可預設的目的在於提高程式的可擴充性。即如果某個寫好的函式要新增新的引數,而原先那些呼叫該函式的語句,未必需要使用新增的引數,那麼為了避免對原先那些函式呼叫語句的修改,就可以使用預設引數。
第二章 類和物件初探
2.1 面向物件程式設計方法
面向物件的程式設計方法
面向物件的四個基本概念:抽象、封裝、繼承、多型
抽象:將一類客觀事物的共同屬性歸納出來,形成一個數據結構。將這類事物所能進行的一些行為和操作歸納起來形成函式,這些函式可以來操作具體的資料結構。
繼承:將資料結構和演算法對應地捆綁在一起,形成類。
2.5 內聯成員函式和過載成員函式
內聯成員函式
- inline + 成員函式
- 整個函式體出現在類定義內部
class B{
inline void func1();
void func2()
{
};
};
<span style="color:#ff0000;">void B::</span>func1() { }//注意void 和 B 的順序,即<span style="color:#ff0000;">返回值型別和類名的順序</span>
成員函式的過載及引數預設
- 過載成員函式
- 成員函式 -- 帶預設引數
- 使用預設引數要注意避免有函式過載時的二義性
2.6 建構函式
基本概念
成員函式的一種,名字與類名相同,可以有引數,不能有返回值(void也不行)作用是對物件進行初始化,如給成員變數賦初值
建構函式只是在物件已經佔用了儲存空間後,在物件的儲存空間裡做一些初始化工作。物件所佔用的儲存空間不是建構函式分配的。
如果定義類時沒寫建構函式,則編譯器生成一個預設的無引數的建構函式。預設建構函式無引數,不做任何操作
如果定義了建構函式,則編譯器不生成預設的無引數的建構函式
物件生成時建構函式自動被呼叫。物件一旦生成,就再也不能在其上執行建構函式
只要有物件生成,不管是以什麼形式生成,物件生成的時候都一定會呼叫建構函式,來對它進行初始化。
可以有多個建構函式
為什麼需要建構函式:
建構函式執行必要的初始化工作,有了建構函式,就不必專門再寫初始化函式,也不用擔心忘記呼叫初始化函式。
有時物件沒被初始化就使用,會導致程式出錯。
建構函式最好是public的,private建構函式不能直接用來初始化物件
建構函式在陣列中的使用
class Test {
public:
Test( int n) { } //(1)
Test( int n, int m) { } //(2)
Test() { } //(3)
};
Test array1[3] = { 1, Test(1,2) };
// 三個元素分別用(1),(2),(3)初始化
Test array2[3] = { Test(2,3), Test(1,2) , 1};
// 三個元素分別用(2),(2),(1)初始化
Test * pArray[3] = { new Test(4), new Test(1,2) };
//兩個元素分別用(1),(2) 初始化
對最後一個,pArray是一個3元素的指標陣列,元素是指標,並不會導致物件的生成。所以只是在呼叫new的時候呼叫了建構函式。2.7 預設建構函式(自己補充的)
基本概念
沒有形參、或所有形參都有預設實參的建構函式為預設建構函式。
預設建構函式說明了當定義物件時沒有為它提供(顯示的)初始化式時應該怎麼辦。
只要定義一個物件時,沒有提供初始化式,就使用預設建構函式。
合成的預設建構函式
即是編譯器自動生成的預設建構函式。(注意區分合成的預設建構函式和預設建構函式!!!)
一個類只要定義了一個建構函式,編譯器就不再生成預設建構函式。(依據是:如果類在某種情況下需要控制物件初始化,則該類很可能在所有情況下都需要控制。)
合成的預設建構函式使用與變數初始化相同的規則來初始化:
具有類型別的成員通過執行各自的預設建構函式來進行初始化;
內建和複合型別的成員,如指標和陣列,只對定義在全域性作用域中的物件才初始化;如果物件是定義在區域性作用域中,則內建和複合型別的成員不進行初始化。
類通常應定義一個預設建構函式
第三章 類和物件進階
3.1 複製建構函式
基本概念
只有一個引數,即對同類物件的引用。(個人理解,它也是一種建構函式,用來初始化。)
形如 X::X( X& )或X::X(const X &), 二者選一。後者能以常量物件作為引數
如果沒有定義複製建構函式,那麼編譯器生成預設複製建構函式。預設的複製建構函式完成複製功能。
如果定義的自己的複製建構函式,則預設的複製建構函式不存在。
不允許有形如 X::X( X )的建構函式。
複製建構函式起作用的三種情況
- 當用一個物件去初始化同類的另一個物件時。
Complex c2(c1); Complex c2 = c1; //初始化語句,非賦值語句 <span style="color:#3333ff;">c2是由複製建構函式來初始化的。</span>
- 如果某函式有一個引數是類 A 的物件,那麼該函式被呼叫時,類A的複製建構函式將被呼叫。
class A { public: A() { }; A( A & a) { cout << "Copy constructor called" <<endl; } }; void Func(A a1){ } int main(){ A a2; Func(a2);//<span style="color:#3366ff;">可以看到這裡a1會呼叫複製建構函式,不一定等於實參a2;而如果呼叫的是預設複製建構函式,則a1和a2相等。說明,通過自己編寫的複製建構函式可使得形參和實參不等。</span> return 0; } 程式輸出結果為: Copy constructor called
- 如果函式的返回值是類A的物件時,則函式返回時, A的複製建構函式被呼叫:
class A { public: int v; A(int n) { v = n; }; A( const A & a) { v = a.v; cout << "Copy constructor called" <<endl; } }; A Func() { A b(4); return b; } int main() { cout << Func().v << endl;//<span style="color:#3366ff;">這裡呼叫.v的物件肯定要先生成,生成的時候肯定要呼叫建構函式來初始化,呼叫的是哪個建構函式呢?呼叫的是複製建構函式,來初始化。這是個臨時物件。 那複製建構函式的形參是什麼呢?是Func函式的返回物件。</span> //<span style="color:#ff0000;">可以看到,臨時物件不一定和Func返回的物件相等。因為取決於你編寫的複製建構函式。這個和2中的形參和實參不一定相等是一樣的。</span> return 0; } 輸出結果: Copy constructor called 4
注:按規範來說,複製建構函式只能做“複製”的工作,而不能做其他如輸入輸出、修改外部變數(包括靜態變數)等事情。
某些編譯器(包括你做測試的這兩個)假設程式設計師嚴格按照上述規範,將一些不必要的複製建構函式過程略去(比如VS2013和Codeblocks2013,因為函式返回的物件給變數後就沒有用了,所以就直接給變量了,而不會嚴格按照標準“複製”一個新的物件給變數然後刪掉函式返回物件)。這樣可以極大地提高程式執行效率,尤其是當複製建構函式很複雜的時候。然而,若程式設計師不按照規範寫,就會因省略複製構造導致出現一些非預想的情況。
這也提醒大家,本課程例題、作業裡出現的在複製建構函式中輸出、修改外部變數等只是為了讓大家充分理解複製建構函式的特性。在實際應用時,請勿使用類似操作,以防出現不必要的問題。
3.2 型別轉換建構函式
- 目的
實現型別的自動轉換 - 特點
只有一個引數
不是複製建構函式 - 編譯系統會自動呼叫—>轉換建構函式
class Complex {
public:
double real, imag;
Complex( int i ) { //型別轉換建構函式
cout << “IntConstructor called” << endl;
real = i; imag = 0;
}
Complex( double r, double i )
{ real = r; imag = i; }
};
int main () {
Complex c1(7, 8);
Complex c2 = 12;//這裡沒有生成一個臨時物件
c1 = 9; // 9被自動轉換成一個臨時Complex物件
cout << c1.real << "," << c1.imag << endl;
return 0;
}
因為型別轉換建構函式也是一種建構函式。比如上面例子中的 Complex c2 = 12; 其實是對c2物件的初始化,此時是把這個型別轉換建構函式當作普通的建構函式來使用,因此不會生成臨時物件。
3.3 解構函式
基本概念
成員函式的一種:名字與型別相同,在前面加 ‘~’,沒有引數和返回值
一個類最多只有一個解構函式
物件消亡時—>自動被呼叫。在物件消亡前做善後工作,釋放分配的空間等
定義類時沒寫解構函式, 則編譯器生成預設解構函式。不涉及釋放使用者申請的記憶體釋放等清理工作
定義了解構函式, 則編譯器不生成預設解構函式
解構函式和陣列
物件陣列生命期結束時—>物件陣列的每個元素的解構函式都會被呼叫
解構函式和運算子 delete
delete 運算導致解構函式呼叫
Ctest * pTest;
pTest = new Ctest; //建構函式呼叫
delete pTest; //解構函式呼叫
------------------------------------------------------------------
pTest = new Ctest[3]; //建構函式呼叫3次
delete [] pTest; //解構函式呼叫3次
建構函式和解構函式呼叫時機的例題
class Demo {
int id;
public:
Demo( int i )
{
id = i;
cout << “id=” << id << “ Constructed” << endl;
}
~Demo()
{
cout << “id=” << id << “ Destructed” << endl;
}
};
Demo d1(1);
void Func(){
static Demo d2(2);
Demo d3(3);
cout << “Func” << endl;
}
int main (){
Demo d4(4);
d4 = 6;
cout << “main” << endl;
{ Demo d5(5);} //作用域
Func();
cout << “main ends” << endl;
return 0;
}
輸出:
id=1 Constructed
id=4 Constructed
id=6 Constructed
id=6 Destructed
main
id=5 Constructed
id=5 Destructed
id=2 Constructed
id=3 Constructed
Func
id=3 Destructed
main ends
id=6 Destructed
id=2 Destructed
id=1 Destructed
每個物件都是在其作用域結束時被釋放的。
如果兩個或多個物件在同一處結束作用域,則“先宣告的後釋放”。
區域性變數比靜態變數先釋放的原因是區域性變數的作用域先結束。
3.4 靜態成員變數和靜態成員函式
基本概念
靜態成員:在說明前面加了static關鍵字的成員。
普通成員變數每個物件有各自的一份,而靜態成員變數一共就一份,為所有物件共享。
普通成員函式必須具體作用於某個物件,而靜態成員函式並不具體作用與某個物件。
因此靜態成員不需要通過物件就能訪問。
sizeof 運算子不會計算靜態成員變數。
class CMyclass { int n;static int s; }; 則 sizeof( CMyclass ) 等於 4
靜態成員變數本質上是全域性變數,哪怕一個物件都不存在,類的靜態成員變數也存在。
靜態成員函式本質上是全域性函式。
設定靜態成員這種機制的目的是將和某些類緊密相關的全域性變數和函式寫到類裡面,看上去像一個整體,易於維護和理解。
如何訪問靜態成員
1) 類名::成員名CRectangle::PrintTotal(); 2) 物件名.成員名
CRectangle r; r.PrintTotal();//這只是一種形式,並不意味著作用於物件r 3) 指標->成員名
CRectangle * p = &r; p->PrintTotal(); 4) 引用.成員名
CRectangle & ref = r; int n = ref.nTotalNumber;
靜態成員示例
考慮一個需要隨時知道矩形總數和總面積的圖形處理程式。可以用全域性變數來記錄總數和總面積。用靜態成員將這兩個變數封裝進類中,就更容易理解和維護。
class CRectangle {
private:
int w, h;
static int nTotalArea;
static int nTotalNumber;
public:
CRectangle(int w_,int h_);
~CRectangle();
static void PrintTotal();
};
CRectangle::CRectangle(int w_,int h_) {
w = w_;
h = h_;
nTotalNumber ++;
nTotalArea += w * h; }
CRectangle::~CRectangle() {
nTotalNumber --
;
nTotalArea
-= w * h;
}
void CRectangle::PrintTotal() {
cout << nTotalNumber << "," << nTotalArea << endl; }
int CRectangle::nTotalNumber = 0;
int CRectangle::nTotalArea = 0;
// 必須在定義類的檔案中對靜態成員變數進行一次說明
//或初始化。否則編譯能通過,連結不能通過。
int main()
{
CRectangle r1(3,3), r2(2,2);
//cout << CRectangle::nTotalNumber; // Wrong , 私有
CRectangle::PrintTotal();
r1.PrintTotal();
return 0;
}
輸出結果:
2,13
2,13
要注意一點,就是在C++裡面啊,靜態成員變數你必須拿到外面,就是所有的函式外面來單獨的給它宣告一下。在宣告的同時,你可以對它進行初始化,也可以不初始化。
void CRectangle::PrintTotal()
{
cout << w << "," << nTotalNumber << "," << nTotalArea << endl; //wrong
}
CRetangle::PrintTotal(); //解釋不通,w 到底是屬於那個物件的?
附:此CRectangle類寫法有缺陷。即在呼叫預設的複製建構函式時沒有對總個數增加,而呼叫解構函式時總個數有減少。解決方法就是,自己定義一個複製建構函式。3.5 成員物件和封閉類
成員物件: 一個類的成員變數是另一個類的物件
包含 成員物件 的類叫 封閉類 (Enclosing)
class CTyre { //輪胎類
private:
int radius; //半徑
int width; //寬度
public:
CTyre(int r, int w):radius(r), width(w) { }
};
class CEngine { //引擎類
};
class CCar { //汽車類,即是“封閉類”
private:
int price; //價格
CTyre tyre;
CEngine engine;
public:
CCar(int p, int tr, int tw);
};
CCar::CCar(int p, int tr, int w):price(p), tyre(tr, w){};
int main(){
CCar car(20000,17,225);
return 0;
}
如果 CCar 類不定義建構函式, 則CCar car; // error 編譯出錯編譯器不知道 car.tyre 該如何初始化
car.engine 的初始化沒有問題: 用預設建構函式。因為它沒有成員變數。
生成封閉類物件的語句—>明確 “物件中的成員物件”
封閉類建構函式的初始化列表
定義封閉類的建構函式時, 新增初始化列表:類名::建構函式(引數表):成員變數1(引數表), 成員變數2(引數表), …
{
…
}
成員物件初始化列表中的引數
任意複雜的表示式函式 / 變數/ 表示式中的函式, 變數有定義
呼叫順序
當封閉類物件生成時,
- S1: 執行所有成員物件 的建構函式
- S2: 執行 封閉類 的建構函式
成員物件的建構函式呼叫順序
- 和成員物件在類中的說明順序一致
- 與在成員初始化列表中出現的順序無關
當封閉類的物件消亡時,
- S1: 先執行 封閉類 的解構函式
- S2: 執行 成員物件 的解構函式
解構函式順序和建構函式的呼叫順序相反
封閉類例子程式
class CTyre {
public:
CTyre() { cout << "CTyre contructor" << endl; }
~CTyre() { cout << "CTyre destructor" << endl; }
};
class CEngine {
public:
CEngine() { cout << "CEngine contructor" << endl; }
~CEngine() { cout << "CEngine destructor" << endl; }
};
class CCar
{
private:
CEngine engine;
CTyre tyre;
public:
CCar( ) { cout << “CCar contructor” << endl; }
~CCar() { cout << "CCar destructor" << endl; }
};
int main()
{
CCar car;
return 0;
}
程式的輸出結果是:
CEngine contructor
CTyre contructor
CCar contructor
CCar destructor
CTyre destructor
CEngine destructor
3.6 友元(個人覺得,友元的設定就是讓它能夠訪問私有成員)
- 友元函式
class CCar; //提前宣告 CCar類, 以便後面CDriver類使用
class CDriver {
public:
void ModifyCar( CCar * pCar) ; //改裝汽車
};
class CCar {
private:
int price;
friend int MostExpensiveCar( CCar cars[], int total); //宣告友元
friend void CDriver::ModifyCar(CCar * pCar); //宣告友元
};
void CDriver::ModifyCar( CCar * pCar)
{
pCar->price += 1000; //汽車改裝後價值增加
}
int MostExpensiveCar( CCar cars[], int total) //求最貴汽車的價格
{
int tmpMax = -1;
for( int i = 0; i < total; ++i )
if( cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
int main()
{
return 0;
}
將一個類的成員函式(包括構造, 解構函式) 設定為另一個類的友元class B {
public:
void function();
};
class A {
friend void B::function();
};
- 友元類
class CCar {
private:
int price;
friend class CDriver; //宣告CDriver為友元類
};
class CDriver {
public:
CCar myCar;
void ModifyCar() { //改裝汽車
myCar.price += 1000; // CDriver是CCar的友元類可以訪問其私有成員
}
};
int main() { return 0; }
Note:友元類之間的關係不能傳遞, 不能繼承
3.7 this 指標
C++程式到C程式的翻譯
class CCar
{
public:
int price;
void SetPrice(int p);
};
void CCar::SetPrice(int p)
{
price = p;
}
int main() {
CCar car;
car.SetPrice(20000);
return 0;
}
翻譯到 C 語言:
struct CCar {
int price;
};
void SetPrice(struct CCar * this,int p)
{
this->price = p;
}
int main() {
struct CCar car;
SetPrice( & car,20000);
return 0;
}
成員函式的翻譯:成員函式會被翻譯成一個全域性的函式。然後這個全域性函式引數個數要比成員函式多一個。多出來的這個引數,就是所謂的一個this指標。那這個this指標指向誰呢?就指向在C++程式裡面那個成員函式所作用的這個對像。 實際上你完全可以這樣理解。就是C++的編譯,你就完全可以把它理解成先翻譯成C,然後再拿C的編譯去編譯。
this指標作用
其作用就是指向成員函式所作用的物件非靜態成員函式中可以直接使用this來代表指向該函式作用的物件的指標。
class A
{
int i;
public:
void Hello() { cout << "hello" << endl; }
}; —> void Hello(A * this ) { cout << "hello" << endl; }
int main()
{
A * p = NULL;
p->Hello();//結果會怎樣? —> Hello(p);
} // 輸出:hello
class A
{
int i;
public:
void Hello() { cout <<i << "hello" << endl; }
}; 若改為: void Hello(A * this ) { cout << this->i << "hello"<< endl; }
//this若為NULL,則出錯!!
int main()
{
A * p = NULL;
p->Hello();
Hello(p);
}
this指標和靜態成員函式
靜態成員函式中不能使用 this 指標!因為靜態成員函式並不具體作用與某個物件! 因此,靜態成員函式的真實的引數的個數,就是程式中寫出的引數個數!而普通的成員函式它的真實的引數個數是比你寫出來的要多一個的,多出來的這個實際上就是this指標。3.8 常量物件、常量成員函式和常引用
常量物件
如果不希望某個物件的值被改變,則定義該物件的時候可以在前面加const關鍵字。class Demo{
private :
int value;
public:
void SetValue() { }
};
const Demo Obj; // 常量物件
常量物件不能修改它的成員變數值
常量物件不能呼叫非常量成員函式(靜態成員函式除外) 見下面例子的兩個錯誤。
常量成員函式
在類的成員函式說明後面可以加const關鍵字,則該成員函式成為常量成員函式。 常量成員函式執行期間不應修改其所作用的物件。因此,在常量成員函式中不能修改成員變數的值(靜態成員變數除外),也不能呼叫同類的非常量成員函式(靜態成員函式除外)。
class Sample
{
public:
int value;
void GetValue() const;
void func() { };
Sample() { }
};
void Sample::GetValue() const
{
value = 0; // wrong
func(); //wrong
}
int main() {
const Sample o;
o.value = 100; //err.常量物件不可被修改
o.func(); //err.常量物件上面不能執行非常量成員函式 編譯器不會去分析func函式到底做了什麼,它只看到o是常量物件,而func是非常量成員函式,func是有可能改變物件的成員變數的。
o.GetValue(); //ok,常量物件上可以執行常量成員函式
return 0;
} //在Dev C++中,要為Sample類編寫無參建構函式才可以,Visual Studio2010中不需要
常量成員函式的過載
兩個成員函式,名字和引數表都一樣,但是一個是const,一個不是,算過載。常引用
- 引用前面可以加const關鍵字,成為常引用。不能通過常引用,修改其引用的變數。
- 物件作為函式的引數時,生成該引數需要呼叫複製建構函式,效率比較低。用指標作引數,程式碼又不好看,如何解決?
class Sample {
…
};
void PrintfObj(Sample & o)
{
……
}
物件引用作為函式的引數有一定風險性,若函式中不小心修改了形參o,則實參也跟著變,這可能不是我們想要的。如何避免?可以用物件的常引用作為引數,如:
class Sample {
…
};
void PrintfObj( const Sample & o)
{
……
}//這樣函式中就能確保不會出現無意中更改o值的語句了。