【问题标题】:How do you declare an interface in C++?如何在 C++ 中声明接口?
【发布时间】:2010-09-24 00:02:14
【问题描述】:

如何设置代表接口的类?这只是一个抽象基类吗?

【问题讨论】:

    标签: c++ inheritance interface abstract-class pure-virtual


    【解决方案1】:

    要扩展bradtgmurray 的答案,您可能希望通过添加虚拟析构函数来对接口的纯虚拟方法列表进行一个例外处理。这允许您在不暴露具体派生类的情况下将指针所有权传递给另一方。析构函数不需要做任何事情,因为接口没有任何具体的成员。将函数定义为虚拟函数和内联函数似乎是矛盾的,但相信我 - 它不是。

    class IDemo
    {
        public:
            virtual ~IDemo() {}
            virtual void OverrideMe() = 0;
    };
    
    class Parent
    {
        public:
            virtual ~Parent();
    };
    
    class Child : public Parent, public IDemo
    {
        public:
            virtual void OverrideMe()
            {
                //do stuff
            }
    };
    

    您不必包含虚拟析构函数的主体 - 事实证明,某些编译器在优化空析构函数时遇到了麻烦,您最好使用默认析构函数。

    【讨论】:

    • 虚拟描述符++!这个非常重要。您可能还希望包含 operator= 的纯虚拟声明并复制构造函数定义,以防止编译器为您自动生成这些声明。
    • 虚拟析构函数的替代方案是受保护的析构函数。这会禁用多态破坏,这在某些情况下可能更合适。在gotw.ca/publications/mill18.htm 中查找“指南#4”。
    • 另一种选择是定义一个带有主体的纯虚拟 (=0) 析构函数。这里的好处是,理论上编译器可以看到 vtable 现在没有有效成员,并完全丢弃它。使用带有主体的虚拟析构函数,可以(虚拟)调用所述析构函数,例如在通过this 指针构造的中间(当构造的对象仍然是Parent 类型时),因此编译器必须提供一个有效的vtable。因此,如果您在构造过程中没有通过this 显式调用虚拟析构函数:) 您可以节省代码大小。
    • 最典型的 C++ 答案没有直接回答问题(尽管显然代码是完美的),而是优化了简单的答案。
    • 不要忘记,在 C++11 中,您可以指定 override 关键字以允许编译时参数和返回值类型检查。比如声明Childvirtual void OverrideMe() override;
    【解决方案2】:

    用纯虚方法创建一个类。通过创建另一个覆盖这些虚拟方法的类来使用该接口。

    纯虚方法是定义为虚并赋值为0的类方法。

    class IDemo
    {
        public:
            virtual ~IDemo() {}
            virtual void OverrideMe() = 0;
    };
    
    class Child : public IDemo
    {
        public:
            virtual void OverrideMe()
            {
                // do stuff
            }
    };
    

    【讨论】:

    • 你应该在 IDemo 中有一个什么都不做的析构函数,以便定义要执行的行为: IDemo *p = new Child; /*随便*/删除p;
    • 为什么 Child 类中的 OverrideMe 方法是虚拟的?有必要吗?
    • @Cemre - 不,没有必要,但也没有什么坏处。
    • 在覆盖虚拟方法时保留关键字“virtual”通常是个好主意。虽然不是必需的,但它可以使代码更清晰 - 否则,您没有迹象表明该方法可以多态使用,甚至存在于基类中。
    • @Kevin C++11 中的 override 除外
    【解决方案3】:

    除了 C#/Java 中的抽象基类之外,您还有一个特殊的接口类型类别的全部原因是因为 C#/Java 不支持多重继承。

    C++ 支持多重继承,因此不需要特殊类型。没有非抽象(纯虚拟)方法的抽象基类在功能上等同于 C#/Java 接口。

    【讨论】:

    • 能够创建接口还是很不错的,这样我们就不用打字了(virtual , =0, virtual destructor)。此外,多重继承对我来说似乎是一个非常糟糕的主意,我从未见过它在实践中使用过,但一直需要接口。糟糕的是,C++ 礼遇不会仅仅因为我想要它们就引入接口。
    • Ha11owed:它有接口。它们被称为具有纯虚方法且没有方法实现的类。
    • @doc: java.lang.Thread 具有您可能不想在对象中包含的方法和常量。如果您从 Thread 和另一个具有公共方法 checkAccess() 的类扩展,编译器应该怎么做?你真的更喜欢像 C++ 中那样使用强命名的基指针吗?这似乎是糟糕的设计,您通常需要在您认为需要多重继承的地方进行组合。
    • @Ha11owed 很久以前了,所以我不记得细节了,但它有我想在我的类中拥有的方法和内容,更重要的是我希望我的派生类对象是一个 @ 987654322@ 实例。多重继承可能是糟糕的设计和组合。这一切都取决于大小写。
    • @Dave:真的吗? Objective-C 有编译时评估和模板?
    【解决方案4】:

    C++ 中没有“接口”的概念。 AFAIK,接口最初是在 Java 中引入的,以解决缺乏多重继承的问题。事实证明,这个概念非常有用,在 C++ 中使用抽象基类也可以达到同样的效果。

    抽象基类是一个类,其中至少一个成员函数(Java 术语中的方法)是使用以下语法声明的纯虚函数:

    class A
    {
      virtual void foo() = 0;
    };
    

    抽象基类不能被实例化,即。 e.您不能声明类 A 的对象。您只能从 A 派生类,但任何不提供 foo() 实现的派生类也将是抽象的。为了停止抽象,派生类必须为其继承的所有纯虚函数提供实现。

    请注意,抽象基类可以不仅仅是一个接口,因为它可以包含非纯虚拟的数据成员和成员函数。接口的等价物是一个抽象基类,没有任何数据,只有纯虚函数。

    而且,正如 Mark Ransom 所指出的,抽象基类应该提供一个虚拟析构函数,就像任何基类一样。

    【讨论】:

    • 我想说的不仅仅是“缺乏多重继承”,而是取代多重继承。 Java 从一开始就是这样设计的,因为多重继承带来的问题多于它解决的问题。好答案
    • 奥斯卡,这取决于你是一个学习过 Java 的 C++ 程序员,反之亦然。 :) 恕我直言,如果使用得当,就像 C++ 中的几乎任何东西一样,多重继承可以解决问题。 “接口”抽象基类是一个非常明智地使用多重继承的例子。
    • @OscarRyz 错了。 MI 只有在误用时才会产生问题。大多数所谓的 MI 问题也会提出替代设计(没有 MI)。 当人们对 MI 的设计有问题时,都是 MI 的错;如果他们对 SI 有设计问题,那是他们自己的错。 “死亡钻石”(重复继承)就是一个典型的例子。 MI 抨击不是纯粹的虚伪,而是接近。
    • 从语义上讲,接口不同于抽象类,所以Java的接口不仅仅是一种技术变通。定义接口或抽象类之间的选择是由语义驱动的,而不是技术考虑。让我们想象一些接口“HasEngine”:这是一个方面,一个特性,它可以应用于/实现非常不同的类型(无论是类还是抽象类),所以我们将为此定义一个接口,而不是抽象类。
    • @MarekStanley,你可能是对的,但我希望你能选择一个更好的例子。我喜欢从继承接口与继承实现的角度来考虑它。在 C++ 中,您可以同时继承接口和实现(公共继承),也可以只继承实现(私有继承)。在 Java 中,您可以选择只继承接口,而无需实现。
    【解决方案5】:

    据我所知,添加虚拟析构函数非常重要。我正在使用用new 创建并用delete 销毁的对象。

    如果接口中不添加虚析构函数,则不调用继承类的析构函数。

    class IBase {
    public:
        virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
        virtual void Describe() = 0; // pure virtual method
    };
    
    class Tester : public IBase {
    public:
        Tester(std::string name);
        virtual ~Tester();
        virtual void Describe();
    private:
        std::string privatename;
    };
    
    Tester::Tester(std::string name) {
        std::cout << "Tester constructor" << std::endl;
        this->privatename = name;
    }
    
    Tester::~Tester() {
        std::cout << "Tester destructor" << std::endl;
    }
    
    void Tester::Describe() {
        std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
    }
    
    
    void descriptor(IBase * obj) {
        obj->Describe();
    }
    
    int main(int argc, char** argv) {
    
        std::cout << std::endl << "Tester Testing..." << std::endl;
        Tester * obj1 = new Tester("Declared with Tester");
        descriptor(obj1);
        delete obj1;
    
        std::cout << std::endl << "IBase Testing..." << std::endl;
        IBase * obj2 = new Tester("Declared with IBase");
        descriptor(obj2);
        delete obj2;
    
        // this is a bad usage of the object since it is created with "new" but there are no "delete"
        std::cout << std::endl << "Tester not defined..." << std::endl;
        descriptor(new Tester("Not defined"));
    
    
        return 0;
    }
    

    如果你在没有virtual ~IBase() {}; 的情况下运行之前的代码,你会看到析构函数Tester::~Tester() 永远不会被调用。

    【讨论】:

    • 此页面上的最佳答案,因为它通过提供一个实用的、可编译的示例来说明这一点。干杯!
    • Testet::~Tester() 仅在 obj 为“Declared with Tester”时运行。
    • 实际上,字符串 privatename 的析构函数将被调用,并且在内存中,这就是所有将被分配的内容。就运行时而言,当类的所有具体成员都被销毁时,类实例也会被销毁。我对具有两个 Point 结构的 Line 类进行了类似的实验,发现两个结构都在删除调用或从包含函数返回时被破坏(哈!)。 valgrind 确认 0 泄漏。
    【解决方案6】:

    我的回答与其他人基本相同,但我认为还有两件重要的事情要做:

    1. 在您的接口中声明一个虚拟析构函数或创建一个受保护的非虚拟析构函数以避免有人试图删除 IDemo 类型的对象时出现未定义的行为。

    2. 使用虚拟继承来避免多重继承问题。 (当我们使用接口时,更常见的是多重继承。)

    和其他答案一样:

    • 使用纯虚方法创建一个类。
    • 通过创建另一个覆盖这些虚拟方法的类来使用接口。

      class IDemo
      {
          public:
              virtual void OverrideMe() = 0;
              virtual ~IDemo() {}
      }
      

      或者

      class IDemo
      {
          public:
              virtual void OverrideMe() = 0;
          protected:
              ~IDemo() {}
      }
      

      还有

      class Child : virtual public IDemo
      {
          public:
              virtual void OverrideMe()
              {
                  //do stuff
              }
      }
      

    【讨论】:

    • 不需要虚拟继承,因为接口中没有任何数据成员。
    • 虚拟继承对于方法也很重要。没有它,你会在使用 OverrideMe() 时遇到歧义,即使它的“实例”之一是纯虚拟的(我自己也试过了)。
    • @Avishay_ "不需要虚拟继承,因为接口中没有任何数据成员。" 错误。
    • 请注意,虚拟继承可能不适用于某些 gcc 版本,如 WinAVR 2010 附带的版本 4.3.3:gcc.gnu.org/bugzilla/show_bug.cgi?id=35067
    • -1 有一个非虚拟保护的析构函数,对不起
    【解决方案7】:

    在 C++11 中,您可以轻松地完全避免继承:

    struct Interface {
      explicit Interface(SomeType& other)
      : foo([=](){ return other.my_foo(); }), 
        bar([=](){ return other.my_bar(); }), /*...*/ {}
      explicit Interface(SomeOtherType& other)
      : foo([=](){ return other.some_foo(); }), 
        bar([=](){ return other.some_bar(); }), /*...*/ {}
      // you can add more types here...
    
      // or use a generic constructor:
      template<class T>
      explicit Interface(T& other)
      : foo([=](){ return other.foo(); }), 
        bar([=](){ return other.bar(); }), /*...*/ {}
    
      const std::function<void(std::string)> foo;
      const std::function<void(std::string)> bar;
      // ...
    };
    

    在这种情况下,接口具有引用语义,即您必须确保对象的寿命比接口长(也可以使用值语义制作接口)。

    这些类型的接口各有优缺点:

    最后,继承是复杂软件设计中万恶之源。在Sean Parent's Value Semantics and Concepts-based Polymorphism(强烈推荐,那里解释了这种技术的更好版本)研究了以下案例:

    假设我有一个应用程序,我在其中使用MyShape 接口以多态方式处理我的形状:

    struct MyShape { virtual void my_draw() = 0; };
    struct Circle : MyShape { void my_draw() { /* ... */ } };
    // more shapes: e.g. triangle
    

    在您的应用程序中,您可以使用YourShape 接口对不同的形状执行相同操作:

    struct YourShape { virtual void your_draw() = 0; };
    struct Square : YourShape { void your_draw() { /* ... */ } };
    /// some more shapes here...
    

    现在假设您想使用我在您的应用程序中开发的一些形状。从概念上讲,我们的形状具有相同的界面,但要使我的形状在您的应用程序中工作,您需要按如下方式扩展我的形状:

    struct Circle : MyShape, YourShape { 
      void my_draw() { /*stays the same*/ };
      void your_draw() { my_draw(); }
    };
    

    首先,修改我的形状可能根本不可能。此外,多重继承导致了意大利面条代码(想象第三个项目使用TheirShape 接口......如果他们也调用他们的绘图函数my_draw 会发生什么?)。

    更新:有几个关于基于非继承的多态性的新参考:

    【讨论】:

    • TBH 继承远比 C++11 的东西清晰得多,它伪装成一个接口,而是绑定一些不一致的设计的粘合剂。 Shapes 示例与现实脱节,Circle 类是一个糟糕的设计。在这种情况下,您应该使用 Adapter 模式。对不起,如果这听起来有点刺耳,但在判断继承之前尝试使用一些现实生活中的库,如Qt。继承让生活更轻松。
    • 听起来一点也不刺耳。形状示例如何脱离现实?您能否举一个使用Adapter 模式修复Circle 的示例(可能在ideone 上)?我很想看看它的优势。
    • 那就不是脱离现实了。当 A 公司收购 B 公司并想将 B 公司的代码库整合到 A 公司时,你有两个完全独立的代码库。想象一下,每个都有不同类型的 Shape 层次结构。您不能轻松地将它们与继承结合起来,并且添加公司 C 并且您会一团糟。我认为您应该观看此演讲:youtube.com/watch?v=0I0FD3N5cgM 我的答案较旧,但您会看到相似之处。您不必一直重新实现所有内容,您可以在接口中提供实现,并在可用的情况下选择成员函数。
    • 我看过部分视频,这是完全错误的。除了调试目的,我从不使用 dynamic_cast。动态演员表意味着您的设计有问题,并且此视频中的设计在设计上是错误的 :)。 Guy 甚至提到了 Qt,但即使在这里他也错了——QLayout 既不是从 QWidget 继承的,也不是相反的!
    • 对。问题是我不明白为什么继承是“万恶之源”。这种说法很荒谬。
    【解决方案8】:

    以上所有好的答案。 您应该记住的另一件事 - 您还可以拥有一个纯虚拟析构函数。唯一的区别是您仍然需要实现它。

    困惑?

    
        --- header file ----
        class foo {
        public:
          foo() {;}
          virtual ~foo() = 0;
    
          virtual bool overrideMe() {return false;}
        };
    
        ---- source ----
        foo::~foo()
        {
        }
    
    

    您想要这样做的主要原因是,如果您想像我一样提供接口方法,但可以选择覆盖它们。

    要使类成为接口类,需要纯虚方法,但所有虚方法都有默认实现,因此唯一可以制作纯虚的方法是析构函数。

    在派生类中重新实现析构函数没什么大不了的 - 我总是在派生类中重新实现析构函数,不管是虚拟的还是非虚拟的。

    【讨论】:

    • 为什么,哦,为什么,在这种情况下,会有人想让 dtor 成为纯虚拟的吗?那会有什么好处呢?您只需在派生类上强加一些他们可能不需要包含的东西 - 一个 dtor。
    • 更新了我的回答来回答你的问题。纯虚析构函数是实现(实现的唯一方法?)一个所有方法都有默认实现的接口类的有效方法。
    【解决方案9】:

    如果您使用的是 Microsoft 的 C++ 编译器,那么您可以执行以下操作:

    struct __declspec(novtable) IFoo
    {
        virtual void Bar() = 0;
    };
    
    class Child : public IFoo
    {
    public:
        virtual void Bar() override { /* Do Something */ }
    }
    

    我喜欢这种方法,因为它可以生成更小的接口代码,并且生成的代码大小可以显着减小。 novtable 的使用删除了对该类中 vtable 指针的所有引用,因此您永远不能直接实例化它。请参阅此处的文档 - novtable

    【讨论】:

    • 我不太明白你为什么使用 novtable 而非标准 virtual void Bar() = 0;
    • 除此之外(我刚刚注意到我添加的缺少的= 0;)。如果您不理解,请阅读文档。
    • 我在没有= 0; 的情况下阅读了它,并认为这只是一种非标准的完全相同的方式。
    【解决方案10】:

    您还可以考虑使用 NVI(非虚拟接口模式)实现的合同类。例如:

    struct Contract1 : boost::noncopyable
    {
        virtual ~Contract1() = default;
        void f(Parameters p) {
            assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
            // + class invariants.
            do_f(p);
            // Check post-conditions + class invariants.
        }
    private:
        virtual void do_f(Parameters p) = 0;
    };
    ...
    class Concrete : public Contract1, public Contract2
    {
    private:
        void do_f(Parameters p) override; // From contract 1.
        void do_g(Parameters p) override; // From contract 2.
    };
    

    【讨论】:

    • 对于其他读者,Jim Hyslop 和 Herb Sutter 的 Dr Dobbs article“Conversations: Virtually Yours” 详细阐述了为什么人们可能想要使用 NVI。
    • 还有this article Herb Sutter 的“虚拟”。
    【解决方案11】:

    对上面写的内容做一点补充:

    首先,确保你的析构函数也是纯虚拟的

    其次,您可能希望在实施时虚拟(而不是通常)继承,只是为了采取良好的措施。

    【讨论】:

    • 我喜欢虚拟继承,因为从概念上讲,它意味着被继承类只有一个实例。诚然,这里的课程没有任何空间要求,因此可能是多余的。我有一段时间没有在 C++ 中做 MI,但非虚拟继承不会使向上转换复杂化吗?
    • 为什么,哦,为什么,在这种情况下,会有人想让 dtor 成为纯虚拟的吗?那会有什么好处呢?您只需在派生类上强加一些他们可能不需要包含的东西 - 一个 dtor。
    • 如果存在对象会通过指向接口的指针被销毁的情况,你应该确保析构函数是虚拟的...
    • 纯虚析构函数没有错。这不是必须的,但它没有任何问题。在派生类中实现析构函数对于该类的实现者来说几乎不是一个巨大的负担。请参阅下面的答案,了解您为什么要这样做。
    • +1 表示虚拟继承,因为对于接口,类更有可能从两个或多个路径派生接口。我在接口中选择受保护的析构函数。
    【解决方案12】:

    在 C++20 中,您可以使用 concept 代替类。 它比继承更有效。

    template <class T>
    concept MyInterface = requires (T t) {
        { t.interfaceMethod() };
    };
    
    class Implementation {
    public:
        void interfaceMethod();
    };
    static_assert(MyInterface<Implementation>);
    

    然后你可以在函数中使用它:

    void myFunction(MyInterface auto& arg);
    

    限制是你不能在容器中使用它。

    【讨论】:

      【解决方案13】:

      我还是 C++ 开发的新手。我从 Visual Studio (VS) 开始。

      然而,似乎没有人在 VS (.NET) 中提到 __interface。我不是很确定这是否是声明接口的好方法。但它似乎提供了额外的强制执行(在the documents 中提到)。这样您就不必显式指定virtual TYPE Method() = 0;,因为它会自动转换。

      __interface IMyInterface {
         HRESULT CommitX();
         HRESULT get_X(BSTR* pbstrName);
      };
      

      但是,我不使用它,因为我担心跨平台编译兼容性,因为它仅在 .NET 下可用。

      如果有人对此有任何有趣的事情,请分享。 :-)

      谢谢。

      【讨论】:

        【解决方案14】:

        虽然virtual 确实是定义接口的事实标准,但我们不要忘记经典的类 C 模式,它带有 C++ 中的构造函数:

        struct IButton
        {
            void (*click)(); // might be std::function(void()) if you prefer
        
            IButton( void (*click_)() )
            : click(click_)
            {
            }
        };
        
        // call as:
        // (button.*click)();
        

        这样做的好处是您可以重新绑定事件运行时而不必再次构造您的类(因为 C++ 没有用于更改多态类型的语法,这是变色龙类的一种解决方法)。

        提示:

        • 您可以从 this 作为基类继承(允许虚拟和非虚拟)并在后代的构造函数中填充 click
        • 您可能将函数指针作为protected 成员并拥有public 引用和/或getter。
        • 如上所述,这允许您在运行时切换实现。因此,它也是一种管理状态的方法。根据ifs 与代码中状态更改的数量,这可能switch()es 或ifs 快(预计周转时间约为 3-4 ifs ,但总是先测量。
        • 如果您选择std::function&lt;&gt; 而不是函数指针,您可能能够管理IBase 中的所有对象数据。从这一点开始,您可以获得IBase 的价值原理图(例如,std::vector&lt;IBase&gt; 将起作用)。请注意,这可能会变慢,具体取决于您的编译器和 STL 代码;此外,与函数指针甚至虚函数相比,std::function&lt;&gt; 的当前实现往往会产生开销(这在未来可能会改变)。

        【讨论】:

          【解决方案15】:

          这是abstract class在c++标准中的定义

          n4687

          13.4.2

          抽象类是只能用作其他类的基类的类;没有抽象对象 类可以被创建,除了作为从它派生的类的子对象。一个类是抽象的,如果它至少有 一个纯虚函数。

          【讨论】:

            【解决方案16】:

            如果您只想静态绑定接口(没有虚拟,没有接口类型本身的实例,接口仅作为指导):

            #include <iostream>
            #include <string>
            
            // Static binding interface
            // Notice: instantiation of this interface should be usefuless and forbidden.
            class IBase {
             protected:
              IBase() = default;
              ~IBase() = default;
            
             public:
              // Methods that must be implemented by the derived class
              void behaviorA();
              void behaviorB();
            
              void behaviorC() {
                std::cout << "This is an interface default implementation of bC().\n";
              };
            };
            
            class CCom : public IBase {
              std::string name_;
            
             public:
              void behaviorA() { std::cout << "CCom bA called.\n"; };
            };
            
            class CDept : public IBase {
              int ele_;
            
             public:
              void behaviorB() { std::cout << "CDept bB called.\n"; };
              void behaviorC() {
                // Overwrite the interface default implementation
                std::cout << "CDept bC called.\n";
                IBase::behaviorC();
              };
            };
            
            int main(void) {
              // Forbid the instantiation of the interface type itself.
              // GCC error: ‘constexpr IBase::IBase()’ is protected within this context
              // IBase o;
            
              CCom acom;
              // If you want to use these interface methods, you need to implement them in
              // your derived class. This is controled by the interface definition.
              acom.behaviorA();
              // ld: undefined reference to `IBase::behaviorB()'
              // acom.behaviorB();
              acom.behaviorC();
            
              CDept adept;
              // adept.behaviorA();
              adept.behaviorB();
              adept.behaviorC();
              // adept.IBase::behaviorC();
            }
            

            【讨论】:

              【解决方案17】:
              class Shape 
              {
              public:
                 // pure virtual function providing interface framework.
                 virtual int getArea() = 0;
                 void setWidth(int w)
                 {
                    width = w;
                 }
                 void setHeight(int h)
                 {
                    height = h;
                 }
              protected:
                  int width;
                  int height;
              };
              
              class Rectangle: public Shape
              {
              public:
                  int getArea()
                  { 
                      return (width * height); 
                  }
              };
              class Triangle: public Shape
              {
              public:
                  int getArea()
                  { 
                      return (width * height)/2; 
                  }
              };
              
              int main(void)
              {
                   Rectangle Rect;
                   Triangle  Tri;
              
                   Rect.setWidth(5);
                   Rect.setHeight(7);
              
                   cout << "Rectangle area: " << Rect.getArea() << endl;
              
                   Tri.setWidth(5);
                   Tri.setHeight(7);
              
                   cout << "Triangle area: " << Tri.getArea() << endl; 
              
                   return 0;
              }
              

              结果: 矩形面积:35 三角区:17

              我们已经看到一个抽象类如何根据 getArea() 定义一个接口,而另外两个类实现了相同的功能,但使用不同的算法来计算特定于形状的面积。

              【讨论】:

              • 这不是接口!这只是一个抽象基类,其中有一个需要重写的方法!接口通常是只包含方法定义的对象——其他类在实现接口时必须履行的“契约”。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2017-08-22
              • 2018-01-19
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-03-02
              • 2018-01-10
              相关资源
              最近更新 更多