天天看點

C++:33---類成員指針

成員指針概述:

  • 當初始化一個這樣的指針時,我們令其指向類的某個成員,但是不指定該成員所屬的對象
  • 直到使用成員指針時,才提供成員所屬的對象
  • 成員指針是指可以指向類的非靜态成員的指針
  • 一般情況下,指針指向一個對象,但是成員指針指向的是類的成員,而不是類的所建立出的對象
  • 類的靜态成員不屬于任何對象,是以無需特殊的指向靜态成員的指針,指向靜态成員的指針與普通指針沒有任何差別
  • 成員指針的類型囊括了類的類型以及成員的類型:
  • 下面我們定義一個類,作為本文講解的基礎:
class Screen {
public:
typedef std::string::size_type pos;
char get_cursor()const { return contents[cursor]; }
char get()const;
char get(pos ht, pos wd)const;
private:
std::string contents;
pos cursor;
pos height, width;
};      

一、資料成員指針

資料成員指針的定義

  • 特點:
  • 需要使用*來表示目前的變量是一個指針
  • 成員指針定義時必須包含所屬的類
  • 指針的定義
  • 下面定義一個指向Screen對象的指針,指針的類型為string,并且指針為常量指針(是以不能通過這個指針修改值)
  • 由于指針的類型為string,是以該指針可以指向常量(非常量)Screen對象的string成員

const string Screen::*pdata;

  • 為指針指派:
  1. //将pdata指向于Screen類的contents成員
  2. pdata = &Screen::contents;
  • 也可以在定義資料成員指針時直接初始化,并且使用auto或decltype來定義:

auto pdata = &Screen::contents;

使用資料成員指針

  • 指針定義之後,該指針沒有指向于任何資料成員,指針隻是指向于成員而非所屬的對象,隻有當解引用成員指針時我們才提供對象的資訊
  • 例如:
  1. Screen myScreen;
  2. Screen *pScreen = &myScreen;
  3. //.*解引用*pdata以獲得myScreen對象的contents成員
  4. auto s = myScreen.*pdata; //相當于myScreen.contents
  5. //->*解引用*pdata以獲得myScreen對象的contents成員
  6. s = pScreen->*pdata; //相當于pScreen->contents

傳回資料成員指針的函數

  • 在上面定義成員指針的時候,pdata不能出現在Screen類的外部,因為contents是private的(上面隻是為了示範說明)
  • 為了展現封裝性,我們通常定義一個成員函數,用該函數來傳回成員的指針
  • 例如:
  1. class Screen {
  2. public:
  3. //成員函數,傳回一個成員的指針
  4. static const std::string Screen::*data() {
  5. return &Screen::contents;
  6. }
  7. private:
  8. std::string contents;
  9. };
  • 我們将函數定義為static,其不屬于任何對象,是以我們可以直接調用該函數來獲得成員的指針
  • 當我們調用data時,就可以得到一個成員的指針。例如:
  1. int main()
  2. {
  3. //調用data()靜态函數來獲得一個成員的指針
  4. const std::string Screen::*pdata = Screen::data();
  5. return 0;
  6. }
  • 定義了之後,我們可以正常的使用這個指針。例如:
  1. int main()
  2. {
  3. Screen *pScreen = new Screen;
  4. const std::string Screen::*pdata = Screen::data();
  5. auto s = pScreen->*pdata; //等價于pScreen->contents
  6. return 0;
  7. }

二、成員函數指針

  • 與指向資料成員的指針類似,我們也可以聲明一個指向于成員函數的指針
  • 文法注意事項:
  • 指向成員函數的指針也需要指定目标函數的傳回類型和形參清單
  • 如果成員函數時const的或者是引用成員,則我們必須将const限定符或引用限定符包含進來

成員函數指針的定義

  • 最簡單的方法就是使用auto來聲明一個指向于成員函數的指針
  1. //pmf是一個函數指針,指向于get_cursor函數
  2. auto pmf = &Screen::get_cursor;
  • 因為定義成員函數指針時,需要同時指定目标函數的傳回值類型和形參清單,是以對于有重載的成員函數不會造成沖突。例如:
  1. class Screen {
  2. public:
  3. typedef std::string::size_type pos;
  4. char get_cursor()const { return contents[cursor]; }
  5. char get()const;
  6. char get(pos ht, pos wd)const;
  7. //...
  8. };
  9. int main()
  10. {
  11. //pmf2是一個成員函數指針,其指向于傳回值為char,形參為兩個Screen::pos類型的成員函數
  12. char (Screen::*pmf2)(Screen::pos, Screen::pos)const;
  13. //為pmf2指針指派
  14. pmf2 = &Screen::get;
  15. return 0;
  16. }
  • 出于優先級的考慮,上述定義中指針兩側的括号不能缺少。如果沒有這對括号,編譯器将認為該聲明是一個(無效的)函數聲明:
  • 錯誤的原因:編譯器會認為p是一個普通函數,并且傳回Screen類的一個char成員。因為其是一個普通函數,是以不能使用const限定符
  1. //錯誤的文法,非成員函數p不能使用const限定符
  2. char Screen::*pmf2(Screen::pos, Screen::pos)const;
  • 和普通函數指針不同的是,在成員函數和指向該成員的指針之間不存在自動轉換規則:
  1. char (Screen::*pmf2)(Screen::pos, Screen::pos)const;
  2. pmf2 = &Screen::get; //正确
  3. pmf2 = Screen::get; //錯誤,缺少&。在成員函數和指針之間不存在自動轉換規則

使用成員函數指針

  • 和使用資料成員的指針一樣,我們需要使用.*或者->*運算符作用域指向成員函數的指針,來調用類的成員函數
  • 例如:
  1. //pmf為成員函數指針,指向于get_cursor()函數
  2. auto pmf = &Screen::get_cursor;
  3. //pmf2為成員函數指針,指向于帶有兩個參數的get()函數
  4. char (Screen::*pmf2)(Screen::pos, Screen::pos)const = &Screen::get;
  5. Screen myScreen;
  6. Screen *pScreen = &myScreen;
  7. char c1 = (myScreen.*pmf)(); //等價于myScreen.get_cursor()
  8. char c2 = (pScreen->*pmf2)(0, 0);//等價于pScreen->get(0,0)
  • 在上面,在解引用成員函數的指針時需要在兩側加上括号,如果不加,那麼會錯誤:
  • 錯誤的原因:我們想要調用名為pmf和pmf2的函數,然後使用這些函數的傳回值作為指針指向成員運算符.*與->*的運算多想。然而pmf與pmf2并不是一個函數,是以代碼錯誤
  1. char c1 = myScreen.*pmf(); //錯誤的
  2. //其等價于myScreen.*(pmf())
  3. char c2 = pScreen->*pmf2(0, 0); //錯誤的
  4. //其等價于myScreen->*(pmf2(0,0))

使用成員指針的類型别名

  • 使用類型别名或typedef可以讓成員指針更容易了解
  • 例如,下面的類型别名将Action定義為兩參數get函數的同義詞:
  1. //Action是一種可以指向Screen成員函數的指針,其接受兩個pos實參,傳回值類型為char
  2. using Action = char (Screen::*)(Screen::pos, Screen::pos)const;
  • 現在我們定義成員函數指針時就比較友善了:
  1. //get是一個指向成員函數的指針
  2. Action get = &Screen::get;
  • 和其他函數指針類似,我們可以将指向成員函數的指針作為某個函數的傳回類型或形參類型。其中,指向成員的指針形參也可以擁有預設實參
  1. using Action = char (Screen::*)(Screen::pos, Screen::pos)const;
  2. //action是一個函數,其中參數2為一個指針,并且其有預設實參,指向于Screen的get成員函數
  3. Screen& action(Screen&, Action = &Screen::get);
  • 當我們調用action函數的時候,隻需将Screen的一個符合要求的函數的指針或位址傳入即可:
  1. using Action = char (Screen::*)(Screen::pos, Screen::pos)const;
  2. Screen& action(Screen&, Action = &Screen::get);
  3. int main()
  4. {
  5. Screen myScreen;
  6. Action get = &Screen::get;
  7. action(myScreen); //使用預設實參
  8. action(myScreen, get); //參數2調用前面定義的指針變量get
  9. action(myScreen, &Screen::get); //參數2顯式地傳入位址
  10. return 0;
  11. }

成員指針函數表

  • 對于普通函數指針和指向成員函數的指針來說,一種常見的用法是将其存入一個函數表中
  • 如果一個類含有幾個相同類型的成員,則這樣一張表可以幫助我們從這些成員中選擇一個
  • 假定Screen類含有幾個成員函數,每個函數負責将光标向指定的方向移動:
  1. class Screen {
  2. public:
  3. typedef std::string::size_type pos;
  4. //移動光标的一系列函數
  5. Screen& home();
  6. Screen& forward();
  7. Screen& back();
  8. Screen& up();
  9. Screen& down();
  10. private:
  11. pos cursor; //光标
  12. };
  • 這幾個函數有一個共同點:都不接受任何參數,并且傳回值是發生光标移動的Screen的引用
  • 現在我們開始設計函數表:
  • 在此之前,先定義一個靜态成員Menu,該成員是指向光标移動函數的指針的數組
  • 定義一個move函數,使其可以調用上面的任意一個函數并執行對應的操作
  • 設計一個枚舉用于函數傳參
  1. class Screen {
  2. public:
  3. typedef std::string::size_type pos;
  4. Screen& home();
  5. Screen& forward();
  6. Screen& back();
  7. Screen& up();
  8. Screen& down();
  9. //函數指針
  10. using Action = Screen& (Screen::*)();
  11. //定義一個枚舉
  12. enum Directions { HOME, FORWARD, BACK, UP, DOWN };
  13. //參數使用枚舉來調用函數表中對應的函數
  14. Screen& move(Directions cm)
  15. {
  16. //必須使用this
  17. return (this->*Menu[cm])();
  18. }
  19. private:
  20. pos cursor;
  21. static Action Menu[]; //函數表
  22. };
  23. //初始化函數表,将内部移動光标的函數都添加進去
  24. Screen::Action Screen::Menu[] = {
  25. &Screen::home,&Screen::forward,
  26. &Screen::back,&Screen::up,
  27. &Screen::down };
  • 現在我們可以調用move函數了:
  1. int main()
  2. {
  3. Screen myScreen;
  4. myScreen.move(Screen::HOME); //調用muScreen.home
  5. myScreen.move(Screen::DOWN); //調用muScreen.down
  6. return 0;
  7. }

三、将成員函數用作可調用對象

成員指針不是可調用對象

  • 通過上面我們知道,想要調用成員函數指針,必須通過一個類配合.*運算符或->*運算符來調用。是以與普通的函數指針不同,成員指針不是一個可調用對象,這樣的指針不支援函數調用運算符.
  • 因為成員指針不是可調用對象,是以我們不能直接将一個指向成員函數的指針傳遞給算法
  • 例如,下面在一個vector中尋找第一個空的string:
  1. std::vector<std::string> svec;
  2. auto fp = &std::string::empty; //fp指向string的empty函數,fp是一個成員函數指針
  3. //錯誤,必須使用.*或->*調用成員指針fp
  4. std::find_if(svec.begin(), svec.end(), fp);
  • find_if算法需要一個可調用對象,但我們提供給它的是一個指向成員函數的指針fp。是以在find_if的源碼内部執行如下形式的代碼,進而導緻無法通過編譯:
  1. //檢查對目前元素的斷言是否為真
  2. if(fp(*it)) //錯誤,想要通過成員指針調用函數,必須使用->*運算符
  • 顯然該語句試圖調用的是傳入的對象,而非函數

①使用function生成一個可調用對象

  • function模闆: 
  • 從指向成員函數的指針擷取可調用對象的一種方法是使用function模闆。下面的代碼就是正确的了:
  1. std::vector<std::string> svec;
  2. //empty函數的傳回值為bool,參數為const string&
  3. function<bool(const std::string&)> fcn = &std::string::empty;
  4. //現在是正确的了,fcn是一個可調用對象,使用.*調用empty
  5. std::find_if(svec.begin(), svec.end(), fcn);
  • 當一個function對象包含有一個指向成員函數的指針時,function類知道它必須使用正确的指向成員的指針運算符來執行函數調用。也就是說,我們可以認為在find_if内部有類似于下面的代碼:
  1. //假設it是find_if内部的疊代器,則*it是一個string對象
  2. if(fcn(*it)) //fcn就是empty的函數指針,等價于empty(*it)
  • 其中,function将使用正确的指向成員的指針運算符。從本質上看,function類将函數調用轉換成如下的形式:
  • 下面的調用代碼與上面的原理是類似的
  1. std::vector<std::string*> svec;
  2. //empty函數的傳回值為bool,參數為const string*
  3. function<bool(const std::string*)> fcn = &std::string::empty;
  4. //現在是正确的了,fcn是一個可調用對象,使用->*調用empty
  5. std::find_if(svec.begin(), svec.end(), fcn);

②使用mem_fn生成一個可調用對象

  • 通過上面知道,想要使用function,必須提供成員的調用形式。我們也可以采取另一種方法,就是使用标準庫功能mem_fn來讓編譯器負責推斷成員的類型
  • mem_fn也定義在functional頭檔案中,并且可以從成員指針生成一個可調用對象
  • 和function不同的是:
  • mem_fn可以根據成員指針的類型推斷可調用對象的類型,而無須使用者顯式地指定
  • 例如:我們使用mem_fn生成一個可調用對象,該對象接受一個string實參,傳回一個bool值(編譯器自動推斷的)
  • mem_fn生成的可調用對象可以通過對象調用,也可以通過指針調用:
  1. std::vector<std::string> svec;
  2. auto f = mem_fn(&std::string::empty); //f接受一個string或者一個string*
  3. f(*svec.begin()); //正确,傳入一個string對象,f使用.*調用empty
  4. f(&svec[0]); //正确,傳入一個string的指針,f使用.*調用empty
  • 實際上,我們可以認為mem_fn生成的可調用對象含有一個重載的函數調用運算符:一個接受string*,另一個接受string&

③使用bind生成一個可調用對象

  • 處于完整性的考慮,我們還可以使用bind從成員函數生成一個可調用對象:
  1. std::vector<std::string> svec;
  2. //選擇範圍中的每個string,并将其bind到empty的第一個隐式實參上
  3. auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));
  • 和function類似的地方是,當我們使用bind時,必須将函數中用于表示執行對象的隐式形參轉換成顯式的。和mem_fn類似的地方是,bind生成的可調用對象的第一個實參既可以是string的指針,也可以是string的引用:
  1. std::vector<std::string> svec;
  2. auto f = bind(&string::empty._1);
  3. f(*svec.begin()); //正确,傳入一個string對象,f使用.*調用empty
  4. f(&svec[0]); //正确,傳入一個string的指針,f使用.*調用empty