一篇文章彻底弄懂C++虚函数的实现机制

目录
  • 1、虚函数简介
  • 2、虚函数表简介
  • 3、有继承关系的虚函数表剖析
    • 3.1、单继承无虚函数覆盖的情况
    • 3.2、单继承有虚函数覆盖的情况
    • 3.3、多重继承的情况
    • 3.4、多层继承的情况
  • 4、总结

1、虚函数简介

C++中有两种方式实现多态,即重载和覆盖。

  • 重载:是指允许存在多个同名函数,而这些函数的参数表不同(参数个数不同、参数类型不同或者两者都不同)。
  • 覆盖:是指子类重新定义父类虚函数的做法,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针拥有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法,比如:模板元编程是在编译期完成的泛型技术,RTTI、虚函数则是在运行时完成的泛型技术。

关于虚函数的具体使用方法,建议大家先去阅读相关的C++的书籍,本文只剖析虚函数的实现机制,让大家对虚函数有一个更加清晰的认识,并不对虚函数的具体使用方法作过多介绍。本文是依据个人经验和查阅相关资料最终编写的,如有错漏,希望大家多多指正。

2、虚函数表简介

学过C++的人都应该知道虚函数(Virtual Function)是通过虚函数表(Virtual Table,简称为V-Table)来实现的。虚函数表主要存储的是指向一个类的虚函数地址的指针,通过使用虚函数表,继承、覆盖的问题都都得到了解决。假如一个类有虚函数,当我们构建这个类的实例时,将会额外分配一个指向该类虚函数表的指针,当我们用父类的指针来操作一个子类的时候,这个指向虚函数表的指针就派上用场了,它指明了此时应该使用哪个虚函数表,而虚函数表本身就像一个地图一样,为编译器指明了实际所应该调用的函数。指向虚函数表的指针是存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下),这就意味着理论上我们可以通过对象实例的地址得到这张虚函数表(实际上确实可以做到),然后对虚函数表进行遍历,并调用其中的函数。

前面说了一大堆理论,中看不中用,下面还是通过一个实际的例子验证一下前面讲的内容,首先定义一个Base类,该类有三个虚函数,代码如下:

#include <iostream>
#include <string>

typedef void (*Fun)(void);

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

    virtual void g()
    {
        std::cout << "Base::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base::h()" << std::endl;
    }
};

接下来按照前面的说法,我们通过Base类的实例对象base来获取虚函数表,代码如下:

int main(int argc, char* argv[])
{
    Base base;
    Fun fun = nullptr;

    std::cout << "指向虚函数表指针的地址:" << (long*)(&base) << std::endl;
    std::cout << "虚函数表的地址:" << (long*)*(long*)(&base) << std::endl;

    fun = (Fun)*((long*)*(long*)(&base));
    std::cout << "虚函数表中第一个函数的地址:" << (long*)fun << std::endl;
    fun();

    fun = (Fun)*((long*)*(long*)(&base) + 1);
    std::cout << "虚函数表中第二个函数的地址:" << (long*)fun << std::endl;
    fun();

    fun = (Fun)*((long*)*(long*)(&base) + 2);
    std::cout << "虚函数表中第三个函数的地址:" << (long*)fun << std::endl;
    fun();
}

运行结果图2-1所示(Linux 3.10.0 + GCC 4.8.5):

图2-1 程序运行结果

在上面的例子中我们通过把&base强制转换成long *,来取得指向虚函数表的指针的地址,然后对这个地址取值就可以得到对应的虚函数表了。得到对应虚函数表的首地址后,就可以通过不断偏移该地址,依次得到指向真实虚函数的指针了。这么说有点绕也有点晕,下面通过一幅图解释一下前面说的内容,详见图2-2

图2-2 基类虚函数表内存布局

当然,上述内容也可以在GDB中调试验证,后续的内容也将全部在GDB下直接验证,调试的示例见图2-3:

图2-3 GDB查看基类虚函数表内存布局

3、有继承关系的虚函数表剖析

前面分析虚函数表的场景是没有继承关系的,然而在实际开发中,没有继承关系的虚函数纯属浪费表情,所以接下来我们就来看看有继承关系下虚函数表会呈现出什么不一样的特点,分析的时候会分别就单继承无虚函数覆盖、单继承有虚函数覆盖、多重继承、多层继承这几个场景进行说明。

3.1、单继承无虚函数覆盖的情况

先定义一个Base类,再定义一个Derived类,Derived类继承于Base类,代码如下:

#include <iostream>
#include <string>

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

    virtual void g()
    {
        std::cout << "Base::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base::h()" << std::endl;
    }
};

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

    virtual void g1()
    {
        std::cout << "Derived::g1()" << std::endl;
    }

    virtual void h1()
    {
        std::cout << "Derived::h1()" << std::endl;
    }
};

继承关系如图3-1所示:

图3-1 类继承关系UML图

测试的代码如下,因为等下要使用GDB来验证,所以就随便写点,定义个Derived类实例就行了

int main(int argc, char* argv[])
{
    Derived derived;
    derived.f();
}

派生类Derived的虚函数表内存布局如图3-2所示:

图3-2 单继承无虚函数覆盖情况下派生类虚函数表内存布局

接下来就用GDB调试一下,验证上图的内存布局是否正确,如图3-3所示:

图3-3 GDB查看单继承无虚函数覆盖情况下派生类虚函数表内存布局

从调试结果可以看出图3-2是正确的,Derived的虚函数表中先放Base的虚函数,再放Derived的虚函数。

3.2、单继承有虚函数覆盖的情况

派生类覆盖基类的虚函数是很有必要的事情,不这么做的话虚函数的存在将毫无意义。下面我们就来看一下如果派生类中有虚函数覆盖了基类的虚函数的话,对应的虚函数表会是一个什么样子。还是老规矩先定义两个有继承关系的类,注意一下我这里只覆盖了基类的g()

#include <iostream>
#include <string>

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

    virtual void g()
    {
        std::cout << "Base::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base::h()" << std::endl;
    }
};

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

    virtual void g()
    {
        std::cout << "Derived::g()" << std::endl;
    }

    virtual void h1()
    {
        std::cout << "Derived::h1()" << std::endl;
    }
};

继承关系如图3-4所示:

图3-4 类继承关系UML图

测试的代码如下,因为等下要使用GDB来验证,所以就随便写点,定义个Derived类实例就行了

int main(int argc, char* argv[])
{
    Derived derived;
    derived.g();
}

派生类Derived的虚函数表内存布局如图3-5所示:

图3-5 单继承有虚函数覆盖情况下派生类虚函数表内存布局

接下来就用GDB调试一下,验证上图的内存布局是否正确,如图3-6所示:

图3-6 GDB查看单继承有虚函数覆盖情况下派生类虚函数表内存布局

从调试结果可以看出图3-5是正确的,并且可以得到以下几点信息:

覆盖的g()被放到了虚表中原来父类虚函数的位置没有被覆盖的虚函数位置排序依旧不变

有了前面的理论基础,我们可以知道对于下面的代码,由base所指的内存中的虚函数表的Base::g()的位置已经被Derived::g()所取代,于是在实际调用发生时,调用的是Derived::g(),从而实现了多态

int main(int argc, char* argv[])
{
    Base* base = new Derived();
    base->f();
    base->g();
    base->h();
}

输出结果如图3-7所示:

图3-7 程序运行结果

注意:在前面的例子中,我们分配内存的实例对象的类型是Derived,但是却用Base的指针去引用它,这个过程中数据并没有发生任何的转换,实例的真实类型依旧是Derived,但是由于我们使用时用的是Base类型,所以函数调用要依据Base类来,不能胡乱调用,比如说我们此时是无法调用Derived的f1()和h1()的。由于这个是个单继承,不存在虚函数表选择问题,相对比较简单。

3.3、多重继承的情况

多重继承就不分开讲有覆盖和无覆盖的情况了,其实结合前面讲的就差不多知道是什么个情况了,下面的例子中会设计成派生类既有自己的虚函数,又有用于覆盖基类的虚函数,这样就能兼顾有覆盖和无覆盖的情况了。

类的设计如下:

#include <iostream>
#include <string>

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

    virtual void g()
    {
        std::cout << "Base1::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base1::h()" << std::endl;
    }
};

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

    virtual void g()
    {
        std::cout << "Base2::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base2::h()" << std::endl;
    }
};

class Base3
{
public:
    virtual void f()
    {
        std::cout << "Base3::f()" << std::endl;
    }

    virtual void g()
    {
        std::cout << "Base3::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base3::h()" << std::endl;
    }
};

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

    virtual void g1()
    {
        std::cout << "Derived::g1()" << std::endl;
    }

    virtual void h1()
    {
        std::cout << "Derived::h1()" << std::endl;
    }
};

继承关系如图3-8所示:

图3-8 类继承关系UML图

测试的代码如下:

int main(int argc, char* argv[])
{
    Derived* d = new Derived();
    Base1* b1 = d;
    Base2* b2 = d;
    Base3* b3 = d;
    std::cout << (long*)(*(long*)b1) << std::endl;
    std::cout << (long*)(*(long*)b2) << std::endl;
    std::cout << (long*)(*(long*)b3) << std::endl;
}

输出结果如图3-9所示:

图3-9 程序运行结果

输出信息非常有趣,明明b1、b2、b3指向的都是d,但是它们各自取出来的虚函数表的地址却完全不同,按理来说不是应该相同吗?别急,下面我们通过图3-10来看一看多继承下派生类虚函数表的内存布局是什么样的

图3-10 多重继承情况下派生类虚函数表内存布局

从图3-10中可以看出以下几点信息:

  • 在派生类中,每个基类都有一个属于自己的虚函数表
  • 派生类自己特有的虚函数被放到了第一个基类的表中(第一个基类是按照继承顺序来确定的)

这里我们就会得出一个新问题了,对于上面例子中的b1,这个没啥问题,因为它的类型Base1就是第一个被继承的,所以我们当然可以认为这个不会出任何问题,但是对于b2呢,它被继承的位置可不是第一个啊,运行时要怎么确定它的虚函数表呢?它有没有可能一不小心找到Base1的虚函数去?恰好这个例子中几个基类的虚函数名字和参数又都是完全相同的。这里其实就涉及到编译器的处理了,当我们执行赋值操作Base2* b2 = d;时,编译器会自动把b2的虚函数表指针指向正确的位置,这个过程应该是编译器做的,所以虚函数所实现的多态应该是“静动结合”的,有部分工作需要在编译时期完成的。

下面我们依然借助GDB来看一下实际的内存布局,详见图3-11,从调试信息中可以看出此时确实有三张虚函数表,对应三个基类

图3-11 GDB查看多重继承情况下派生类虚函数表内存布局

第一张表的数据如图3-12所示,可以看到和图3-10描述的内容是一致的,Derived自己特有的虚函数确实被加入到了第一张表中了,这里指示虚函数表结束的表示好像是那个0xfffffffffffffff8,不知道是不是固定的,有知道的小伙伴麻烦评论区告诉我一下谢谢

图3-12 派生类第一张虚函数表

第二张表的数据如图3-13所示,这里的结束符变成了0xfffffffffffffff0,搞不懂

图3-13 派生类第二张虚函数表

第三张表的数据如图3-14所示,这里的结束符终于是0x0了

图3-14 派生类第三张虚函数表

补充说明:如果继承的某个类没有虚函数的话,比如说将上面的Base2修改为以下格式:

class Base2
{
public:
    void f()
    {
        std::cout << "Base2::f()" << std::endl;
    }

    void g()
    {
        std::cout << "Base2::g()" << std::endl;
    }

    void h()
    {
        std::cout << "Base2::h()" << std::endl;
    }
};

main函数不变,再运行以下程序,输出结果如图3-15所示,说明此时就没有指向Base2虚函数表的指针了,因为它本来就没有虚函数表

图3-15 程序运行结果

3.4、多层继承的情况

多层继承的在有前面的基础上来理解就非常简单了,测试程序如下:

#include <iostream>
#include <string>

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

    virtual void g()
    {
        std::cout << "Base::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base::h()" << std::endl;
    }
};

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

    virtual void g1()
    {
        std::cout << "Derived::g1()" << std::endl;
    }
};

class DDerived : public Derived
{
public:
    virtual void f()
    {
        std::cout << "DDerived::f()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "DDerived::h()" << std::endl;
    }

    virtual void g2()
    {
        std::cout << "DDerived::g2()" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    DDerived dd;
    dd.f();
}

继承关系如图3-16所示:

图3-16 类继承关系UML图

派生类DDerived的虚函数表内存布局如图3-17所示:

图3-17 多层继承情况下派生类虚函数表内存布局

多层继承的情况这里就不使用GDB去看内存布局了,比较简单,大家可以自行去测试一下。

4、总结

本文先对虚函数的概念进行了简单介绍,引出了虚函数表这个实现虚函数的关键要素,然后对不同继承案例下虚函数表的内存布局进行说明,并使用GDB进行实战验证。相信看完这篇文章后聪明的你会对虚函数有更加深刻的理解了。

到此这篇关于C++虚函数实现机制的文章就介绍到这了,更多相关C++虚函数实现机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++ 中的虚函数表及虚函数执行原理详解

    为了实现虚函数,C++ 使用了虚函数表来达到延迟绑定的目的.虚函数表在动态/延迟绑定行为中用于查询调用的函数. 尽管要描述清楚虚函数表的机制会多费点口舌,但其实其本身还是比较简单的. 首先,每个包含虚函数的类(或者继承自的类包含了虚函数)都有一个自己的虚函数表.这个表是一个在编译时确定的静态数组.虚函数表包含了指向每个虚函数的函数指针以供类对象调用. 其次,编译器还在基类中定义了一个隐藏指针,我们称为 *__vptr,*__vptr 是在类实例创建时自动设置的,以指向类的虚函数表.*__vptr

  • 详解C++虚函数的工作原理

    静态绑定与动态绑定 讨论静态绑定与动态绑定,首先需要理解的是绑定,何为绑定?函数调用与函数本身的关联,以及成员访问与变量内存地址间的关系,称为绑定. 理解了绑定后再理解静态与动态. 静态绑定:指在程序编译过程中,把函数调用与响应调用所需的代码结合的过程,称为静态绑定.发生在编译期. 动态绑定:指在执行期间判断所引用对象的实际类型,根据实际的类型调用其相应的方法.程序运行过程中,把函数调用与响应调用所需的代码相结合的过程称为动态绑定.发生于运行期. C++中动态绑定 在C++中动态绑定是通过虚函数

  • c++语言中虚函数实现多态的原理详解

    前言 自上一个帖子之间跳过了一篇总结性的帖子,之后再发,今天主要研究了c++语言当中虚函数对多态的实现,感叹于c++设计者的精妙绝伦 c++中虚函数表的作用主要是实现了多态的机制.首先先解释一下多态的概念,多态是c++的特点之一,关于多态,简而言之就是 用父类的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数,这种方法呢,可以让父类的指针具有多种形态,也就是说不需要改动很多的代码就可以让父类这一种指针,干一些很多子类指针的事情,这里是从虚函数的实现机制层面进行研究 在写这篇帖子之前

  • C/C++杂记 虚函数的实现的基本原理(图文)

    1. 概述 简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针.例: 其中: B的虚函数表中存放着B::foo和B::bar两个函数指针. D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz. 提示:为了描述方便,本文在探讨对象内存布局时,将忽略内存对齐对布局的影响. 2. 虚函数表构造过程 从编译器的角度来说,

  • 浅谈C++中虚函数实现原理揭秘

    编译器到底做了什么实现的虚函数的晚绑定呢?我们来探个究竟. 编译器对每个包含虚函数的类创建一个表(称为V TA B L E).在V TA B L E中,编译器放置特定类的虚函数地址.在每个带有虚函数的类 中,编译器秘密地置一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E.通过基类指针做虚函数调 用时(也就是做多态调用时),编译器静态地插入取得这个V P T R,并在V TA B L E表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生

  • 虚函数表-C++多态的实现原理解析

    参考:http://c.biancheng.net/view/267.html 1.说明 我们都知道多态指的是父类的指针在运行中指向子类,那么它的实现原理是什么呢?答案是虚函数表 在 关于virtual 一文中,我们详细了解了C++多态的使用方式,我们知道没有 virtual 关键子就没法使用多态 2.虚函数表 我们看一下下面的代码 class A { public: int i; virtual void func() { cout << "A func" <<

  • 一篇文章彻底弄懂C++虚函数的实现机制

    目录 1.虚函数简介 2.虚函数表简介 3.有继承关系的虚函数表剖析 3.1.单继承无虚函数覆盖的情况 3.2.单继承有虚函数覆盖的情况 3.3.多重继承的情况 3.4.多层继承的情况 4.总结 1.虚函数简介 C++中有两种方式实现多态,即重载和覆盖. 重载:是指允许存在多个同名函数,而这些函数的参数表不同(参数个数不同.参数类型不同或者两者都不同). 覆盖:是指子类重新定义父类虚函数的做法,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让

  • 一篇文章彻底弄懂Python字符编码

    目录 1. 字符编码简介 1.1. ASCII 1.2. MBCS 1.3. Unicode 2. Python2.x中的编码问题 2.1. str和unicode 2.2. 字符编码声明 2.3. 读写文件 2.4. 与编码相关的方法 3.建议 3.1.字符编码声明 3.2. 抛弃str,全部使用unicode. 3.3. 使用codecs.open()替代内置的open(). 3.4. 绝对需要避免使用的字符编码:MBCS/DBCS和UTF-16. 1. 字符编码简介 1.1. ASCII

  • 一篇文章彻底弄懂Python中的if __name__ == __main__

    目录 1. 引言 2. 特殊变量 3. 复杂的例子 4. 使用场景 5. 解决方案 6. 总结 1. 引言 在Python相关代码中,我们经常会遇到如下代码段: # stuff if __name__ == "__main__": # do stuff 本文将尽可能使用简单的样例来解释这里发生了什么,以及需要使用if __name__=="__main__"的情形.请注意,上述代码中name和main前后有2个下划线字符. 闲话少说,我们直接开始吧! 2. 特殊变量

  • 一篇文章彻底弄懂Java中二叉树

    目录 一.树形结构 1.1 相关概念 1.2树的表示形式 1.3树的应用:文件系统管理(目录和文件) 二.二叉树 2.1相关概念 2.2 二叉树的基本形态 2.3 两种特殊的二叉树 2.4 二叉树的性质 2.5 二叉树的存储 2.6 二叉树的基本操作 2.6.1二叉树的遍历 2.6.2 二叉树的基本操作 总结 一.树形结构 树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合.把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的.它具有以下的特点:

  • 一文读懂C++ 虚函数 virtual

    探讨 C++ 虚函数 virtual 有无虚函数的对比 C++ 中的虚函数用于解决动态多态问题,虚函数的作用是允许在派生类中重新定义与积累同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数. 首先写两个简单的类,类 B 继承自类 A,即 A 是基类,B 是派生类. class A{ public: void print(){ cout << "A" << endl; } }; class B : public A { public: void

  • 一篇文章彻底搞懂Python切片操作

    目录 引言 一.Python可切片对象的索引方式 二.Python切片操作的一般方式 三.Python切片操作详细例子 1.切取单个值 2.切取完整对象 3.start_index和end_index全为正(+)索引的情况 4.start_index和end_index全为负(-)索引的情况 5.start_index和end_index正(+)负(-)混合索引的情况 6.连续切片操作 7.切片操作的三个参数可以用表达式 8.其他对象的切片操作 四.Python常用切片操作 1.取偶数位置 2.

  • C++虚函数的实现机制分析

    本文针对C++的虚函数的实现机制进行较为深入的分析,具体如下: 1.简单地说,虚函数是通过虚函数表实现的.那么,什么是虚函数表呢? 事实上,如果一个类中含有虚函数,则系统会为这个类分配一个指针成员指向一张虚函数表(vtbl),表中每一项指向一个虚函数的地址,实现上就是一个函数指针的数组. 例如下面这个例子: class Parent { public: virtual void foo1() { } virtual void foo1() { } void foo1(); }; class Ch

  • 一篇文章轻松搞懂Java中的自旋锁

    前言 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我们开发提供了便利. 在之前的文章<一文彻底搞懂面试中常问的各种"锁" >中介绍了Java中的各种"锁",可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙去脉,那么这篇文章就先来会一会"自旋锁". 正文 出现原因 在我们的

  • 一篇文章彻底搞懂C++常见容器

    目录 1.概述 2.容器详解 2.1vector(向量) 2.2deque(双端队列) 2.3list(列表) 2.4 array(数组) 2.5 string(字符串) 2.6 map(映射) 2.7 set(集合) 3.后记 1.概述 C++容器属于STL(标准模板库)中的一部分(六大组件之一),从字面意思理解,生活中的容器用来存放(容纳)水或者食物,东西,而C++中的容器用来存放各种各样的数据,不同的容器具有不同的特性,下图(思维导图)中列举除了常见的几种C++容器,而这部分C++的容器与

  • 一篇文章彻底搞懂面试中常被问的各种“锁”

    前言 锁,顾名思义就是锁住一些资源,当只有我们拿到钥匙的时候,才能操作锁住的资源.在我们的Java,数据库,还有一些分布式的环境中,总是充斥着各种各样的锁让人头疼,例如"公平锁"."自旋锁"."读写锁"."分布式锁"等等. 其实真实的情况是,锁并没有那么多,很多概念只是从不同的功能特性,设计,以及锁的状态这些不同的侧重点来说明的,因此我们可以根据不同的分类来搞明白为什么会有这些"锁"?坐稳扶好了,准备开车.

随机推荐