首頁 > 軟體

c++primer類詳解

2021-09-27 13:01:32

類的基本思想是資料抽象和封裝。

資料抽象是依賴介面和實現分離的程式設計技術。

1. 定義抽象資料型別

1.1 設計Sales_data類

  • 成員函數的宣告必須在類內部,定義可以在內部或外部
  • 作為介面的非成員函數,如print、read,宣告定義都在類的外部。
  • 定義在類內部的函數都是隱式的inline函數
  • 呼叫一個成員函數時,隱式初始化this指標
  • 任何自定義名為this的引數或者變數都是非法的
  • const成員函數
    •  const成員函數:在參數列後加上const關鍵字的函數
    • const的作用是修改隱式this指標的型別
    • 預設情況下,this的型別是指向型別非常數的常數指標。因此,不能將this繫結在一個非常數物件上(不能把this繫結到其他物件),所以也不能在常數物件上呼叫普通成員函數(不能用const 物件存取普通成員函數)。
    • const成員函數提高了函數靈活性
  • 常數物件,以及常數物件的參照或指標只能呼叫常數成員函數。
  • 編譯器分兩步處理類。
    • 1.編譯成員宣告。
    • 2.所有成員宣告編譯完後,編譯成員函數體。因此,成員宣告出現在成員函數體後,編譯器也可以正常編譯
  • 在類外定義函數體
    • 需要在函數名前加上類名::,在類名之後剩餘的程式碼位於作用域之內
    • 若返回型別也是在類內宣告的,就需要在函數名和返回型別前都加上類名::。
    • 若在類內宣告成了const成員函數,在外部定義時,const關鍵字也不能省略。
  • 若需要返回類本身,使用return *this 

1.2 定義類相關的非成員函數

  • 類相關非成員函數:屬於類的介面,但是不屬於類本身。
  • 通常把函數宣告和定義分開。和類宣告在同一標頭檔案內。
  • 通常情況下,拷貝一個類其實是拷貝其成員。(若想拷貝執行其他操作,查閱拷貝賦值函數)
Sale_data s1;
Sale_data s2=s1;//s2拷貝了s1的成員

1.3建構函式

  • 建構函式的任務是初始化類物件的資料成員
  • 只要類物件被建立,一定會執行建構函式
  • 建構函式名與類名相同,並且沒有返回型別,其他與普通函數相同。
  • 建構函式不能宣告成const
  • 預設建構函式
    • 預設建構函式無需任何實參
    • 若沒有為類顯式定義任何建構函式,編譯器隱式構造一個合成的預設建構函式。
    • 合成的預設建構函式按照如下規則初始化類成員
      • 若存在類內初始值,用它來初始化成員
      • 否則,預設初始化成員
    • 某些類不能依賴合成的預設建構函式
      • 若類包含內建型別或複合型別成員,只有當這些值全被賦予了類內初始值時,這個類才適合使用合成的預設建構函式。
      • 若類a包含一個成員類b,若b沒有預設建構函式,則編譯器無法為a構造正確的預設建構函式
      • 若定義了其他建構函式,則編譯器不會構造預設初始函數
class A{
//定義了一個實參為string的建構函式
//此時,編譯器不會合成預設建構函式
	A(std::string a){}
}
A a;//錯誤,沒有預設建構函式
A a1(std::string("小黑"));//只能用string引數
  • 參數列後加上 =defualt表示要求編譯器生成預設建構函式
  • =defualt可以和宣告一起出現在類內,也可以作為定義出現在類外。

若在類內部,則預設建構函式時內聯的,若在類外部,預設不是內聯的。

class A{
	A()=defualt;
}
A a;//正確,編譯器生成預設建構函式
  • 建構函式初始值列表
    • 存在編譯器不支援類內初始值,這樣的話預設建構函式不適用(因為預設建構函式使用類內初始值初始化類成員),這時應該使用建構函式初始值列表。
    • 函數初始值列表是參數列如下所示(冒號以及冒號和花括號間的程式碼::bookNo(s))
    • 建構函式不應該輕易覆蓋掉類內初始值,除非新賦的值與原值不同在
    • 建構函式的過程中,沒有出現在函數初始化列表中的成員將被執行預設初始化
class Sales_data{
	Sales_data(const std::string &s,unsigned n,double p):
	bookNo(s),units_sold(n),revenue(p*n){}
	//當編譯器不支援類內初始值時,可用如下方法定義
	Sales_data(const std::string &s):
	bookNo(s),units_sold(0),revenue(0){}
}
  • 在類外部定義建構函式,要宣告是哪個類別建構函式,在函數名前加上類名::
Sales_data::Sales_data(std::istream cin){
	read(cin,*this);
}

1.4 拷貝、賦值和解構

  • 編譯器會為類合成拷貝、賦值和銷燬操作。
  • 編譯器生成的版本對物件的每個成員執行拷貝、賦值和銷燬操作

2 存取控制和封裝

  • 存取說明符

public說明符後的成員在整個程式內可以被存取

private說明符後的成員可以被類的成員函數存取

  • 一個類可以包含0個或多個存取說明符,有效範圍到下一個說明符出現為止。
  • class和struct關鍵字定義類的唯一區別是
    • class在第一個存取說明符出現之前的區域預設是private
    • struct在第一個存取說明符出現之前的區域預設是public

2.1 友元

  • 類可以允許其他類或函數存取他的非公有成員。方法是用關鍵字friend宣告友元。
  • 友元的宣告只能在類內部
  • 友元宣告的位置不限,最好在類定義開始或結束前集中宣告友元。
  • 封裝的好處
    • 確保使用者程式碼不會無意間破壞封裝物件的狀態
    • 被封裝的類的具體實現細節可以隨時改變
  • 友元在類內的宣告僅僅指定了存取許可權,並不是一個通常意義的函數宣告
    • 若希望類的使用者能夠呼叫某個友元函數,需要在友元宣告之外再專門對函數進行一次宣告
    • 為了使友元對類使用者可見,友元宣告與類本身防止在同一個標頭檔案中
    • 一些編譯器強制限定友元函數必須在使用之前在類的外部宣告

2.2 類的其他特性

接下來介紹:型別成員、類的成員的類內初始值、可變資料成員、內聯成員函數、從成員函數返回*this、如何定義使用類型別、友元類

2.2.1 類成員再探

  • 類別名(型別成員):

 在類中定義的型別名字和其他成員一樣存在存取限制,可以是public或者private

類別名必須先定義後使用

(回憶:類成員變數可以在類成員函數之後定義,但是在類函數中使用,原因是編譯器先編譯類成員變數後邊一類成員函數)

型別成員通常出現在類開始的地方

class Screen{	
	public:
	//等價於 using pos = std::string::size_type
	typedef std::string::size_type pos;
}
  • 令成員作為行內函式

定義在類內部的函數是自動inline的,定義在類外部的函數,若需要宣告行內函式,要加上inline;inline成員函數也應該和相應的類定義在同一個標頭檔案夾

inline 
Screen& Screen::move(pos r,pos c){
	pos row = r*width;
	cursor = row + c;
	return *this; 
}
  • 可變資料成員,永遠不會是const,即使他是const物件的成員
class Screen{
public void some_member() const;
private:
	mutable size_t access_ctr;//使用mutable宣告可變資料成員
}
void Screen::some_member() const {
	++access_ctr;//即使在const成員函數中,仍然可以修改可變資料成員
}
  • 類內初始值使用=的初始化形式或者花括號括起來的直接初始化形式

2.2.2 返回*this的成員函數

  • 注意返回型別是否是參照。是否是參照對函數的使用方法影響很大
inline Screen &Screen::set(char ch){
	content[cursor] =ch;
	return *this;
}
inline Screen &Screen ::move(pos r,pos col){
	cursor= r * width + col ;
	return *this;
}
Screen s(3,2,'');
//move函數返回s本身,所以可以接著呼叫set函數
//並且move函數返回的是Screen的參照,若返回的不是參照,則會返回一個新的Screen物件
s.move(3,2).set('!');
  • 從const函數返回的是常數參照,在const函數中無法修改類成員變數
  • 使用const函數進行過載

編寫函數display列印Screen中的contents,因為只是展示,不需要修改值,所以這應該是一個const函數。

但是希望實現在展示後,能移動遊標:s.display().move(2,3)。這要求display返回的值是可以修改的,所以這不應該是const函數。

基於const過載,可以根據Screen物件是否是const來進行過載。

  • 建議多使用do_display這類函數完成實際工作,使公共程式碼使用私有函數
    • 可以集中修改
    • 沒有額外開銷
class Screen{
public:
	Screen* display(std::ostream &os){
		do_display(os);
		return *this;
	}  
	const Screen* display(std::ostream &os) const{
		do_display(os);
		return *this;
	} 
private:
	void do_display(std::ostream &os) const{
		os<<content;
	}
}
int main(){
	const Screen cs(3,3,'!');
	Screen s(3,3,'.')
	cs.display();//因為cs是const的,呼叫第二個const函數
	s.display();//呼叫第一個非const的函數
}

2.2.3 類型別

  • 每個類定義了唯一的型別,即使成員完全相同,也是不一樣的類。
class A{
int member;
}
class B{
int member;
}
A a;
B b = a;//錯誤!!
  • 不完全型別
    • 類似於函數,類也可以只宣告,不定義,這被叫做不完全型別
    • 不完全型別是向程式說明這是一個類名
    • 不完全型別使用環境很有限,只是可以定義指向這種型別的指標或參照,宣告(但不能定義)以不完全型別作為引數或返回型別的函數。
  • 類在建立前必須被定義
  • 類的成員不能有類本身(除了後面介紹的static類),但是可以是指向自身的參照或指標

2.2.4 友元再探

  •  一個類制定了其友元類,則友元函數可以存取該類的所有成員
  • 友元關係不存在傳遞性
  • 每個類自己負責控制自己的友元類或友元函數
    • 定義友元函數的順序:

有一個screen類,有私有成員content;

有clear函數,可以清除content的內容。

1.先宣告clear函數

2.在screen類中將clear函數函數定義為友元函數

3.定義clear函數,使用screen類

  • 定義友元類

有類window,window有私有成員content;友元類 window_mgr需要直接操作content。

  •  正常編寫window類,在window類中宣告:friend class window_mgr;
  • 正常編寫 window_mgr類,可以直接使用window的content
  • 注意將類寫在標頭檔案中,要按照如下格式;否則編譯會報錯重複的類定義
#ifndef xxx_H
#define xxx_H
/class 定義///
#endif
  • 一個類想把一組過載函數定義為它的友元,需要對這組函數中的每一個進行友元宣告。
  • 友元宣告僅僅表示對友元關係的宣告,但並不表示友元這個函數本身的宣告
struct X{
	friend viod f(){/*友元函數可以定義在類的內部,但是我認為這樣沒有意義*/
	X(){f();}//錯誤,f還沒有被定義
	void g();
	void h();
	}
	void X::g(){ return f();}//錯誤,f還沒有被定義
	void f();
	void X::h(){return f();}//正確,f的宣告已經在定義中了
};

2.4 類的作用域

  • 定義在類外的方法需要在方法名前使用::說明該方法屬於哪一個類,在說明屬於的類後,該函數的作用域位於該類內。
    • 即返回型別使用的名字位於類的作用域之外。若返回型別也是類的成員,需要在返回型別前使用::指明返回型別屬於的類
//pos的型別宣告在window類中,並且返回型別在類的作用域外,因此要使用window::pos
window::pos window::get_pos(){
//在window::get_pos後的所有程式碼作用域在類內,所以返回cursor,相當於this->cursor
return cursor;
}

2.4.1 名字查詢和類的作用域

  • 類的定義分兩步處理
    • 1.編譯成員的宣告
    • 2.直到類成員全部可見後編譯函數體
  • 一般來說,內層作用域可以重新定義外層作用域名字;但在類中若使用了某個外層作用域中的名字,並且該名字表示一種型別,則類不能在之後重新定義該名字
typedef double Money;
class Acount{
public:
	Money balace(){return bal;}//使用外層定義的Money
private:
	typedef double Money;//錯誤,不能重新定義Money
	Money bal;
}
  • 型別名的定義通常出現在類的開始處,來確保所有使用該型別的成員都出現在定義之後;
  • 類中同名變數會被隱藏,但是可以用this指標存取成員變數
double height;
class Window{
	double height;
}
void Window::dummy_fcn(double height){
	double class_height = this->height;
	double para_height = height; 
	double  global_height = ::height;
}

2.5 建構函式再探

2.5.1

  • 建構函式初始值列表 在類的有參照成員和const成員時,必須在建構函式中使用初始值列表進行初始化
  • 建議養成使用建構函式初始值的習慣
  • 初始值列表只說明初始值成員的值,並不限定初始值的具體執行順序;初始值順序與他們在類定義中的出現順序一致(某些編譯器在初始值列表和類中順序不一致時,會生成一條警告資訊)
//範例危險操作
strcut X{
//實際上按照宣告順序初始化,先初始化rem時,base的值未知
X(int i,int j):base(i),rem(base%j){}
int rem,base;
}
  • 建議,使用初始值列表和類中變數順序一致,可能的話,儘量避免使用某些成員初始化其他成員。

2.5.2 委託建構函式

  • 成員初始值列表的唯一入口是類名,可以用建構函式可以呼叫其他建構函式,呼叫過程應該寫在初始值列表位置
class Sale_data{
public:
	Sales_data(const std::string &s,unsigned s_num,double price):units_sold(s_num),revenue(s_num*price),BookNo(s){}
	Sales_data():Sales_data("",0,0){}//委託給上一個建構函式
}

2.5.3 預設建構函式的作用

  • 當物件被預設初始化或值初始化時執行預設構造引數
  • 預設初始化發生在:

 1.塊作用域內不使用任何初始值定義的一個非靜態變數或者陣列時

2.類本身含有類型別的成員且使用合成的預設建構函式

3.類型別成員沒有在建構函式初始值列表中顯式初始化時

  • 值初始化發生在:

1.初始化陣列時,提供的初始值數量少於陣列大小

2.不使用初始值定義一個區域性變數時

3.書寫形如T()的表示式顯式請求值初始化時,其中T是型別名。如vector接受一個實參說明vector的大小

  • 若定義了其他建構函式,編譯器不在生成預設建構函式,因此最好需要我們程式設計師來提供一個建構函式

2.5.4 隱式的類型別轉換

  • 轉換建構函式:能夠通過一個實參呼叫的建構函式定義一條從建構函式的引數構造型別向類型別轉換的規則。
vector<string> str_vec;
//需要push一個string,但傳參一個字串。這裡使用了string的轉換建構函式
str_vec.push_back("小黑~");
  • 轉換建構函式只允許一步構造轉換
  • 需要多個引數的建構函式無法執行隱式轉換
//Sales_data有引數為string的建構函式
//Sales_data的combine為方法:
//Sales_data & Sales_data::combine(const Sales_data& );
Sales_data item;
item.combine("無限~")//錯誤,只允許一步構造
item.combine(string("無限~"))//正確,只有string到Sales_data的一步隱式構造轉換
  • 使用explicit抑制建構函式定義的隱式轉換
class Sales_data{
explicit Sales_data(const string&):bookNo(s){};
...//其他宣告
}
item.combine(string("無限~"));//錯誤,explicit阻止了隱式轉換

explicit函數只能用於直接初始化

//Sales_data的建構函式:explicit Sales_data(const string&):bookNo(s){};
string bookNo = "001";
Sales_data item1(bookNo);//正確,直接初始化
Sales_data item2 = bookNo;//錯誤,拷貝初始化

儘管編譯器不會將explicit的建構函式用於隱式轉換過程,但是可以使用顯式強制轉化

string bookNo ="001";
item.combine(bookNo);//錯誤,explicit阻止了隱式轉換
item.combine(static_cast<Sales_data>(bookNo));//正確,強制轉換

2.5.5 聚合類

  • 聚合類的定義。一個滿足下列條件的類被稱為聚合類

1.所有成員都是public的

2.沒有定義任何建構函式

3.沒有類內初始值

4.沒有基礎類別,也沒有virtual函數

  • 可以使用{}括起來的成員初始值列表來初始化聚合類
class Data{
public:
	int ival;
	string s;
	}
//順序一定相同
Data val1={0,"孔子"};		

2.5.6 字面值常數

  • constexpr函數的引數和返回值必須是字面值型別
  • 字面值型別包括:算術型別、指標、參照、資料成員都是字面值型別的聚合類和滿足下面條件的類。

1.資料成員都是字面值型別

2.類必須含有一個 constexpr建構函式

3.使用預設定義的解構函式

4.如果一個資料成員含有類內初始值,則該初始值必須是一條常數表示式;如果資料成員屬於某種類型別,則初始值必須使用自己的constexpr建構函式

  • 建構函式不能是const的,但是可以是constexpr的。
  • 字面值常數類,至少提供一個constexpr建構函式
  • constexpr建構函式
    • 是建構函式,沒有返回語句
    • 是 constexpr函數,唯一可執行語句就是返回語句
    • 所以constexpr建構函式函數體為空,只能通過初始化列表值來執行構造初始化
class Data{
public:
	constexpr Data(int para_i,string para_s):ival(para_i),s(para_s){}
	int ival;
	string s;
	}
constexpr Data data(1,"吃烤肉");

1.6 類的靜態成員

  • 使用static在類中宣告靜態成員,該靜態成員和類關聯,而不是和類物件關聯
  • 靜態成員函數也不與任何類物件繫結起來,並且靜態成員函數不含this指標。(包括this的顯式呼叫和對非靜態成員的隱式呼叫)
  • 在外部定義static函數時,不能重複static,static關鍵字出現類內部的宣告語句
  • 不能在類內部初始化靜態成員,必須在類的外部定義和初始化每個靜態成員,因此一旦被定義,就一直存在在程式的整個生命週期
  • 想要確保物件只定義一次,最好的辦法就是把靜態資料成員的定義和其他非行內函式的定義放在同一個檔案
  • 靜態成員的類內初始化
    • 一般情況下, 類的靜態成員不應該在類的內部初始化
    • 靜態成員必須是字面值常數型別(constexpr)
    • 即使一個常數靜態資料成員在類的內部初始化了,通常也應該放在類的外部定義一下該成員(這樣才能使生命週期直到程式結束)。
  • 靜態成員的特殊使用場景
    • 靜態資料成員的型別可以就是它所屬的型別
class Bar{
public:
//...
provite:
	static Bar mem1;//正確,static成員可以是不完整型別
	Bar* mem2;//正確,指標成員可以是不完整型別
	Bar mem3;//錯誤
}
- 靜態成員可以作為預設實參
class Screen{
	public:
	Screen& clear(char = bkground)
	private:
	static const char bkground;
}

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!


IT145.com E-mail:sddin#qq.com