2014年4月6日 星期日

成員函數指標的奇異現象

這方法蠻特殊的,不知是否有實用的價值,很像為 Base 附加功能,不過的我寧願採用延伸介面的方法

 1 #include <iostream>
 2  
 3  using namespace std;
 4  
 5  class Base1
 6  {
 7  public:
 8    // Constructor
 9    Base1(){}
10    // Destructor
11    ~Base1(){}
12  };
13  
14  class Derived1:public Base1
15  {
16    int m_data;
17  public:
18    // Constructor
19    Derived1()
20    {
21      m_data = 0;
22    }
23    // Destructor
24    ~Derived1(){}
25  
26    // Derived 的成員函數應有能力轉換 Base 的 this 指標
27    int Func(int i)
28    {
29      m_data += i;
30      return m_data;
31    }
32  };
33  
34  int main()
35  {
36    Derived1 vDerived1;
37    Base1 vBase1;
38  
39    // 成員函數指標似乎可以胡扯,不在乎是否有符合的函數存在
40    typedef int (Base1::*pmFunc_Ptr1)(int);
41    pmFunc_Ptr1 Func_Ptr1 = (pmFunc_Ptr1)&Derived1::Func; // 須強制轉型
42  
43    cout << (vDerived1.*Func_Ptr1)(1) << endl; // 正確執行
44  
45    // Base1 中並無任何函數符合 pmFunc 要求的形式,照樣給它跑,
46    // 但得到錯誤的回傳值,而且 Base1 並沒有 m_data,因此寫入的動作很危險
47    cout << (vBase1.*Func_Ptr1)(1) << endl;
48  
49    typedef int (Derived1::*pmFunc_Ptr2)(int);
50    pmFunc_Ptr2 Func_Ptr2 = &Derived1::Func;
51  
52    cout << (vDerived1.*Func_Ptr2)(1) << endl;
53  
54    // cout << (vBase1.*Func_Ptr2)(1) << endl; // 編譯不過
55  
56  
57      return 0;
58  }

再補上一篇,這次應更能看出 成員函數指標 的奇怪能力

 1 #include <iostream>
 2  
 3  using namespace std;
 4  
 5  class Base1
 6  {
 7    int i;  // 加上這個才能 Base1 Base2 位置不同
 8  public:
 9    // Constructor
10    Base1(){}
11    // Destructor
12    ~Base1(){}
13  };
14  
15  class Base2
16  {
17    int i;  // 加上這個才能 Base1 Base2 位置不同
18  public:
19    // Constructor
20    Base2(){}
21    // Destructor
22    ~Base2(){}
23  };
24  
25  class Derived1:public Base1, public Base2
26  {
27    int m_data;
28  public:
29    // Constructor
30    Derived1()
31    {
32      m_data = 0;
33    }
34    // Destructor
35    ~Derived1(){}
36  
37    // Derived 的成員函數應有能力轉換 Base 的 this 指標
38    int Func(int i)
39    {
40      cout << "Derived1's this Addr = " << this << endl;
41      cout << "&m_data = " << &m_data << endl;
42      m_data += i;
43      return m_data;
44    }
45  };
46  
47  // 成員函數指標似乎可以胡扯,不在乎是否有符合的函數存在
48  typedef int (Base1::*pmFunc_Base1_Ptr)(int);
49  
50  // Obj 是 Base1 延伸類別產生的物件
51  // Func 須是 Obj 的成員函數
52  void test1(Base1 *Obj, pmFunc_Base1_Ptr Func)
53  {
54    cout << "Base1 Addr = " << Obj << endl;
55    cout << (Obj->*Func)(1) << endl;
56  }
57  
58  
59  // 成員函數指標似乎可以胡扯,不在乎是否有符合的函數存在
60  typedef int (Base2::*pmFunc_Base2_Ptr)(int);
61  
62  // Obj 是 Base2 延伸類別產生的物件
63  // Func 須是 Obj 的成員函數
64  void test2(Base2 *Obj, pmFunc_Base2_Ptr Func)
65  {
66    cout << "Base2 Addr = " << Obj << endl;
67    cout << (Obj->*Func)(1) << endl;
68  }
69  
70  
71  int main()
72  {
73    Derived1 vDerived1;
74  
75    pmFunc_Base1_Ptr Base1_Func_Ptr = (pmFunc_Base1_Ptr)&Derived1::Func; // 須強制轉型
76    pmFunc_Base2_Ptr Base2_Func_Ptr = (pmFunc_Base2_Ptr)&Derived1::Func; // 須強制轉型
77  
78    cout << "vDerived1's Addr = " << &vDerived1 << endl;
79    cout << (vDerived1.*Base1_Func_Ptr)(1) << endl; // 正確執行
80    test1(&vDerived1, Base1_Func_Ptr);
81    test2(&vDerived1, Base2_Func_Ptr);
82  
83  }

順便貼上執行結果,編譯器是 gcc 4.6.1。

Base1 Addr 和 Base2 Addr 位址是不一樣的,但都能正確執行 Derived1::Func()

vDerived1's Addr = 0x22ff44
Derived1's this Addr = 0x22ff44
&m_data = 0x22ff4c
1
Base1 Addr = 0x22ff44
Derived1's this Addr = 0x22ff44
&m_data = 0x22ff4c
2
Base2 Addr = 0x22ff48
Derived1's this Addr = 0x22ff44
&m_data = 0x22ff4c
3


以下貼文轉自 http://www.programmer-club.com.tw/showsametitleN/c/45694.html 的討,經由討論和測試證實是在手動轉型動手腳

-----------------------------------------------
首先要說的是 父子類別 的關係,就比較超然的高度來看,也就是 oo 的觀點來看,就如 R 大說的「任何指向 Derived1 的指標同時也指向 Base1, 因為 Derived1 包括 Base1」,但實務來看,也就是 oop 的觀點來看,物件執行時期的依據是 this 指標,一個物件是多個類別組成的,各類別執行時期的區分就靠 this 指標,

  1. 也許各類別 this 的位址恰巧相同,但也有可能不同。
  2. C++ 執行時期,可由子類別去呼叫父類別,因有子必有父。
  3. C++ 執行時期,不能可由父類別去呼叫子類別,因它不一定有兒子,但由呼叫自己的虛擬函數有可能可以呼叫到子類別,那就算是後繼有人。
  4. 不管是父叫子還是子叫父,都必須假設 this 指標的位址可能會不一樣,因此有可能會有調整 this 的動作
  • 由子叫父,因清楚父的模樣,因此編譯器在呼叫父前可以很容易先多加調整 this 的程式碼
  • 由父叫子是經由虛擬函數表,虛擬函數表是是由編譯器做的,使用這表的 this 和被呼叫的函數的 this 間的差距編譯器也很清楚,因此可先執行調整程式碼再進入子類別
再來看看 成員函數指標 算什麼,就我給的兩個例子來看,就像是父類別的虛擬函數,可以正確的叫用子類別,由以上說明可知,那 調整程式碼 是從哪來的?Derived1::Func() 只是普通成員函數,R 大也說 "這裡沒有「轉換」的成分",但總得有地方做手腳吧,我看來看去最有可能在以下這地方,
pmFunc_Ptr1 Func_Ptr1 = (pmFunc_Ptr1)&Derived1::Func; // 須強制轉型
因只有這裡能讓編譯器知道父子各是誰,可以為它準備好調整程式碼,若我猜得沒錯 Func_Ptr1 得到的不是 Derived1::Func() 的位址,而是像虛擬函數表一樣,先指向調整程式碼,可是看看 Derived1::Func() 只是普通的成員函數,只能被本身及子類別呼叫,一般只會做子到父的調整程式碼,現在很貼心的做父到子的調整程式碼,因此這個 強制轉型 根本就是使用這個超智能指標的標準動作嘛,想想編譯器擋掉自動轉型已是盡了把關的責任,手動的強制轉型已表示程式設計師知道在做什麼 往更深層的觀點來看,你不用在父類別準備虛擬函數,就能任意為父類別擴增功能,這自由度不是更高,我說過 "成員函數指標似乎可以胡扯,不在乎是否有符合的函數存在",其實是讚美之意。 還有不知這算不算是標準,在 gcc 是沒問題,vs2008 就不行了







沒有留言:

張貼留言