问题

虚函数一定是运行期才绑定么?

回答
这个问题触及了面向对象编程(OOP)的核心概念之一:多态性。而虚函数的本质,正是实现运行时多态的关键。所以,直接回答是:是的,虚函数的设计目的就是为了实现运行期绑定。

但这仅仅是个答案,要理解为什么,咱们得把这层“为什么”给扒开,一点点捋清楚。

什么是“绑定”?

在C++这样的编译型语言里,“绑定”指的是一个函数调用请求,在程序执行过程中,究竟会跳转到哪个具体的函数实体(也就是那段实际的代码)去执行。

这个过程,就好比你要去一家餐厅吃饭。

编译期绑定 (静态绑定/早期绑定): 你在计划行程的时候,就已经决定了要去“红烧肉饭店”。无论你什么时候去,或者有多少个朋友和你一起去,你们最终都会出现在“红烧肉饭店”。这里的“决定”是在你出门前(编译前)就做好了。
运行期绑定 (动态绑定/晚期绑定): 你只知道要去“一家提供本地菜的餐厅”。具体是去“张家菜馆”还是“李家小炒”,这个决定是在你走到街口,看到招牌,或者听朋友临时推荐的时候才确定的。这个“决定”是在你实际行动(运行时)才做出的。

函数调用是如何进行的?

对于一个普通的函数调用,编译器在编译阶段就知道这个函数的名字、它的参数列表以及它位于哪个类(如果它是成员函数)。编译器会生成直接的机器码指令,指向那个特定的函数地址。这就是编译期绑定。

比如:

```c++
class Dog {
public:
void bark() {
std::cout << "Woof!" << std::endl;
}
};

int main() {
Dog myDog;
myDog.bark(); // 这是一个编译期绑定的调用
return 0;
}
```

编译器看到 `myDog.bark()`,它知道 `myDog` 是个 `Dog` 对象,`bark` 是 `Dog` 类里的一个普通成员函数。它会在编译时就生成跳转到 `Dog::bark` 函数的指令。这个过程是确定的,高效的。

那么,虚函数是如何打破这个“编译期就确定”的规则的?

关键在于 `virtual` 关键字。

当你在基类中声明一个函数为 `virtual` 时,你就告诉编译器:“这个函数,它的具体实现,可能在派生类里被重写了,而且,我希望在运行时才能确定到底调用哪个版本的函数。”

虚函数背后的机制:虚表 (vtable)

为了实现这个“运行时才确定”,C++编译器通常会引入一个叫做虚表 (Virtual Table,简称 vtable) 的机制。

1. 虚表是什么?
对于一个包含虚函数的类,编译器会在内存中为这个类创建一个虚表。
虚表是一个数组,里面存放着该类所有虚函数的实际地址。
每个该类的对象,都会在对象内部包含一个指向这个虚表的虚指针 (vptr)。

2. 工作流程:
当一个基类指针指向一个派生类对象时,虽然编译器知道这是一个基类指针(因为它不知道具体是什么派生类),但指针指向的对象(派生类对象)里面藏着一个 vptr。
这个 vptr 指向的就是派生类对应的虚表。
当通过基类指针调用一个虚函数时,程序执行的步骤是:
a. 通过基类指针找到对象内部的 vptr。
b. 通过 vptr 找到对象的 vtable。
c. 在 vtable 中查找要调用的虚函数(根据函数在虚表中的位置,这通常是编译期确定的)。
d. 执行在 vtable 中找到的那个函数的实际地址。

举个例子,让它更具体:

```c++
include

class Animal {
public:
virtual void makeSound() { // 声明为虚函数
std::cout << "Animal makes a sound." << std::endl;
}
// 即使没有 virtual,如果基类里有 virtual 函数,
// 那么它内部就可能包含 vptr,派生类也会继承 vptr。
// 但为了清楚,我们通常会把想要运行时绑定的都加上 virtual。

// 这里的虚函数可以不加 virtual,因为它不是虚函数本身,
// 但是它的存在可能影响基类对象是否携带 vptr
// (更准确地说,如果基类有任何一个虚函数,那么基类对象就携带 vptr)
void sleep() {
std::cout << "Animal is sleeping." << std::endl;
}
};

class Dog : public Animal {
public:
// 重写基类的虚函数
void makeSound() override { // 'override' 是 C++11 后的好习惯,表示意图重写
std::cout << "Woof! Woof!" << std::endl;
}

// 这是一个新的非虚函数,也是编译期绑定
void fetch() {
std::cout << "Fetching the ball." << std::endl;
}
};

class Cat : public Animal {
public:
// 重写基类的虚函数
void makeSound() override {
std::cout << "Meow!" << std::endl;
}
};

int main() {
Animal ptrAnimal; // 一个 Animal 类型的指针

Dog myDog;
Cat myCat;

ptrAnimal = &myDog
ptrAnimal>makeSound(); // 这里是关键!

ptrAnimal = &myCat
ptrAnimal>makeSound(); // 再次关键!

ptrAnimal = &myDog
ptrAnimal>sleep(); // 这个调用是编译期绑定的,因为 sleep() 不是虚函数

// myDog.fetch(); // 这个也可以,但不是通过基类指针调用的
// ((Dog)ptrAnimal)>fetch(); // 这是一个 Cstyle cast,不安全,也不推荐

return 0;
}
```

深入剖析 `ptrAnimal>makeSound();` 这一行:

1. `ptrAnimal` 是一个 `Animal` 类型的指针。
2. `makeSound()` 在 `Animal` 基类中被声明为 `virtual`。
3. 在 `ptrAnimal = &myDog` 这一行执行时:
`myDog` 对象是一个 `Dog` 类型的实例。
`Dog` 类继承了 `Animal` 的虚函数 `makeSound`(并重写了它)。
`Dog` 类(作为 `Animal` 的派生类)的每个对象,都会有一个 `Animal` 基类部分,这个部分会包含一个 vptr。
这个 vptr 指向 `Dog` 类在编译期生成的虚表。
`Dog` 类的虚表里,`makeSound` 的位置上存放的是 `Dog::makeSound` 的实际地址。
4. 当 `ptrAnimal>makeSound();` 被执行时:
程序通过 `ptrAnimal` 找到 `myDog` 对象。
找到 `myDog` 对象内部的 vptr。
通过 vptr 找到 `Dog` 类的 vtable。
在 `Dog` 类的 vtable 中,找到 `makeSound` 对应的条目,获取到 `Dog::makeSound` 的地址。
跳转并执行 `Dog::makeSound()`。

反之,`ptrAnimal>sleep();` 为什么是编译期绑定的?

`sleep()` 在 `Animal` 类中是非虚函数。
即使 `ptrAnimal` 指向 `myDog`,编译器知道 `sleep()` 不是虚函数,它就不会去查 vtable。
它会根据 `ptrAnimal` 的 静态类型(即 `Animal`)来确定调用的函数。
所以,它会生成跳转到 `Animal::sleep()` 的机器码。

还有一些重要的点:

继承关系和虚表: 当一个类派生自另一个包含虚函数的类时,它会继承基类的虚表。如果派生类重写了基类的虚函数,它会修改自己对应的虚表中的函数地址。如果派生类新增了自己的虚函数,它会在自己的虚表中添加新的条目。
纯虚函数: 纯虚函数( `= 0;`)也是虚函数,它强制派生类必须提供实现,并且无法实例化抽象基类。其绑定方式同样是运行期。
虚函数开销: 引入虚函数是有一定开销的。每个包含虚函数的类对象会额外占用一个指针(vptr)的空间。每次调用虚函数时,都需要通过 vptr 和 vtable 进行两次额外的间接跳转。所以,如果一个函数不需要运行时多态,就不要将其声明为虚函数。
非虚函数重写: 在派生类中,你可以“重写”基类的非虚函数,但这是通过隐藏(Hiding)实现的,而不是多态。通过基类指针调用时,永远会调用基类版本。

总结来说,虚函数之所以一定是运行期绑定,是因为:

1. 设计初衷: 虚函数的定义就是为了启用运行时多态,允许通过基类指针或引用调用派生类重写后的函数。
2. 实现机制: 编译器通过引入 vptr 和 vtable 来支持这一特性。vptr 存储在对象中,指向类的 vtable,vtable 包含了该类所有虚函数的实际地址。
3. 动态查找: 运行时,通过基类指针或引用调用虚函数时,程序会动态地查找到对象所属派生类的 vtable,并根据函数在 vtable 中的位置来确定要执行的实际函数。

这就像给每个对象都配了一个“寻址指南”(vptr),它能告诉程序去哪里找到这个对象“真正应该执行”的方法列表(vtable),然后在这个列表中找到具体是哪个方法(函数地址)。这个“寻址”过程,发生在程序运行时。

网友意见

user avatar

评论区实在是看不下去,把书本知识当作死教条,所以必须要出来答一下。


Rule 0 如果你是在校要考试的学生或者是准备面试,那请按照你的教材来,一般都是会一刀切。再强调一遍,按照你的教材来,教的是什么答什么。否则与我无关。

Rule 1,严格来说,各个编译器会怎么实现虚函数是各个平台自己的事,标准并没有一个统一的要求。甚至都不一定要用虚表实现。这里我们忽略这条,只讨论现有主流的实现方式。


一、

首先,我们回顾以下这个最经典的例子,也是题主在题目中举到的例子。

       void f(); void g();  struct Base {     virtual void virtualFun()     {         f();     } };  struct Derived : Base {     virtual void virtualFun()     {         g();     } };  void fun(Base * b) {     b->virtualFun();  }      

编译结果:

在编译函数 fun 时,函数参数 b 表面上是个指向 Base 类型的指针。但是,由于多态机制的存在,“指向 Base 类型”只是个马甲,实际上这个 b 可能指向 Base 类型的变量,可能指向的是 Derived 类型的变量,甚至也有可能指向的是一个定义在其他源文件里的、编译器暂时还不知道的某个子类。再甚至,还有可能是一个程序员还没有编写出来的类型。它们可能直接复用了基类 BasevirtualFun,也有可能是自己定义了新版的 virtualFun 覆盖掉了上一个祖先类的版本。不管怎样,在编译器编译 fun 函数的这一刻,尚无法知道 b 实际指向对象的 virtualFun 是谁。因此,只能去虚表中取函数指针 (movq (%rdi), %rax),然后再调用 (call *(%rax))。

这便是我们所讲的“运行期绑定”(或者动态绑定/迟绑定,都是同一个概念,不同叫法)。


二、

稍稍修改下 fun,将参数改为传值:

       void f(); void g();  struct Base {     virtual void virtualFun()     {         f();     } };  struct Derived : Base {     virtual void virtualFun()     {         g();     } };  void fun(Base b) {     b.virtualFun();  }      

编译结果:

发现编译结果变了。给出的汇编结果中,fun 里很明确地调用了 void f()

这是因为,此例下的 b 变量是真真实实的 Base 类型。函数调用方向 fun 里传的是参数,无论是什么样形形色色的子类,在值传递下,都是将子类中继承自 Base 的那部分单独拎出来,复制出一份副本,成为这里的 b。所以 b.virtualFun() 这里,尽管调用的是一个虚函数,但是决不会涉及到运行期的绑定。因为 b 的类型和它实际的 virtualFun 已经是确定的了

所以,为什么教材上会说:

只有通过指针或者引用才能表现出多态性,值语义是不能表现出多态的。

虚函数运行期绑定的性质只有在指针或者引用下能用,通过值调用的虚函数是编译器静态绑定,是没有运行期绑定的性质的。

@DLM-fakeS


后来想到一条,回来补充一个 2.5

在使用限定名字查找时,即使是通过指针或者引用,虚函数也不表现多态性:

       void f(Base * p, Base & r) {     p->Base::virtualFun();     r.Base::virtualFun(); }      

在此例中,已明确要求调用 BasevirtualFun,故是不会在运行期去查找虚表的。


三、

以上两条是 C++98 时代人人都该会的老知识了,食大便了,该整点新的了。

C++11 引入的 final 关键字,乍一看只是一个从其他语言抄来的小功能,但对于这个问题而言,却是一个颠覆游戏规则的存在。

代码:

       void f(); void g();  struct Base {     virtual void virtualFun()     {         f();     } };  struct Derived : Base {     virtual void virtualFun() final     {         g();     } };  void fun(Derived * p) {     p->virtualFun(); }      

编译结果:

第三例的 fun 里就明确地 call 了 void g()。有的读者看到这里可能会问,之前不是说可能会有其他子类么,这里编译器怎么就这么肯定 p 一定是指向的 Derived 类呢?

其实,p 完全有可能指向的是其他子类,但是注意 Derived::virtualFun 后面的 final。有了这个 final 就阻止了 Derived 的子类写新版的 virtualFun 覆盖 Derived::virtualFun,肯定也就保证了无论怎么继承下去,这些子类一定都是用的 Derived 版的 virtualFun

因此,此处一定是编译时绑定,而不是运行时绑定。

再扩大一点,用 final 修饰类:

       void f(); void g();  struct Base {     virtual void virtualFun()     {         f();     } };  struct Derived final : Base {     virtual void virtualFun()     {         g();     } };  void fun(Derived * p) {     p->virtualFun(); }      

编译结果:


这里 Derived 都成断子绝孙类了,也就不用担心将来会有什么子类覆盖 virtualFun 了。

因此,这里也是编译时绑定。


小结:

(since C++11) final 对虚函数的多态性具有向下阻断作用。经 final 修饰的虚函数或经 final 修饰的类的所有虚函数,自该级起,不再具有多态性。


代码优化小 Tips:

业务代码中,对于多态类,如果确定一个虚函数不会再被覆盖,或者该类不会再被继承,则推荐标上 final。这可以为编译器提供非常有价值的编译优化信息,总而将原本需要推迟到运行期才能确定的虚函数调用提前在编译期就已确定。如被调用的函数能与上层调用方一起进一步地做函数内联、常量折叠、无用代码消除等优化,则可以压榨出非常可观的性能提升。


四、

最后,必须指出,这个问题的探讨应当是结合现代编译器的优化能力谈的。2021 了还抱着一些“新世纪初编写的教材”只会让你越来越来与时代脱节。

题主问题代码改:

       void f(); void g();  struct Base {     virtual void virtualFun()     {         f();     } };  struct Derived : Base {     virtual void virtualFun()     {         g();     } };  int main() {    // 编译期就分配了虚拟地址    Derived d;     // 编译期也有虚拟地址    Base & b = d;         // 此时b 对象是分配了内存了的    // 且虚指针指向了他的虚函数表    // 就是说可以拿到该虚函数的地址    // 按道理来说,此时可以在编译期就在绑定他的虚函数地址    b.virtualFun();   }      

这是当前最新的 g++-12 的编译结果:


哪怕是早在 2013 年发布的 g++-4.6.4 都已经不会去查虚表了。。。


@ZhiHuReader



这种生命周期比较局部的对象,从理论上讲,其虚函数调用完全可以在编译期就得到确定。实际上,也确实在这几年得到了实现。

这种通过人眼一眼都能看出指针/引用实际指向类型的简单情形,编译器没优化才是 silly 的。

题主甚至都可以大胆一点,加点动态内存分配,让代码再“动态”一点:

       void f(); void g();  struct Base {     virtual void virtualFun()     {         f();     } };  struct Derived : Base {     virtual void virtualFun()     {         g();     } };  int main() {    Derived * p = new Derived();    Base * pb = p;    pb->virtualFun();     delete p; }      

g++-4.6.4 依然可以认出 pb 实际指向的是 Derived 类型:

甚至在 g++-12 里面,new 跟 delete 都可以跟你优化没了:


正常人都能看得出调用的肯定是 DerivedvirtualFun,为什么必须留到运行时绑定?为什么不能优化?


五、

所以正是基于以上的优化的可能,C++20 在常量求值中放宽了 new/delete 和虚函数后,以下的代码也从原来的不敢想变成了可能:

       struct Base {     constexpr virtual ~Base() {}     constexpr virtual int virtualFun()     {         return 1;     } };  struct Derived : Base {     constexpr virtual ~Derived() {}     constexpr virtual int virtualFun()     {         return 2;     } };  constexpr int f() {     Base * p = new Derived();     int r = p->virtualFun();     delete p;     return r; }  int main() {     constexpr int r = f();     static_assert(r == 2, ""); // 静态断言通过!     return r; }      

编译结果:

直接编译期算出 2,汇编中干干净净。

早期的编译期画风则是这样的:


所以呐,建议各位勤动手做做实践,死守着书本眼光真的会很狭窄啊!

类似的话题

  • 回答
    这个问题触及了面向对象编程(OOP)的核心概念之一:多态性。而虚函数的本质,正是实现运行时多态的关键。所以,直接回答是:是的,虚函数的设计目的就是为了实现运行期绑定。但这仅仅是个答案,要理解为什么,咱们得把这层“为什么”给扒开,一点点捋清楚。什么是“绑定”?在C++这样的编译型语言里,“绑定”指的是.............
  • 回答
    为什么会有 i 这一虚数?它的“值”究竟是什么?生活中我们处理长度、重量、时间这些都是实实在在的,我们称之为“实数”。但数学的魅力就在于它能超越我们感官的局限,构建出更加广阔的抽象世界。而“i”这个虚数,正是在这个数学世界的探索中应运而生的。 从解方程的困境说起要理解 i 的由来,我们得回到数学史上.............
  • 回答
    好的,我们来详细探讨一下为什么薛定谔方程中会出现虚数 $i$ 这个看似“不属于”实数世界的问题。这背后蕴含着量子力学的深刻内涵和数学结构的必要性。核心观点:虚数 $i$ 在薛定谔方程中,是描述波函数演化规律和概率幅的关键,它与量子系统的时间演化、能量的量子化以及波的性质紧密相连。让我们一步步来理解:.............
  • 回答
    说虚函数“效率低”,其实是一种比较片面的说法。更准确地说,虚函数会带来一定的性能开销,但这并不意味着它就一定“低效”,更不代表就应该避免使用。理解这个问题,我们需要深入到 C++ 的底层机制中去。想象一下,我们写了这样一段 C++ 代码:```c++class Base {public: vi.............
  • 回答
    这个问题很有意思,也触及了C++设计中的一个核心哲学。你觉得纯虚函数不提供函数体更方便,这其实是一个很自然的直觉,尤其是在我们习惯了写函数并需要为其提供实现的场景下。但C++的设计者之所以选择这种方式,背后有着更深刻的考量,是为了在面向对象设计中实现更强的抽象和约定,同时避免潜在的二义性。我们来一步.............
  • 回答
    在C++的世界里,“virtual”这个词被翻译成“虚函数”,这可不是随意为之,而是因为它精确地抓住了这种函数在继承和多态机制中的核心特征。理解“虚”这个字的关键,在于它暗示了一种“不确定性”,或者说是一种“在运行时才确定”的行为。设想一下,你有一系列动物,比如猫、狗,它们都属于一个更大的“动物”类.............
  • 回答
    在 C++ 面向对象编程(OOP)的世界里,理解非虚继承和非虚析构函数的存在,以及它们与虚继承和虚析构函数的对比,对于构建健壮、可维护的类层级结构至关重要。这不仅仅是语法上的选择,更是对对象生命周期管理和多态行为的一种深刻设计。非虚继承:追求性能与简单性的默认选项当你使用 C++ 的非虚继承(即普通.............
  • 回答
    虚数“i”,那个被定义为平方等于负一的数,确实是一个引人入胜的话题。它不像我们触摸得到的苹果或感受到的阳光那样有“实在”的物理存在,但说它是“被人们创造出的数学工具”,这种说法也未免过于轻描淡写了。要理解虚数 i 的本质,我们需要深入地审视它在数学和科学中的作用以及它如何从一个令人费解的概念演变成一.............
  • 回答
    这可真是个好问题,也是困扰了数学家们相当一段时间的疑惑!你说“虚数是负数的平方根”,这本身没错,但它为何会在三次方程里“登堂入室”,甚至显得尤为重要,这背后有着更深邃的故事。咱们得从头说起。在我们的数学世界里,数字就像不同类型的工具,各有各的用处。最初,我们只有自然数(1, 2, 3...),用来数.............
  • 回答
    虚数:不仅仅是数学游戏,更是物理世界的钥匙长久以来,虚数(包含虚数单位 $i$,即 $i^2 = 1$)常常被视为一个抽象的数学概念,似乎只存在于纸面上的推演和逻辑游戏。然而,事实并非如此。虚数在现代物理学的各个领域都扮演着至关重要的角色,它们并非仅仅是数学上的“花哨”,而是揭示物理现象本质、构建理.............
  • 回答
    虚数,这个词本身就带着几分神秘感,常常让人联想到一些遥不可及、飘渺虚无的东西。在数学的领域里,它确实打破了我们对“数”的传统认知,从实数的直线延伸到了一个全新的平面。但问题来了,当我们把目光投向现实世界,投向我们赖以生存的物理世界时,虚数还有着怎样的足迹?它真的只是一个数学上的“游戏”吗?答案是否定.............
  • 回答
    关于虚数和实数单位的讨论,这是一个很有意思的数学概念,也容易让人产生一些联想。我们来聊聊这个话题,尽量说得清晰明白,并且去掉那些冰冷的AI痕迹。你提到了虚数单位“i”。确实,虚数的世界是以“i”为基石构建起来的。我们知道,i² = 1,这个定义打破了实数范围内的规则,让我们可以处理那些平方之后是负数.............
  • 回答
    咱们聊聊虚数这玩意儿,它到底有啥用,为啥数学家们会捣鼓出这么个“不存在”的数来。刚接触虚数的时候,很多人都会觉得奇怪,甚至有点别扭。你说一个数的平方是负数,这可能吗?在咱们日常生活的经验里,正数乘正数得正数,负数乘负数也得正数,怎么会有这么个“平方是负数”的玩意儿呢?这就是虚数出现的原因,它最根本的.............
  • 回答
    虚数不能比大小,这个说起来有点绕,但其实仔细想想就能明白其中的道理。咱们就用大白话聊聊这事儿,尽量不搞得像教科书那么生硬。你想想,咱们平时比大小,比如 3 比 2 大,5 比 7 小,这是怎么来的? 是因为我们有一个共同的参照系,一条线,就是我们常说的数轴。在这条数轴上,数字是有序排列的,越往右越大.............
  • 回答
    这确实是一个很有意思的问题,涉及到了复数运算中一个相当奇妙的领域。我们来好好掰扯掰扯。首先,我们要明确一下我们熟悉的数系。我们有实数,比如 1, 5, $pi$, $sqrt{2}$,它们都可以在数轴上找到位置。然后我们引入了虚数单位 $i$,它的核心定义就是 $i^2 = 1$。正是这个 $i$ .............
  • 回答
    量子力学中引入虚数 i,这可不是一个随随便便的数学技巧,它触及了我们理解世界本质的根基。简单地说,i 的出现,不是为了让公式“好看”一点,而是因为我们所描述的微观粒子,其行为本身就带着一种我们日常经验无法完全捕捉的“转动”或“相位”的特性。想象一下,我们试图描述一个振动的弦,它的位置随时间变化。在经.............
  • 回答
    我们来聊聊一个挺有意思的问题:维数,它有没有可能是虚数呢?说实话,在我脑子里浮现这个想法的时候,也觉得有点跳跃,毕竟我们平时感知到的世界,那个三维的空间,时间是一维的,这些都是实实在在的“数”。但科学的魅力就在于不断探索边界,打破常规认知,所以,问问“虚数的维数”这个问题,本身就是一种很棒的思维训练.............
  • 回答
    当然可以!虽然在数学的某些抽象概念里,i 和 i 的确有着对称的地位,但当我们谈论“区分”它们时,尤其是在我们熟悉的实数世界和复数运算的语境下,是完全可以做到的,而且方法相当直接。试着这样想:我们生活在一个“直的”、“横的”世界里,也就是实数轴。当我们在实数轴上向前走一步,我们就是在增加一个实数值。.............
  • 回答
    实系数多项式方程的虚数解为何总是成对出现?相信许多人在学习代数时都曾遇到过这样一个看似神秘的现象:当一个系数全是实数的(实系数)多项式方程有虚数解时,这些虚数解总是成双成对地出现,而且是一对共轭复数。例如,如果 $a+bi$ (其中 $b eq 0$) 是一个方程的解,那么 $abi$ 也一定是它.............
  • 回答
    哈哈,《FGO》的虚数大海战啊,这可是个让不少御主又爱又恨的活动!要我说,这活动确实有它独到之处,但也确实把不少人给“磨”得够呛。我来给你掰扯掰扯,尽量说得细致点,就像跟老朋友聊天一样,保证不是那种生硬的AI味儿。首先,咱得说它的优点,为啥这活动能火起来? 剧情跌宕起伏,格局宏大: 这是虚数大海.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有