首页 > 试题广场 >

以下代码的运行结果为:

[单选题]
以下代码的运行结果为:
class Base {
    public:
        Base() {
            echo();
        }
        virtual void echo() {
            printf("Base");
        }
};
  
class Derived:public Base {
    public:
        Derived() {
            echo();
        }
        virtual void echo() {
            printf("Derived");
        }
};
  
int main() {
    Base* base = new Derived();
    base->echo();
    return 0;
}

  • Derived
    Derived
    Derived
  • Derived
    Base
    Derived
  • Base
    Derived
    Base
  • Base
    Derived
    Derived
我记得还有个题说父类构造函数里不能调用虚函数,因为虚函数是运行时调用确定的,父类对象会调用子类函数,这样会出问题,求大神给出正确详细的解释
发表于 2017-01-02 14:59:22 回复(6)
http://blog.csdn.net/kkk328/article/details/7081215
发表于 2017-03-08 21:28:33 回复(0)
Base* base = new Derived();
//首先将上面的语句分成两部分:
//前一部分是基类指针指向子类对象实现动态绑定,后一部分是new了一个子类对象;
//语句执行时,先执行后一部分new Derived()这句话,它会调用Derived() {echo();}
//而执行派生类的构造函数时,会先去执行基类的构造函数,所以会调用Base() {echo();},此时不涉及到虚函数的动态绑定,
//因为我们现在才执行了后一部分,还没有动态绑定,所以正常调用基类的echo()函数,执行完后返回到子类的构造函数,执行子类的echo();
//然后执行前一部分,实现虚函数的动态绑定。
base->echo();
//此时考察的就是虚函数的使用了,基类echo申明为虚函数,所以调用时会去子类寻找对应的虚函数执行。
发表于 2017-09-11 10:01:35 回复(2)
先说结论:不要在构造函数和析构函数中调用虚函数
下面说说原理:

假如基类有个虚函数
那么编译器会为其创建虚函数表vtbl
并在对象的内存空间创建虚函数指针vptr
虚函数表的原理是每个类会在里面有自己的所能调用到的虚函数地址

对象的内存空间一般只有两样东西:
虚函数指针vptr 和 数据成员(包括直接基类和间接基类的)
在对象初始化过程中
先构造基类对象再构造派生类对象
也就是说当前正在执行基类的构造函数时
执行完初始化列表之后 执行构造函数体之前
编译器插入了初始化vptr的逻辑
令对象的vptr指向是基类的vtbl
所以此时无论如何也访问不到派生类的vtbl
也就无法调用派生类中覆盖(overrided)了的虚函数
只有执行到派生类的构造函数时
才能更新vptr指向派生类的vtbl
此后调用的虚函数才是派生类中覆盖了的虚函数

析构函数函数是构造函数的逆过程
所以在派生类的析构函数中的vptr
基类析构函数中的vptr是不同的
同样是执行析构函数体之前就被改为了对应当前类的vptr

下面我们做个实验验证一下

struct Base {
    void printVPTR() {
        void (***pvptr)() = reinterpret_cast<void (***)()>(this);
        void (**vptr)() = *pvptr;
        printf("%p\n%p\n", this, vptr);
    }
    Base() {printVPTR();}
    virtual ~Base() {printVPTR();}
};
struct Derived : Base {
    Derived() {printVPTR();}
    ~Derived() override {printVPTR();}
};
int main() {
    Derived d;
}






打印结果:

0x7fff5fbff748

0x1000010a8

0x7fff5fbff748

0x100001060

0x7fff5fbff748

0x100001060

0x7fff5fbff748
0x1000010a8

各位可以看到构造/析构过程中this指针是没变的
因为基类子对象和派生类子对象的区域是重叠的
都是从对象的基地址开始偏移为0的内存
如果是非第一直接基类的指针
偏移就不是0了
而是要加上在其之前的基类的成员的size
回到正题
我们看vptr的值在不同的构造函数中是不同的
基类是0x1000010a8
派生类是0x100001060
中间有72bytes也就是9个ptr的间隔
这段空间是派生类vtbl所占用的
主要存储了重载的虚函数和type_info指针
对于每种编译器 vtbl的内容千差万别千奇百怪
一般都会有虚函数和type_info指针
感兴趣的朋友可以用示例代码中得到的vptr解引用得到的函数指针来遍历这段内存
看看你所用的编译器在里面究竟存了什么玩意儿
我在另一个问题里详细地讨论了继承模型的内存布局
回到正题
这两个指针说明构造和析构会及时地修改vptr以匹配当前的类
由此证明了开头的论点:
不要在构造函数和析构函数中调用虚函数
因为基类构造函数执行时对象的vptr是指向基类的vtbl
而不是你所要创建的派生类的vtbl
因此你无法获得预期的虚函数多态
编辑于 2017-09-27 12:32:19 回复(14)
首先声明Base类型的指针指向实际类型为Derived的对象,先调用基类构造函数,再调用派生类构造函数。输出Base, Derived.

base->echo(); 指针是base类型,但是因为有关键词 virtual,所以不是隐藏而是重写.调用的是Derived的方法,输出Derived。

1.重载:重载从overload翻译过来,是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
2.隐藏:隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
3.重写:重写翻译自override,也翻译成覆盖(更好一点),是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
发表于 2017-05-15 18:11:43 回复(8)
首先声明Base类型的指针指向实际类型为Derived的对象,先调用基类构造函数,再调用派生类构造函数。输出Base, Derived.

base->echo(); 指针是base类型,但是因为有关键词 virtual,所以不是隐藏而是重写.调用的是Derived的方法,输出Derived。

发表于 2018-07-10 10:16:53 回复(0)
这个父类指针指向子类对象,在生成对象时是先构造父类,此时子类对象还没有呢,所以父类构造函数中的虚函数失去了意义,而就是调用了父类的对应函数,子类构造函数也是调用自己的,接下来用父类指针去调用虚函数时才真正发生了运行时多态。
发表于 2017-04-22 17:48:50 回复(4)
本题在构造函数时调用了echo函数,因此我们需要讨论
Base *base = new Derived(); 中的入栈顺序---->
1.new Derived ---> 首先调用父类构造 再调用子类构造
2.base->echo ----> 由于多态调用子类构造
发表于 2021-10-12 08:51:28 回复(0)

一、如图

图片说明

好像有多态的C++题目,统一两个分析点:
1、指针,我们主要关注它的类型,也就是主要关注“指针步长
2、调用函数,我们主要要手工进行,“编译期”处理不是虚函数的一般函数,然后在“运行期”处理虚函数
PS:编译期处理,普通函数(静态绑定),运行期处理,虚函数(动态绑定)

分析:
Base * 指针指向Derived,然后调用函数echo

  • 1、“编译期”检查,发现Base(注意!!因为指针类型是Base而不是Derived)类型中echo是虚函数,我们无法在编译期进行处理(函数调用)
  • 2、“运行期”检查,发现Derived有虚函数echo,我们沿着如图所示虚函数指针vptr找到对应函数,进行“动态绑定”(多态机制)

二、补充——对指针步长的理解

指针步长,决定了指针调用函数的寻址

#include
using namespace std;
class A
{
    public:
        void mytest()
        {
            cout<<"编译期——静态绑定"<<endl;
        }
};
class B: public A
{
    public:
        virtual void mytest()
        {
            cout<<"运行期——动态绑定"<<endl;
        }    
}; 
int main()
{
    A * p= new B;
    p->mytest();//输出  编译期——静态绑定
    //注意上面输出,注意理解
    return 0;
 } 
编辑于 2020-11-01 07:45:02 回复(0)
看成大卫贝克汉姆😂
发表于 2020-05-12 20:55:31 回复(0)
此题主要考面向对象的多态性问题,虚函数调用,而且还考了要构造子类就必须先构造父类
发表于 2016-12-30 03:40:28 回复(0)

解析:

  1. 首先创建指向 Derived 类对象的 Base 类指针 base。
  2. 在创建 Derived 对象时,会先调用 Base 的构造函数,然后再调用 Derived 的构造函数。
  3. 在 Base 的构造函数中调用 echo() 函数,由于 echo() 函数是虚函数,所以会根据实际对象类型来确定调用的函数版本。此时,对象类型是 Derived,因此会调用 Derived 类的 echo() 函数,输出 "Derived"。
  4. 紧接着,在 Derived 的构造函数中调用 echo() 函数,依然根据实际对象类型来确定调用的函数版本。由于正在构造 Derived 对象,所以此时调用的仍然是 Derived 类的 echo() 函数,输出 "Derived"。
  5. 最后,通过 base 指针调用 echo() 函数。由于虚函数的动态绑定机制,调用的是指向 Derived 类对象的指针的 echo() 函数,输出 "Derived"。
发表于 2023-10-19 17:44:33 回复(1)
new的时候先调用父类构造函数再调用子类构造函数,然后通过指针调用子类函数
发表于 2017-09-25 15:47:39 回复(0)
当在构造函数内部调用一个虚函数时,将使用当前正在构造的对象的类型(即基类的类型)来解析该函数调用,而不是最终的对象类型(派生类)。
编辑于 2024-03-13 11:09:39 回复(0)
Base* base = new Derived();
1、new的时候,直接调用派生类的构造函数
2,在执行派生类的构造函数会先调用基类的构造函数;
3、返回派生类进行构造,实现虚函数的动态绑定;
4、base->echo();去子类寻找虚函数
发表于 2021-07-05 14:45:20 回复(0)
行派生类的构造函数时,会先去执行基类的构造函数,行完后返回到子类的构造函数
发表于 2020-10-08 15:40:49 回复(0)
结合高赞解析,总结下我的理解,不知道对不对,如果有问题,请评论指出。谢谢!
Base* base = new Derived();
1、上述语句首先定义了一个Base指针,指向一个Derived子类对象(此时该对象还没有被new出来);然后new一个Derived子类对象;
2、该Derived子类对象首先调用基类Base的构造函数(**输出Base**),初始化基类的数据成员;
        然后再来调用Derived子类自己的构造函数初始化子类数据成员(**输出Derived**。(整个过程就是这么规定的)。
3、
base->echo();
该过程被调用的函数取决于用来调用函数的句柄,而不是句柄所指向的类型
该句柄是一个Base基类的指针,因为通过基类的指针或者引用调用了虚函数,此时发生了动态绑定原则上会访问基类Base的虚函数echo(),但是由于派生类的虚函数覆盖/隐藏了基类的虚函数,所以最终该句柄调用的是派生类Derived的echo()虚函数(**输出Derived**)

**************************************************************************************************************************************************

对于划横线的部分,我不太确定,直接修改代码进行了验证,如果echo不是虚函数,那么结果应该是 Base  Derived  Base
#include <cstdio>
using namespace std;

class Base {
public:
Base() {echo();}
/*virtual */void echo() {printf("Base\n");}
};
  
class Derived:public Base {
public:
Derived() {echo();}
/*virtual */void echo() {printf("Derived\n");}
};
  
int main() {
Base* base = new Derived();
base->echo();
return 0;
}
运行结果如下:


为了验证,由于派生类的虚函数覆盖/隐藏了基类的虚函数,所以最终该句柄调用的是派生类Derived的echo()虚函数。我调试了未修改之前含有virtual函数echo的程序,当执行到base->echo()时,程序确实是跳转到了子类的虚函数echo()处。

对于具体是覆盖还是隐藏,我看了解析还是不太明白,希望大神能给解答下。两者有什么么区别,有解析说是覆盖,为什么不是隐藏?
编辑于 2020-04-28 09:29:17 回复(0)
1、构造函数调用链
C++新建一个对象时,会从基类开始,按照继承顺序,逐个调用构造函数。析构函数正好相反。

2、虚函数表
C++的多态底层是通过虚函数表实现的,每个带有虚函数的类,都会类开头隐含带有一个虚函数表指。例如
class A {
   int v;
   virtual fun() {}
}
在32位下,sizeof(A)为8(虚表指针 + int)
每个带有虚函数的类,都有自己对应的虚函数表,有且唯一

3、构造函数与虚函数表指针
对于带有虚函数的类,编译器会构造函数开头,添加虚函数表指针的初始化代码(动态绑定),指向类对应的虚函数表,只有在虚函数表指针被初始化后,才有多态。

4、对象内存模型
在单继承、基类、派生类都有虚函数时,派生类的内存模型如下:
————————
vptr(虚表指针)
————————
基类成员
————————
派生类成员
————————

本题中,Derived继承与Base,代码的执行顺序为
调用派生类构造函数
    调用基类构造函数
        初始化虚函数表指针(这个时候指向基类的虚函数表)
        执行基类构造函数的代码(这个时候虚表指针指向了基类的虚表,因此只能调用基类的函数)
    初始化虚表指针(虚表指针被重新指向了派生类的虚函数表)
    执行派生类构造函数的代码

PS:逻辑上讲时,是先调用基类构造函数,再调用派生类构造函数。在实现上,编译器会在派生类构造函数的开头插入基类的构造函数。
    
发表于 2019-06-06 22:32:00 回复(0)
Base* base = new Derived();
//首先将上面的语句分成两部分:
//前一部分是基类指针指向子类对象实现动态绑定,后一部分是new了一个子类对象;
//语句执行时,先执行后一部分new Derived()这句话,它会调用Derived() {echo();}
//而执行派生类的构造函数时,会先去执行基类的构造函数,所以会调用Base() {echo();},此时不涉及到虚函数的动态绑定,
//因为我们现在才执行了后一部分,还没有动态绑定,所以正常调用基类的echo()函数,执行完后返回到子类的构造函数,执行子类的echo();
//然后执行前一部分,实现虚函数的动态绑定。
base->echo();
//此时考察的就是虚函数的使用了,基类echo申明为虚函数,所以调用时会去子类寻找对应的虚函数执行。
发表于 2018-07-24 15:47:00 回复(0)
Base* base = new Derived();
首先看左边,定义一个指针,此时没有发生任何类的构造。
然后看右边,new一个Derived类,先构造基类对象,用的Base()构造,在构造派生类的对象,用Derived()构造。
base->echo();
用于echo在基类中用了virtual,派生类中又重写了这个函数,所以直接调用的派生类中的echo();
发表于 2018-04-06 10:59:31 回复(0)