成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

探秘C++虛函數(shù)表:從內(nèi)存深處解析多態(tài)的奧秘

開發(fā) 前端
如果后續(xù)需要添加新的圖形類型,如Square(正方形),只需要定義一個新的類繼承自Shape類并實現(xiàn)Draw函數(shù),而無需修改DrawShapes函數(shù)和其他已有的代碼,大大提高了代碼的可擴展性和靈活性 。

在 C++ 這座宏偉的編程大廈中,多態(tài)性無疑是其最閃耀的明珠之一,它賦予了程序在運行時根據(jù)對象的實際類型來決定調(diào)用哪個函數(shù)版本的神奇能力,讓代碼更加靈活、可擴展 。而虛函數(shù)表,作為實現(xiàn) C++ 多態(tài)機制的幕后英雄,就像一把隱藏的鑰匙,掌控著多態(tài)實現(xiàn)的核心秘密。

想象一下,你正在編寫一個大型游戲開發(fā)項目,其中涉及到各種不同類型的游戲角色,如戰(zhàn)士、法師、刺客等。每個角色都有自己獨特的攻擊方式,戰(zhàn)士擅長近身物理攻擊,法師則能釋放強大的魔法技能,刺客以敏捷的身手和致命的一擊見長。如果使用 C++ 的多態(tài)性和虛函數(shù)表,你只需要定義一個基類 “角色”,并在其中聲明一個虛函數(shù) “攻擊”,然后讓戰(zhàn)士、法師、刺客等類繼承自這個基類,并各自重寫 “攻擊” 函數(shù)。這樣,在游戲運行時,當需要某個角色進行攻擊時,程序就能根據(jù)該角色的實際類型,準確地調(diào)用其對應的攻擊方式,無需大量繁瑣的條件判斷語句,大大提高了代碼的簡潔性和可維護性 。

虛函數(shù)表在內(nèi)存中究竟是如何存儲和布局的?它又是怎樣在程序運行時精準地實現(xiàn)函數(shù)的動態(tài)綁定,讓多態(tài)性得以完美呈現(xiàn)?接下來,就讓我們一起深入 C++ 的內(nèi)存世界,揭開虛函數(shù)表神秘的面紗,探尋其中的奧秘。

一、虛函數(shù)表概述

1.1虛函數(shù)表是什么

虛函數(shù)表,英文名為 Virtual Function Table,通常簡稱為 vtable ,它是一個編譯器在編譯階段為包含虛函數(shù)的類生成的存儲虛函數(shù)地址的數(shù)組,是 C++ 實現(xiàn)多態(tài)的關鍵機制。可以將虛函數(shù)表形象地看作是一個 “函數(shù)地址目錄”,在這個特殊的 “目錄” 里,每一項都記錄著對應虛函數(shù)在內(nèi)存中的入口地址。當程序運行過程中需要調(diào)用某個虛函數(shù)時,就可以借助這個 “目錄” 快速定位到函數(shù)的具體位置,從而順利執(zhí)行函數(shù)代碼 。

例如,有一個游戲開發(fā)場景,定義一個基類Character(角色),其中包含一個虛函數(shù)Attack(攻擊):

class Character {
public:
    virtual void Attack() {
        std::cout << "Character attacks in a general way." << std::endl;
    }
};

然后,派生出子類Warrior(戰(zhàn)士)和Mage(法師),它們分別重寫Attack函數(shù),實現(xiàn)各自獨特的攻擊方式:

class Warrior : public Character {
public:
    void Attack() override {
        std::cout << "Warrior attacks with a sword!" << std::endl;
    }
};

class Mage : public Character {
public:
    void Attack() override {
        std::cout << "Mage casts a fireball!" << std::endl;
    }
};

在這個例子中,編譯器會為Character類、Warrior類和Mage類分別生成各自的虛函數(shù)表。Character類的虛函數(shù)表中,Attack函數(shù)的地址指向基類中Attack函數(shù)的實現(xiàn)代碼;Warrior類的虛函數(shù)表,由于重寫了Attack函數(shù),所以表中Attack函數(shù)的地址指向Warrior類中重寫后的Attack函數(shù)實現(xiàn)代碼,Mage類同理。這樣,在程序運行時,就能根據(jù)對象的實際類型,通過虛函數(shù)表準確地找到并調(diào)用相應的攻擊函數(shù)。

1.2為什么需要虛函數(shù)表

在 C++ 中,虛函數(shù)表對于實現(xiàn)運行時多態(tài)起著至關重要的作用。當使用基類指針或引用指向不同的派生類對象時,程序需要在運行時根據(jù)對象的實際類型來確定調(diào)用哪個版本的虛函數(shù),而虛函數(shù)表就是實現(xiàn)這一動態(tài)綁定過程的核心。

假設沒有虛函數(shù)表,當用基類指針指向子類對象并調(diào)用一個被重寫的函數(shù)時,編譯器只能根據(jù)指針的靜態(tài)類型(即基類類型)來確定調(diào)用基類中的函數(shù)版本,無法實現(xiàn)根據(jù)對象實際類型來調(diào)用對應函數(shù)的多態(tài)效果 。例如在前面的游戲角色例子中,如果沒有虛函數(shù)表,當Character* ptr = new Warrior(); 后,調(diào)用ptr->Attack() ,就會一直調(diào)用Character類的Attack函數(shù),而不是Warrior類中重寫后的更符合實際需求的攻擊函數(shù),這顯然無法滿足游戲中不同角色具有不同攻擊方式的多樣化需求。

而有了虛函數(shù)表,當通過基類指針或引用調(diào)用虛函數(shù)時,程序首先會根據(jù)對象內(nèi)存中存儲的虛指針(vptr,每個包含虛函數(shù)的類的對象都會有一個指向其對應類虛函數(shù)表的虛指針,且通常位于對象內(nèi)存布局的前端 )找到對應的虛函數(shù)表,然后在虛函數(shù)表中根據(jù)函數(shù)的索引找到實際要調(diào)用的虛函數(shù)地址,最終調(diào)用該函數(shù)。

這樣,無論基類指針指向哪個派生類對象,都能準確地調(diào)用到派生類中重寫后的虛函數(shù)版本,實現(xiàn)了運行時多態(tài) 。虛函數(shù)表就像是一個智能的 “導航儀”,在復雜的繼承體系中,為程序指引著正確調(diào)用函數(shù)的方向,讓 C++ 的多態(tài)性得以完美呈現(xiàn),大大提高了代碼的靈活性、可擴展性和可維護性 。

二、在內(nèi)存中的布局

2.1對象內(nèi)存布局中的虛函數(shù)表指針

在 C++ 中,當一個類包含虛函數(shù)時,該類的對象內(nèi)存布局會有一個特殊的成員 —— 虛函數(shù)表指針(vptr) 。這個指針就像一把指向虛函數(shù)表的 “鑰匙”,是實現(xiàn)多態(tài)的關鍵紐帶。

在絕大多數(shù)編譯器實現(xiàn)中,虛函數(shù)表指針通常位于對象內(nèi)存的起始處 。以 32 位編譯器為例,指針占用 4 個字節(jié)的內(nèi)存空間;在 64 位編譯器下,指針則占用 8 個字節(jié) 。假設我們有如下簡單的類定義:

class Animal {
public:
    virtual void Speak() {
        std::cout << "Animal makes a sound." << std::endl;
    }
    int m_age;
};

當創(chuàng)建一個Animal類的對象時,如Animal dog; ,在內(nèi)存中,dog對象的前 4 個字節(jié)(32 位編譯器)或前 8 個字節(jié)(64 位編譯器)就是虛函數(shù)表指針 。我們可以通過以下代碼來驗證這一點:

#include <iostream>

class Animal {
public:
    virtual void Speak() {
        std::cout << "Animal makes a sound." << std::endl;
    }
    int m_age;
};

int main() {
    Animal dog;
    dog.m_age = 5;

    // 獲取對象的地址并轉(zhuǎn)換為整數(shù)指針,用于讀取內(nèi)存中的數(shù)據(jù)
    int* ptr = reinterpret_cast<int*>(&dog);
    // 讀取對象內(nèi)存起始處的4個字節(jié),即為虛函數(shù)表指針的值
    int vptr_value = *ptr; 

    std::cout << "The value of vptr in the dog object: " << std::hex << vptr_value << std::endl;

    return 0;
}

在這段代碼中,reinterpret_cast<int*>(&dog)將dog對象的地址轉(zhuǎn)換為整數(shù)指針,這樣就可以通過指針操作讀取對象內(nèi)存中的數(shù)據(jù) 。*ptr讀取的就是對象內(nèi)存起始處的 4 個字節(jié),也就是虛函數(shù)表指針的值 。通過輸出這個值,我們能直觀地看到虛函數(shù)表指針在對象內(nèi)存中的位置和它所指向的虛函數(shù)表地址。

2.2虛函數(shù)表自身在內(nèi)存中的位置

虛函數(shù)表在內(nèi)存中的位置也是一個關鍵知識點 。通常情況下,虛函數(shù)表位于只讀數(shù)據(jù)段(.rodata),也就是 C++ 內(nèi)存模型中的常量區(qū) 。這是因為虛函數(shù)表中的內(nèi)容在程序運行期間是不會改變的,將其放置在只讀數(shù)據(jù)段可以保證數(shù)據(jù)的安全性和穩(wěn)定性,防止程序意外修改虛函數(shù)表內(nèi)容導致運行時錯誤 。

為了驗證這一結(jié)論,我們來看下面的代碼示例:

#include <iostream>

class Base {
public:
    virtual void Func1() {
        std::cout << "Base::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base::Func2" << std::endl;
    }
};

int main() {
    Base obj;
    // 獲取對象的虛函數(shù)表指針
    int* vptr = reinterpret_cast<int*>(&obj);
    // 通過虛函數(shù)表指針獲取虛函數(shù)表的地址
    int vtable_address = *vptr;

    std::cout << "The address of the virtual function table: " << std::hex << vtable_address << std::endl;

    return 0;
}

編譯并運行這段代碼后,我們得到虛函數(shù)表的地址 。接下來,使用工具(如 Linux 下的objdump -s命令來解析 ELF 格式的可執(zhí)行文件中的分段信息)來查看該地址屬于哪個內(nèi)存段 。假設運行程序后得到虛函數(shù)表地址為0x400b40 ,在終端中執(zhí)行objdump -s your_executable_file (your_executable_file為生成的可執(zhí)行文件名),然后在輸出結(jié)果中查找0x400b40所在的內(nèi)存段 。通常會發(fā)現(xiàn),該地址位于.rodata段中,這就驗證了虛函數(shù)表位于只讀數(shù)據(jù)段的結(jié)論 。

三、虛函數(shù)表的動態(tài)變化

3.1單繼承無覆蓋

在單繼承且子類沒有覆蓋父類虛函數(shù)的情況下,子類的虛函數(shù)表結(jié)構(gòu)相對較為直觀 。我們來看下面的代碼示例:

class Base {
public:
    virtual void Func1() {
        std::cout << "Base::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base::Func2" << std::endl;
    }
};

class Derived : public Base {
public:
    virtual void Func3() {
        std::cout << "Derived::Func3" << std::endl;
    }
    virtual void Func4() {
        std::cout << "Derived::Func4" << std::endl;
    }
};

在這個例子中,Base類包含兩個虛函數(shù)Func1和Func2,Derived類繼承自Base類,并且新增了兩個虛函數(shù)Func3和Func4 。此時,Derived類的虛函數(shù)表中,首先會按照聲明順序依次排列父類Base的虛函數(shù)Func1和Func2的地址,然后再接著排列子類Derived新增的虛函數(shù)Func3和Func4的地址 。

我們可以通過一些技巧來驗證這一結(jié)構(gòu) 。在 32 位系統(tǒng)下,假設Derived類對象的內(nèi)存起始地址為0x1000 ,由于虛函數(shù)表指針(vptr)通常位于對象內(nèi)存起始處,占用 4 個字節(jié),所以通過*(int*)0x1000可以獲取到虛函數(shù)表的地址,假設為0x2000 。

虛函數(shù)表是一個存儲虛函數(shù)指針的數(shù)組,每個指針占用 4 個字節(jié) 。那么*(int*)0x2000就是Func1的函數(shù)地址,*(int*)(0x2000 + 4)就是Func2的函數(shù)地址,*(int*)(0x2000 + 8)是Func3的函數(shù)地址,*(int*)(0x2000 + 12)是Func4的函數(shù)地址 。通過這種方式,我們可以清晰地看到在單繼承無覆蓋情況下,子類虛函數(shù)表中父類虛函數(shù)和子類新增虛函數(shù)的排列順序 。

3.2單繼承有覆蓋

當子類覆蓋父類虛函數(shù)時,虛函數(shù)表會發(fā)生重要的變化 。還是以上面的代碼為基礎,假設Derived類覆蓋了Base類的Func1函數(shù):

class Base {
public:
    virtual void Func1() {
        std::cout << "Base::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base::Func2" << std::endl;
    }
};

class Derived : public Base {
public:
    virtual void Func1() {
        std::cout << "Derived::Func1" << std::endl;
    }
    virtual void Func3() {
        std::cout << "Derived::Func3" << std::endl;
    }
    virtual void Func4() {
        std::cout << "Derived::Func4" << std::endl;
    }
};

在這種情況下,Derived類的虛函數(shù)表中,原本指向Base::Func1的函數(shù)地址會被替換為Derived::Func1的函數(shù)地址 。而Func2的地址保持不變,因為它沒有被覆蓋 。新增的虛函數(shù)Func3和Func4依然按照順序排在后面 。

同樣以32位系統(tǒng)下的內(nèi)存地址為例,假設Derived類對象內(nèi)存起始地址為0x1000 ,虛函數(shù)表地址為0x2000 。此時*(int*)0x2000指向的就是Derived::Func1的函數(shù)地址,*(int*)(0x2000 + 4)仍然是Base::Func2的函數(shù)地址,*(int*)(0x2000+8)是Derived::Func3的函數(shù)地址,*(int*)(0x2000 + 12)是Derived::Func4的函數(shù)地址 。這種覆蓋機制確保了在通過基類指針或引用調(diào)用虛函數(shù)時,能夠準確地調(diào)用到子類中重寫后的函數(shù)版本,實現(xiàn)了多態(tài)性 。

3.3多繼承情況

多繼承時,虛函數(shù)表的結(jié)構(gòu)變得更加復雜 。假設有如下代碼:

class Base1 {
public:
    virtual void Func1() {
        std::cout << "Base1::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base1::Func2" << std::endl;
    }
};

class Base2 {
public:
    virtual void Func3() {
        std::cout << "Base2::Func3" << std::endl;
    }
    virtual void Func4() {
        std::cout << "Base2::Func4" << std::endl;
    }
};

class Derived : public Base1, public Base2 {
public:
    virtual void Func1() {
        std::cout << "Derived::Func1" << std::endl;
    }
    virtual void Func5() {
        std::cout << "Derived::Func5" << std::endl;
    }
};

在多繼承中,Derived類會擁有兩個虛函數(shù)表,分別對應Base1和Base2 。在Derived類對象的內(nèi)存布局中,首先是對應Base1的虛函數(shù)表指針,然后是Base1類的其他成員(如果有),接著是對應Base2的虛函數(shù)表指針,再后面是Base2類的其他成員(如果有),最后是Derived類自己的成員 。

對于對應Base1的虛函數(shù)表,其中Func1的地址會被Derived::Func1的地址覆蓋(因為Derived類重寫了Func1 ),F(xiàn)unc2的地址保持為Base1::Func2的地址 。而新增的虛函數(shù)Func5會被添加到這個虛函數(shù)表的末尾 。對應Base2的虛函數(shù)表中,F(xiàn)unc3和Func4的地址分別是Base2::Func3和Base2::Func4的地址,因為Derived類沒有重寫這兩個函數(shù) 。

假設在 64 位系統(tǒng)下,Derived類對象內(nèi)存起始地址為0x1000:

第一個虛函數(shù)表指針(對應Base1 )位于0x1000 ,通過*(int*)0x1000獲取其虛函數(shù)表地址,假設為0x2000 。在這個虛函數(shù)表中,*(int*)0x2000是Derived::Func1的函數(shù)地址,*(int*)(0x2000 + 8)是Base1::Func2的函數(shù)地址,*(int*)(0x2000 + 16)是Derived::Func5的函數(shù)地址 。

第二個虛函數(shù)表指針(對應Base2 )位于0x1008 (64 位系統(tǒng)指針占 8 字節(jié)),通過*(int*)0x1008獲取其虛函數(shù)表地址,假設為0x3000 ,在這個虛函數(shù)表中,*(int*)0x3000是Base2::Func3的函數(shù)地址,*(int*)(0x3000 + 8)是Base2::Func4的函數(shù)地址 。這種復雜的結(jié)構(gòu)使得多繼承在帶來強大功能的同時,也增加了理解和維護的難度 。

四、虛函數(shù)表在編程中的實踐

4.1通過代碼訪問虛函數(shù)表

在 C++ 中,雖然直接訪問虛函數(shù)表并不是常見的操作,但通過了解如何訪問虛函數(shù)表,可以更深入地理解多態(tài)的實現(xiàn)機制 。下面是一個簡單的代碼示例,展示如何通過指針操作獲取虛函數(shù)表地址和虛函數(shù)地址,并調(diào)用虛函數(shù):

#include <iostream>

class Base {
public:
    virtual void Func1() {
        std::cout << "Base::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base::Func2" << std::endl;
    }
};

typedef void(*FunPtr)();// 定義函數(shù)指針類型,用于指向虛函數(shù)

int main() {
    Base obj;
    // 獲取對象的虛函數(shù)表指針,由于虛函數(shù)表指針通常位于對象內(nèi)存起始處,先將對象地址轉(zhuǎn)換為整數(shù)指針,再解引用獲取虛函數(shù)表指針
    int* vptr = reinterpret_cast<int*>(&obj);
    // 通過虛函數(shù)表指針獲取虛函數(shù)表的地址
    int vtable_address = *vptr;

    std::cout << "The address of the virtual function table: " << std::hex << vtable_address << std::endl;

    // 獲取第一個虛函數(shù)(Func1)的地址,虛函數(shù)表是一個存儲虛函數(shù)指針的數(shù)組,每個指針占用4個字節(jié)(32位系統(tǒng)),所以將虛函數(shù)表地址轉(zhuǎn)換為整數(shù)指針后,解引用獲取第一個虛函數(shù)地址
    FunPtr func1_ptr = reinterpret_cast<FunPtr>(*(int*)vtable_address);
    // 調(diào)用第一個虛函數(shù)
    func1_ptr();

    // 獲取第二個虛函數(shù)(Func2)的地址,將指向第一個虛函數(shù)地址的指針偏移4個字節(jié)(32位系統(tǒng)),解引用獲取第二個虛函數(shù)地址
    FunPtr func2_ptr = reinterpret_cast<FunPtr>(*((int*)vtable_address + 1));
    // 調(diào)用第二個虛函數(shù)
    func2_ptr();

    return 0;
}

在這段代碼中,首先通過reinterpret_cast<int*>(&obj)將obj對象的地址轉(zhuǎn)換為整數(shù)指針,然后解引用得到虛函數(shù)表指針vptr 。通過*vptr獲取虛函數(shù)表的地址vtable_address 。接下來,通過將vtable_address轉(zhuǎn)換為FunPtr類型的函數(shù)指針,分別獲取并調(diào)用了虛函數(shù)表中的Func1和Func2函數(shù) 。

這種方式雖然可以直接操作虛函數(shù)表,但在實際開發(fā)中,通常不建議這樣做,因為這依賴于編譯器的實現(xiàn)細節(jié),可能導致代碼的可移植性變差 。不過,通過這種方式可以更直觀地了解虛函數(shù)表在內(nèi)存中的布局和工作原理 。

4.2虛函數(shù)表在多態(tài)編程中的應用場景

虛函數(shù)表在多態(tài)編程中有著廣泛的應用,它使得 C++ 能夠?qū)崿F(xiàn)不同類型對象的統(tǒng)一接口調(diào)用,大大提高了代碼的可擴展性和靈活性 。下面以一個圖形繪制系統(tǒng)為例,來說明虛函數(shù)表在實際項目中的應用 。

假設我們正在開發(fā)一個簡單的圖形繪制系統(tǒng),需要繪制不同類型的圖形,如圓形、矩形和三角形 。我們可以定義一個抽象基類Shape,其中包含一個虛函數(shù)Draw用于繪制圖形 :

#include <iostream>

class Shape {
public:
    virtual void Draw() const = 0;
    virtual ~Shape() = default;
};

然后,分別定義Circle(圓形)、Rectangle(矩形)和Triangle(三角形)類,繼承自Shape類,并實現(xiàn)各自的Draw函數(shù) :

class Circle : public Shape {
private:
    int m_radius;
public:
    Circle(int radius) : m_radius(radius) {}
    void Draw() const override {
        std::cout << "Drawing a circle with radius " << m_radius << std::endl;
    }
};

class Rectangle : public Shape {
private:
    int m_width;
    int m_height;
public:
    Rectangle(int width, int height) : m_width(width), m_height(height) {}
    void Draw() const override {
        std::cout << "Drawing a rectangle with width " << m_width << " and height " << m_height << std::endl;
    }
};

class Triangle : public Shape {
private:
    int m_base;
    int m_height;
public:
    Triangle(int base, int height) : m_base(base), m_height(height) {}
    void Draw() const override {
        std::cout << "Drawing a triangle with base " << m_base << " and height " << m_height << std::endl;
    }
};

在客戶端代碼中,我們可以使用Shape類型指針或引用來操作不同類型的圖形對象,無需關心具體的圖形類型 :

void DrawShapes(const Shape* shapes[], int count) {
    for (int i = 0; i < count; ++i) {
        shapes[i]->Draw();
    }
}

int main() {
    Circle circle(5);
    Rectangle rectangle(10, 5);
    Triangle triangle(8, 6);

    const Shape* shapes[] = { &circle, &rectangle, &triangle };
    int count = sizeof(shapes) / sizeof(shapes[0]);

    DrawShapes(shapes, count);

    return 0;
}

在這個例子中,DrawShapes函數(shù)接受一個Shape類型的指針數(shù)組和數(shù)組的大小,通過遍歷數(shù)組并調(diào)用每個Shape對象的Draw函數(shù),實現(xiàn)了對不同類型圖形的統(tǒng)一繪制操作 。在運行時,根據(jù)每個指針實際指向的對象類型(Circle、Rectangle或Triangle),虛函數(shù)表會動態(tài)地確定調(diào)用哪個類的Draw函數(shù),從而實現(xiàn)了多態(tài)性 。

如果后續(xù)需要添加新的圖形類型,如Square(正方形),只需要定義一個新的類繼承自Shape類并實現(xiàn)Draw函數(shù),而無需修改DrawShapes函數(shù)和其他已有的代碼,大大提高了代碼的可擴展性和靈活性 。這就是虛函數(shù)表在多態(tài)編程中的強大之處,它使得代碼能夠以一種優(yōu)雅、靈活的方式處理各種不同類型的對象 。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2024-04-22 13:22:00

虛函數(shù)象編程C++

2024-01-23 10:13:57

C++虛函數(shù)

2022-07-18 15:32:37

C++虛函數(shù)表

2010-01-18 17:38:54

C++虛函數(shù)表

2024-01-22 11:33:17

C++編程語言開發(fā)

2024-12-19 14:42:15

C++內(nèi)存泄漏內(nèi)存管理

2010-02-01 11:22:09

C++虛函數(shù)

2010-01-18 13:54:28

函數(shù)

2021-03-29 07:40:32

Swift Hook 虛函數(shù)表

2024-12-17 12:00:00

C++對象模型

2011-05-24 16:20:27

虛函數(shù)

2010-01-15 10:22:24

C++語言

2024-12-10 08:00:00

C++CRTP函數(shù)

2010-01-20 18:06:06

C++虛基類

2010-01-18 17:48:46

C++類對象

2011-07-15 00:47:13

C++多態(tài)

2010-01-28 16:16:32

C++多態(tài)性

2010-01-21 11:23:58

C++函數(shù)調(diào)用

2024-03-05 09:55:00

C++右值引用開發(fā)

2010-01-25 17:05:37

C++語言
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产a一区二区 | 成年人在线观看视频 | 99久久夜色精品国产亚洲96 | 色橹橹欧美在线观看视频高清 | 国产探花在线精品一区二区 | 国产一级在线 | 不卡的av在线 | 亚洲一区二区av | 久久久久久国产精品免费免费 | 激情视频中文字幕 | 欧美午夜视频 | 精品一区久久 | 日韩中文字幕在线观看 | 国产一区二区三区在线 | 97伦理最新伦理 | а天堂中文最新一区二区三区 | 欧美激情一区二区三区 | 久久r免费视频 | 日本特黄特色aaa大片免费 | 97精品久久 | 一区二区三区不卡视频 | 免费观看一级特黄欧美大片 | 一区二区三区在线观看视频 | 精品国产伦一区二区三区观看方式 | 亚洲97| 国产1页| 国产精品一区三区 | 日日操夜夜操天天操 | 亚洲社区在线 | 欧美亚洲一区二区三区 | 91色在线 | 秋霞a级毛片在线看 | 国产精品久久久久久久午夜片 | 亚洲精久 | 自拍偷拍第一页 | 黄网站涩免费蜜桃网站 | 亚洲精品电影网在线观看 | 久久久精品久久久 | 三级免费| av天天看| 成人亚洲精品久久久久软件 |