C++面试高频(函数)
函数
1 请你说说内联函数,为什么使用内联函数?需要注意什么?⭐⭐⭐⭐⭐
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline。
为什么使用内联函数?
函数调用是有调用开销的,执行速度要慢很多,调用函数要先保存寄存器,返回时再恢复,复制实参等等。
如果本身函数体很简单,那么函数调用的开销将远大于函数体执行的开销。为了减少这种开销,我们才使用内联函数。
内联函数使用的条件
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的内联。
内联函数和宏函数的区别
1 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率;而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。
2 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率。
3 宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等。
2 内联函数和函数的区别,内联函数的作用。⭐⭐⭐⭐⭐
1 内联函数比普通函数多了关键字inline
2 内联函数避免了函数调用的开销;普通函数有调用的开销
3 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址。
内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。
内联函数的作用:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。
3 析构函数 ⭐⭐⭐⭐⭐
什么是析构函数?
在C++中,一个类的析构函数是用来在对象销毁时执行清除和资源回收操作的函数,它与该类的构造函数成对存在。析构函数名与类名相同,但在函数名前面加一个波浪号(~)。
当某个对象超出其作用域时(通常是函数的末尾),C++会自动调用该对象的析构函数。析构函数通常用来清理该对象的资源,例如释放动态分配的内存、关闭文件或网络连接、销毁子对象等。
如果一个类没有定义析构函数,C++会自动生成一个默认的析构函数,这个默认析构函数不做任何操作,因此并不一定能处理某些需要特定操作的资源清理。
析构函数的声明方式如下:
class ClassName { public: ClassName(); // 构造函数 ~ClassName(); // 析构函数 }
例如:
class MyClass { public: MyClass(); // 构造函数 ~MyClass(); // 析构函数 } MyClass::~MyClass() { // 执行资源清理操作... }
需要注意的是,虚析构函数是指在继承中对父类指针进行释放时需要使用的析构函数,用来确保释放子类对象时使用的是正确的析构函数。例如:
class BaseClass { public: BaseClass(); virtual ~BaseClass(); }; class DerivedClass : public BaseClass { public: DerivedClass(); virtual ~DerivedClass(); }; BaseClass *p = new DerivedClass; delete p;
上面的代码中,由于“p”指向的是DerivedClass对象,所以应该使用DerivedClass的析构函数。因此需要在基类BaseClass中设置虚析构函数以确保正确的析构函数被调用。
析构函数可以抛出异常吗?为什么不能抛出异常?
异常点之后的代码不会执行:当析构函数抛出异常时,异常将导致程序流程跳转到异常处理代码,导致异常点之后的代码不会被执行。这可能会导致对象销毁过程中的必要动作无法执行,例如释放资源,从而引发资源泄漏等问题。
还有以下几点原因:
- 安全性:抛出异常可能导致资源泄漏或不一致的状态。
- 可追踪性:异常的发生会增加代码的复杂性和调试的困难。
- 可移植性:不同编译器可能对析构函数中的异常支持不同。
为了解决这些问题,一种常见的做法是在析构函数中尽量避免抛出异常,而是使用try-catch块捕获和处理可能发生的异常。通过在try块中执行资源清理操作,并在catch块中进行适当的异常处理,可以确保对象的销毁过程能够正常进行,同时提供更好的程序安全性和可追踪性。
如果析构函数抛出异常,并且在异常点之后的程序不会执行,造成了资源泄漏等问题,可以考虑以下解决方法:
- 使用智能指针:使用C++中的智能指针(如std::unique_ptr、std::shared_ptr)来管理资源,可以自动处理资源的释放,避免手动管理资源导致的错误和异常。智能指针的析构函数会自动释放资源,即使在析构函数中抛出异常,也可以保证资源的正常释放。
- 分离资源管理:将资源的释放操作从析构函数中分离出来,使用独立的函数或类来管理资源的释放。在析构函数中调用这些资源管理函数,如果资源释放过程中发生异常,可以通过合适的方式处理异常,避免资源泄漏。
- 做好异常处理:在析构函数中合理地使用异常处理机制,例如使用try-catch块捕获异常,并在catch块中适当地处理异常。这样可以保证即使在析构过程中发生异常,也不会导致程序崩溃或其他严重问题。
4 析构函数必须为虚函数吗?构造函数可以为虚函数吗?⭐⭐⭐⭐⭐
C++默认析构函数不是虚函数,因为申明虚函数会创建虚函数表,占用一定内存,当不存在继承的关系时,析构函数不需要申明为虚函数。
若存在继承关系时,析构函数必须申明为虚函数,这样父类指针指向子类对象,释放基类指针时才会调用子类的析构函数释放资源,否则内存泄漏。
构造函数不能为虚函数,当申明一个函数为虚函数时,会创建虚函数表,那么这个函数的调用方式是通过虚函数表来调用。若构造函数为虚函数,说明调用方式是通过虚函数表调用,需要借助虚表指针,但是没构造对象,哪里来的虚表指针?但是没有虚表指针,怎么访问虚函数表从而调用构造函数呢?这就成了一个先有鸡还是先有蛋的问题。
5 构造与析构的顺序⭐⭐⭐⭐⭐
构造顺序:基类构造函数》对象成员构造函数》子类构造函数
析构顺序:子类析构函数》对象成员析构函数》基类析构函数
从里向外构造,从外向里析构
答案解析
我们给一个例子:
#include <iostream> using namespace std; class A{ public: A(){cout<<"A::constructor"<<endl;}; ~A(){cout<<"A::deconstructor"<<endl;}; }; class B{ public: B(){cout<<"B::constructor"<<endl;}; ~B(){cout<<"B::deconstructor"<<endl;}; }; class C : public A{ public: C(){cout<<"C::constructor"<<endl;}; ~C(){cout<<"C::deconstructor"<<endl;}; private: // static B b; B b; }; class D : public C{ public: D(){cout<<"D::constructor"<<endl;}; ~D(){cout<<"D::deconstructor"<<endl;}; }; int main(void){ C* pd = new D(); delete pd; return 0; }
运行结果如下:
A::constructor B::constructor C::constructor D::constructor C::deconstructor B::deconstructor A::deconstructor
6 虚函数 ⭐⭐⭐⭐⭐
什么是虚函数?
在 C++ 中,虚函数是通过在函数声明前加上关键字“virtual”来定义的。虚函数的作用是让派生类可以根据自己的需要重写基类的函数实现。
当基类的指针或引用指向一个派生类的对象时,如果基类中的成员函数被声明为虚函数,那么该成员函数在运行时将根据实际对象类型调用相应的覆盖函数。这个过程称为“动态绑定”或“后期绑定”。
一个类只有在需要作为基类使用时才将其成员函数声明为虚函数,因为虚函数是带有一定开销的,如果没有必要,可以不使用虚函数来避免多余的开销。
虚函数的声明方法如下:
class ClassName { public: virtual ReturnType FunctionName(ParameterList); }; 例如: class Shape { public: virtual float getArea() { return 0; } }; class Circle: public Shape { public: Circle(float r) { radius = r; } virtual float getArea() { // 圆形面积的计算公式 return 3.14 * radius * radius; } private: float radius; };
在上面的例子中,getArea()函数是一个虚函数。在派生类Circle中,我们可以用与基类完全不同的方式重新实现它,但在派生类的对象中调用getArea()函数时总是会根据对象类型选择相应的函数实现。
总之,虚函数是允许派生类改写基类函数的一种特殊的函数。使用虚函数可以方便地实现多态性,使得程序更加灵活和可读。
7 说说虚函数的工作机制⭐⭐⭐⭐
- 概念:每个包含虚函数的类都会有一个虚函数表。虚函数表是一个存储类成员函数地址的数组,它由编译器在编译时创建。虚函数表中存储了该类所有虚函数的地址,并且每个派生类如果有自己的虚函数实现,也会有自己的虚函数表。
- 作用:虚函数表的主要作用是在运行时根据对象的实际类型来查找并调用相应的虚函数。通过虚函数表,程序可以在不知道对象具体类型的情况下,正确地调用对象的虚函数。
- 概念:每个包含虚函数的类的对象都会有一个隐藏的成员变量,即虚表指针(VPTR)。虚表指针是一个指向该对象所属类的虚函数表的指针,它通常位于对象的起始位置。
- 作用:当创建一个包含虚函数的类的对象时,编译器会自动为该对象初始化虚表指针,使其指向该类的虚函数表。通过虚表指针,对象可以访问其所属类的虚函数表,从而调用相应的虚函数。
虚函数是 C++ 中实现多态性的重要手段,它允许在运行时根据对象的实际类型来调用相应的函数,而不是在编译时就确定调用的函数。以下是虚函数的工作机制:
虚函数表(VTable)
虚表指针(VPTR)
运行时绑定
过程:当通过基类指针或引用调用虚函数时,程序会在运行时根据对象的实际类型来确定要调用的函数。具体步骤如下:
- 首先,通过对象的虚表指针找到其所属类的虚函数表。
- 然后,根据虚函数在虚函数表中的偏移量,找到要调用的虚函数的地址。
- 最后,调用该地址所对应的函数。
8 虚函数表在什么时候创建?每个对象都有一份虚函数表吗?⭐⭐⭐⭐
虚函数表的创建时间
虚函数表是由编译器在编译阶段创建的。在编译过程中,编译器会为每个包含虚函数的类生成一个虚函数表。编译器会分析类的定义,确定其中的虚函数,并将这些虚函数的地址按照一定的顺序存储在虚函数表中。对于派生类,编译器会根据其对基类虚函数的重写情况,更新派生类的虚函数表。
当程序运行时,这些虚函数表已经存在于可执行文件的特定区域(通常是只读数据段),程序加载时会将其加载到内存中。
虚函数表的数量
不是每个对象都有一份虚函数表,而是每个包含虚函数的类有一,同一个类的所有对象共享该类的虚函数表。
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
该专栏面向嵌入式开发工程师、C++开发工程师,包括C语言、C++,操作系统,ARM架构、RTOS、Linux基础、Linux驱动、Linux系统移植、计算机网络、数据结构与算法、数电基础、模电基础、5篇面试题目、HR面试常见问题汇总和嵌入式面试简历模板等文章。超全的嵌入式软件工程师面试题目和高频知识点总结! 另外,专栏分为两个部分,大家可以各取所好,为了有更好的阅读体验,后面会持续更新!!!