C++多態詳解

目錄

一、多態的概念

二、多態的定義及實現

1.多態的構成條件?

?2.虛函數

3.虛函數的重寫?

4.例題理解(超級重要,強烈建議做一下)

?5.C++11 override和 final

6.重載、覆蓋(重寫)、隱藏(重定義)的對比

三、抽象類

1.概念?

2.接口繼承與實現繼承

四、多態的原理

1.虛函數表?

2.原理

3.靜態綁定與動態綁定

?五、單繼承和多繼承關系的虛函數表

1.單繼承中的虛函數表

?2.多繼承中的虛函數表

l六、一些經典的面試問題


一、多態的概念

多態用通俗的話來說就是多種形態,具體點就是去完成某個行為,當不同的對象去完成時會產生出不同的狀態。

舉個栗子:買票

普通人買票時,是全價買票;學生買票時,是半價買票;軍人買票時是優先買票。

二、多態的定義及實現

1.多態的構成條件?

首先,多態是在不同繼承關系的類對象,去調用同一函數,產生了不同的行為。

比如Student繼承了 Person。Person對象買票全價,Student對象買票半價。

在繼承中構成多態有兩個條件:

1.必須通過基類的指針和引用調用虛函數

2.被調用的函數必須是虛函數 ,且派生類必須對基類的虛函數進行重寫

例:

class Person
{public:virtual void BuyTicket(){ cout << "買票-全價" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){ cout << "買票-半價" << endl;}
};// 多態
// 條件1:虛函數的重寫 -> 父子類中兩個虛函數,三同(函數名、參數、返回)
// 條件2:父類指針或引用去調用虛函數
void Func(Person& p)
{p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

?2.虛函數

被virtual修飾的類成員函數稱為虛函數

class Person
{public:virtual void BuyTicket(){ cout << "買票-全價" << endl;}
};

3.虛函數的重寫?

虛函數的重寫(或者覆蓋):派生類中有一個跟基類完全相同的虛函數(返回類型、函數名、參數列表完全相同),則稱子類的虛函數重寫了基類的虛函數

class Person
{public:virtual void BuyTicket(){ cout << "買票-全價" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){ cout << "買票-半價" << endl;}
};

注:在重寫基類虛函數時,派生類的虛函數在不加virtual關鍵字時,雖然也可以構成重寫(因為繼承后基類的虛函數被繼承下來了在派生類依舊保持虛函數屬性),但是該種寫法不是很規范,不建議這樣使用

虛函數重寫的兩個特殊:

1.協變(基類與派生類虛函數返回值類型不同)

即基類虛函數返回基類對象的指針或者引用,派生類虛函數返回派生類對象的指針或者引用時,稱為協變

class A
{};class B : public A
{};class Person
{
public:virtual A* f(){ return new A;}
};class Student : public Person
{
public:virtual B* f(){ return new B;}
};

2.析構函數的重寫(基類與派生類析構函數的名字不同)

如果基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字, 都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。

雖然函數名不相同, 看起來違背了重寫的規則,其實不然,這里可以理解為編譯器對析構函數的名稱做了特殊處理,編譯后析構函數的名稱統一處理成destructor。

class Person
{
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生類Student的析構函數重寫了Person的析構函數,下面的delete對象調用析構函
//數,才能構成多態,才能保證p1和p2指向的對象正確的調用析構函數。
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

4.例題理解(超級重要,強烈建議做一下)

class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl;}virtual void test() { func(); }
};class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}

A:A->0? ? B:B->1? ?C:A->1? ?D:B->0? ?E:編譯出錯? ?F:以上都不正確?

烙鐵們猜猜選哪個??

?首先這是一個多態調用,看起來不太像,但是它滿足多態調用的兩個條件

1.被調用的函數必須是虛函數 ,且派生類必須對基類的虛函數進行重寫?:func()是虛函數重寫

2.通過基類的指針和引用調用虛函數:首先派生類B中繼承了test(),可以調用test(),然后再test()中調用func(),但是在這個test()中調用func函數的this指針是A*還是B*呢?答案是A*,當this是A*時即滿足通過基類指針或引用調用虛函數,可以構成多態,為什么呢?因為在派生類B中調用test函數時,實則是調用基類A中的test函數

這里我們可以了解一個知識:子類繼承父類的成員函數只是說子類可以調用父類的函數,并不是拷貝一份在子類里

函數的調用是先子后父,先看看子類中有沒有,沒有再去父類中找,因此滿足第二個條件

此時我想大多數烙鐵都會說答案是 D,但是答案是B

這里有一個很隱晦的知識點:在多態調用中(記住是在多態調用中,不是普通函數調用),虛函數的重寫是重寫它的實現,但用的是基類函數的聲明

就像這樣:

然后變成下面這樣:?

?

所以答案選B,出題人真是太心機了?

?5.C++11 override和 final

C++中對函數重寫的要求比較嚴格,有些情況下由于疏忽,可能會導致函數名字母次序寫反或其他錯誤而無法構成重寫,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有得到預期結果才會發現,因此:C++11提供了override和final兩個關鍵字,可以幫助用戶檢測是否重寫

?1.final:修飾虛函數,表示虛函數不能被重寫

class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "小米su7" << endl; }
};

2.override: 檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯

class Car {
public:virtual void Drive() {}
};
class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒適" << endl; }
};

6.重載、覆蓋(重寫)、隱藏(重定義)的對比

1,重載首先必須在同一個作用域中,而隱藏和重寫在不同類的作用域中
2,隱藏只要函數名相同即可,重寫則要是虛函數,且是三同(返回類型,參數類型,函數名)
3,重寫相當于隱藏的一個分支

?

三、抽象類

1.概念?

在虛函數的后面寫上 =0 ,則這個函數為純虛函數,包含純虛函數的類叫做抽象類(也叫接口類)

抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生類才能實例化出對象。

純虛函數規范了派生類必須重寫,另外純虛函數更體現出了接口繼承。

class Car
{
public:virtual void Drive() = 0;
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz" << endl;}
};
class Xiaomisu7 :public Car
{
public:virtual void Drive(){cout << "xiaomisu7" << endl;}
};
void Test()
{Car* pBenz = new Benz;pBenz->Drive();Car* xiaomisu7 = new Xiaomisu7;xiaomisu7->Drive();
}

子類不實現父類所有的純虛函數,則子類還屬于抽象類,仍然不能實例化對象?

2.接口繼承與實現繼承

普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實 現。

虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。

四、多態的原理

1.虛函數表?

觀察下面代碼:

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
int main()
{Base a;return 0;
}

通過觀察測試我們發現b對象中除了_b成員,還多一個__vfptr放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關),對象中的這個指針我們叫做虛函數表指針(v代表virtual,f代表function)。

一個含有虛函數的類中都至少都有一個虛函數表指針,因為虛函數的地址要被放到虛函數表中,虛函數表也簡稱虛表,那么派生類中這個表放了些什么呢?我們接著往下分析?

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive1::Func1()" << endl;}
private:int _d = 2;
};int main()
{Base a;Derive d;return 0;
}

通過觀察和測試,我們發現:

1. 派生類對象d中也有一個虛表指針,d對象由兩部分構成,一部分是父類繼承下來的成員,虛表指針也就是存在部分的另一部分是自己的成員。

2. 基類b對象和派生類d對象虛表是不一樣的,這里我們發現Func1完成了重寫,所以d的虛表中存的是重寫的Derive::Func1,所以虛函數的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。

3. 另外Func2繼承下來后是虛函數,所以放進了虛表,Func3也繼承下來了,但是不是虛函數,所以不會放進虛表。

4. 虛函數表本質是一個存虛函數指針的指針數組,一般情況這個數組最后面放了一個nullptr。

5. 總結一下派生類的虛表生成:

a.先將基類中的虛表內容拷貝一份到派生類虛表中

b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數

c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后。

6. 這里還有一個烙鐵們很容易混淆的問題:虛函數存在哪的?虛表存在哪的?

答:虛函數存在虛表,虛表存在對象中。注意上面的回答的錯的。

但是很多童鞋都是這樣深以為然的。注意虛表存的是虛函數指針,不是虛函數,虛函數和普通函數一樣的,都是存在代碼段的,只是他的指針又存到了虛表中

另外對象中存的不是虛表,存的是虛表指針。那么虛表存在哪的呢?實際我們去驗證一下會發現vs下是存在代碼段的

?7.內存:內存對齊,多個虛函數會被放在一個虛表中,由一個虛表指針指向

8.多繼承的時候,就會可能有多張虛表

9.父類對象的虛表與子類對象的虛表沒有任何關系,這是兩個不同的對象

10.虛表是在編譯期間生成的

11.一個類的不同對象共享該類的虛表,可以自行寫代碼驗證之

2.原理

了解了虛函數表,下面我們來具體分析一下多態的原理

還是以買票舉例:

class Person
{
public:virtual void BuyTicket(){ cout << "買票-全價" << endl;}
};
class Student : public Person
{
public:virtual void BuyTicket(){cout << "買票-半價" << endl;}
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}

?

1. 觀察下圖的紅色箭頭我們看到,p是指向mike對象時,p->BuyTicket在mike的虛表中找到虛函數是Person::BuyTicket。

2. 觀察下圖的藍色箭頭我們看到,p是指向johnson對象時,p->BuyTicket在johson的虛表中找到虛函數是Student::BuyTicket。

3. 這樣就實現出了不同對象去完成同一行為時,展現出不同的形態

4.要了解的是滿足多態的函數調用,不是在編譯時確定的,是運行起來以后到對象的中取找的。不滿足多態的函數調用時編譯時確認好的

總之多態調用時在運行時在虛函數表找函數的地址,進行調用,所以指向父類調的是父類的虛函數,指向子類調用的是子類的虛函數,而普通調用時是在編譯時,通過調用者類型確定函數地址

3.靜態綁定與動態綁定

?1. 靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態, 比如:函數重載,總之就是在編譯時確定函數的地址,在編譯時時實現多態,本質是函數名修飾規則

2. 動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體行為,調用具體的函數,也稱為動態多態。總之就是在運行時確定函數的地址,在運行時實現多態

?五、單繼承和多繼承關系的虛函數表

1.單繼承中的虛函數表

class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};int main()
{Base b;Derive d;return 0;
}

下圖中的監視窗口中我們發現看不見func3和func4。這里是編譯器的監視窗口故意隱藏了這兩個函數,也可以認為是他的一個小bug。

Base類對象和Derive類對象前4個字節存儲的都是虛表的地址,只是各自指向各自的虛表

我們可以將虛表中的函數打印出來,取出b、d對象的頭4bytes,就是虛表的指針,前面我們說了虛函數表本質是一個存虛函數指針的指針數組,這個數組最后面放了一個nullptr,具體代碼就不寫了,主要是觀察?

?2.多繼承中的虛函數表

class Base1
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2
{
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};int main()
{Derive d;return 0;
}

同上打印一下

??

?可以看出,多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中

就像這樣

l六、一些經典的面試問題

1. 什么是多態?

可以從動態多態和靜態多態兩個角度去講

?靜:靜態多態是編譯時的多態,例如函數重載,會自動匹配類型,本質是函數名修飾規則用修飾? ?后的函數名字去符號表里去找函數的地址
?動:運行時到指向對象的虛表里去找?

2. 什么是重載、重寫(覆蓋)、重定義(隱藏)? 參考上述博客

3. 多態的實現原理?多態調用時在運行時在虛函數表找函數的地址,進行調用,所以指向父類調的是父類的虛函數,指向子類調用的是子類的虛函數(具體參考上述博客)

4. inline函數可以是虛函數嗎?

可以,不過編譯器就忽略inline屬性,這個函數就不再是 inline,因為虛函數要放到虛表中去。

5. 靜態成員可以是虛函數嗎?

不能,因為靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。

6. 構造函數可以是虛函數嗎?

不能,因為對象中的虛函數表指針是在構造函數初始化列表階段才初始化的。

7. 析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?

可以,并且最好把基類的析構函數定義成虛函數

8. 對象訪問普通函數快還是虛函數更快?

首先如果是普通對象,是一樣快的。

如果是指針對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找。

9. 虛函數表是在什么階段生成的,存在哪的?

虛函數表是在編譯階段就生成的,一般情況下存在代碼段(常量區)

10.抽象類的作用?

抽象類強制重寫了虛函數,另外抽象類體現出了接口繼承關系。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/14806.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/14806.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/14806.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【yijiej】mysql報錯 之 報錯:Duplicate entry 字段 for key ‘表名.idx_字段’

一、問題操作 Mysql 進行insert 操作&#xff0c;報錯&#xff1a;Duplicate entry 字段 for key ‘表名.idx_字段’ 原因解析&#xff1a;idx 是做的索引鍵&#xff0c;是具有唯一性二、問題原因&#xff08;三種情況&#xff0c;當前我遇到的情況是第一種&#xff09; 1、當 …

零基礎代碼隨想錄【Day42】|| 1049. 最后一塊石頭的重量 II,494. 目標和,474.一和零

目錄 DAY42 1049.最后一塊石頭的重量II 解題思路&代碼 494.目標和 解題思路&代碼 474.一和零 解題思路&代碼 DAY42 1049.最后一塊石頭的重量II 力扣題目鏈接(opens new window) 題目難度&#xff1a;中等 有一堆石頭&#xff0c;每塊石頭的重量都是正整…

(Qt) 默認QtWidget應用包含什么?

文章目錄 ?前言?創建&#x1f6e0;?選擇一個模板&#x1f6e0;?Location&#x1f6e0;?構建系統&#x1f6e0;?Details&#x1f6e0;?Translation&#x1f6e0;?構建套件(Kit)&#x1f6e0;?匯總 ?項目??概要??構建步驟??清除步驟 ?Code&#x1f526;untitled…

【EasyX】快速入門——消息處理,音頻

1.消息處理 我們先看看什么是消息 1.1.獲取消息 想要獲取消息,就必須學會getmessage函數 1.1.1.getmessage函數 有兩個重載版本,它們的作用是一樣的 參數filter可以篩選我們需要的消息類型 我們看看參數filter的取值 當然我們可以使用位運算組合這些值 例如,我們…

華為CE6851-48S6Q-HI升級設備版本及補丁

文章目錄 升級前準備工作筆記本和交換機設備配置互聯地址啟用FTP設備訪問FTP設備升級系統版本及補丁 升級前準備工作 使用MobaXterm遠程工具連接設備&#xff0c;并作為FTP服務器準備升級所需的版本文件及補丁文件 筆記本和交換機設備配置互聯地址 在交換機接口配置IP&#…

Facebook隱私保護:數據安全的前沿挑戰

在數字化時代&#xff0c;隨著社交媒體的普及和應用&#xff0c;個人數據的隱私保護問題日益受到關注。作為全球最大的社交平臺之一&#xff0c;Facebook承載了數十億用戶的社交活動和信息交流&#xff0c;但與此同時&#xff0c;也面臨著來自內外部的數據安全挑戰。本文將深入…

AWS Elastic Beanstalk 監控可觀測最佳實踐

一、概述 Amazon Web Services (AWS) 包含一百多種服務&#xff0c;每項服務都針對一個功能領域。服務的多樣性可讓您靈活地管理 AWS 基礎設施&#xff0c;然而&#xff0c;判斷應使用哪些服務以及如何進行預配置可能會非常困難。借助 Elastic Beanstalk&#xff0c;可以在 AW…

【LinuxC語言】一切皆文件的理念

文章目錄 引言一、什么是“一切皆文件”&#xff1f;1. 文件柜的類比2. 統一的操作方式3. 舉個具體例子4. 設備文件5. 進程和網絡連接6. 簡化管理 二、這一設計的優勢1. 統一接口2. 靈活性3. 簡化了系統管理4. 增強了系統安全性 結論 引言 Linux 操作系統以其獨特的設計理念和…

如何使用JMeter 進行全鏈路壓測

使用 JMeter 進行全鏈路壓測&#xff1a;詳細步驟指南 全鏈路壓測旨在測試整個系統的性能&#xff0c;包括所有的組件和服務。通過 Apache JMeter 進行全鏈路壓測&#xff0c;可以模擬真實用戶行為&#xff0c;測試系統在高負載下的表現。以下是詳細的步驟指南&#xff0c;分為…

AWTK實現汽車儀表Cluster/DashBoard嵌入式GUI開發(七):快啟

前言: 汽車儀表是人們了解汽車狀況的窗口,而儀表中的大部分信息都是以指示燈形式顯示給駕駛者。儀表指示燈圖案都較為抽象,對駕駛不熟悉的人在理解儀表指示燈含義方面存在不同程度的困難,尤其對于駕駛新手,如果對指示燈的含義不求甚解,有可能影響駕駛的安全性。即使是對…

Pytest框架實戰二

在Pytest框架實戰一中詳細地介紹了Pytest測試框架在參數化以及Fixture函數在API測試領域的實戰案例以及具體的應用。本文章接著上個文章的內容繼續闡述Pytest測試框架優秀的特性以及在自動化測試領域的實戰。 conftest.py 在上一篇文章中闡述到Fixture函數的特性&#xff0c;第…

shell循環

一、for循環 用法&#xff1a; for 變量 in 取值列表 do 命令序列 done 例1&#xff1a;打印1到10的數字列表 #!/bin/bashfor i in {1..10} do echo $i done 例2&#xff1a;#批量添加用戶,用戶名存放在users.txt文件中&#xff0c;每行一個,初始密碼均設為123456 #!/bin/bas…

KMP算法【C++】

KMP算法測試 KMP 算法詳解 根據解釋寫出對應的C代碼進行測試&#xff0c;也可以再整理成一個函數 #include <iostream> #include <vector>class KMP { private:std::string m_pat;//被匹配的字符串std::vector<std::vector<int>> m_dp;//狀態二維數組…

怎樣解決Redis高并發競爭Key難點?

Redis作為一種高性能的鍵值存儲系統&#xff0c;在現代分布式系統中發揮著重要作用。然而&#xff0c;高并發場景下對同一Key的操作可能引發競爭條件&#xff0c;給系統穩定性和數據一致性帶來挑戰。本文將探討如何解決這一問題&#xff0c;為讀者提供有效的應對策略。 1. Red…

【002】FlexBison實現原理

0. 前言 Flex和Bison是用于構建處理結構化輸入的程序的工具。它們最初是用于構建編譯器的工具&#xff0c;但它們已被證明在許多其他領域都很有用。 &#xfeff; 在第一章中&#xff0c;我們將首先看一點(但不是太多)它們背后的理論&#xff0c;然后我們將深入研究一些使用它…

Mysql和Postgresql創建用戶和授權命令

Mysql和Postgresql創建用戶和授權命令 MySQL/MariaDB/TiDB mysql -uroot -P3306 -p 輸入密碼&#xff1a;xxx create user user1% identified by xxx; grant all privileges on *.* to user1%; create user user2% identified by xxx; grant all privileges on *.* to user2%;…

Winform /C# 截圖當前窗體,指定區域,當前屏幕

1.當前窗體 public static Image CaptureControl(Control ctrl){System.Drawing.Bitmap bmp new System.Drawing.Bitmap(ctrl.Width, ctrl.Height);ctrl.DrawToBitmap(bmp, new Rectangle(0, 0, ctrl.Width, ctrl.Height));return bmp;}private void DownLoad(){string filePa…

java類中運行main方法時報錯:找不到或無法加載主類 XXX

運行main類報了這個錯 錯誤: 找不到或無法加載主類 XXX 經過好一番查證才找出了問題所在 原因是 maven項目的provided導致的&#xff0c;現在記錄一下。 將pom.xml中標注provided的注釋掉&#xff0c;就不報錯了。

ERROR [internal] load metadata for docker.io/library/node:20-alpine

docker編譯時報錯&#xff0c;除標題外&#xff0c;還報如下信息 ERROR: failed to solve: node:20-alpine: failed to resolve source metadata for docker.io/library/node:20-alpine: failed to do request: Head "https://registry-1.docker.io/v2/library/node/mani…

常用個人信息

目錄 常用聯系方式我的自動思維常用媒體專業相關康米相關黑歷史 常用聯系方式 QQ&#xff1a;2868679921 微信&#xff1a;Commieee 郵箱&#xff1a;sharvefoxmail.com 我的自動思維 常用媒體 嗶哩嗶哩 專業相關 博客 康米相關 QQ&#xff1a;1203361015 黑歷史 貼吧…