類的復制構造函數在以下情況被調用:
使用類的對象去初始化此類的另一個對象時 函數的參數是類的對象,在調用函數進行實參和形參的結合時 函數的返回值是類的對象,在函數執行完畢返回值時深復制和淺復制
默認的復制構造函數實現的是淺復制 為類中的每個內嵌對象都實現復制構造函數才能實現深復制 一般將復制構造函數的參數設為const
類型
聲明和實現復制構造函數的一般方法:
class Point { public: Point(Point &p); private: int x, y; }; Point::Point(const Point &p){ x = p.x; y = p.y; }
創建組合類的對象時的構造函數調用順序:
首先調用內嵌對象的構造函數,初始化內嵌對象 內嵌對象的構造函數的調用順序和該對象在類中的聲明順序相同 內嵌對象的構造函數的調用順序和其在初始化列表中的順序無關 若對象沒有出現在初始化列表中,則調用該對象的默認構造函數 最後調用本類構造函數 析構函數的調用順序與構造函數的調用順序相反 內嵌對象的析構函數的調用順序和其在類中的聲明順序相反 若沒有編寫復制構造函數,則會自動生成隱含的復制構造函數,該函數自動調用內嵌對象的復制構造函數必須在初始化列表中初始化的數據成員:
沒有默認的無參構造函數的內嵌對象——此類對象初始化時必須提供參數 引用類型的數據成員——引用類型變量必須在聲明的同時進行初始化組合類構造函數定義的一般形式:
類名 :: 構造函數名(形參表) : 內嵌對象1(形參表), ... { /*構造函數體*/ }
形參表中的形參可以是此類對象的引用(將調用復制構造函數)。
其中內嵌對象1(形參表), 內嵌對象2(形參表), ...
稱為初始化列表,其作用是對內嵌對象進行初始化。
class Inner {
public:
Inner(Iparam1, Iparam2, ...);
};
...
class Outer {
public:
Outer(Oparam1, Oparam2, ...);
private:
Inner1 i1;
Inner2 i2;
...
};
Outer :: Outer(Oparam1, Oparam2, ...) : i1(Iparam1, Iparam2, ...), i2(...), ...{
//構造函數主體
}
常對象
常對象的數據成員值在對象整個生存期間內不能改變 常對象必須在聲明的同時初始化,而且不能更新 通過常對象只能調用其常成員函數常成員函數
常成員函數在定義和聲明定義的時都要使用const
關鍵字 const
可以用作區分重載 在僅以const
作為重載的區分時,普通對象將默認調用普通成員函數 可以通過常函數訪問普通數據成員 常函數不能更新目的對象的數據成員(下一條為原因) 在調用常成員函數期間,即使目的對象是普通對象,也按常對象處理
常數據成員
任何函數中都不能對常數據成員進行賦值 類中的常數據成員只能通過其構造函數在初始化列表中進行初始化class A { public: A(int i); private: const int a; }; //在初始化列表中初始化常量 A::A(int i):a(i){ //構造函數的其他內容 }
類中靜態變量和常量的初始化
類中的靜態變量和常量都應該在類外加以定義 上一條的例外:若類的靜態常量如果具有整形或者枚舉類型則可以直接在類中為其指定常量值常引用
常引用所引用的對象不能被更新 非常引用只能綁定到普通對象,不能綁定到常對象通過常引用訪問普通對象時,將該對象按常對象處理
帶有默認值的參數必須在參數表的最後邊
相同作用域內不可以對同一個參數的默認值重新定義,即使值相同也不行 類成員函數的默認值必須寫在類定義中,不能寫在類實現中枚舉元素按整型常量處理,不能賦值,所以不能進行++、–等運算 枚舉元素默認值從0開始,可以在定義枚舉時賦初值,元素值自動遞增 使用枚舉元素時直接使用元素名,不可以加枚舉名,在元素前加//下面的做法是錯的,不能重復設置默認值 void fun(int a = 1, int b = 2); int main(){ } void fun(int a = 1, int b = 2){ }
MyEnum.
或MyEnum::
是錯的 將整數賦值給枚舉類型變量時,需要強制類型轉換myEnum = MyEnum(number);
MyEnum myEnum; myEnum = MyEnum(1);
則e的值為1,無論枚舉中是否有1這個值
throw
可以拋出普通數據類型,也可以拋出類的對象
catch
的參數可以是普通數據類型,也可以是類對象 catch
按順序檢查是否可以捕獲所拋出的異常 如果異常被前面的catch
捕獲,則後面的catch
不會執行 如果異常類型聲明是一個省略號catch(...)
,則該catch子句可以捕獲所有類型的異常 能夠捕獲所有類型的異常的catch
塊必須放在最後
catch
後的異常類型可以不聲明形參,但這樣無法訪問所捕獲到的異常對象。
使用不帶操作數的throw
可以將當前捕獲的異常再次拋出,但是只能在catch
塊或catch
塊中調用的函數中使用。
若異常拋出點本身不在任何try-catch
塊內,則結束當前函數的執行,回到當前函數的調用點,把調用點作為異常的拋出點,然後重復這一過程。
/*throw表達式語法*/ throw 表達式 ; /*try-catch塊語法*/ try { //可能發生異常的內容 } catch (異常類型1 形參名) { //捕獲到異常後的處理方法 } catch (異常類型2 形參名) { //將當前捕獲到的異常再次拋出,將拋出源異常對象,而不是其副本 throw; } catch (...){ //使用...捕獲所有類型的異常 }
throw ()
的形式,則此函數不拋出任何異常 若要使用異常接口聲明,則在函數定義和實現時都要聲明異常接口
如果函數拋出了異常接口聲明中所沒有的異常,unexpected
函數會被調用,該函數默認會調用terminate
函數中止程序。用戶可以自定義unexpected
函數的行為。
/*在函數聲明中說明函數可以拋出哪些異常*/ 返回類型 函數名(參數表) throw (異常類型1, 異常類型2, ...); /*不拋出任何異常的函數*/ 返回類型 函數名(參數表) throw ();
發生異常時,從進入try
塊(捕獲到異常的catch
所對應的那個)直到異常拋出前,這期間棧上構造的並且沒被析構的所有對象都會被自動析構,這一過程被稱為棧的解旋。
static
和const
成員 如果派生類中聲明了和基類成員函數同名的新函數,即使函數參數表不同,也會將從基類繼承來的同名函數的全部重載形式隱藏 如果要訪問被隱藏的成員,要使用作用域分辨符或基類名來限定
//使用基類名限定 obj.Base1::var; obj.Base2::fun(); //使用作用域標識符限定 class Derived:public Base1, public Base2{ ... using Base1::var; using Base2::fun;//不加括號 ... }
公有繼承 public
當類的繼承方式為公有繼承時,基類的public
成員和protected
成員的訪問屬性在派生類中不變,而基類的private
成員不可直接訪問。
保護繼承 protected
當類的繼承方式為保護繼承時,基類的public
成員和protected
成員的訪問屬性在派生類中變為protected
,而基類的private
成員不可直接訪問。
私有繼承 private
當類的繼承方式為私有繼承時,基類的public
成員和protected
成員的訪問屬性在派生類中變為private
,而基類的private
成員不可直接訪問。默認的繼承方式為private
。
若派生的的多個直接基類還有共同的基類,則直接基類中從共同基類繼承來的成員有相同的名稱。在派生類對象中這些同名數據成員在內存中同時擁有多個副本,同名函數會有多個映射。
可以使用作用域標識符來區分它們,也可以將共同基類設為虛基類,這時從不同路徑繼承來的同名數據成員在內存中就只有一個副本,同名函數也只有一個映射。
//class 派生類名:virtual 繼承方式 基類名 class Base0{}; class Base1:virtual public Base0{}; class Base2:virtual public Base0{}; class Drived:public Base1, public Base2{};//Base0不是Drived的直接基類,不加virtual
派生類構造函數的語法形式:
派生類名::構造函數名(參數表):基類名(參數表), ..., 派生類初始化列表{ //派生類函數體 }
如果虛基類有含參構造函數,並且沒有聲明無參構造函數,則在整個繼承過程中,直接或者間接繼承虛基類的所有派生類,都必須在構造函數的初始化列表中顯式對虛基類初始化。
虛基類的構造函數不會被多次調用,只有距離虛基類最遠的派生類的構造函數才會調用虛基類的構造函數,而其他派生類對虛基類的構造函數的調用會被自動忽略。
class Base0{ public: Base0(param);//虛基類含參構造函數 }; class Base1:virtual public Base0{ public: Base1(param):Base0(param);//初始化列表傳參 }; class Base2:virtual public Base0{ public: Base2(param):Base0(param);//初始化列表傳參 }; class Drived:public Base1, public Base2{ public: Drived(param):Base0(param);//初始化列表傳參 };
派生類對象的構造順序:
按照聲明派生類時的繼承順序調用基類構造函數來初始化基類的數據成員 初始化派生類新增的成員對象 執行派生類構造函數的函數體如果為派生類編寫復制構造函數,一般要為其基類的復制構造函數傳遞參數。
應該將派生類對象作為其基類復制構造函數的參數。
Derived::Derived(const Derived ¶m):Base(param), ...{ //派生類復制構造函數體 }
在定義類之前使用該類,需要使用前向引用聲明。
在提供類的完整定義之前,不能定義該類的對象,也不能在內聯成員函數中使用該類的對象。
可以在函數內部聲明函數,不可以在函數內部定義函數 在函數內部聲明的函數只在此函數體內有效class B;//前向引用聲明 class A { ... B b;//錯誤!類A不完整,不能定義它的對象 B &rb;//正確 B *pb;//正確 }; class B { ... };
使用cin
讀取數據時,遇到空格會停止讀入。
使用gets(char*)
和getline(cin, string, char)
讀入一整行數據
getline()
默認使用換行\n
作為讀取結束的標志,也可以自定義結束標志。
在getline()
函數的第三個參數處設置結束標志,傳入的字符將會最為結束的標志(\n
仍然有效)。
使用char ch[100]; string str; gets(ch); getline(cin, str); getline(cin, str, ',');//將半角逗號`,`設為讀取結束標志 /* 如果gets()或者getline()函數的前一行使用cin讀取數據, * 那麼應該在gets()或者getline()函數之前使用getchar(), * 否則gets()或者getline()會把cin結束的換行符讀入而產生錯誤 */ cin>>n; getchar();//使用getchar()防止下一行讀入cin的換行符 gets(ch); getline(cin, str);
inline
關鍵字聲明內聯函數 內聯函數不在調用時發生跳轉,而是在編譯時將函數體嵌入到每一個調用函數的地方 內聯函數應該是比較簡單的函數
inline
關鍵字
type * ptr = new type(val);
或
type * ptr;
ptr = new type(val);
val
將成為所申請的空間中所存儲的默認值 如果()
中不寫任何值,則將初值設為0
如果不希望設定初值,可以將()
省略
創建方法同上,將val換成初始化列表
若類存在自定義的無參構造函數,則new ClassName
等效於new ClassName()
若類無自定義的無參構造函數,則new ClassName
調用隱含的默認構造函數,new ClassName()
調用隱含的默認構造函數,還會將類中基本數據成員和指針成員賦初值0
,並且該約束遞歸作用於類的內嵌對象
type * ptr = new type[len];//末尾加()可以全部初始化為0
delete ptr; delete[] ptr;
.
、*
、::
和三目運算符?:
不可以重載,其他運算符都可以重載 =
、[]
、()
和->
只能重載為類的成員函數 派生類中的=
運算符函數總是會隱藏基類中的=
運算符函數 只能重載C++中已有的運算符 運算符重載後優先級和結合性不變
運算符重載有兩種形式(op代指被重載的運算符):
重載為類的非靜態成員函數,obj1 op obj2
相當於obj1.operator op(obj2)
重載為非類成員函數,obj1 op obj2
相當於operator op(obj1, obj2)
上述4種寫法都能調用相應的運算符重載函數 運算符重載函數的參數通常是參與運算的對象的引用
返回類型 operator 運算符 (形參表){ //運算方法體 }
當以非類成員函數的方式重載運算符時,有時需要訪問類的私有成員,可以將運算符重載函數設為類的友元函數。可以不使用友元函數時,不要使用。
當運算符重載為類的成員函數時,函數的參數要比運算符原來的操作數少一個(後置++
和--
除外);
當運算符重載為非類成員函數時,函數的參數和運算符原來的操作數相同。
++
和--
的重載++
和--
重載為前置運算符時,運算符重載函數不需要任何參數 當++
和--
重載為後置運算符時,運算符重載函數需要一個int
型形參,該參數只用作區分前置運算和後置運算,沒有其他作用,聲明和實現函數時,都可以不給出形參名
class A{ public: A(int n){this->n = n;} //重載為類的非靜態成員函數 A operator + (const A &a){ return A(n + a.n); } int n; } ; //重載為非類成員函數 A operator - (const A &a1, const A &a2){ return A(a1.n - a2.n); } int main(){ A a1(10); A a2(20); cout<< a1.n <<" "<< a2.n <
指針和數組
將指針賦值為0
或NULL
表示空指針,它不指向任何地址 通過指針訪問內含的成員時要使用->
對於指針和數組:
*(ptr + val) 等價於 ptr[val]
指向常量的指針
const type * ptr = &const_val;
指向常量的指針本身可以被改變,再指向其他的對象。
指針類型的常量
type * const ptr = &val;
常指針不能再指向其他對象,若其所指對象不是常量,則所指對象可以被修改
void
類型的指針可以指向任何類型的對象
函數指針
聲明一個函數指針,初始化
type (* ptrName)(params); type fun(params){...} ptrName = fun;
不加*
和&
,指針所指函數必須和指針具有相同的返回類型和形參表。
this指針
this
指向調用當前方法的那個對象自身
class A { public: A(int a); private: int a; }; A::A(int a){ //通過this消除內部變量對外部變量的屏蔽 this->a = a; }
指向類的非靜態成員的指針
指向數據成員的指針
type ClassName::*ptrName;
ptrName = &ClassName::varName;
指向函數成員的指針
type (ClassName::*ptrName)(params);
以上要注意訪問權限。
訪問數據成員
objName.*ptrName 或者 objPtrName->*ptrName
指向類的非靜態成員的指針
訪問類的非靜態成員不依賴對象,所以可以用普通指針訪問類的非靜態成員。
type *ptrName = &ClassName::varName;
數組初始化
int arr[len] = {1, 2, 3, ..., len}; int arr[] = {1, 2, 3, ..., len};//數組長度為len int arr[len] = {1, 2, 3, ..., i};//i
指定的初值的個數小於數組大小時,剩下的數組元素自動初始化為0
靜態生存期的數組元素會被默認初始化為0
動態生存期的數組元素默認的值是不確定的
字符數組
char str[5] = {'a', 'b', 'c', 'd', '\0'};//最後一位要放'\0' char str[5] = "abcd";//最多存放5-1個 char str[] = "abcdef";
聲明引用的同時必須對其進行初始化 引用被初始化之後,不能再更改其指向的對象 使用&
聲明引用
type var;//聲明變量 type &ref = var;//聲明變量var的引用
結構體
結構體使用struct
定義 結構體成員的默認訪問權限為public
結構體可以有數據成員、成員函數、構造函數和析構函數 結構體支持訪問權限控制、繼承和多態
聯合體
聯合體使用union
定義 聯合體的全部數據成員共享同一組內存單元 聯合體的數據成員同一時刻最多有一個是有效的 聯合體可以有數據成員、成員函數、構造函數、析構函數和訪問權限控制 聯合體不支持繼承,因此不支持多態 聯合體中的對象成員不能有自定義的構造函數、析構函數和復制賦值運算符 聯合體中的對象中的對象也要滿足上一條限制
結構體成員初始化
如果結構體的全部數據成員都是public
類型的,並且沒有自定義的構造函數、基類和虛函數,則可以使用如下方式直接初始化:
struct A { int i; char ch; ... }; int main(){ A a = {100, 'c', ...} }
滿足上述條件的類對象也可以使用如上方式進行初始化。
函數模板
所有模板的定義都是用關鍵字template
標識的 模板參數表中的多個參數用逗號分隔 模板參數表可以是class
或typename
關鍵字加參數名
/*函數模板的定義形式*/ template<模板參數表> 返回類型 函數名 (形參表){ //函數體 }
函數模板本身在編譯時不會產生任何目標代碼,只有函數模板生成的實例才會生成目標代碼 被多個源文件引用的函數模板,應當連同函數體一起放在頭文件中,而不能只將聲明放在頭文件中 函數指針只能指向函數模板的實例,而不能指向模板本身
類模板
模板類聲明自身不是類,它說明了類的一個家族 只有在被其他代碼引用時,模板才根據引用的需要生成具體的類
/*類模板的定義形式*/ template<模板參數表> class 類名 { //類成員 }; /*在類模板意外定義其成員函數*/ template<模板參數表> 返回類型 類名<模板參數標識符列表>::函數名(參數表){ //函數體 } /*使用類模板建立對象*/ 模板類名<模板參數表> 對象名;
虛函數是動態綁定的基礎,只有虛函數才能實現多態 只有類成員才能是虛函數 虛函數必須是非靜態的成員函數 virtual
關鍵字只能出現在函數原型聲明中,而不能出現在函數定義中 只有通過基類的指針會引用調用虛函數時,才會發生動態綁定 派生類在覆寫基類成員函數時,使不使用virtual
關鍵字都一樣 虛函數在其類的所有直接和間接派生類中任然是虛函數 虛函數聲明為內聯函數後仍可以動態綁定 虛函數一般不聲明為內聯函數,因為對內聯函數的處理是靜態的 虛函數的參數默認值是靜態綁定的,它只能來自基類的定義 構造函數不能是虛函數(構造函數不能被繼承,聲明為虛函數沒有意義,會報錯) 析構函數應該是虛函數,除非不作為基類(避免內存洩露)
如果沒有將基類的析構函數設為虛函數,在通過基類指針刪除派生類對象時調用的是基類的析構函數,派生類的析構函數沒有執行,因此派生類對象中動態分配的內存空間沒有被釋放,造成了內存洩露。
運行時多態的條件
類之間滿足兼容規則 使用虛函數 通過指針或引用來訪問虛函數(直接通過對象名訪問虛函數不能做到動態綁定)
//聲明虛函數 virtual 返回類型 函數名(形參表);
#include
using namespace std; class Base0 { public: virtual void fun(); }; void Base0::fun(){ cout<<"Base0"< fun(); } void normal(Base0 b){ b.fun(); } int main(){ Base0 b0; Base1 b1; Drived d; //使用基類引用可以做到動態綁定 ref(b0); ref(b1); ref(d); /**輸出 * Base0 * Base1 * Drived */ //使用基類指針訪問虛函數可以做到動態綁定 ptr(&b0); ptr(&b1); ptr(&d); /**輸出 * Base0 * Base1 * Drived */ //使用對象名訪問虛函數不能做到動態綁定 normal(b0); normal(b1); normal(d); /**輸出 * Base0 * Base0 * Base0 */ return 0; }
純虛函數和抽象類
抽象類是帶有純虛函數的類 抽象類含有沒有實現的函數,是不完整的類,所以不能實例化 聲明為純虛函數後,基類中不需要給出函數的實現部分 基類可以給出純虛函數的實現,但是仍然不能實例化 基類可以給出純虛函數的實現,但是派生類中仍然必須實現該函數後才能實例化 如果將析構函數聲明為純虛函數,必須給出其實現 如果要訪問在基類中給出的純虛函數的實現,需要使用基類名::函數名(參數表)
//聲明純虛函數 virtual 返回類型 函數名(形參表) = 0;
//如果要訪問在基類中給出的純虛函數的實現,需要使用`基類名::函數名(參數表)` #include<iostream> using namespace std; class Base { public: virtual void vfun() = 0; virtual void fun1(){ vfun();//訪問到的是派生類中的實現 } virtual void fun2(){ Base::vfun();//訪問到的是基類中的實現 } }; void Base::vfun(){ cout<<"Base"<<endl; } class Drived:public Base{ public: void vfun(){ cout<<"Drived"<<endl; } }; int main(){ Drived d; d.vfun();//Drived d.fun1();//Drived d.fun2();//Base return 0; }