天天看點

條款 41: 區分繼承和模闆

結論:

當對象的類型不影響類中函數的行為時,就要使用模闆來生成這樣一組類。

當對象的類型影響類中函數的行為時,就要使用繼承來得到這樣一組類。

下面的代碼通過定義一個連結清單來實作 Stack 類,假設堆棧的對象類型為 T:

class Stack {
public:
Stack();
~Stack();
void push(const T& object);
T pop();
bool empty() const; //  堆棧為空?
private:
struct StackNode { //  連結清單節點
T data; //  此節點資料
StackNode *next; //  連結清單中下一節點
// StackNode 構造函數,初始化兩個域
StackNode(const T& newData, StackNode *nextNode)
: data(newData), next(nextNode) {}
};
StackNode *top; //  堆棧頂部
           
Stack(const Stack& rhs); //  防止拷貝和
Stack& operator=(const Stack& rhs); //  指派(見條款 27)
};
           
Stack::Stack(): top(0) {} //  頂部初始化為 null
void Stack::push(const T& object)
{
top = new StackNode(object, top); //  新節點放在
} //  連結清單頭部
T Stack::pop()
{
StackNode *topOfStack = top; //  記住頭節點
top = top->next;
T data = topOfStack->data; //  記住節點資料
delete topOfStack;
return data;
}
Stack::~Stack() //  删除堆棧中所有對象
{
while (top) {
StackNode *toDie = top; //  得到頭節點指針
top = top->next; //  移向下一節點
delete toDie; //  删除前面的頭節點
}
}
bool Stack::empty() const
{ return top == 0; }
           

        這些代碼毫無吸引人之處。實際上, 唯一有趣的一點在于:即使對 T 一無所知,你還是能夠寫出每個成員函數。 (上面的代碼中實際上有個假設,即,假設可以調用 T 的拷貝構造函數;但正如條款 45 所說明的,這是一個絕對合理的假設) 不管 T 是什麼,對構造,銷毀,壓棧,出棧,确定棧是否為空等操作所寫的代碼不會變。 除了"可以調用 T 的拷貝構造函數"  這一假設外,stack 的行為在任何地方都不依賴于 T。 這就是模闆類的特點:行為不依賴于類型。

       另一種情況:

      作為一位愛貓的寵物迷,你想設計一個類來表示貓。這也将需要多個不同的類,因為每個品種的貓都會有點不同。和所有對象一樣,貓可以被建立和銷毀,但,正如所有貓迷所知道的,貓所做的其它事不外乎吃和睡。然而,每一種貓吃和睡都有各自惹人喜愛的方式。

      注意這一條:"每一種貓吃和睡都有各自惹人喜愛的方式"。這意味着必須為每種不同的貓實作不同的行為。不可能寫一個函數來處理所有的貓,所能做的隻能是制定一個函數接口,所有種類的貓都必須實作它。啊哈!衍生一個函數接口的方法隻能是去聲明一個純虛函數

class Cat {
public:
virtual ~Cat(); //  參見條款 14
virtual void eat() = 0; //  所有的貓吃食
virtual void sleep() = 0; //  所有的貓睡覺
};
           
class Siamese: public Cat {
public:
void eat();
void sleep();
...
};
           
class BritishShortHairedTabby: public Cat {
public:
void eat();
void sleep();
...
};
           

總結:

代碼重用的兩種主要方式是,模闆和繼承:

模闆:當對象的類型不影響類中函數的行為時,就要使用模闆來生成這樣一組類。

            當對象的類型影響類中函數的行為時,就要使用繼承來得到這樣一組類。