前言
你好,我是小莱,我想把秋招的面试经历做成一个系列,抽出面试中的知识点,为大家不论是找实习也好,求职也好,提供一些高质量的参考,复习起来更有针对性。每日一遍,让我们一起吊打面试官~
知识点
1. C++ inline内联函数说一下,static有什么用,static函数能inline吗?
简答:
inline
函数建议编译器在每次调用时插入函数体,从而提高效率。static
用于限制函数的作用域,使其仅在定义它的文件中可见。static
函数可以是inline
的,这样既能限制作用域,又能提高效率。
-
inline
内联函数:
-
inline
函数是一种特殊的函数,它建议编译器在每次调用该函数的地方直接插入函数体的代码,而不是进行常规的函数调用。这样做的目的是为了减少函数调用的开销,特别是在函数体较小的情况下。 -
通常用于提高程序的执行效率,减少函数调用的栈帧创建和销毁的开销。
-
编译器对
inline
只是作为一个优化建议,编译器可以选择忽略这个建议。
-
-
static
关键字:
-
static
修饰的函数只能在定义它的文件内部被调用,它限制了函数的作用域。 -
在类中,
static
成员函数属于类本身而不是类的任何对象,因此它们可以通过类名直接调用,而不需要创建类的实例。 -
static
函数也可用于存储静态数据,即数据只会在程序的生命周期内初始化一次。
-
-
static
函数能否inline:
-
可以。实际上,这种组合很常见。当你在类内部定义一个
static
成员函数时,它通常也会被定义为inline
,因为这样可以提高访问速度,并且由于它不依赖于任何对象的状态,所以它总是可以被内联。
-
2. C++多态怎么实现,虚函数原理,inline函数可以是虚函数吗,static函数可以是虚函数吗?
简答:重载实现编译时多态,重写实现运行时多态。虚函数机制实现重写,通过基类指针或引用调用派生类中的方法,在运行时选择具体的方法。虚函数被声明为 inline 也许不会报错,但表现多态性的虚函数实际无法被内联。static函数声明为虚函数会报错。
编译时多态(静态多态)
编译时多态主要通过函数重载和运算符重载实现。
-
函数重载:在同一个作用域内,允许定义多个同名函数,但参数列表不同(参数类型、数量或顺序不同)。
-
运算符重载:允许已有的运算符拥有不同的实现,这取决于它们的参数类型。
运行时多态(动态多态)
运行时多态通过继承和虚函数实现。实现步骤:
- 基类:定义一个基类,并在其中声明至少一个虚函数(使用 virtual 关键字)。
- 派生类:创建一个或多个派生类,这些派生类继承基类,并重写(override)基类中的虚函数。
- 指针或引用:通过基类的指针或引用来操作派生类的对象。
- 动态绑定:在运行时,根据对象的实际类型调用相应的函数。
经典案例:
ps:当通过基类指针删除一个派生类对象时,如果基类的析构函数不是虚的,那么只有基类的析构函数会被调用,而派生类的析构函数则不会被调用,导致内存泄漏。
#include <iostream>
// 基类
class Animal {
public:
virtual void speak() const {
std::cout << "Some generic sound" << std::endl;
}
virtual ~Animal() {} // 虚析构函数以支持多态
};
// 派生类
class Dog : public Animal {
public:
void speak() const override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() const override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
Animal* myAnimal = new Dog();
myAnimal->speak(); // 输出: Woof!
Animal* anotherAnimal = new Cat();
anotherAnimal->speak(); // 输出: Meow!
delete myAnimal; // 清理资源
delete anotherAnimal; // 清理资源
return 0;
}
虚函数原理
1. 类的虚表
每个包含虚函数的类及其子类在编译时会构建一个虚表(vtable),虚表是一个指针数组,每个指针指向虚函数的具体实现。在基类和派生类中,虚表的内容可能不同(没重写就相同)。具体来说:
-
基类的虚表:包含基类所有虚函数的地址。
-
派生类的虚表:包含派生类重写的虚函数的地址,没有重写的就还指向基类的实现。
2.对象的虚表指针
含有虚函数类的对象在创建时,会隐式初始化一个虚表指针(vptr)。这个指针指向对象所属类的虚表。
所以,为什么总说要“通过基类指针或引用调用派生类中的方法”,这乍一看是个很反常的操作,创建一个子类对象却用基类指针指着它,但你细想其实只有这样才能调用一个成员函数执行多种实现,也就是所谓的“多态”。
拿经典案例来说,当 myAnimal->speak() 的时候,Animal 基类的指针调用了 Dog 子类的 speak() 方法,实际就是从 new 出来的 Dog 对象内存中找虚表指针 vptr,再从找到的虚表中定位 speak 的虚函数指针,然后调用对应代码段的实现。
inline函数可以是虚函数吗?
-
不可以。inline 函数是编译时优化的,虚函数是运行时动态调用的,矛盾。换句话说,虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联展开。如果硬要说可以声明为 inline,那要看不同编译器如何处理,它可能不报错,但如果你需要一个函数支持多态,你不会将它声明为 inline
。
static函数可以是虚函数吗?
-
不可以。static 函数属于类,不属于类的实例。而虚函数依赖于对象的特定实例,因为它要在运行时根据对象的实际类型来确定调用哪个函数。说白了一个类只能调用自己的静态函数,没法调用别类的,这是 static 本身的性质。将 static 函数声明为 virtual 会导致编译错误。
3. C++假如一个指针指向的对象被删除,但是这个指针还在用,会发生什么?
简答:如果一个指针指向的对象已经被删除,但这个指针仍然存在并被使用,这种情况被称为悬挂指针(Dangling Pointer)。悬挂指针会导致未定义行为,因此可能会产生各种不同的错误或意外行为。
- 后果
- 访问已释放内存,读取未定义数据,导致逻辑错误或数据损坏。
- 在现代操作系统中,访问非法内存区域通常会触发内存保护错误,如段错误(segmentation fault)或访问冲突(access violation)。
- 如何避免
- 删除一个对象后将其指针置空
- 使用智能指针
ps:悬挂指针 ≠ 野指针,当指针所指向的对象被释放,但是该指针没有任何改变,以至于其仍然指向已经被回收的内存地址,这种情况下该指针被称为悬挂指针; 而未初始化的指针被称为野指针。指针的错误使用通常会导致这些问题——
-
内存泄漏(Memory Leak):分配了内存但用完没有释放,随着程序的运行,越来越多的内存资源没释放,最终可能导致系统资源耗尽;
-
缓冲区溢出(Buffer Overflow):向一个固定大小的缓冲区写入的数据超出了其分配的大小,导致数据覆盖相邻的内存区域,这种错误可能被黑客利用;
-
空指针解引用(Null Pointer Dereference):试图通过空指针来访问内存;
-
内存越界(Out-of-Bounds Access):试图访问数组或数据结构之外的内存;
-
重复释放(Double Free):试图释放同一块内存两次。
4. 假如要设计一个排行榜,在大量整数范围的战力数据中如何排出前k个数据?
简答:不关心前k个数据之外的顺序,可用快排剪枝(快速选择算法)或者堆优化。
-
快速选择算法
快速排序是一种高效的排序算法,其基本思想是分治策略。在处理“找出前 k 大”的问题时,可以采用快速排序的变种,即快速选择(Quickselect)算法。
思路:
- 选择枢轴:在数据集中选择一个元素作为枢轴。
- 分区调整:重新排列数据,使得所有比枢轴大的元素都在它的右边,比枢轴小的元素都在它的左边。
- 剪枝:根据枢轴的位置和 k 的关系来剪枝:
- 如果枢轴的位置恰好是 k,则左侧的所有元素都是前 k 大的。
- 如果枢轴的位置大于 k,则只需要在左侧继续查找。
- 如果枢轴的位置小于 k,则只需要在右侧继续查找。
- 平均时间复杂度为 O(n),但最坏情况下为 O(n^2),适用于数据量 n 不是特别大或者数据分布较为均匀的情况。
-
ps: 如何理解 O(n),设想枢轴变量取中间,第一次需要调整的数据范围为 n,第二次要么调整左半边要么调整右半边,范围为 n/2,第三次调整一半的一半,这样进行下去实际调整的次数范围就是 n+n/2+n/4+...,不会超过 2n,也就在 O(n) 的数量级上。
-
堆优化法
使用堆是解决这类问题的另一种高效方法。可以维护一个大小为 k 的最小堆,堆中存储的是当前找到的最大的 k 个元素。堆中数据不完全有序(它的侄子可能比它大),但保证堆顶元素的极性,每次调整时间复杂度为 O(logk)
思路:
- 初始化堆:首先将数组的前 k 个元素放入最小堆中。
- 遍历数组:从第 k 个元素开始遍历数组:
- 如果当前元素大于堆中最小的元素(堆顶元素),则将堆顶元素移出堆,并将当前元素加入堆中。
- 如果当前元素小于或等于堆顶元素,则继续遍历。
- 结束时堆中的元素:堆中的元素即为最大的 k 个元素。
-
时间复杂度为 O(nlogk),适用于 k 较小而 n 非常大的情况。
Pardon?你说你精通C++?
To be continued
参考资料: