1、C++類的定義
C++中使用關鍵字
class 來定義類, 其基本形式以下:
class 類名
{
};
示例:
定義一個點(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:
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整形變量能夠在類內部初始化。 };