關(guān)于C++虛函數(shù)與靜態(tài)、動態(tài)綁定的問題
覆蓋:如果派生類中的方法,和基類繼承來的某個方法,返回值、函數(shù)名、參數(shù)列表都相同,而且基類的方法是virtual虛函數(shù),那么派生類的這個方法,自動處理成虛函數(shù),它們之間成為覆蓋關(guān)系;也就是說派生類會在自己虛函數(shù)表中將從基類繼承來的虛函數(shù)進行替換,替換成派生類自己的。
靜態(tài)綁定:編譯時期的多態(tài),通過函數(shù)的重載以及模板來實現(xiàn),也就是說調(diào)用函數(shù)的地址在編譯時期我們就可以確定,在匯編代碼層次,呈現(xiàn)的就是 call 函數(shù)名;
動態(tài)綁定:運行時期的多態(tài),通過派生類重寫基類的虛函數(shù)來實現(xiàn)。在匯編代碼層次,呈現(xiàn)的就是 call 寄存器,寄存器的值只有運行起來我們才可以確定。
不存在虛函數(shù)
#include <iostream> #include <typeinfo> class Base { public: Base(int data = 10): ma(data) {} ~Base() {}; void show() { std::cout << "Base::show()" << std::endl; } void show(int data) { std::cout << "Base::show()" << data << std::endl; } protected: int ma; }; class Derive :public Base { public: Derive(int data) :Base(data), mb(data) {} ~Derive() {} void show() { std::cout << "Derive::show()" << std::endl; } private: int mb; }; int main() { Derive d(50); Base *pb = &d; pb->show();//靜態(tài)(編譯時期)綁定(函數(shù)調(diào)用) Base::show (06F12E4h) pb->show(10);//Base::show (06F12BCh) std::cout << "Base size:" << sizeof(Base) << std::endl;//4 std::cout << "Derive size:" << sizeof(Derive) << std::endl;//8 std::cout << typeid(pb).name() << std::endl;//class Base * std::cout << typeid(*pb).name() << std::endl;//class Base return 0; }
打斷點,F(xiàn)5進入調(diào)試,點擊反匯編
可以看到調(diào)用的都是基類的show(),在編譯階段已經(jīng)生成指令調(diào)用Base下的show;
可以看到結(jié)果:
因為pb是Base類型的指針,所以調(diào)用的都是Base類的成員方法;
基類Base只有一個數(shù)據(jù)成員ma,所以大小只有4字節(jié);
派生類Derive繼承了ma,其次還有自己的mb,所以有8字節(jié);
pb的類型是一個class Base *;
*pb的類型是一個class Base。
為了更好地理解上述過程,我們簡單畫圖如下:
為什么Base *類型的指針,Derive類型的對象,調(diào)用方法的時候是Base而不是Derive呢?
原因如上圖:
Derive類繼承了Base類,導(dǎo)致了派生類的大小要比基類大,而pb的類型是基類的指針,所以通過pb調(diào)用方法時只能訪問到Derive中從Base繼承而來的方法,訪問不到自己重寫的方法(指針的類型限制了指針解引用的能力)
基類定義虛函數(shù)
#include <iostream> #include <typeinfo> class Base { public: Base(int data = 10): ma(data) {} ~Base() {}; //虛函數(shù) virtual void show() { std::cout << "Base::show()" << std::endl; } void show(int data) { std::cout << "Base::show()" << data << std::endl; } protected: int ma; }; class Derive :public Base { public: Derive(int data) :Base(data), mb(data) {} ~Derive() {} void show() { std::cout << "Derive::show()" << std::endl; } private: int mb; }; int main() { Derive d(50); Base *pb = &d; /* pb->show(); pb 指針是base類型,如果發(fā)現(xiàn)Base中的show是虛函數(shù),就進行動態(tài)綁定 mov ecx,dword ptr [pb] 00292B01 8B 45 D4 mov eax,dword ptr [pb] //將pb指向的內(nèi)存前4個字節(jié)放入ecx寄存器,pb指向derive對象,前四個字節(jié)即vfptr,將虛函數(shù)表地址加載到eax 00292B04 8B 10 mov edx,dword ptr [eax] //將eax 的前四個字節(jié) 即Derive::show 加載到edx中 00292B06 8B F4 mov esi,esp 00292B08 8B 4D D4 mov ecx,dword ptr [pb] 00292B0B 8B 02 mov eax,dword ptr [edx] 00292B0D FF D0 call eax //虛函數(shù)的地址 00292B0F 3B F4 cmp esi,esp 00292B11 E8 9C E7 FF FF call __RTC_CheckEsp (02912B2h) 我們可以看到這一次,匯編碼call的就不是確切的函數(shù)地址了,而是寄存器eax; 那么就很好理解了: eax寄存器里存放的是什么內(nèi)容,編譯階段根本無從知曉,只能在運行的時候確定; 故,動態(tài)綁定。 pb->show(10); 如果發(fā)現(xiàn)show是普通函數(shù),就進行靜態(tài)綁定 call Base::show */ pb->show();// pb->show(10);// std::cout << "Base size:" << sizeof(Base) << std::endl;//8 std::cout << "Derive size:" << sizeof(Derive) << std::endl;//12 std::cout << typeid(pb).name() << std::endl;//class Base * /* pb的類型:Base類型,查看Base中有沒有虛函數(shù) (1)Base中沒有虛函數(shù)*pb識別的就是編譯時期的類型 *pb 就是Base類型 (2) Base中有虛函數(shù),*pb識別的就是運行時期的類型 RTTI類型:Derive */ std::cout << typeid(*pb).name() << std::endl;//class Derive return 0; }
在我們添加了virtual關(guān)鍵字后,對應(yīng)的函數(shù)就變成了虛函數(shù);
那么,一個類添加了虛函數(shù),對這個類有什么影響呢?
- 首先,如果類里面定義了虛函數(shù),那么編譯階段,編譯器給這個類類型產(chǎn)生一個唯一的vftable虛函數(shù)表,虛函數(shù)表中主要存儲的內(nèi)容是:RTTI(Run-time Type Information)指針和虛函數(shù)的地址,當程序運行時,每一張?zhí)摵瘮?shù)表都會加載到內(nèi)存的.rodata區(qū);
- 一個類里面定義了虛函數(shù),那么這個類定義的對象,在運行時,內(nèi)存中會多存儲一個vfptr虛函數(shù)指針,指向了對應(yīng)類型的虛函數(shù)表vftable;
- 一個類型定義的n個對象,他們的vfptr指向的都是同一張?zhí)摵瘮?shù)表;
- 一個類里面虛函數(shù)的個數(shù),不影響對象內(nèi)存的大小(vfptr),影響的是虛函數(shù)表的大小。
- 如果派生類中的方法和從基類繼承來的某個方法中返回值、函數(shù)名以及參數(shù)列表都相同,且基類的方法是virtual,那么派生類的這個方法,自動處理成虛函數(shù)
圖示如下:(以Base為例)
虛函數(shù)表
1、RTTI,存放的是類型信息,也就是(Base或者Derive)
2、偏移地址:虛函數(shù)指針相對于對象內(nèi)存空間的偏移,一般vfptr都在0偏移位置
3、下面的函數(shù)時虛函數(shù)入口地址
在Derive類中,由于重寫了show(),因此在Derive的虛函數(shù)表中,是使用子類的show()方法代替了Base類的show()
VS的工具來查看虛函數(shù)表的有關(guān)信息
1 找到
2 在打開的窗口中切換到當前工程所在目錄:
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community>cd C:\Users\Admin\source\repos\C++test\
3 輸入命令:cl XXX.cpp /d1reportSingleClassLayoutXX
(第一個XXX表示源文件的名字,第二個代表你想查看的類類型,我這里就是Derive
)
以看到class Derived的對象的內(nèi)存布局,在派生類對象的開始包含了基類Base的對象,其中有一個虛表指針,指向的就是下面的Derived::$vftable@ (virtual function table),表中包含了Derived類中所有的虛函數(shù)
多重繼承、多繼承 的虛函數(shù)表 1 內(nèi)存分布
假設(shè)有一個基類ClassA,一個繼承了該基類的派生類ClassB,并且基類中有虛函數(shù),派生類實現(xiàn)了基類的虛函數(shù)。
我們在代碼中運用多態(tài)這個特性時,通常以兩種方式起手:
(1) ClassA *a = new ClassB();
(2) ClassB b; ClassA *a = &b;
以上兩種方式都是用基類指針去指向一個派生類實例,區(qū)別在于第1個用了new關(guān)鍵字而分配在堆上,第2個分配在棧上
請看上圖,不同兩種方式起手僅僅影響了派生類對象實例存在的位置。
以左圖為例,ClassA *a是一個棧上的指針。
該指針指向一個在堆上實例化的子類對象。基類如果存在虛函數(shù),那么在子類對象中,除了成員函數(shù)與成員變量外,編譯器會自動生成一個指向**該類的虛函數(shù)表(這里是類ClassB)**的指針,叫作虛函數(shù)表指針。通過虛函數(shù)表指針,父類指針即可調(diào)用該虛函數(shù)表中所有的虛函數(shù)。
2 類的虛函數(shù)表與類實例的虛函數(shù)指針
首先不考慮繼承的情況。如果一個類中有虛函數(shù),那么該類就有一個虛函數(shù)表。
這個虛函數(shù)表是屬于類的,所有該類的實例化對象中都會有一個虛函數(shù)表指針去指向該類的虛函數(shù)表。
從第一部分的圖中我們也能看到,一個類的實例要么在堆上,要么在棧上。也就是說一個類可以有很多很多個實例。但是!一個類只能有一個虛函數(shù)表。在編譯時,一個類的虛函數(shù)表就確定了,這也是為什么它放在了只讀數(shù)據(jù)段中。
3 多態(tài)代碼及多重繼承情況
在第二部分中,我們討論了在沒有繼承的情況下,虛函數(shù)表的邏輯結(jié)構(gòu)。
那么在有繼承情況下,只要基類有虛函數(shù),子類不論實現(xiàn)或沒實現(xiàn),都有虛函數(shù)表。
#include <iostream> using namespace std; class ClassA { public: ClassA() { cout << "ClassA::ClassA()" << endl; } virtual ~ClassA() { cout << "ClassA::~ClassA()" << endl; } void func1() { cout << "ClassA::func1()" << endl; } void func2() { cout << "ClassA::func2()" << endl; } virtual void vfunc1() { cout << "ClassA::vfunc1()" << endl; } virtual void vfunc2() { cout << "ClassA::vfunc2()" << endl; } private: int aData; }; class ClassB : public ClassA { public: ClassB() { cout << "ClassB::ClassB()" << endl; } virtual ~ClassB() { cout << "ClassB::~ClassB()" << endl; } void func1() { cout << "ClassB::func1()" << endl; } virtual void vfunc1() { cout << "ClassB::vfunc1()" << endl; } private: int bData; }; class ClassC : public ClassB { public: ClassC() { cout << "ClassC::ClassC()" << endl; } virtual ~ClassC() { cout << "ClassC::~ClassC()" << endl; } void func2() { cout << "ClassC::func2()" << endl; } virtual void vfunc2() { cout << "ClassC::vfunc2()" << endl; } private: int cData; }; int main() { ClassC c; return 0; }
請看上面代碼
(1) ClassA是基類, 有普通函數(shù): func1() func2() 。虛函數(shù): vfunc1() vfunc2() ~ClassA()
(2) ClassB繼承ClassA, 有普通函數(shù): func1()。虛函數(shù): vfunc1() ~ClassB()
(3) ClassC繼承ClassB, 有普通函數(shù): func2()。虛函數(shù): vfunc2() ~ClassB()
基類的虛函數(shù)表和子類的虛函數(shù)表不是同一個表。下圖是基類實例與多態(tài)情形下,數(shù)據(jù)邏輯結(jié)構(gòu)。注意,虛函數(shù)表是在編譯時確定的,屬于類而不屬于某個具體的實例。虛函數(shù)在代碼段,僅有一份
ClassB繼承與ClassA,其虛函數(shù)表是在ClassA虛函數(shù)表的基礎(chǔ)上有所改動的,變化的僅僅是在子類中重寫的虛函數(shù)。如果子類沒有重寫任何父類虛函數(shù),那么子類的虛函數(shù)表和父類的虛函數(shù)表在內(nèi)容上是一致的
ClassA *a = new ClassB(); a->func1(); // "ClassA::func1()" 隱藏了ClassB的func1() a->func2(); // "ClassA::func2()" a->vfunc1(); // "ClassB::vfunc1()" 重寫了ClassA的vfunc1() a->vfunc2(); // "ClassA::vfunc2()"
這個結(jié)果不難想象,看上圖,ClassA類型的指針a能操作的范圍只能是黑框中的范圍,之所以實現(xiàn)了多態(tài)完全是因為子類的虛函數(shù)表指針與虛函數(shù)表的內(nèi)容與基類不同
這個結(jié)果已經(jīng)說明了C++的隱藏、重寫(覆蓋)特性。
同理,也就不難推導(dǎo)出ClassC的邏輯結(jié)構(gòu)圖了
類的繼承情況是: ClassC繼承ClassB,ClassB繼承ClassA
這是一個多次單繼承的情況。(多重繼承)
4、多繼承下的虛函數(shù)表 (同時繼承多個基類)
多繼承是指一個類同時繼承了多個基類,假設(shè)這些基類都有虛函數(shù),也就是說每個基類都有虛函數(shù)表,那么該子類的邏輯結(jié)果和虛函數(shù)表是什么樣子呢?
#include <iostream> using namespace std; class ClassA1 { public: ClassA1() { cout << "ClassA1::ClassA1()" << endl; } virtual ~ClassA1() { cout << "ClassA1::~ClassA1()" << endl; } void func1() { cout << "ClassA1::func1()" << endl; } virtual void vfunc1() { cout << "ClassA1::vfunc1()" << endl; } virtual void vfunc2() { cout << "ClassA1::vfunc2()" << endl; } private: int a1Data; }; class ClassA2 { public: ClassA2() { cout << "ClassA2::ClassA2()" << endl; } virtual ~ClassA2() { cout << "ClassA2::~ClassA2()" << endl; } void func1() { cout << "ClassA2::func1()" << endl; } virtual void vfunc1() { cout << "ClassA2::vfunc1()" << endl; } virtual void vfunc2() { cout << "ClassA2::vfunc2()" << endl; } virtual void vfunc4() { cout << "ClassA2::vfunc4()" << endl; } private: int a2Data; }; class ClassC : public ClassA1, public ClassA2 { public: ClassC() { cout << "ClassC::ClassC()" << endl; } virtual ~ClassC() { cout << "ClassC::~ClassC()" << endl; } void func1() { cout << "ClassC::func1()" << endl; } virtual void vfunc1() { cout << "ClassC::vfunc1()" << endl; } virtual void vfunc2() { cout << "ClassC::vfunc2()" << endl; } virtual void vfunc3() { cout << "ClassC::vfunc3()" << endl; } }; int main() { ClassC c; return 0; }
ClassA1是第一個基類,擁有普通函數(shù)func1(),虛函數(shù)vfunc1() vfunc2()。
ClassA2是第二個基類,擁有普通函數(shù)func1(),虛函數(shù)vfunc1() vfunc2(),vfunc4()。
ClassC依次繼承ClassA1、ClassA2。普通函數(shù)func1(),虛函數(shù)vfunc1() vfunc2() vfunc3()。
在多繼承情況下,有多少個基類就有多少個虛函數(shù)表指針,前提是基類要有虛函數(shù)才算上這個基類。
如圖,虛函數(shù)表指針01指向的虛函數(shù)表是以ClassA1的虛函數(shù)表為基礎(chǔ)的,子類的ClassC::vfunc1(),和vfunc2()的函數(shù)指針覆蓋了虛函數(shù)表01中的虛函數(shù)指針01的位置、02位置。當子類有多出來的虛函數(shù)時,添加在第一個虛函數(shù)表中。注意:
1.子類虛函數(shù)會覆蓋每一個父類的每一個同名虛函數(shù)。
2.父類中沒有的虛函數(shù)而子類有,填入第一個虛函數(shù)表中,且用父類指針是不能調(diào)用。
3.父類中有的虛函數(shù)而子類沒有,則不覆蓋。僅子類和該父類指針能調(diào)用
虛基類和多重繼承
什么是多重繼承
多重繼承,很好理解,一個派生類
如果只繼承一個基類
,稱作單繼承;
一個派生類
如果繼承了多個基類
,稱作多繼承。
如圖所示:
多重繼承的優(yōu)點
這個很好理解:
多重繼承可以做更多的代碼復(fù)用!
派生類通過多重繼承,可以得到多個基類的數(shù)據(jù)和方法,更大程度的實現(xiàn)了代碼復(fù)用。
關(guān)于菱形繼承的問題
凡事有利也有弊,對于多繼承而言,也有自己的缺點。
我們先通過了解菱形繼承來探究多重繼承的缺點:
菱形繼承是多繼承的一種情況,繼承方式如圖所示:
從圖中我們可以看到:
類B
和類C
從類A
單繼承而來;
而類D
從類B
和類C
多繼承而來。
那么這樣繼承會產(chǎn)生什么問題呢?
我們來看代碼:
#include <iostream> using namespace std; class A { public: A(int data) :ma(data) { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } protected: int ma; }; class B :public A { public: B(int data) :A(data), mb(data) { cout << "B()" << endl; } ~B() { cout << "~B()" << endl; } protected: int mb; }; class C :public A { public: C(int data) :A(data), mc(data) { cout << "C()" << endl; } ~C() { cout << "~C()" << endl; } protected: int mc; }; class D :public B, public C { public: D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; } ~D() { cout << "~D()" << endl; } protected: int md; }; int main() { D d(10); return 0; }
通過運行結(jié)果,我們發(fā)現(xiàn)了問題:
對于基類A
而言,構(gòu)造了兩次,析構(gòu)了兩次!
并且,通過分析各個派生類的內(nèi)存布局我們可以看到:
對于派生類D
來說,間接繼承的基類A
中的數(shù)據(jù)成員ma
重復(fù)了!
這對資源來說是一種浪費與消耗。
(如果多繼承的數(shù)量增加,那么派生類中重復(fù)的數(shù)據(jù)也會增加!)
查看D類的內(nèi)存布局:
其他多重繼承的情況
除了菱形繼承
外,還有其他多重繼承的情況,也會出現(xiàn)相同的問題
比如說圖中呈現(xiàn)的:半圓形繼承。
如何解決多重繼承的問題
通過分析我們知道了,多重繼承的主要問題是,通過多重繼承,有可能得到重復(fù)的基類數(shù)據(jù),并且可能重復(fù)的構(gòu)造和析構(gòu)同一個基類對象。
那么如何能夠避免重復(fù)現(xiàn)象的產(chǎn)生呢?
答案就是:=》虛基類。
什么是虛基類
要理解虛基類,我們首先需要認識virtual關(guān)鍵字的使用場景:
修飾成員方法時:產(chǎn)生虛函數(shù);
修飾繼承方式時:產(chǎn)生虛基類。
對于被虛繼承的類,稱作虛基類。
比如說:
class A { XXXXXX; }; class B : virtual public A { XXXXXX; };
對于這個示例而言,B虛繼承
了A,所以把A稱作虛基類。
虛基類如何解決問題
那么虛基類如何解決上述多重繼承產(chǎn)生的重復(fù)問題呢?
我們來看代碼:
#include <iostream> using namespace std; class A { public: A(int data) :ma(data) { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } protected: int ma; }; class B :virtual public A { public: B(int data) :A(data), mb(data) { cout << "B()" << endl; } ~B() { cout << "~B()" << endl; } protected: int mb; }; class C :virtual public A { public: C(int data) :A(data), mc(data) { cout << "C()" << endl; } ~C() { cout << "~C()" << endl; } protected: int mc; }; class D :public B, public C { public: D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; } ~D() { cout << "~D()" << endl; } protected: int md; };
提示說:"A::A" : 沒有合適的默認構(gòu)造函數(shù)可用
;
為什么會這樣呢?
我們可以這么理解:
剛開始B
和C
單繼承A
的時候,實例化對象時,會首先調(diào)用基類的構(gòu)造函數(shù),也就是A
的構(gòu)造函數(shù),到了D
,由于多繼承了B
和C
,所以在實例化D
的對象時,會首先調(diào)用B
和C
的構(gòu)造函數(shù),然后調(diào)用自己(D)的。
但是這樣會出現(xiàn)A重復(fù)構(gòu)造的問題,所以,采用虛繼承,把有關(guān)重復(fù)的基類A改為虛基類,這樣的話,對于A構(gòu)造的任務(wù)就落到了最終派生類D的頭上,但是我們的代碼中,對于D的構(gòu)造函數(shù):D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }并沒有對A進行構(gòu)造。
所以會報錯。
那么我們就給D
的構(gòu)造函數(shù),調(diào)用A
的構(gòu)造函數(shù):
D(int data) :A(data), B(data), C(data), md(data) { cout << "D()" << endl; }
這一次再運行
我們會發(fā)現(xiàn),問題解決了。
查看虛基類的內(nèi)存布局
我們可以看到當前B
的內(nèi)存空間:
當前B
的內(nèi)存空間里,前四個字節(jié)是vbptr(這個就代表里虛基類指針:virtual base ptr
);
和vfptr
(虛函數(shù)指針)指向了vftable
(虛函數(shù)表)一樣,
vbptr
(虛基類指針)指向了vbtable
(虛基類表)。
vbtable(虛基類表)的布局也如圖所示,
首先是偏移量0
:表示了虛基類指針再內(nèi)存布局中的偏移量;
接著是偏移量8
:表示從虛基類
中繼承而來的數(shù)據(jù)成員在內(nèi)存中的偏移量。
對比普通繼承下的內(nèi)存布局
我們可以對比沒有虛繼承
下的B
的內(nèi)存布局來理解:
我們把他們放在一起對比可以看到:
繼承虛基類
的類(B
和C
)會把自己從虛基類
繼承而來的數(shù)據(jù)ma
放在自己內(nèi)存的最末尾(偏移量最大),并在原來ma
的位置填充一個vbptr
(虛基類指針),這個指針指向了vbtable
(虛基類表)。
理解了B
,我們可以看看更為復(fù)雜的D
可以看到,將ma
移動到了末尾處,并在含有ma
的地方,都用vbptr
進行填充。
這樣一來,就只有一個ma
了!解決了多重繼承的重復(fù)問題。
到此這篇關(guān)于關(guān)于C++虛函數(shù)與靜態(tài)、動態(tài)綁定的問題的文章就介紹到這了,更多相關(guān)C++虛函數(shù)與靜態(tài)、動態(tài)綁定內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談C++的語句語法與強制數(shù)據(jù)類型轉(zhuǎn)換
這篇文章主要介紹了淺談C++的語句語法與強制數(shù)據(jù)類型轉(zhuǎn)換,是C++入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-09-09數(shù)據(jù)結(jié)構(gòu)與算法中二叉樹子結(jié)構(gòu)的詳解
這篇文章主要介紹了數(shù)據(jù)結(jié)構(gòu)與算法中二叉樹子結(jié)構(gòu)的詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04Visual Studio 2022 的安裝和創(chuàng)建C++項目(圖文教程)
本文主要介紹了Visual Studio 2022 的安裝和創(chuàng)建C++項目,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05C++基于reactor的服務(wù)器百萬并發(fā)實現(xiàn)與講解
這篇文章主要介紹了C++基于reactor的服務(wù)器百萬并發(fā)實現(xiàn)與講解,本文通過實例圖文相結(jié)合給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07C++設(shè)計模式編程之Flyweight享元模式結(jié)構(gòu)詳解
這篇文章主要介紹了C++設(shè)計模式編程的Flyweight享元模式結(jié)構(gòu),享元模式在實現(xiàn)過程中主要是要為共享對象提供一個存放的"倉庫"(對象池),需要的朋友可以參考下2016-03-03