• 整个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;
  • 6、如果鸟是可以飞的,那么鸵鸟是鸟么,鸵鸟如何继承鸟类?

    答案:

    鸵鸟并不是鸟类,但是它有鸟的属性,他们之间不是is-a关系,而是has-a关系!

    #include<iostream>
    #include<string> using namespace std;
    class bird{
    public:
    void eat();
    void sleep();
    void fly();
    };
    class ostrich{
    public :
    bird eat(){cout<<"ostrich.eat!"<<endl;}
    bird sleep(){cout<<"ostrich.sleep!"<<endl;} };
    int main(){
    ostrich tuoniao;
    tuoniao.eat();
    tuoniao.sleep();
    }

    扩展:如果在逻辑上面A是B的一部分,则不允许B从A派生,而是要用A和其他的东西组合出B。例如:头由耳,眼,鼻子,嘴巴组合而成。

    #include<iostream>
    #include<string> using namespace std;
    class eye{
    public:
    void look(void);
    };
    class nose{
    public:
    void smell(void);
    };
    class mouth{
    public:
    void eat(void);
    };
    class ear{
    public:
    void listen(void);
    }; class head{
    public:
    void smell(void){m_nose.smell();}
    void look(void){m_eye.look();}
    void eat(void){m_mouth.eat();}
    void listen(void){m_ear.listen();}
    private:
    eye m_eye;
    nose m_nose;
    mouth m_mouth;
    ear m_ear;
    };
      • 7、纯虚函数的问题

        答案:

          虚函数和纯虚函数有以下所示方面的区别

        (1)类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样的话,这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
          (2)虚函数在子类里面也可以不重载的;但纯虚函数必须在子类去实现,这就像Java的接口一样。通常把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为很难预料到父类里面的这个函数不在子类里面不去修改它的实现。
          (3)虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然大家也可以完成自己的实现。纯虚函数关注的是接口的统一性,实现由子类完成。

        (4)带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类也叫抽象类。

          这里面一个带有纯虚函数的类是不能够去构造一个对象的。例如  A a;这个是错误的,因为A是抽象类。无法构造一个具体的类。

        虚函数的入口地址和普通函数有什么不同?

          每个虚函数都在V表中占了一个表项,保存着一条跳转到它的入口地址的指令。当一个包含虚函数的对象被创建的时候,它在头部附加了一个指针,指向V表中相应的位置。调用虚函数的时候,不管你是用什么指针调用的它先根据V表找到入口地址再执行,从而实现了动态编译。

        C++中如何阻止一个类被实例化?

          这个类变为抽象类或者是将它的构造函数声明为private。

        一般什么时候构造函数被声明为private?

           比如要阻止编译器生成默认的构造函数。

的问题

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

  1. Java学习笔记——继承、接口、多态

    浮点数的运算需要注意的问题: BigDecimal operand1 = new BigDecimal("1.0"); BigDecimal operand2 = new BigD ...

  2. Effective C++ 笔记:条款 34 实现继承和接口继承

    Differentiate between inheritance of interface and inheritance of implementation. 行为含义 声明一个pure virt ...

  3. Typescript 学习笔记六:接口

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  4. Java学习笔记之---比较接口与抽象类

    Java学习笔记之---比较接口与抽象类 抽象类是描述事物的本质,接口是描述事物的功能 接口与抽象类的异同 1.一个类只能继承一个父类,但是可以有多个接口 2.抽象类中的抽象方法没有方法体,但是可以有 ...

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

    Javascript面向对象特性实现(封装.继承.接口) Javascript作为弱类型语言,和Java.php等服务端脚本语言相比,拥有极强的灵活性.对于小型的web需求,在编写javascript ...

  6. 《java JDK7 学习笔记》之接口与多态

    1.对于"定义行为"也就是接口,可以使用interface关键字定义,接口中的方法不能操作,直接标示为abstract,而且一定是public修饰的. 类要操作接口,必须使用imp ...

  7. C#类继承和接口继承时一些模棱两可的问题[转]

    原文地址:http://www.cnblogs.com/harleyhu/archive/2012/11/29/2794809.html 1.在father定义的方法若含有virtual关键字,chi ...

  8. Java笔记8-抽象接口

    高级特性部分: 抽象(abstract) 接口(interface) 提纲: 抽象类的定义和使用 模板设计模式的使用 接口的定义和使用 回调函数 区别抽象类和接口的异同 软件设计原则--------- ...

  9. Java继承和接口

    接口最关键的作用,也是使用接口最重要的一个原因:能上溯造型至多个基础类.使用接口的第二个原因与使用抽象基础类的原因是一样的:防止客户程序员制作这个类的一个对象,以及规定它仅仅是一个接口.这样便带来了一 ...

随机推荐

  1. CodeForces - 1025D: Recovering BST (区间DP)

    Dima the hamster enjoys nibbling different things: cages, sticks, bad problemsetters and even trees! ...

  2. 20179203 《Linux内核原理与分析》第十一周作业

    一.环境的使用和搭建 首先我的攻击机和靶机都搭建在虚拟机上,选用的是VMware Workstation Pro虚拟机. 攻击机选用的是Linux kali 2017.2版本,而靶机安装的是XP sp ...

  3. FastAdmin 的 Bootstrap-Table 如何合并字段?

    FastAdmin 的 Bootstrap-Table 如何合并字段? ★hey-成都 14:13:34 把下面那个字段合并到上面那个字段是用什么方法 ^★暗物质-江西 14:17:21 city加上 ...

  4. Azure Managed Disk操作

    Azure Managed Disk对原有的Page Blob进行了一次封装.使得Azure VM的Disk操作变得非常简单.本文将介绍实际操作中针对Manage Disk的一些操作. 一.创建Man ...

  5. Linux查看物理CPU个数、核数,逻辑CPU个数

    学习swoole的时候,建议开启的worker进程数为cpu核数的1-4倍.于是就学习怎么查看CPU核数 # 查看物理CPU个数 cat /proc/cpuinfo| grep "physi ...

  6. Celery-4.1 用户指南: Configuration and defaults (配置和默认值)

    这篇文档描述了可用的配置选项. 如果你使用默认的加载器,你必须创建 celeryconfig.py 模块并且保证它在python路径中. 配置文件示例 以下是配置示例,你可以从这个开始.它包括运行一个 ...

  7. mysql 回顾小练习

    Student(id,sname,age,sex) 学生表 Course(id,cname,t_id) 课程表 SC(s_id,c_id,score) 成绩表 Teacher(id,Tname) 教师 ...

  8. Class python31

    # class Teacher: # def __init__(self, name, age, sex, salary, level): # self.name = name # self.age ...

  9. 通过在Oracle子表外键上建立索引提高性能

    根据我的经验,导致死锁的头号原因是外键未加索引(第二号原因是表上的位图索引遭到并发更新).在以下两种情况下,Oracle在修改父表后会对子表加一个全表锁: 1)如果更新了父表的主键(倘若遵循关系数据库 ...

  10. leetcode645

    vector<int> findErrorNums(vector<int>& nums) { ; int S[N]; int n = nums.size(); ; i ...