C++易錯知識點整理
目錄
構造與析構
建構函式
- 建構函式是類的成員函式
解構函式
- 解構函式在物件生存期結束前自動呼叫
- 解構函式不接受任何引數
- 解構函式可以是虛擬函式
複製建構函式
- 類的複製建構函式的形參是此類物件的引用
類的複製建構函式在以下情況被呼叫:
- 使用類的物件去初始化此類的另一個物件時
- 函式的引數是類的物件,在呼叫函式進行實參和形參的結合時
- 函式的返回值是類的物件,在函式執行完畢返回值時
深複製和淺複製
- 預設的複製建構函式實現的是淺複製
- 為類中的每個內嵌物件都實現複製建構函式才能實現深複製
- 一般將複製建構函式的引數設為
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
與static
常物件
- 常物件的資料成員值在物件整個生存期間內不能改變
- 常物件必須在宣告的同時初始化,而且不能更新
- 通過常物件只能呼叫其常成員函式
常成員函式
- 常成員函式在定義和宣告定義的時都要使用
const
關鍵字 const
可以用作區分過載- 在僅以
const
作為過載的區分時,普通物件將預設呼叫普通成員函式 - 可以通過常函式訪問普通資料成員
- 常函式不能更新目的物件的資料成員(下一條為原因)
- 在呼叫常成員函式期間,即使目的物件是普通物件,也按常物件處理
常資料成員
- 任何函式中都不能對常資料成員進行賦值
- 類中的常資料成員只能通過其建構函式在初始化列表中進行初始化
類中靜態變數和常量的初始化
- 類中的靜態變數和常量都應該在類外加以定義
- 上一條的例外:若類的靜態常量如果具有整形或者列舉型別則可以直接在類中為其指定常量值
class A {
public:
A(int i);
private:
const int a;
static int b;
};
//在初始化列表中初始化常量
A::A(int i):a(i){
//建構函式的其他內容
}
//在類外初始化靜態成員
int A::a = 1;//必須要加資料型別關鍵字
常引用
- 常引用所引用的物件不能被更新
- 非常引用只能繫結到普通物件,不能繫結到常物件
- 通過常引用訪問普通物件時,將該物件按常物件處理
函式
函式宣告
- 可以在函式內部宣告函式,不可以在函式內部定義(實現)函式
- 在函式內部宣告的函式只在此函式體內有效
函式預設值
- 帶有預設值的引數必須在引數表的最後邊
- 相同作用域內不可以對同一個引數的預設值重新定義,即使值相同也不行
- 如果函式宣告中沒有為函式引數設定預設值,則可以在函式實現時為引數設定預設值
- 類成員函式的預設值必須寫在類定義中,不能寫在類實現中
//下面的做法是錯的,宣告和實現不能重複設定預設值
void fun(int a = 1, int b = 2);
void fun(int a = 1, int b = 2){ }
列舉
- 列舉元素按整型常量處理,不能賦值,所以不能進行++、–等運算
- 列舉元素預設值從0開始,可以在定義列舉時賦初值,元素值自動遞增
- 使用列舉元素時直接使用元素名,不可以加列舉名,在元素前加
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
所對應的那個)直到異常丟擲前,這期間棧上構造的並且沒被析構的所有物件都會被自動析構,這一過程被稱為棧的解旋。
丟擲string
型別的異常的注意事項
當丟擲的異常為string
型別時,要注意不能直接丟擲匿名字串,而要先宣告一個string
型別的變數或常量,然後再將該變數或常量丟擲。
void fun() throw (string) {
...
const string str = "msg";
throw msg;
//throw "msg";//這樣寫的話捕獲不到異常
}
友元
- 友元關係不能傳遞
- 友元關係不能繼承
- 宣告友元類不需要前向引用宣告,因為不涉及友元類的物件
- 宣告友元類時,即使友元類還不存在,也不會出錯
- 一個類的友元類中的函式全部是該類的友元函式
class A {
...
friend class B;//宣告友元類B
friend void fun();//宣告友元函式fun()
...
}
類的繼承與派生
- 派生類將接受基類的除建構函式和解構函式以外的全部成員,包括
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()
和getline()
使用cin
讀取資料時,遇到空格會停止讀入。
使用gets(char*)
和getline(cin, string, char)
讀入一整行資料。
如果在gets()
或getline()
函式前使用了cin
,注意要清除cin
留下的換行符,參見下面的示例。
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++中
.
、*
、::
和三目運算子?:
不可以過載,其他運算子都可以過載 =
、[]
、()
和->
只能過載為類的成員函式- 派生類中的
=
運算子函式總是會隱藏基類中的=
運算子函式 - 只能過載C++中已有的運算子
- 運算子過載後優先順序和結合性不變
運算子過載的兩種形式
(使用op代指被過載的運算子)
- 過載為類的非靜態成員函式,
obj1 op obj2
相當於obj1.operator op(obj2)
- 過載為非類成員函式,
obj1 op obj2
相當於operator op(obj1, obj2)
- 上述4種寫法都能呼叫相應的運算子過載函式
- 運算子過載函式的引數通常是參與運算的物件的引用
返回型別 operator 運算子 (形參表){
//運算方法體
}
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 <<endl;//10 20
//兩種呼叫方式
A a31 = a1 + a2;
A a32 = a1.operator + (a2);
cout<< a31.n <<" "<< a32.n <<endl;//30 30
//兩種呼叫方式
A a41 = a1 - a2;
A a42 = operator - (a1, a2);//-10 -10
cout<< a41.n <<" "<< a42.n <<endl;
return 0;
}
當以非類成員函式的方式過載運算子時,有時需要訪問類的私有成員,可以將運算子過載函式設為類的友元函式。可以不使用友元函式時,不要使用。
當運算子過載為類的成員函式時,函式的引數要比運算子原來的運算元少一個(後置++
和--
除外);
當運算子過載為非類成員函式時,函式的引數和運算子原來的運算元相同。
對於++
和--
的過載
- 當
++
和--
過載為前置運算子時,運算子過載函式不需要任何引數 - 當
++
和--
過載為後置運算子時,運算子過載函式需要一個int
型形參,該引數只用作區分前置運算和後置運算,沒有其他作用,宣告和實現函式時,都可以不給出形參名
需要過載為非類成員函式的情況
- 要過載的操作符的第一個引數為不可更改的型別
- 以非類成員函式的形式過載,可以支援更靈活的型別轉換
引用、 指標和陣列
引用
- 宣告引用的同時必須對其進行初始化
- 引用被初始化之後,不能再更改其指向的物件
- 使用
&
宣告引用
type var;//宣告變數
type &ref = var;//宣告變數var的引用
指標
- 將指標賦值為
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;
陣列初始化
- 指定的初值的個數小於陣列大小時,剩下的陣列元素自動初始化為
0
- 靜態生存期的陣列元素會被預設初始化為
0
- 動態生存期的陣列元素預設的值是不確定的
int arr[len] = {1, 2, 3, ..., len};
int arr[] = {1, 2, 3, ..., len};//陣列長度為len
int arr[len] = {1, 2, 3, ..., i};//i<len,後續元素自動初始化為0
int arr[row][col] = {1, 2, 3, ..., row * col};
int arr[row][col] = {
{1, 2, 3, ..., col},
{2, 3, 4, ...},
...,
{col, ...}
};
int arr[][col] = {1, 2, 3, ...};//可以省略行數,但不能省略列數
字元陣列
char str[5] = {'a', 'b', 'c', 'd', '\0'};//最後一位要放'\0'
char str[5] = "abcd";//最多存放5-1個
char str[] = "abcdef";
結構體和聯合體
結構體
- 結構體使用
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<iostream>
using namespace std;
class Base0 {
public:
virtual void fun();
};
void Base0::fun(){
cout<<"Base0"<<endl;
}
class Base1:public Base0{
public:
void fun();
};
void Base1::fun(){
cout<<"Base1"<<endl;
}
class Drived:public Base1{
public:
void fun();
};
void Drived::fun(){
cout<<"Drived"<<endl;
}
void ref(Base0 & r){
r.fun();
}
void ptr(Base0 * p){
p->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;
}