【面试题】封装/继承/多态

面向对象

  • C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题;
    C++是基于面向对象的,关注的是对象,将一件事情拆分成不同对象,靠对象之间的交互完成
  • 面向对象程序设计(Object-oriented programming,OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。
  • 面向对象的三大特征:封装、继承、多态

封装

  • 概念:
    将数据和操作数据的方法有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
    封装的本质是一种管理
  • C++实现封装的方式:
    用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用,对外隐藏类内部的实现细节
  • 访问限定符:
    public修饰的成员可以在类外直接被访问;
    protected和private修饰的成员在类外不能直接被访问;
    访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止;
    class的默认访问权限为private,struct为public(因为struct要兼容C)

C++中struct和class的区别?

  1. C++需要兼容C语言,所以C++中struct可以当成结构体去使用。
  2. C++中struct可以用来定义类,和class定义类是一样的,区别是struct的成员默认访问方式是public,而class是private
  3. class可以作为模板类型,struct不行

C和C++中struct的区别?

  1. C语言中,struct是用户自定义数据类型(UDT);C++中,struct是抽象数据类型(ADT),支持成员函数的定义
  2. C语言中,struct没有访问权限,且struct只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不能是函数
  3. C++中,struct增加了访问权限,且成员即可以是变量,也可以是函数,成员默认访问方式是public

this指针

C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问的。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

this指针的特性:

  1. this指针的类型:类类型* const
  2. 只能在成员函数内部使用
  3. this指针本质上是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

继承

继承机制是面向对象程序设计使代码可以复用的最重要手段,它在保持原有类特性的基础上进行扩展,增加功能,这样产生的类,称为派生类。继承呈现了面向对象程序设计的层次结构。继承是类设计层次的复用。

继承机制中对象之间如何转换?指针和引用之间如何转换?(基类和派生类对象赋值转换)

  1. 向上类型转换
    将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的
  2. 向下类型转换
    将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。RTTI技术,用dynamic_cast进行向下类型转换
  3. 派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用
  4. 基类对象不能赋值给派生类对象
  5. 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI的dynamic_cast来进行识别后进行安全转换

什么是菱形继承?如何解决?

菱形继承的问题:
菱形继承有数据冗余和二义性的问题
解决方式:
虚拟继承可以解决菱形继承的数据冗余和二义性的问题。

什么是菱形虚拟继承?如何解决数据冗余和二义性的?

虚继承是用于解决多继承条件下的菱形继承问题
底层实现原理与编译器有关,一般通过虚基类指针虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基表(不占用类对象的存储空间) (需要注意的是,虚基类依旧会在其子类中存在拷贝,但是只有一份);当虚基类的子类被当作父类继承时,虚基类指针也会被继承
实际上,vbptr指的是虚基表指针,该指针指向了一个虚基表,虚基表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基表)的两份同样的拷贝,节省了存储空间

继承和组合的区别?什么时候用继承?什么时候用组合?

  1. 继承
    继承是is a的关系,比如说Student继承Person,则说明Student is a Person
    1)优点:子类可以重写父类的方法来方便实现对父类的扩展
    2)缺点:
    ① 这种通过生成派生类的复用称为白箱复用。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类是可见的;
    ② 子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法或行为;
    ③ 如果对父类的方法做了修改的话(比如增加了一个参数),则子类方法必须做出相应的修改。子类与父类是一种高耦合,违背了面向对象思想。

  2. 组合
    组合也就是设计类的时候要把组合的类的对象作为自己的成员变量加入到该类中
    1)优点:
    ① 黑箱复用,因为当前对象只能通过所包含的那个对象去调用其方法,被包含的对象的内部细节对当前对象是不可见的
    ② 当前对象与包含对象是一个低耦合关系,如果修改被包含类中的代码不需要修改当前类的代码
    ③ 当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值
    2)缺点:
    ① 容易产生更多对象
    ② 为了能组合多个对象,必须对接口进行定义

  3. 优先使用组合,组合的耦合度低,代码维护性好


多态

  • 概念:
    多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为,即“一个接口,多种方法”
  • 构成多态的两个条件:
    1)必须通过基类的指针或者引用调用虚函数
    2)被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

重载、重写(覆盖)、隐藏(重定义)

函数重载

C++允许在同一作用域中声明几个功能相似的同名函数,这些同名函数的形参列表(参数个数或类型或顺序)必须不同

为什么C++支持函数重载,而C语言不支持?

在LInux下,采用gcc编译完成后,函数名字的修饰没有发生改变;采用g++编译完成后,函数名字的修饰变成【_Z+函数长度+函数名+类型首字母】
所以C语言没办法支持函数重载,因为同名函数没办法区分;而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载

extern "C"

有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译

重写(覆盖)(虚函数的重写)

  • 派生类中有一个跟基类完全相同的虚函数(即派生类函数与基类函数的返回值类型、函数名、参数列表完全相同),称子类的虚函数重写了基类的虚函数
  • 虚函数重写的两个例外
    ① 协变:基类与派生类函数返回值类型不同,即基类虚函数返回基类对象的指针或引用,派生类返回派生类对象的指针或引用
    ② 析构函数的重写:基类与派生类析构函数的名字不同。如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然函数名不同,其实编译器对析构函数名做了特殊处理,编译后析构函数的名称同一处理成destructor

隐藏(重定义)

  • 两个函数参数相同,但是基类函数不是虚函数。和重写的区别在于基类是否是虚函数
  • 两个函数参数不同,无论基类是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个作用域

重载、重写(覆盖)、隐藏(重定义)的区别

在这里插入图片描述

override和final

// override的使用
class A
{
    virtual void foo();
}
class B : public A
{
    void foo(); //OK
    virtual void foo(); // OK
    void foo() override; //OK
}

// final的使用
class Base
{
    virtual void foo();
};

class A : public Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};

class C : B // Error: B is final
{
};

C++对函数重写的要求很严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override和final两个关键字,可以帮助用户检测是否重写

  1. final:修饰虚函数,表示该虚函数不能再被继承
  2. overrride:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

什么是抽象类?作用?

纯虚函数:在虚函数的后面写上=0。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
抽象类:包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

什么是接口继承?什么是实现继承?

实现继承:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
接口继承:虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。

对象访问普通函数快还是虚函数快?

  1. 如果是普通对象,是一样快的
  2. 如果是指针对象或引用对象,则调用普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找

inline函数可以是虚函数吗?

不能。因为inline函数没有地址,无法把地址放到虚函数表中。

静态成员可以是虚函数吗?

不能。因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

构造函数可以是虚函数吗?

不能。因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

析构函数可以是虚函数吗? 什么场景下析构函数是虚函数?

可以。最好把基类的析构函数定义成虚函数。

虚函数表(虚表)是在什么阶段生成的,存在哪?

虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

虚函数存在哪?

虚函数和普通函数一样,都是存在代码段的,只是他的指针存在虚函数表中。对象中存的不是虚表,存的是虚表指针。

动态绑定与静态绑定

  1. 静态绑定:又称为前期绑定(早绑定),在程序编译器期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定:又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

虚函数表和虚基表

虚函数表虚表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
虚基表中存放的是偏移量

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值