面试浅谈之 C++ 面向对象篇

面试浅谈之 C++ 面向对象篇

一 🏠 概述

HELLO,各位博友好,我是阿呆 🙈🙈🙈

这里是面试浅谈系列,收录在专栏面试中 😜😜😜

本系列将记录一些阿呆个人整理的面试题 🏃🏃🏃

OK,兄弟们,废话不多直接开冲 🌞🌞🌞

二 🏠 核心

1、面向对象的三大特征是哪些 ?各自有什么样的特点 ?

封装:将客观事物封装成抽象的类,类数据和方法可依场景暴露或隐藏

继承:派生类继承于基类,无需二次实现基类功能,提高代码复用性 ,也可对原功能拓展

多态:⼀个类接口在不同场景下有不同的表现形式,不同派生类对象共享相同基类接口

2、C++ 中类成员的访问权限

C++ 通过成员访问限定符 public(公有的)、protected(受保护的)、private(私有的) 控制成员变量和成员函数的访问权限

类内部无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制

类外部,只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private,protected 属性成员

补充内容 C++ 访问权限和继承方式

不同继承方式会影响基类成员在派生类中的访问权限

在这里插入图片描述

3、多态实现有哪几种 ?

多态分为静态多态和动态多态

静态多态通过重载和模板实现,在编译期间确定

动态多态通过虚函数和继承实现,执⾏动态绑定,在运行期确定

4、动态多态有什么作用?有哪些必要条件 ?

动态多态的作用:

提⾼代码可复用性(接口重用),可扩充性和可维护性(向后兼容)

动态多态的必要条件:

继承 + 虚函数 + 基类指针/引⽤指向⼦类对象

5、动态绑定是如何实现的 ?

当编译器发现类中有虚函数时,会创建⼀张虚表,把虚函数地址放到虚表中,并且给对象增加⼀个虚表指针成员 vptr(指向类虚表)

子类继承基类,子类继承了基类的vptr指针,vptr 指针是指向基类虚函数表;当子类调用构造函数,子类的 vptr 指针指向子类虚函数表,从而实现动态绑定

6、纯虚函数有什么作用 ?如何实现 ?

定义纯虚函数,继承这个类的子类必须实现该函数

① 抽象基类,该基类只做能被继承,不能被实例化
② 某个方法必须在派生类中被实现

实现方式是在虚函数声明的结尾加上 = 0

7、虚表是针对类还是针对对象 ?同⼀个类的两个对象虚表如何维护 ?

虚表针对类,类所有对象共享类虚表,每个对象内部都保存⼀个指向该类虚表指针 vptr ,每个对象 vptr 不同,但都指向同⼀虚表

8、为什么基类构造函数不能定义为虚函数 ?

虚函数调用依赖于虚表,而虚表指针 vptr 需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数

9、为什么基类的析构函数需要定义为虚函数 ?

为了动态绑定,基类指针指向派生类对象,如果析构函数不是虚函数,那么在对象销毁时,就会调用基类的析构函数,只能销毁派生类对象的部分数据(基类部分)

所以必须将析构函数定义为虚函数,在对象销毁时,调用派生类析构,销毁派⽣类对象所有数据

10、构造函数和析构函数能抛出异常吗 ?

构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)

因为析构函数不能被调用,所以可能会造成内存泄露或系统资源未被释放

构造函数中可以抛出异常,但必须保证在构造函数抛出异常之前,把系统资源释放掉,防止内存泄露(智能指针)

从语法角度,构造函数可以抛出异常,但从逻辑和风险控制的⻆度来说,尽量不要抛出异常,否则可能导致内存泄漏

析构函数不可以抛出异常,当异常发生时,C++ 通常会调⽤对象的析构函数来释放资源,如果此时析构函数也抛出异常,即前⼀个异常未处理又出现了新的异常,从而造成程序崩溃

【在一个作用域内,抛出异常到了上层,由于作用域对象也应该被释放,结果又有一个异常产生了】

11、如何让⼀个类不能实例化 ?

将类定义为抽象类(存在纯虚函数)或将构造函数声明为 private

12、多继承存在什么问题?如何消除多继承中的二义性 ?

1、复杂度增加,程序维护容易出错

2、在继承时,基类间或基类与派⽣类间成员同名时,将出现对成员访问的不确定性,即同名⼆义性

消除同名二义性

① 域运算符 :: ,限定派⽣类使用哪个基类成员

② 派⽣类中定义同名成员,覆盖基类相关成员

3、当派⽣类从多个基类派生,而基类又从同⼀个基类派生,访问根基类成员时,将产⽣路径⼆义性,消除路径二义性的方法,① 消除同名⼆义性方法 ② 虚继承,使不同路径继承的同名成员在内存只有⼀份拷贝

13、如果类 A 是⼀个空类,那么 szeof(A) 的值为多少 ?为什么 ?

sizeof(A) 值为 1,编译器需要区分这个空类的不同实例,分配⼀个字节,使空类不同实例拥有独立地址

14、 覆盖和重载之间有什么区别 ?

覆盖是派生类中对函数重定义只发生在类成员函数中;其函数名、参数列表、返回类型与⽗类完全相同,只是函数体存在区别

重载是两个函数具有相同的函数名,不同的参数列表,不关心返回值,对函数类型无要求;调用函数时,根据传递参数列表判断调用哪个函数

15、拷贝构造函数和赋值运算符重载之间有什么区别 ?

拷贝构造函数用于构造新对象

Student s;
Student s1 = s; // 隐式调用拷贝构造函数
Student s2(s); // 显式调用拷贝构造函数

赋值运算符重载用于将源对象内容拷贝至到⽬标对象中(源对象包含未释放内存需先将其释放)

Student s;
Student s1;
s1 = s; // 使⽤赋值运算符

⼀般情况下,类中包含指针变量时需要重载拷贝构造函数、赋值运算符和析构函数

16、对虚函数和多态的理解

多态分为静态和动态,静态多态 重载,编译期确定;动态多态 虚函数机制,运行期动态绑定

父类指针子类对象并调父类虚函数(子类已重写),会调用子类重写的实现(⽗类中声明的虚函数,在子类重写时不加 virtual 也是虚函数)

虚函数的实现:当编译器发现类中有虚函数时,会创建⼀张虚表,把虚函数地址放到虚表中,并且给对象增加⼀个虚表指针成员 vptr(指向类虚表)

子类继承基类,子类继承了基类的 vptr 指针,vptr 指针是指向基类虚函数表;当子类调用构造函数,子类的 vptr 指针指向子类虚函数表

虚函数表指针(vptr) 创建时机 + 虚函数表创建时机

虚表在编译期创建,每个类对应的虚表内容

vptr 在运行期构造函数结尾被创建(在编译期,编译器为构造函数中添加 vptr 赋值语句)

程序运行时,编译器把虚表首地址赋值给 vptr

使用虚函数,会增加访问内存开销,降低效率(本质是函数指针)

对于单继承

类内存分布先父后子,子类和父类不共用一个虚表,但共用一个虚表指针

单继承,对象只有只有一个虚表,且按虚函数声明顺序排列,派生类虚函数紧接着基类虚函数排列,若存在改写,那么覆盖相应父类虚函数

对于多重继承

几重继承有几个虚表,虚表按照派生顺序依次排列。如果子类改写了父类的虚函数,那么就会用子类自己的虚函数覆盖相应的父类虚函数;如果子类有新的虚函数,那么就添加到第一个虚函数表的末尾

对于虚继承

虚基表与虚基表指针有且只有一份

实现共享概念,为了解决多重继承,不同途径继承基类的两个问题 ① 浪费内存空间 ② 存在二义性

17、请你来说⼀下 C++ 中 struct 和 class 的区别

class 和 struct 两点区别:

默认继承权限不同,class 继承默认是 private 继承,而 struct 默认 public 继承

class 可用于定义模板参数,像 typename,但 struct 不能

保存 struct 关键字原因

与 C 语⾔兼容,C++ 对 struct 进行了扩展(可有成员函数)

为避免兼容限制,C++ 中最基本的对象单元规定为 class

18、四种类型转换

1、const_cast

常量指针或常量引用转换为非常量指针或非常量引用,并仍然指向原来对象;把非常量指针或非常量引用转换为常量指针或常量引用,并仍然指向原来的对象;

强制转换类型必须是指针或引用

2、static_cast

static_ cast 内置数据类型间转换,对于类只能在有联系的指针类型间进行转换

用于内置类型间的转换,不能用于内置类型指针间转换

上行转换(类对象或类指针,子转父)

下行转换,或无类型指针转换(不安全)

3、reinterpret_cast

指针、引用、算术类型、函数指针或者成员指针,用于类型之间的强制转换

4、dynamic_cast

含有虚函数的类指针或引用间,执行类型转换时,将检查能否成功转换

能成功转换则转换之,否则返回 nullptr( 如果引用,则抛异常 )

19、RTTI 是什么?其原理是什么?

运行时类型识别,使用基类指针或引用检查实际所指对象的类型

由两个运算符实现:

typeid 返回表达式类型,通过基类指针获取派⽣类的数据类型;

dynamic_cast 类型检查的,将基类指针或引用安全转换为派生类指针或引用

20、为什么不使用 C 强制转换?

C 转化不明确,不进行语法检查,易出错

21、C++ 空类有哪些成员函数

没有实现构造和析构,编译器会自动生成,也叫缺省构造和析构函数

错误,参见深度探索 C++ 对象模型 (P47)

缺省构造函数

缺省拷贝构造函数

缺省析构函数

缺省赋值运算符

缺省取址运算符

缺省取址运算符 const

空类六个默认函数,只有当实际使用这些函数时,编译器才会显式实现

显式实现构造

1、成员需初始化

2、vptr 需初始化

22、模板函数和模板类特化

场景:对于某种类型,需要特定功能,单⼀模板无法实现

**「定义」**对单⼀模板提供的⼀个特殊实例,它将⼀个或多个模板参数绑定到特定的类型或值上

(1)模板函数特例化

必须为原函数模板的每个模板参数都提供实参,且使⽤关键字template后跟⼀个空尖括号对<>,表明将原模板的

所有模板参数提供实参,举例如下:

template<typename T> //模板函数
int compare(const T &v1,const T &v2) {
    if(v1 > v2) return 1;
    else return -1;
}

//模板特化, 针对字符串的比较
template<> 
int compare(const char* const &v1,const char* const &v2) {
    return strcmp(p1,p2);
}

特化本质是实例化⼀个模板函数,而非重载,模板函数参数匹配以最佳匹配为原则

模板及特例应声明在一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本

避免位于不同文件,特例化版本丢失时(或者不在同一个作用域),编译器依然可以用原有的模板进行匹配

模板特化要求向上可得到模板本体声明,所以非特化版本声明要放置到特化版本的上方,(这样特化时才知道自己特化的是谁 )

(2)类模板特化

template<typename T>
class Foo
{
    void Bar(); //特例化类中的部分成员函数而不是整个类
    void Barst(T a)(); 
};

template<>
void Foo<int>::Bar()
{
    //进行int类型的特例化处理
    cout << "我是int型特例化" << endl;
}

Foo<string> fs;
Foo<int> fi;//使用特例化
fs.Bar();//使用的是普通模板,即Foo<string>::Bar()
fi.Bar();//特例化版本,执行Foo<int>::Bar()
//Foo<string>::Bar()和Foo<int>::Bar()功能不同

三 🏠 结语

身处于这个浮躁的社会,却有耐心看到这里,你一定是个很厉害的人吧 👍👍👍

各位博友觉得文章有帮助的话,别忘了点赞 + 关注哦,你们的鼓励就是我最大的动力

博主还会不断更新更优质的内容,加油吧!技术人! 💪💪💪

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注