面试浅谈之 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()功能不同
三 🏠 结语
身处于这个浮躁的社会,却有耐心看到这里,你一定是个很厉害的人吧 👍👍👍
各位博友觉得文章有帮助的话,别忘了点赞 + 关注哦,你们的鼓励就是我最大的动力
博主还会不断更新更优质的内容,加油吧!技术人! 💪💪💪