C++—類Class總結
C++類(Class)總結
一、C++類的定義
C++中使用關鍵字 class 來定義類, 其基本形式如下:
class 類名
{
public:
protected:
private:
};
示例:
定義一個點(Point)類, 具有以下屬性和方法:
■ 屬性: x座標, y座標
■ 方法: 1.設定x,y的座標值; 2.輸出座標的資訊。
實現程式碼:
class Point
{
public:
void setPoint(int x, int y);
void printPoint();
private:
int xPos;
int yPos;
};
程式碼說明:
上段程式碼中定義了一個名為 Point 的類, 具有兩個私密屬性, int型的xPos和yPos, 分別用來表示x點和y點。
在方法上, setPoint 用來設定屬性, 也就是 xPos 和 yPos 的值; printPoint 用來輸出點的資訊。
1 資料抽象和封裝
抽象是通過特定的例項抽取共同特徵以後形成概念的過程。一個物件是現實世界中一個實體的抽象,一個類是一組物件的抽象。
封裝
2 類定義
幾個重要名詞:
(1) 類名
遵循一般的命名規則; 字母,數字和下劃線組合,不要以數字開頭。
(2) 類成員
類可以沒有成員,也可以定義多個成員。成員可以是資料、函式或類型別名。所有的成員都必須在類的內部宣告。
沒有成員的類是空類,空類也佔用空間。
class People
{
};
sizeof(People) = 1;
(3) 建構函式
建構函式是一個特殊的、與類同名的成員函式,用於給每個資料成員設定適當的初始值。
(4) 成員函式
成員函式必須在類內部宣告,可以在類內部定義,也可以在類外部定義。如果在類內部定義,就預設是行內函數。
3 類定義補充
3.1 可使用類型別名來簡化類
除了定義資料和函式成員之外,類還可以定義自己的區域性型別名字。
使用類型別名有很多好處,它讓複雜的型別名字變得簡單明瞭、易於理解和使用,還有助於程式設計師清楚地知道使用該型別的真實目的。
class People
{
public:
typedef std::string phonenum; //電話號碼型別
phonenum phonePub; //公開號碼
private:
phonenum phonePri;//私人號碼
};
3.2 成員函式可被過載
可以有多個過載成員函式,個數不限。
3.3 行內函數
有三種:
(1)直接在類內部定義。
(2)在類內部宣告,加上inline關鍵字,在類外部定義。
(3)在類內部宣告,在類外部定義,同時加上inline關鍵字。
注意:此種情況下,行內函數的定義通常應該放在類定義的同一標頭檔案中,而不是在原始檔中。這是為了保證行內函數的定義在呼叫該函式的每個原始檔中是可見的。
3.4 訪問限制
public,private,protected 為屬性/方法限制的關鍵字。
3.5 類的資料成員中不能使用 auto、extern和register等進行修飾, 也不能在定義時進行初始化
如 int xPos = 0; //錯;
例外:
靜態常量整型(包括char,bool)資料成員可以直接在類的定義體中進行初始化,例如:
static const int ia= 30;
4 類宣告與類定義
4.1 類宣告(declare)
class Screen;
在宣告之後,定義之前,只知道Screen是一個類名,但不知道包含哪些成員。只能以有限方式使用它,不能定義該型別的物件,只能用於定義指向該型別的指標或引用,宣告(不是定義)使用該型別作為形參型別或返回型別的函式。
void Test1(Screen& a){};
void Test1(Screen* a){};
4.2 類定義(define)
在建立類的物件之前,必須完整的定義該類,而不只是宣告類。所以,類不能具有自身型別的資料成員,但可以包含指向本類的指標或引用。
class LinkScreen
{
public:
Screen window;
LinkScreen* next;
LinkScreen* prev;
}; //注意,分號不能丟
因為在類定義之後可以接一個物件定義列表,可類比內建型別,定義必須以分號結束:
class LinkScreen{ /* ... */ };
class LinkScreen{ /* ... */ } scr1,scr2;
5 類物件
定義類物件時,將為其分配儲存空間。
Sales_item item; //編譯器分配了足以容納一個 Sales_item 物件的儲存空間。item 指的就是那個儲存空間。
6 隱含的 this 指標
成員函式具有一個附加的隱含形參,即 this指標,它由編譯器隱含地定義。成員函式的函式體可以顯式使用 this 指標。
6.1 何時使用 this 指標
當我們需要將一個物件作為整體引用而不是引用物件的一個成員時。最常見的情況是在這樣的函式中使用 this:該函式返回對呼叫該函式的物件的引用。
class Screen
{
...
public:
Screen& set(char);
};
Screen& Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
7 類作用域
每個類都定義了自己的作用域和唯一的型別。
類的作用域包括:類的內部(花括號之內), 定義在類外部的成員函式的引數表(小括號之內)和函式體(花括號之內)。
class Screen
{
//類的內部
...
};
//類的外部
char Screen::get(index r, index c) const
{
index row = r * width; // 計算行位置
return contents[row + c]; // 由c偏移獲得特定字元
}
注意:成員函式的返回型別不一定在類作用域中。可通過 類名::來判斷是否是類的作用域,::之前不屬於類的作用域,::之後屬於類的作用域。
例如
Screen:: 之前的返回型別就不在類的作用域,Screen:: 之後的函式名開始到函式體都是類的作用域。
class Screen
{
public:
typedef std::string::size_type index;
index get_cursor() const;
};
Screen::index Screen::get_cursor() const //注意:index前面的Screen不能少
{
return cursor;
}
該函式的返回型別是 index,這是在 Screen 類內部定義的一個型別名。在類作用域之外使用,必須用完全限定的型別名 Screen::index 來指定所需要的 index 是在類 Screen 中定義的名字。
二 建構函式
建構函式是特殊的成員函式,用來保證每個物件的資料成員具有合適的初始值。
建構函式名字與類名相同,不能指定返回型別(也不能定義返回型別為void),可以有0-n個形參。
在建立類的物件時,編譯器就執行一個建構函式。
1 建構函式可以過載
可以為一個類宣告的建構函式的數量沒有限制,只要每個建構函式的形參表是唯一的。
class Sales_item;
{
public:
Sales_item(const std::string&);
Sales_item(std::istream&);
Sales_item(); //預設建構函式
};
2 建構函式自動執行
只要建立該型別的一個物件,編譯器就執行一個建構函式:
Sales_item item1("0-201-54848-8");
Sales_item *p = new Sales_item();
第一種情況下,執行接受一個 string 實參的建構函式,來初始化變數item1。
第二種情況下,動態分配一個新的 Sales_item 物件,通過執行預設建構函式初始化該物件。
3 建構函式初始化式
與其他函式一樣,建構函式具有名字、形參表和函式體。
與其他函式不同的是,建構函式可以包含一個建構函式初始化列表:
Sales_item::Sales_item(const string &book): isbn(book), units_sold(0), revenue(0.0)
{ }
建構函式初始化列表以一個冒號開始,接著是一個以逗號分隔的資料成員列表,每個資料成員後面跟一個放在圓括號中的初始化式。
建構函式可以定義在類的內部或外部。建構函式初始化只在建構函式的定義中指定。
建構函式分兩個階段執行:(1)初始化階段;(2)普通的計算階段。初始化列表屬於初始化階段(1),建構函式函式體中的所有語句屬於計算階段(2)。
初始化列表比建構函式體先執行。不管成員是否在建構函式初始化列表中顯式初始化,類型別的資料成員總是在初始化階段初始化。
3.1 哪種類需要初始化式
const 物件或引用型別的物件,可以初始化,但不能對它們賦值,而且在開始執行建構函式的函式體之前要完成初始化。
初始化 const 或引用型別資料成員的唯一機會是建構函式初始化列表中,在建構函式函式體中對它們賦值不起作用。
沒有預設建構函式的類型別的成員,以及 const 或引用型別的成員,必須在初始化列表中完成初始化。
class ConstRef
{
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
ConstRef::ConstRef(int ii)
{
i = ii; // ok
ci = ii; // error
ri = i;
}
應該這麼初始化:
ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
3.2 成員初始化的次序
每個成員在建構函式初始化列表中只能指定一次。重複初始化,編譯器一般會有提示。
成員被初始化的次序就是定義成員的次序,跟初始化列表中的順序無關。
3.3 初始化式表示式
初始化式可以是任意表達式
Sales_item(const std::string &book, int cnt, double price): isbn(book), units_sold(cnt), revenue(cnt * price) { }
3.4 類型別的資料成員的初始化式
初始化類型別的成員時,要指定實參並傳遞給成員型別的一個建構函式,可以使用該型別的任意建構函式。
Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}
3.5 類物件的資料成員的初始化
在類A的建構函式初始化列表中沒有顯式提及的每個成員,使用與初始化變數相同的規則來進行初始化。
類型別的資料成員,執行該型別的預設建構函式來初始化。
內建或複合型別的成員的初始值依賴於該類物件的作用域:在區域性作用域中不被初始化,在全域性作用域中被初始化為0。假設有一個類A,
class A
{
public:
int ia;
B b;
};
A類物件A a;不管a在區域性作用域還是全域性作用域,b使用B類的預設建構函式來初始化,ia的初始化取決於a的作用域,a在區域性作用域,ia不被初始化,a在全域性作用域,ia初始化0。
4 預設建構函式
不含形參的建構函式就是預設建構函式。
只要定義一個物件時沒有提供初始化式,就使用預設建構函式。如: A a;
為所有形參提供預設實參的建構函式也定義了預設建構函式。例如:
class A
{
public:
A(int a=1,char c =''){}
private:
int ia;
char c1;
};
4.1 合成的預設建構函式
只有當一個類沒有定義建構函式時,編譯器才會自動生成一個預設建構函式。
一個類只要定義了一個建構函式,編譯器也不會再生成預設建構函式。
建議:
如果定義了其他建構函式,也提供一個預設建構函式。
如果類包含內建或複合型別(如 int& 或 string*)的成員,它應該定義自己的建構函式來初始化這些成員。每個建構函式應該為每個內建或複合型別的成員提供初始化。
5 隱式類型別轉換
5.1 只含單個形參的建構函式能夠實現從形參型別到該類型別的一個隱式轉換
class A
{
public:
A(int a)
{
ia =a;
}
bool EqualTo(const A& a)
{
return ia == a.ia;
}
private:
int ia;
};
A a(1);
bool bEq = false;
bEq = a.EqualTo(1);//引數為1,實現從int型到A的隱式轉換
5.2抑制由建構函式定義的隱式轉換
通過將建構函式宣告為 explicit,來防止在需要隱式轉換的上下文中使用建構函式:
class A
{
public:
explicit A(int a )
{
ia =a;
}
bool EqualTo(const A& a)
{
return ia == a.ia;
}
private:
int ia;
};
通常,除非有明顯的理由想要定義隱式轉換,否則,單形參建構函式應該為 explicit。將建構函式設定為 explicit 可以避免錯誤。
三 複製控制
1 複製建構函式
1.1 幾個要點
(1) 複製建構函式
複製建構函式是一種特殊建構函式,只有1個形參,該形參(常用 const &修飾)是對該類型別的引用。
class Peopel
{
public:
Peopel();//預設建構函式
Peopel(const Peopel&);//複製建構函式
~Peopel();//解構函式
};
當定義一個新物件並用一個同類型的物件對它進行初始化時,將顯式使用複製建構函式。
Peopel a1; Peopel a2 = a1;
當將該型別的物件傳遞給函式或函式返回該型別的物件時,將隱式使用複製建構函式。
Peopel Func(Peopel b){...}
(2)解構函式
解構函式是建構函式的互補:當物件超出作用域或動態分配的物件被刪除時,將自動應用解構函式。
解構函式可用於釋放構造物件時或在物件的生命期中所獲取的資源。
不管類是否定義了自己的解構函式,編譯器都自動執行類中非 static 資料成員的解構函式。
(3) 複製控制
複製建構函式、賦值操作符和解構函式總稱為複製控制。編譯器自動實現這些操作,但類也可以定義自己的版本。
(4) 兩種初始化形式
C++ 支援兩種初始化形式:直接初始化和複製初始化。直接初始化將初始化式放在圓括號中,複製初始化使用 = 符號。
對於內建型別,例如int, double等,直接初始化和複製初始化沒有區別。
對於類型別:直接初始化直接呼叫與實參匹配的建構函式;複製初始化先使用指定建構函式建立一個臨時物件,然後用複製建構函式將那個臨時物件複製到正在建立的物件。直接初始化比複製初始化更快。
(5)形參和返回值
當形參或返回值為類型別時,由該類的複製建構函式進行復制。
(6)初始化容器元素
複製建構函式可用於初始化順序容器中的元素。例如:
vector<string> svec(5);
編譯器首先使用 string 預設建構函式建立一個臨時值,然後使用複製建構函式將臨時值複製到 svec 的每個元素。
(7)建構函式與陣列元素
如果沒有為類型別陣列提供元素初始化式,則將用預設建構函式初始化每個元素。
如果使用常規的花括號括住的陣列初始化列表來提供顯式元素初始化式,則使用複製初始化來初始化每個元素。根據指定值建立適當型別的元素,然後用複製建構函式將該值複製到相應元素:
Sales_item primer_eds[] = { string("0-201-16487-6"),
string("0-201-54848-8"),
string("0-201-82470-1"),
Sales_item()
};
1.2 合成的複製建構函式
(1)合成的複製建構函式
如果沒有定義複製建構函式,編譯器就會為我們合成一個。
合成複製建構函式的行為是,執行逐個成員初始化,將新物件初始化為原物件的副本。
逐個成員初始化:合成複製建構函式直接複製內建型別成員的值,類型別成員使用該類的複製建構函式進行復制。
例外:如果一個類具有陣列成員,則合成複製建構函式將複製陣列。複製陣列時合成複製建構函式將複製陣列的每一個元素。
1.3 定義自己的複製建構函式
(1) 只包含類型別成員或內建型別(但不是指標型別)成員的類,無須顯式地定義複製建構函式,也可以複製。
class Peopel
{
public:
std::string name;
unsigned int id;
unsigned int age;
std::string address;
};
(2) 有些類必須對複製物件時發生的事情加以控制。
例如,類有一個數據成員是指標,或者有成員表示在建構函式中分配的其他資源。而另一些類在建立新物件時必須做一些特定工作。這兩種情況下,都必須定義自己的複製建構函式。
最好顯式或隱式定義預設建構函式和複製建構函式。如果定義了複製建構函式,必須定義預設建構函式。
1.4 禁止複製
有些類需要完全禁止複製。例如,iostream 類就不允許複製。延伸:容器內元素不能為iostream
為了防止複製,類必須顯式宣告其複製建構函式為 private。
2 賦值操作符
與複製建構函式一樣,如果類沒有定義自己的賦值操作符,則編譯器會合成一個。
(1)過載賦值操作符
Sales_item& operator=(const Sales_item &);
(2)合成賦值操作符
合成賦值操作符會逐個成員賦值:右運算元物件的每個成員賦值給左運算元物件的對應成員。除陣列之外,每個成員用所屬型別的常規方式進行賦值。對於陣列,給每個陣列元素賦值。
(3)複製和賦值常一起使用
一般而言,如果類需要複製建構函式,它也會需要賦值操作符。
3 解構函式
建構函式的用途之一是自動獲取資源;與之相對的是,解構函式的用途之一是回收資源。除此之外,解構函式可以執行任意類設計者希望在該類物件的使用完畢之後執行的操作。
(1) 何時呼叫解構函式
- 撤銷(銷燬)類物件時會自動呼叫解構函式。
- 變數(類物件)在超出作用域時應該自動撤銷(銷燬)。
- 動態分配的物件(new A)只有在指向該物件的指標被刪除時才撤銷(銷燬)。
- 撤銷(銷燬)一個容器(不管是標準庫容器還是內建陣列)時,也會執行容器中的類型別元素的解構函式(容器中的元素總是從後往前撤銷)。
(2)何時編寫顯式解構函式
如果類需要定義解構函式,則它也需要定義賦值操作符和複製建構函式,這個規則常稱為三法則:如果類需要解構函式,則需要所有這三個複製控制成員。
(3)合成解構函式
合成解構函式按物件建立時的逆序撤銷每個非 static 成員,因此,它按成員在類中宣告次序的逆序撤銷成員。
對於每個類型別的成員,合成解構函式呼叫該成員的解構函式來撤銷物件。
合成解構函式並不刪除指標成員所指向的物件。 所以,如果有指標成員,一定要定義自己的解構函式來刪除指標。
解構函式與複製建構函式或賦值操作符之間的一個重要區別:即使我們編寫了自己的解構函式,合成解構函式仍然執行。
四 友元
友元機制允許一個類將對其非公有成員的訪問權授予指定的函式或類。
友元可以出現在類定義的內部的任何地方。
友元不是授予友元關係的那個類的成員,所以它們不受宣告出現部分的訪問控制影響。
建議:將友元宣告成組地放在類定義的開始或結尾。
1 友元類
class Husband
{
public:
friend class Wife;
private:
double money;//錢是老公私有的,別人不能動,但老婆除外
};
class Wife
{
public:
void Consume(Husband& h)
{
h.money -= 10000;//老婆可以花老公的錢
}
};
Husband h;
Wife w;
w.Consume(h);
2 使其他類的成員函式成為友元
class Husband; //1.宣告Husband
class Wife //2.定義Wife類
{
public:
void Consume(Husband& h);
};
class Husband //3.定義Husband類
{
public:
friend void Wife::Consume(Husband& h);//宣告Consume函式。
private:
double money;//錢是老公私有的,別人不能動,但老婆除外
};
void Wife::Consume(Husband& h) //4.定義Consume函式。
{
h.money -= 10000;//老婆可以花老公的錢
}
注意類和函式的宣告和定義的順序:
(1)宣告類Husband
(2)定義類Wife,宣告Consume函式
(3)定義類Husband
(4)定義Consume函式。
五 static 類成員
static 成員,有全域性物件的作用,但又不破壞封裝。
1 static 成員變數
static 資料成員是與類關聯的物件,並不與該類的物件相關聯。
static 成員遵循正常的公有/私有訪問規則。
2 使用 static 成員而不是全域性物件有三個優點。
(1) static 成員的名字是在類的作用域中,因此可以避免與其他類的成員或全域性物件名字衝突。
(2) 可以實施封裝。static 成員可以是私有成員,而全域性物件不可以。
(3) 通過閱讀程式容易看出 static 成員是與特定類關聯的,這種可見性可清晰地顯示程式設計師的意圖。
3 static 成員函式
在類的內部宣告函式時需要新增static關鍵字,但是在類外部定義函式時就不需要了。
因為static 成員是類的組成部分但不是任何物件的組成部分,所以有以下幾個特點:
1) static 函式沒有 this 指標
2) static 成員函式不能被宣告為 const (將成員函式宣告為 const 就是承諾不會修改該函式所屬的物件)
3) static 成員函式也不能被宣告為虛擬函式
4 static 資料成員
static 資料成員可以宣告為任意型別,可以是常量、引用、陣列、類型別,等等。
static 資料成員必須在類定義體的外部定義(正好一次),並且應該在定義時進行初始化。
建議:定義在類的原始檔中名,即與類的非行內函數的定義同一個檔案中。注意,定義時也要帶上類型別+"::"
double Account::interestRate = 0.035;
5 特殊的靜態常量整型成員
靜態常量整型資料成員可以直接在類的定義體中進行初始化,例如:
static const int period = 30;
當然char 可以轉換成整形,也是可以的, static const char bkground = '#';
6 其他
(1)static 資料成員的型別可以是該成員所屬的類型別。非 static 成員只能是自身類物件的指標或引用
class Screen
{
public:
// ...
private:
static Screen src1; // ok
Screen *src2; // ok
Screen src3; // error
};
(2)非 static 資料成員不能用作預設實參,static 資料成員可用作預設實參
class Screen
{
public:
Screen& clear(char = bkground);
private:
static const char bkground = '#';//static const整形變數可以在類內部初始化。
};