《C++Primer》第十二章-類-學習筆記(1)-類定義聲明&成員&this指針

《C++Primer》第十二章-類-學習筆記(1)-類定義聲明&成員&this指針

日誌:
1,2020-03-02 筆者提交文章的初版V1.0

作者按:
最近在學習C++ primer,初步打算把所學的記錄下來。

傳送門/推廣
《C++Primer》第二章-變量和基本類型-學習筆記(1)
《C++Primer》第三章-標準庫類型-學習筆記(1)
《C++Primer》第八章-標準 IO 庫-學習筆記(1)

摘要

類是 C++ 中最重要的特徵。C++ 語言的早期版本被命名爲“帶類的 C(C with Classes)”,以強調類機制的中心作用。隨着語言的演變,創建類的配套支持也在不斷增加。語言設計的主要目標也變成提供這樣一些特性:允許程序定義自己的類型,它們用起來與內置類型一樣容易和直觀。

C++ 程序中,類都是至關重要的:我們能夠使用類來定義爲要解決的問題定製的數據類型,從而得到更加易於編寫和理解的應用程序。設計良好的類類型可以像內置類型一樣容易使用

類定義了數據成員函數成員數據成員用於存儲與該類類型的對象相關聯的狀態,而函數成員則負責執行賦予數據意義的操作。通過類我們能夠將實現接口分離,用接口指定類所支持的操作,而實現的細節只需類的實現者瞭解或關心。這種分離可以減少使編程冗長乏味和容易出錯的那些繁瑣工作。

本節介紹如何定義類,包括類的使用中非常基本的主題:類作用域數據隱藏構造函數。此外,還介紹了類的一些新特徵:友元使用隱含的 this 指針,以及靜態(static)和可變(mutable)成員的作用。
在 C++ 中,用類來定義自己的抽象數據類型(abstract data types)。通過定義類型來對應所要解決的問題中的各種概念,可以使我們更容易編寫、調試和修改程序。數據抽象能夠隱藏對象的內部表示,同時仍然允許執行對象的公有(public)操作。 抽象數據類型是面向對象編程和泛型編程的基礎。

類的定義和聲明

最簡單地說,類就是定義了一個新的類型和一個新作用域。接下來的說明都是圍繞着Sales_item 類來舉例子的:

//Sales_item 類:
class Sales_item {
public:
// operations on Sales_item objects
	double avg_price() const;  //常量成員函數!
	bool same_isbn(const Sales_item &rhs) const
	{ return isbn == rhs.isbn; }
// default constructor needed to initialize members of built-in type
	Sales_item(): units_sold(0), revenue(0.0) { }
private:
	std::string isbn;
	unsigned units_sold;
	double revenue;
};
double Sales_item::avg_price() const
{
	if (units_sold)
	return revenue/units_sold;
	else
	return 0;
}

類成員

每個類可以沒有成員,也可以定義多個成員,成員可以是數據、函數或類型別名。
一個類可以包含若干公有的、私有的和受保護的部分。

  • 類成員的訪問限制是通過在類主體內部對各個區域標記 publicprivateprotected 來指定的。關鍵字 public、private、protected 稱爲訪問修飾符
  • 一個類可以有多個 public、protected 或 private 標記區域。每個標記區域在下一個標記區域開始之前或者在遇到類主體結束右括號之前都是有效的。
  • 成員和類的默認訪問修飾符是 private
  • 在 public 部分定義的成員可被使用該類的所有代碼訪問;在 private 部分定義的成員可被其他的類成員訪問。
  • 所有成員必須在類的內部聲明,一旦類定義完成後,就沒有任何方式可以增加成員了。

構造函數

創建一個類類型的對象時,編譯器會自動使用一個構造函數來初始化該對象。構造函數是一個特殊的、與類同名的成員函數,用於給每個數據成員設置適當的初始值。
構造函數一般就使用一個構造函數初始化列表,來初始化對象的數據成員:

// default constructor needed to initialize members of built-in type
Sales_item(): units_sold(0), revenue(0.0) { }

成員函數

在類內部,聲明成員函數是必需的,而定義成員函數則是可選的。在類內部定義的函數默認爲 inline
類外部定義的成員函數必須指明它們是在類的作用域中。
Sales_item::avg_price 的定義使用作用域操作符來指明這是Sales_item 類中 avg_price 函數的定義。

成員函數有一個附加的隱含實參,將函數綁定到調用函數的對象——當我們編寫下面的函數時:

trans.avg_price()

就是在調用名 trans 的對象的 avg_price 函數。如果 trans 是一個Sales_item 對象,則在 avg_price 函數內部對 Sales_item 類成員引用就是對trans 成員的引用。

將關鍵字 const 加在形參表之後,就可以將成員函數聲明爲常量

double avg_price() const;

const 成員不能改變其所操作的對象的數據成員。const 必須同時出現在聲明和定義中,若只出現在其中一處,就會出現一個編譯時錯誤。

數據抽象和封裝

類背後蘊涵的基本思想是數據抽象封裝
數據抽象是一種依賴於接口和實現分離的編程(和設計)技術。類設計者必須關心類是如何實現的,但使用該類的程序員不必瞭解這些細節。相反,使用一個類型的程序員僅需瞭解類型的接口,他們可以抽象地考慮該類型做什麼,而不必具體地考慮該類型如何工作。
封裝是一項低層次的元素組合起來的形成新的、高層次實體的技術。函數是封裝的一種形式:函數所執行的細節行爲被封裝在函數本身這個更大的實體中。被封裝的元素隱藏了它們的實現細節——可以調用一個函數但不能訪問它所執行的語句。同樣地,類也是一個封裝的實體:它代表若干成員的聚焦,大多數(良好設計的)類類型隱藏了實現該類型的成員。

  • 標準庫類型 vector 同時具備數據抽象和封裝的特性。在使用方面它是抽象的,只需考慮它的接口,即它能執行的操作。它又是封裝的,因爲我們既無法瞭解該類型如何表示的細節,也無法訪問其任意的實現製品。
  • 數組在概念上類似於 vector,但既不是抽象的,也不是封裝的。可以通過訪問存放數組的內存來直接操縱數組

數據抽象和封裝提供了兩個重要優點:

  • 避免類內部出現無意的、可能破壞對象狀態的用戶級錯誤。
  • 隨時間推移可以根據需求改變或缺陷(bug)報告來完美類實現,而無須改變用戶級代碼。

類中同一類型的多個數據成員

類的數據成員的聲明類似於普通變量的聲明。如果一個類具有多個同一類型的數據成員,則這些成員可以在一個成員聲明中指定,這種情況下,成員聲明和普通變量聲明是相同的。

class Screen {   //Screen 的類型表示計算機上的窗口
public:
// interface member functions
private:
	std::string contents; //保存窗口內容的 string 成員
	std::string::size_type cursor;//指定光標當前停留的字符
	std::string::size_type height, width; //指定窗口的高度和寬度
}

使用類型別名來簡化類

除了定義數據和函數成員之外,類還可以定義自己的局部類型名字。如果爲std::string::size_type 提供一個類型別名,那麼 Screen 類將是一個更好的抽象:

class Screen {
public:
// interface member functions
	typedef std::string::size_type index;
	private:
	std::string contents;
	index cursor;
	index height, width;
};

類所定義的類型名遵循任何其他成員的標準訪問控制。將 index 的定義放在類的 public 部分,是因爲希望用戶使用這個名字。

類的成員函數可被重載

這些類之所以簡單,另一個方面也是因爲它們只定義了幾個成員函數。特別地,這些類都不需要定義其任意成員函數的重載版本。然而,像非成員函數一樣,成員函數也可以被重載。
重載操作符有特殊規則,是個例外,成員函數只能重載本類的其他成員函數。類的成員函數與普通的非成員函數以及在其他類中聲明的函數不相關,也不能重載它們。
(重載成員函數的規則)重載的成員函數和普通函數應用相同的規則:兩個重載成員的形參數量和類型不能完全相同
調用非成員重載函數所用到的函數匹配過程也應用於重載成員函數的調用。

定義重載成員函數

爲了舉例說明重載,可以給出 Screen 類的兩個重載成員,用於從窗口返回一個特定字符。兩個重載成員中,一個版本返回由當前光標指示的字符,另一個返回指定行列處的字符:

class Screen {
public:
	typedef std::string::size_type index;
// return character at the cursor or at a given position
	char get() const { return contents[cursor]; }
	char get(index ht, index wd) const;
// remaining members
private:
	std::string contents;
	index cursor;
	index height, width;
};

與任意的重載函數一樣,給指定的函數調用提供適當數目或類型的實參來選擇運行哪個版本:

Screen myscreen;
char ch = myscreen.get();// calls Screen::get()
ch = myscreen.get(0,0); // calls Screen::get(index, index)

顯式指定 inline 成員函數

在類內部定義的成員函數,例如上面代碼中不接受實參的 get 成員,將自動作爲inline 處理。也就是說,當它們被調用時,編譯器將試圖在同一行內擴展該函數。也可以顯式地將成員函數聲明爲 inline

class Screen {
public:
	typedef std::string::size_type index;
// implicitly inline when defined inside the class declaration
	char get() const { return contents[cursor]; }
// explicitly declared as inline; will be defined outside the
	class declaration
	inline char get(index ht, index wd) const;
// inline not specified in class declaration, but can be defined inline later
	index get_cursor() const;
// ...
};
// inline declared in the class declaration; no need to repeat on the definition
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
}
// not declared as inline in the class declaration, but ok to make inline in definition
inline Screen::index Screen::get_cursor() const //在類定義外部的函數定義上指定 inline
{
	return cursor;
}

可以在類定義體內部指定一個成員爲inline,作爲其聲明的一部分。或者,也可以在類定義外部的函數定義上指定 inline。在聲明和定義處指定 inline都是合法的。在類的外部定義 inline 的一個好處是可以使得類比較容易閱讀。

像其他 inline 一樣,inline 成員函數的定義必須在調用該函數的每個源文件中是可見的。不在類定義體內定義的 inline成員函數,其定義通常應放在有類定義的同一頭文件中。

類聲明與類定義

一旦遇到右花括號,類的定義就結束了。並且一旦定義了類,那以我們就知道了所有的類成員,以及存儲該類的對象所需的存儲空間。在一個給定的源文件中,一個類只能被定義一次。如果在多個文件中定義一個類,那麼每個文件中的定義必須是完全相同的。
可以聲明一個類而不定義它:

class Screen; // declaration of the Screen class

這個聲明,有時稱爲前向聲明(forward declaraton),在程序中引入了類類型的 Screen。在聲明之後、定義之前,類 Screen 是一個不完全類型(incompete type),即已知 Screen 是一個類型,但不知道包含哪些成員。
不完全類型(incomplete type)只能以有限方式使用。不能定義該類型的對象。不完全類型的用途:用於定義指向該類型的指針及引用,或者用於聲明(而不是定義)使用該類型作爲形參類型或返回類型的函數。
在創建類的對象之前,必須完整地定義該類。必須定義類,而不只是聲明類,這樣,編譯器就會給類的對象預定相應的存儲空間。同樣地,在使用引用或指針訪問類的成員之前,必須已經定義類。

爲類的成員使用類聲明

只有當類定義已經在前面出現過,數據成員才能被指定爲該類類型。如果該類型是不完全類型,那麼數據成員只能是指向該類類型的指針或引用。
因爲只有當類定義體完成後才能定義類,因此類不能具有自身類型的數據成員。然而,只要類名一出現就可以認爲該類已聲明。因此,類的數據成員可以是指向自身類型的指針或引用

class LinkScreen {
	Screen window;
	LinkScreen *next; //類的數據成員可以是指向自身類型的指針或引用
	LinkScreen *prev;
};

類的前身聲明一般用來編寫相互依賴的類

類對象

定義一個類時,也就是定義了一個類型。一旦定義了類,就可以定義該類型的對象。定義對象時,將爲其分配存儲空間,但(一般而言)定義類型時不進行存儲分配:

class Sales_item {
public:
// operations on Sales_item objects
private:
	std::string isbn;
	unsigned units_sold;
	double revenue;
};

定義了一個新的類型,但沒有進行存儲分配。當我們定義一個對象Sales_item item;
時,編譯器分配了足以容納一個 Sales_item 對象的存儲空間。item 指的就是那個存儲空間。每個對象具有自己的類數據成員的副本。修改 item 的數據成員不會改變任何其他 Sales_item 對象的數據成員。

定義類類型的對象

定義了一個類類型之後,可以按以下兩種方式使用。

  • 將類的名字直接用作類型名。
  • 指定關鍵字 class 或 struct,後面跟着類的名字:
Sales_item item1; // default initialized object of type
Sales_item
class Sales_item item1; // equivalent definition of item1

兩種引用類類型方法是等價的。第二種方法是從 C 繼承而來的,在 C++ 中仍然有效。第一種更爲簡練,由 C++ 語言引入,使得類類型更容易使用。

爲什麼類的定義以分號結束

類的定義分號結束。分號是必需的,因爲在類定義之後可以接一個對象定義列表。定義必須以分號結束:

class Sales_item { /* ... */ };
class Sales_item { /* ... */ } accum, trans;

通常,將對象定義成類定義的一部分是個壞主意。這樣做,會使所發生的操作難以理解。對讀者而言,將兩個不同的實體(類和變量)組合在一個語句中,也會令人迷惑不解。

隱含的 this 指針

成員函數具有一個附加的隱含形參,即指向該類對象的一個指針。這個隱含形參命名爲this,與調用成員函數的對象綁定在一起。成員函數不能定義 this 形參,而是由編譯器隱含地定義。成員函數的函數體可以顯式使用 this 指針,但不是必須這麼做。如果對類成員的引用沒有限定,編譯器會將這種引用處理成通過 this 指針的引用。

何時使用 this 指針

儘管在成員函數內部顯式引用 this 通常是不必要的,但有一種情況下必須這樣做:當我們需要將一個對象作爲整體引用而不是引用對象的一個成員時。最常見的情況是在這樣的函數中使用 this:該函數返回對調用該函數的對象的引用。
某種類可能具有某些操作,這些操作應該返回引用,Screen 類就是這樣的一個類。迄今爲止,我們的類只有一對 get 操作。邏輯上,我們可以添加下面的操作。
• 一對 set 操作,將特定字符或光標指向的字符設置爲給定值。
• 一個 move 操作,給定兩個 index 值,將光標移至新位置。
理想情況下,希望用戶能夠將這些操作的序列連接成一個單獨的表達式:

// move cursor to given position, and set that character
myScreen.move(4,0).set('#'); //myScreen.move(4,0)返回的是Screen&對象愛,所以可以這麼操作

這個語句等價於:

myScreen.move(4,0);  //下面一個代碼塊會講
myScreen.set('#');

返回 *this

在單個表達式中調用 move 和 set 操作時,每個操作必須返回一個引用,該引用指向執行操作的那個對象:

class Screen {
public:
// interface member functions
Screen& move(index r, index c);
Screen& set(char);
Screen& set(index, index, char);
// other members as before
};

注意,這些函數的返回類型是 Screen&,指明該成員函數返回對其自身類類型的對象的引用。每個函數都返回調用自己的那個對象。使用 this 指針來訪問該對象。
下面是對兩個新成員的實現:

Screen& Screen::set(char c)   
{
	contents[cursor] = c;
	return *this;  //一個對象作爲整體引用而不是引用對象的一個成員
}
Screen& Screen::move(index r, index c)
{
	index row = r * width; // row location
	cursor = row + c;
	return *this;
}

函數中唯一需要關注的部分是 return 語句。在這兩個操作中,每個函數都返回 *this。在這些函數中,this 是一個指向非常量 Screen 的指針。如同任意的指針一樣,可以通過對 this 指針解引用來訪問 this 指向的對象。

從 const 成員函數返回 *this

普通的非 const 成員函數中,this 的類型一個指向類類型對象的 const指針。可以改變 this 所指向的值,但不能改變 this 所保存的地址。
在 const 成員函數中,this 的類型是一個指向 const 類類型對象的const 指針。既不能改變 this 所指向的對象,也不能改變 this 所保存的地址。不能從 const 成員函數返回指向類對象的普通引用。const 成員函數只能返回 *this作爲一個 const 引用。

例如,我們可以給 Screen 類增加一個 display 操作。這個函數應該在給定的 ostream 上打印 contents。邏輯上,這個操作應該是一個 const 成員。打印 contents 不會改變對象。如果將 display 作爲 Screen 的 const 成員,則 display 內部的 this 指針將是一個 const Screen* 型的 const。
然而,與 move 和 set 操作一樣,我們希望能夠在一個操作序列中使用display:

// move cursor to given position, set that character and display thescreen
myScreen.move(4,0).set('#').display(cout);  //非 const 對象上調用const 成員函數

這個用法暗示了 display 應該返回一個 Screen 引用,並接受一個ostream 引用。如果 display 是一個 const 成員,則它的返回類型必須是const Screen&。
不幸的是,這個設計存在一個問題。如果將 display 定義爲 const 成員,就可以在非 const 對象上調用 display,但不能將對 display 的調用嵌入到一個長表達式中。下面的代碼將是非法的:

Screen myScreen;
// this code fails if display is a const member function
// display return a const reference; we cannot call set on a const
myScreen.display().set('*'); //display 返回的對象是 const 類類型對象,不能在 const 對象上調用 set
//const對象只能使用 const 成員

問題在於這個表達式是在由 display 返回的對象上運行 set。該對象是const,因爲 display 將其對象作爲 const 返回。我們不能在 const 對象上調用 set。

基於 const 的重載

爲了解決上面一節這個問題,我們必須定義兩個 display 操作:一個是 const,另一個不是 const。基於成員函數是否爲 const,可以重載一個成員函數;同樣地,基於一個指針形參是否指向 const可以重載一個函數。const對象只能使用 const 成員。非 const 對象可以使用任一成員,但非 const 版本是一個更好的匹配。
在此,我們將定義一個名爲 do_display 的 private 成員來打印 Screen。每個 display 操作都將調用此函數,然後返回調用自己的那個對象:

class Screen {
public:
// interface member functions
// display overloaded on whether the object is const or not
Screen& display(std::ostream &os)
{ do_display(os); return *this; }
const Screen& display(std::ostream &os) const    //const成員函數返回值類型是const對象的引用
{ do_display(os); return *this; }
private:
// single function to do the work of displaying a Screen,
// will be called by the display operations
void do_display(std::ostream &os) const  //const成員函數
{ os << contents; }
// as before
};

現在,當我們將 display 嵌入到一個長表達式中時,將調用非 const 版本。當我們 display 一個 const 對象時,就調用 const 版本:

Screen myScreen(5,3);
const Screen blank(5, 3);
myScreen.set('#').display(cout); // calls nonconst version  非 const 對象可以使用任一成員,但非 const 版本是一個更好的匹配
blank.display(cout); // calls const version const對象只能使用 const 成員

可變數據成員

有時(但不是很經常),我們希望類的數據成員(甚至在 const 成員函數內)可以修改。這可以通過將它們聲明爲 mutable 來實現。
可變數據成員(mutable data member)永遠都不能爲 const,甚至當它是const 對象的成員時也如此。因此,const 成員函數可以改變 mutable 成員。要將數據成員聲明爲可變的,必須將關鍵字 mutable 放在成員聲明之前:

class Screen {
public:
// interface member functions
private:
	mutable size_t access_ctr; // may change in a const members
// other data members as before
};

我們給 Screen 添加了一個新的可變數據成員 access_ctr。使用access_ctr 來跟蹤調用 Screen 成員函數的頻繁程度:

void Screen::do_display(std::ostream& os) const
{
++access_ctr; // keep count of calls to any member function 
os << contents;
}

儘管 do_display 是 const,它也可以增加 access_ctr。該成員是可變成員,所以,任意成員函數,包括 const 函數,都可以改變 access_ctr 的值。

編程角色的不同類別

程序員經常會將運行應用程序的人看作“用戶”

  • 應用程序爲最終“使用”它的用戶而設計,並響應用戶的反饋而完善。
  • 類也類似:類的設計者爲類的“用戶”設計並實現類。在這種情況下,“用戶”是程序員,而不是應用程序的最終用戶。

一方面,成功的應用程序的創建者會很好地理解和實現用戶的需求。同樣地,良好設計的、實用的類,其設計也要貼近類用戶的需求。
另一方面,類的設計者類的實現者之間的區別,也反映了應用程序的用戶與設計和實現者之間的區分。用戶只關心應用程序能否以合理的費用滿足他們的需求。同樣地,類的使用者只關心它的接口。好的類設計者會定義直觀和易用的類接口,而使用者只關心類中影響他們使用的部分實現。如果類的實現速度太慢或給類的使用者加上負擔,則必然引起使用者的關注。在良好設計的類中,只有類的設計者會關心實現。
在簡單的應用程序中,類的使用者和設計者也許是同一個人。即使在這種情況下,保持角色區分也是有益的。設計類的接口時,設計者應該考慮的是如何方便類的使用;使用類的時候,設計者就不應該考慮類如何工作。

注意,C++ 程序員經常會將應用程序的用戶和類的使用者都稱爲“用戶”。

const 應用概述 [C++Primer]

任何不會修改數據成員(即函數中的變量)的函數都應該聲明爲const 類型。如果在編寫const 成員函數時,不慎修改了數據成員,或者調用了其它非const 成員函數,編譯器將指出錯誤,這無疑會提高程序的健壯性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章