一、定義
(1)、所有使用基類的地方必須能夠使用子類進行替換,而程序的行為不會發生任何變化(替換為子類之后不會產生錯誤或者異常)。
只有這樣,父類才能真正被復用,子類能夠在父類的基礎上增減新的屬性和行為。才能真正的實現多態行為。
(2)、當子類繼承父類的時候,子類就擁有了父類的屬性和行為。(注意:只是類型而已) 但是如果子類覆蓋父類的某些方法,那么
原來使用父類的地方就可能出現錯誤。(如何理解呢?表面上看是調用的是父類的方法,實際運行的時候子類方法覆蓋了父類的方
法,注意父類方法其實是存在的,通過作用域限定符可以訪問到,兩個方法的實現可能不一樣,這樣不符合LSP里氏替換原則。)
(3)、里氏替換原則是實現開閉原則的重要方式之一。由於使用基類對象的地方可以使用子類對象,因此程序中盡量使用基類類型進
行定義,而在運行的時候確定子類類型,子類對象替換父類對象。 (有點面向接口編程的味道,對外提供接口,而不是實現類)。
或者可以實現公共父類(父類中公共屬性和行為)。
編程實驗:長方形和正方形的駁論
1、正方形是一種特殊的長方形(is-a關系):類圖:

正方形類繼承於長方形類。
1 int main() 2 { 3 //LSP原則:父類出現的地方必須能用子類替換 4 Rectangle* r = new Rectangle();//Square *r = new Square(); 5 r->setWidth(5); 6 r->setHeight(4); 7 printf("Area = %d\n",r->getArea()); //當用子類時,結果是16。用戶就不 8 //明白為什么長5,寬4的結果不是20,而是16. 9 //所以正方形不能代替長方形。即正方形不能 10 //繼承自長方形的子類 11 return 0; 12 }
2、改進的繼承關系---符合LSP原則(面向接口編程)
類圖:

1 int main() 2 { 3 //LSP原則:父類出現的地方必須能用子類替換 4 QuadRangle* q = new Rectangle(5, 4); //Rectangle* q = new Rectangle(5, 4);或Square *q = new Square(5); 5 6 printf("Area = %d, Perimeter = %d\n",q->getArea(), q->getPerimeter()); 7 8 return 0; 9 }
3、鴕鳥不是鳥類
1 //面向對象設計原則:LSP里氏替換原則 2 //鴕鳥不是鳥的測試程序 3 4 #include <stdio.h> 5 6 //鳥類 7 class Bird 8 { 9 private: 10 double velocity; //速度 11 public: 12 virtual void fly() {printf("I can fly!\n");} 13 virtual void setVelocity(double v){velocity = v;} 14 virtual double getVelocity(){return velocity;} 15 }; 16 17 //鴕鳥類Ostrich 18 class Ostrich : public Bird 19 { 20 public: 21 void fly(){printf("I can\'t fly!");} 22 void setVelocity(double v){Bird::setVelocity(0);} 23 double getVelocity(){return Bird::getVelocity();} 24 }; 25 26 //測試函數 27 void calcFlyTime(Bird& bird) //參數是引用 父類引用子類的時候,會有多態的行為 28 { 29 try 30 { 31 double riverWidth = 3000; 32 33 if(bird.getVelocity()==0) throw 0; 34 35 printf("Velocity = %f\n", bird.getVelocity()); 36 printf("Fly time = %f\n", riverWidth /bird.getVelocity()); 37 } 38 catch(int) //異常處理 39 { 40 printf("An error occured!") ; 41 } 42 } 43 44 int main() 45 { 46 //遵守LSP原則時,父類對象出現的地方,可用子類替換 47 Bird b; //用子類Ostrich替換Bird 48 49 b.setVelocity(100); //替換之后,會直接調用子類的方法 50 51 calcFlyTime(b); //父類測試時是正常的,子類時會拋出異常,違反LSP 52 53 return 0; 54 }
二、歷史替換原則的4層含義(良好的繼承定義規范,主要包括4層含義)
1、子類必須實現父類中聲明的所有方法。

java里面的接口可以直接定
義接口對象。
(1)、步槍、手槍和機關槍都繼承於AbstractGun接口類,都必須實現shoot(射擊)的功能。
(2)、玩具槍不能直接繼承AbstractGun。因為玩具槍不能實現父類的shoot功能(即子類不能完全實現父類的方法,違反LSP原則)。
按照繼承原則,上面的玩具槍繼承AbstractGun是沒有問題的,玩具槍也是槍,但是在具體的應用場景中就要考慮這個問題了:子類
是否能夠完整的實現父類的業務,否則就會出現拿槍殺敵人時是把玩具槍的笑話。
因此,ToyGun不能繼承於AbstractGun,而是繼承AbstracToy,然后仿真槍的行為。因為士兵類要求傳入的參數AbstractGun類的對
象,所以不能使用玩具槍殺人。

感覺用C++表示這種關系比較牽強。
(3)、如果子類不能完整的實現父類的方法,或者父類的某些方法在子類中已經發生"畸變",則建議斷開父子繼承關系,采用依賴、
聚合、組合等關系代替繼承。
2、子類可以擴展功能,但不能改變父類原有的功能(理解:不能出現方法覆蓋的情況,多態可以)
(1)、子類可以有自己的屬性和方法。因此,里氏替換原則只能正着用,父類出現的地方可以用子類替換,但是不能反過來用。即子
類出現的地方,父類未必可以替換。例如:Snipper類的killEnemy方法中不能傳入Rifle類的對象,因為父類中沒有子類的zoomOut
方法。

(2)、父類向下轉換是不安全的,可能會調用只有在子類中出現的方法造成異常。
java里面的接口其實就是C++里面的抽象類,而java里面的抽象類其實就是C++里面的普通的父類(可以有成員變量和方法)。
多繼承的實現:單繼承+多接口
3、子類可以實現父類的抽象方法,但一般不要覆蓋父類的非抽象方法。
注意:父類抽象方法(多態),一般不要覆蓋非抽象方法(子類中公有的父類成分)
4、如果覆蓋或實現父類方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。方法的后置條件(方法的返回值)要比父類更嚴
格。
(1)、子類只能使用相等或者更寬松(表示使用的是父類類型)的前置條件來替換父類的前置條件。相等時表示覆蓋,不同時表示的是重載(java中)。
為什么是放大?因為父類方法的參數類型相對較小,所以當傳入父類方法的參數類型,重載的時候優先匹配父類的方法,而子類的重載方法不會匹
配,因此仍保證執行父類的方法(子類繼承的時候其實操作的是子類中的父類成分),所以業務邏輯不會改變(C++中,父子類的同名函數發生隱藏而不
是重載,因為父類的函數被隱藏,當用子類替換父類時,永遠不會調用父類的函數,LSP將無法遵守)。若是覆蓋時,子類的方法會被執行。
(2)、只能使用相等或更強的后置條件來替換父類的后置條件。即返回值應該是父類方法返回值的子類或更小。
如果是重載,由於前置條件的要求,會調用到父類的函數,因此子函數不會被調用。
如果是覆蓋,則調用子類的函數,這時子類的返回值比父類要求的小。因為父類調用函數的時候,返回值的類型是父類的類型,而子類的返回值更小,
賦值合法。
Father F = ClassF.Func();//;用子類替換時Father F = ClassC.Func()是合法的 子類賦值父類轉是合法的,父類賦值給子類是不合法的
利用設計模式之禪上面的例子更能詳細的說明這點:
實驗:(實驗也是網上的,但是能用設計模式之禪上面的例子更好)
1 #include <iostream> 2 3 using namespace std; 4 5 //定義兩個空類型用於實驗 6 class Shape 7 { 8 }; 9 10 class Rectangle : public Shape 11 { 12 13 }; 14 //C++中的抽象類就相當於java中的接口實現 15 //C++中普通的父類(帶有虛函數的,抽象方法)相當於java中的抽象類 16 class Father 17 { 18 public: 19 virtual void drawShape(Shape s) // 20 { 21 printf("Father:drawShape(Shape s)\n"); 22 } 23 24 virtual void showShape(Rectangle r) // 25 { 26 printf("Father:ShowShape(Rectangle r)\n"); 27 } 28 29 Shape CreateShape() 30 { 31 Shape s; 32 printf("Father: Shape CreateShape()"); 33 return s; 34 } 35 }; 36 37 class Son : public Father 38 { 39 public: 40 41 //對於C++而言,重載只能發生在同一作用域。顯示Son和Father是不同作用域 42 //下面發生的是管下列函數中的形參是否比父類更嚴格,只要同名,父類virtual一律被隱藏。 43 44 //子類的形參類型比父類更嚴格, 45 void drawShape(Rectangle r) 46 { 47 printf("Son:drawShape(Rectangle r)\n"); 48 } 49 50 //子類的形參類型比父類嚴寬松:表示的是父類 51 void showShape(Shape s) 52 { 53 printf("Son:showShape(Shape s)\n"); 54 } 55 56 //返回值類型比父類嚴格 57 Rectangle CreateShape() 58 { 59 Rectangle r; 60 printf("Son: Rectangle CreateShape()"); 61 62 return r; 63 } 64 }; 65 66 int main() 67 { 68 //當遵循LSP原則時,使用父類地方都可以用子類替換 69 70 //Father* f = new Father(); //該行可用子類替換 71 Son* f = new Son(); //用子類替換父類出現的地方 72 73 Rectangle r; 74 75 //子類形參類型更嚴格時,下一行輸出結果會發生變化,不符合LSP原則 76 f->drawShape(r); //Father類型的f時,調用父類的drawShape(Shape s) 77 //Son類型的f時,發生隱藏,會匹配子類的drawShape 78 79 //子類形參類型更寬松時,對於C++而言,會因發生隱藏而不符合LSP原則。但Java發生重載,會符合LSP 80 f->showShape(r); //Father類型的f時,直接匹配父類的showShape(Rectangle r) 81 //Son類型的f時,因發生隱藏,會匹配子類的showShape(Shape s) 82 83 //子類的返回值類型更嚴格 84 Shape s = f->CreateShape(); //替換為子類時,返回值為Rectangle,比Shape類型小,這種賦值是合法的 85 86 delete f; 87 cin.get(); 88 return 0; 89 }

覺得不錯,轉自https://blog.csdn.net/li_101357/article/details/52902600
