這是一個很容易讓人迷惑的principle!按照我們對C++多态性的了解:定義基類中某個函數為虛函數是為了允許用基類的指針來調用子類的這個函數。通過virtual函數實作程式運作時候的動态調用。
However,是不是隻要在基類中使用了virtual函數就一定能夠實作這種動态調用呢?是否隻要在base class中定義了虛函數,那麼在通過基類指針指向子類的時候就一定會調用子類實作的對應的virtual函數呢?
我們來實際驗證一下:
假設有一個class繼承體系,用來模拟股市交易如買進、賣出等。這樣的交易一定要經過審計,是以每當建立一個交易對象,在審計日志中需要建立一筆适當的記錄。下面是一個看似頗為合理的做法:
#include<iostream>
using namespace std;
class Transaction //所有交易的基類
{
public:
Transaction();
virtual void logTransaction() const //交易記錄
{
cout<<"Log in Transaction"<<endl;
}
};
Transaction::Transaction()
{
logTransaction();
}
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const
{
cout<<"Log in BuyTransaction"<<endl;
}
};
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const
{
cout<<"Log in SellTransaction"<<endl;
}
};
int main()
{
Transaction *action;
action = new BuyTransaction();
action = new SellTransaction();
}
我們希望每當有一個交易(ButTransaction or SellTransaction)建立的時候,就自動記錄此次交易,是以我們在基類的構造函數裡面調用日志記錄函數。能否真正實作記錄不同的交易的功能呢?我們運作程式:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICdzFWRoRXdvN1LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX90EVNdXSUpVdW5WYsVjMiZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39TNyEDO1UjM0ETNxETMzEDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
結果并沒有調用各個子類對應的日志記錄函數logTransaction。調用的都是基類的函數!
為什麼會出現這種結果呢?原因如下:
當我們執行New BuyTrasaction()的時候,會調用基類和本來的構造函數,但是base class的構造函數會先于derived class的構造函數被調用,當base class構造函數執行時derived class的成員變量尚未初始化(因為還未調用其構造函數啊),如果此時在base class的構造函數裡面調用了base class定義的virtual函數,如果這個virtual函數是子類實作的那個的話,就會出現“使用對象内部尚未初始化的成分”的糟糕情況,這是C++禁止的,是以此時調用的virtual隻可能是base class中的那個virtual 函數,不是子類的那個。
該道理同樣适用于析構函數,如果在base class中調用了virtual函數的話,因為對象釋放的時候會先調用子類的析構函數,然後再調用基類的析構函數,當調用基類的析構函數的時候,子類中的成員變量已經被釋放了,不存在了,是以執行的還是基類中的virtual函數。
如何來解決這個問題呢?有兩個方案:
方案一:在base class中将日志記錄函數logTransaction定義為純虛函數,其構造函數不調用這個函數,然後在子類中實作這個函數,并在每個子類的構造函數中調用這個函數!
這種方法雖然能夠實作其功能,however,不是一種好的“代碼複用”方式,因為需要在每個子類中都要定義和調用同一個日志記錄函數,what a fucking waste!
方案二:在base class中将logTransaction函數改為非virtual函數,然後要求子類構造函數傳遞必要資訊給Transaction構造函數,而後那個構造函數便可以安全地調用非virtual函數logTransaction了!
看下各自的實作代碼:
方案一:
#include<iostream>
using namespace std;
class Transaction
{
public:
Transaction();
virtual void logTransaction() = 0;
};
Transaction::Transaction(){}
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction()
{
cout<<"Log in BuyTransaction"<<endl;
}
BuyTransaction()
{
logTransaction();
}
};
class SellTransaction : public Transaction
{
public:
virtual void logTransaction()
{
cout<<"Log in SellTransaction"<<endl;
}
SellTransaction()
{
logTransaction();
}
};
int main()
{
Transaction *action;
action = new BuyTransaction();
action = new SellTransaction();
}
執行結果:
方案二的代碼:
#include<iostream>
#include<string>
using namespace std;
class Transaction
{
public:
Transaction(const string& logInfo);
void logTransaction(const string& logInfo) const; //non-virtual
static string createLogString(string parameters)
{
return parameters;
}
};
Transaction::Transaction(const string& logInfo)
{
logTransaction(logInfo);
}
void Transaction::logTransaction(const string& logInfo) const
{
cout<<"log " + logInfo + " info successfully!"<<endl;
}
class BuyTransaction : public Transaction
{
public:
BuyTransaction(string parameters): Transaction(createLogString(parameters)) {}
};
class SellTransaction : public Transaction
{
public:
SellTransaction(string parameters): Transaction(createLogString(parameters)) {}
};
int main()
{
Transaction *action;
action = new BuyTransaction("buy transation");
action = new SellTransaction("sell transaction");
}
此種方式相對較為靈活,代碼複用性比較好!本代碼在《effective C++》的基礎上做了些許優化!