C++類(Class)總結,class總結
C++類(Class)總結
一、C++類的定義
C++中使用關鍵字
class 來定義類, 其基本形式如下:
class 類名
{
public:
//公共的行為或屬性
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; // compute the row location
return contents[row + c]; // offset by c to fetch specified character
}
注意:成員函數的返回類型不一定在類作用域中。可通過 類名::來判斷是否是類的作用域,::之前不屬於類的作用域,::之後屬於類的作用域。例如
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整形變量可以在類內部初始化。
};