C++面试笔记--继承和接口

  • 整个C++程序设计全面围绕面向对象的方式进行。类的继承特性是C++的一个非常重要的机制。继承特性可以使一个新类获得其父类的操作和数据结构,程序员只需在新类中增加原有类没有的成分。

    在面试过程中,各大企业会考量你对虚函数、纯虚函数、私有继承、多重继承等知识点的掌握程度,因此就有了我们这一节的内容,开始吧。

    1、以下代码的输出结果是什么?

    #include<iostream>
    using namespace std;
    
    class A
    {
    protected:
    	int m_data;
    public:
    	A(int data = 0)
    	{
    		m_data = data;
    	}
    	int GetData()
    	{
    		return doGetData();
    	}
    	virtual int doGetData()
    	{
    		return m_data;
    	}
    };
    
    class B : public A
    {
    protected:
    	int m_data;
    public:
    	B(int data = 1)
    	{
    		m_data = data;
    	}
    	int doGetData()
    	{
    		return m_data;
    	}
    
    };
    
    class C : public B
    {
    protected:
    	int m_data;
    public:
    	C(int data = 2)
    	{
    		m_data = data;
    	}
    };
    
    int main ()
    {
    	C c(10);
    
    	cout << c.GetData() <<endl;
    	cout << c.A::GetData() <<endl;
    	cout << c.B::GetData() <<endl;
    	cout << c.C::GetData() <<endl;
    
    	cout << c.doGetData() <<endl;
    	cout << c.A::doGetData() <<endl;
    	cout << c.B::doGetData() <<endl;
    	cout << c.C::doGetData() <<endl;
    	return 0;
    }
    

    解析:构造函数从最初始的基类开始构造的,各个类的同名变量没有形成覆盖,都是单独的变量.理解这两个重要的C++特性后解决这个问题就比较轻松了,下面我们看看:

    cout << c.GetData() <<endl;

    本来是要调用C类的GetData(),C中未定义,故调用B中的,但是B中也未定义,故调用A中的GetData(),因为A中的doGetData()是虚函数,所以调用B类中的doGetData(),而B类的doGetData()返回B::m_data,故输出 1。

    cout << c.A::GetData() <<endl;

    因为A中的doGetData()是虚函数,所以调用B类中的doGetData(),而B类的doGetData()返回B::m_data,故输出 1。

    cout << c.B::GetData() <<endl;

    肯定是B类的返回值 1 了。

    cout << c.C::GetData() <<endl;

    跟cout << c.GetData() <<endl;语句是一样的。

    cout << c.doGetData() <<endl;

    B类的返回值 1 了。

    cout << c.A::doGetData() <<endl;

    因为直接调用了A的doGetData() ,所以输出0。

    cout << c.B::doGetData() <<endl;
         cout << c.C::doGetData() <<endl;

    这两个都是调用了B的doGetData(),所以输出 1。

    这里要注意存在一个就近调用,如果父类存在相关接口则优先调用父类接口,如果父类也不存在相关接口则调用祖父辈接口。

    答案:

    1 1 1 1 1 0 1 1

  • 2、以下代码输出结果是什么?

    #include<iostream>
    using namespace std;
    
    class A
    {
    public:
    	void virtual f()
    	{
    		cout<<"A"<<endl;
    	}
    };
    
    class B : public A
    {
    public:
    	void virtual f()
    	{
    		cout<<"B"<<endl;
    	}
    };
    
    int main ()
    {
    	A* pa=new A();
    	pa->f();
    	B* pb=(B*)pa;
    	pb->f();
    
    	delete pa,pb;
    	pa=new B();
    	pa->f();
    	pb=(B*)pa;
    	pb->f();
    
    	return 0;
    }

            解析:这是一个虚函数覆盖虚函数的问题。A类里的f()函数是一个虚函数,虚函数是被子类同名函数所覆盖的。而B类里的f()函数也是一个虚函数,它覆盖A类f()函数的同时,也会被它的子类覆盖。但是在 B* pb=(B*)pa;里面,该语句的意思是转化pa为B类型并新建一个指针pb,将pa复制到pb。但是这里有一点请注意,就是pa的指针始终没有发生变化,所以pb也指向pa的f()函数。这里并不存在覆盖的问题。

    delete pa,pb;删除了pa和pb所指向的地址,但是pa、pb指针并没有删除,也就是我们通常说的悬浮指针,现在重新给pa指向新地址,所指向的位置是B类的,而之前pa指针类型是A类的,所以就产生了一个覆盖。pa->f();的值是B。

    pb=(B*)pa;转化pa为B类指针给pb赋值,但pa所指向的f()函数是B类的f() 函数,所以pb所指向的f()函数是B类的f()函数。pb->f();的值是B。

           答案:

    A A B B

  • 3、派生类的3种继承方式?

           答案:

    (1)公有继承方式:

    基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。这里保护成员与私有成员相同。

    基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态;基类的私有成员不可见,基类的私有成员仍然是私有的,派生类不可访问基类中的私有成员。

    基类成员对派生类对象的可见性对派生类对象来说,基类的公有成员是可见的,其他成员是不可见的。

    (2)私有继承方式:

    基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。

    基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问;基类的私有成员不可见,派生类不可访问基类中的私有成员。

    基类成员对派生类对象的可见性对派生类对象来说,基类的所以成员都是不可见的。

    所以说,在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。

    (3)保护继承方式:

    这种继承方式与私有继承方式的情况相同,两者的区别仅在于对派生类的成员而言,基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。

    基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员都作为派生类的保护成员,并且不能被这个派生类的子类所访问;基类的私有成员不可见,派生类不可访问基类中的私有成员。

    基类成员对派生类对象的可见性对派生类对象来说,基类的所以成员都是不可见的。

    所以说,在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。

  • 3、每个对象里面都有一个虚表指针,指向虚表,虚表里面存放了虚函数的地址。虚函数表示顺序存放虚函数地址的,不需要用到链表。
  • 4、下面程序运行结果是什么?

    #include<iostream>
    using namespace std;
    
    class A
    {
    	char k[3];
    public:
    	virtual void aa(){};
    };
    
    class B : public virtual A
    {
    	char j[3];
    public:
    	virtual void bb(){};
    };
    
    class C : public virtual B
    {
    	char i[3];
    public:
    	virtual void cc(){};
    };
    
    int main ()
    {
    	cout << "sizeof(A):" << sizeof(A) << endl;
    	cout << "sizeof(B):" << sizeof(B) << endl;
    	cout << "sizeof(C):" << sizeof(C) << endl;
    	return 0;
    }

          解析:(1)对于A类,由于有一个虚函数,那么必须有一个对应的虚函数表来记录对应的函数入口地址。每个地址需标有一个虚指针,指针的大小为4。类中还有一个char k[3],每一个char值所占空间是1,所以char k[3]所占大小是3。做一个数据对齐后变为4。所以,sizeof(A)的结果就是char k[3]所占大小4和虚指针所占大小4之和等于8。

    (2)对于B类,由于B继承了A,同时还拥有自己的虚函数,那么B中首先拥有一个vfptr_B,指向自己的虚函数表。还有char j[3],大小为4,可虚继承该如何实现呢?首先要通过加入一个虚类指针(记vbptr_B_A)来指向其父类,然后还要包含父类的所有内容,所以sizeof(B)的大小是:A类所占大小8,char j[3]所占大小4,vfptr_B所占大小4,vbptr_B_A所占大小4,它们之和等于20。

    (3)对于C类和B类差不多,结果是32。

    答案:

    sizeof(A):8

    sizeof(B):20

    sizeof(C):32

    知识扩展:编译器对每个包含虚函数的类创建一个表(Vtable:V表)。在V表中,表放置特定类的虚函数地址。在每个带着虚函数的类中,编译器秘密的设置一指针,称为V-ptr,指向这个对象的V表,通过基类指针做函数调用时(也就是多态调用时),编译器静态地插入取得这个V-ptr,并在V表中哈找函数地址的代码,这样就能调用正确的函数使晚绑定发生。

    为每个类设置V-ptr,初始化vptr,为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。利用虚函数这个对象的合适的函数就能被调用了,哪编译器还不知道这个对象的特定类型。

  • 5、什么是虚继承?它与一般的继承有什么不同?它有什么用?写出一段虚继承的C++代码。

    答案:

    虚拟继承是多重继承中特有的概念。虚拟基类是为了解决多重继承而出现的,请看下图:

    在图 1中,类D接触自类B和类C,而类B和类C都继承自类A,因此出现了图 2所示的情况。

    在图 2中,类D中会出现两次A。为了节省内存空间,可以将B、C对A的继承定义为虚拟继承,而A成了虚拟基类。最后形成了图 3。

    代码如下:

    ?


    1

    2

    3

    4

    class A;

    class B : public virtual A;

    class C : public virtual A;

    class D : public B,public C;

时间: 2024-08-04 09:58:09

C++面试笔记--继承和接口的相关文章

黑马程序员——JAVA学习笔记四(继承、接口、内部类)

1,    通过extends关键字让类与类之间产生继承关系.多个类中存在相同属性和行为时,将这些内容抽取到单独的一个类中,那么多个类无需定义这些属性和行为,只要继承那个类即可,已存在的类叫做超类,基类,或父类.新类称为子类,派生类,孩子类. 子类可以直接访问父类中的非私有的属性和行为.子类无法继承父类中私有的内容.JAVA不支持多继承,只支持单继承,多实现. 继承提高了代码复用性,让类与类之间产生了关系.为多态提供了前提. 2,    super关键字代表父类中成员变量内存空间的标示.两个作用

Java面试笔记

1.&和&& if(str != null& !str.equals("")){ System.out.println("ok"); }//抛空指针异常 if(str != null&& !str.equals("")){ System.out.println("ok"); }//正常编译 &还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&a

继承与接口1

下面程序输出什么: #include<iostream> using namespace std; class A { public: void virtual f(){ cout<<"A"<<endl; } }; class B:public A{ public: void virtual f(){ cout<<"B"<<endl; } }; int main() { A* pa=new A();//定义

Javascript面向对象特性实现封装、继承、接口详细案例——进级高手篇

Javascript面向对象特性实现(封装.继承.接口) Javascript作为弱类型语言,和Java.php等服务端脚本语言相比,拥有极强的灵活性.对于小型的web需求,在编写javascript时,可以选择面向过程的方式编程,显得高效:但在实际工作中,遇到的项目需求和框架较大的情况下,选择面向对象的方式编程显得尤其重要,Javascript原生语法中没有提供表述面向对象语言特性的关键字和语法(如extends.implement).为了实现这些面向对象的特性,需要额外编写一些代码,如下.

Java笔记8-抽象接口

高级特性部分: 抽象(abstract) 接口(interface) 提纲: 抽象类的定义和使用 模板设计模式的使用 接口的定义和使用 回调函数 区别抽象类和接口的异同 软件设计原则---------------------------------------------------------------------抽象类 定义: public abstract class XXX{ //1.抽象类中可以有构造方法,但是抽象类不能够被实例化 //2.抽象类中可以普通属性 //3.抽像类中可以有

关于Java中继承和接口的理解

关于Java中继承和接口的理解 Java语言中,为了实现代码重用,设计了继承这一机制,但是,其设计成单继承,这样设计是有原因的,如下图: Figure1:deadly diamond of death 此图问题称为菱形问题(diamond problem),就是说,当A的子类B和C同时实现了A中的方法,则同时继承了B和C的子类D在调用该方法时会出现混乱,无法得知该调用哪一个方法. 既然不能实现多继承,我们就会考虑把很多方法就写在父类里,或者继承抽象类,实现其方法,但是,这样会导致一个问题,比如说

【C#学习笔记】 IDisposable 接口

托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源.托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收. 非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标等.这类资源,垃圾回收器在清理的时候会调用Object.Finalize()方法.默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源. 在

面向对象,继承和接口的使用理解

面向对象: 将你要将各种小类组合成一个大类的时候,面向对象的思想只需要在大类中添加一个小类的实例.这样可以提升代码的重用率. 继承: 当需要对父类进行扩张来得到一个拥有一些额外方法或者属性的时候使用.当父类的一些东西需要重写时使用也相当重要.抽象类:抽象类.中的抽象方法必须被实现,而可以有非abstract的方法和数据.这就区别于接口使得它更加灵活.在某些特定方法只会在这个类的子类中需要使用的时候,就没有必要特地为这个方法书写一个接口.抽象类用于抽象一个实际物品的时候特别有效. 接口: 因为接口

编写高质量代码改善C#程序的157个建议——建议56:使用继承ISerializable接口更灵活地控制序列化过程

建议56:使用继承ISerializable接口更灵活地控制序列化过程 接口ISerializable的意义在于,如果特性Serializable,以及与其像配套的OnDeserializedAttribute.OnDeserializingAttribute.OnSerializedAttribute.OnSerializingAttribute.NoSerializable等特性不能完全满足自定义序列化的要求,那就需要继承ISerializable了. 以下是格式化器的工作流程:如果格式化器