近期在找工作,面的基本上是C/C++相关岗位,整理了一些网上提到的面试题或者知识点,慢慢补充吧,有错误的地方欢迎指出。
下面整理归纳了面试中常问到的题目,分为5大类:
- C++知识点;
- 操作系统;
- 多线程编程;
- 网络;
- 算法;
- 其他。
1. C++知识点
1.1 构造函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
1.2 虚函数的定义
定义
虚函数是一种在基类定义为virtual的函数,并在一个或多个派生类中再定义的函数。虚函数的特点是,只要定义一个基类的指针,就可以指向派生类的对象。
虚函数是在基类中使用关键字 virtual声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
注:无虚函数时,遵循以下规则:C++规定,定义为基类的指针,也能作指向派生类的指针使用,并可以用这个指向派生类对象的指针访问继承来的基类成员;但不能用它访问派生类的成员。
- 使用虚函数实现运行时的多态性的关键在于:必须通过基类指针访问这些函数。
- 一旦一个函数定义为虚函数,无论它传下去多少层,一直保持为虚函数。
- 把虚函数的再定义称为过载(overriding)而不叫重载(overloading)。
纯虚函数
定义在基类中的一种只给出函数原型,而没有任何与该基类有关的定义的函数。纯虚函数使得任何派生类都必须定义自己的函数版本。否则编译报错。
您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
纯虚函数定义的一般形式:
Virtual type func_name(参数列表)=0;
含有纯虚函数的基类称为抽象基类。抽象基类又一个重要特性:抽象类不能建立对象。但是抽象基类可以有指向自己的指针,以支持运行时的多态性。
1.3 虚函数表
虚函数表的创建和继承
a. 基类的虚函数表的创建:
首先在基类声明中找到所有的虚函数,按照其声明顺序,编码0,1,2,3,4……;
然后按照此声明顺序为基类创建一个虚函数表,其内容就是指向这些虚函数的函数指针,按照虚函数声明的顺序将这些虚函数的地址填入虚函数表中。例如若show放在虚函数声明的第二位,则在虚函数表中也放在第二位。
b. 对于子类的虚函数表:
首先将基类的虚函数表复制到该子类的虚函数表中。
若子类重写了基类的虚函数show,则将子类的虚函数表中存放show的函数地址(未重写前存放的是子类的show虚函数的函数地址)更新为重写后函数的函数指针。
若子类增加了一些虚函数的声明,则将这些虚函数的地址加到该类虚函数表的后面。
通过虚函数表访问对象的方法
当执行Base->show()时,要观察show在Base基类中声明的是虚函数还是非虚函数。若为虚函数将使用动态联编(使用虚函数表决定如何调用函数),若为非虚函数则使用静态联编(根据调用指针Base的类型来确定调用哪个类的成员函数)。此处假设show为虚函数,首先:由于检查到Base指针类型所指的类Base中show定义为虚函数,因此找到Base所指的对象,访问对象得到该对象所属类的虚函数表地址。其次:查找show在Base类中声明的位置在Base类中所有虚函数声明中的位序。然后到Base所指对象的所属类的虚函数表中访问该位序的函数指针,从而得到要执行的函数。
1.4 为什么要把析构函数定义为虚函数?
new出来的是子类son的对象,采用一个父类father的指针来接收,故在析构的时候,编译器因为只知道这个指针是父类的,所以只将父类部分的内存析构了,而不会去析构子类的内存,就造成了内存泄露。基类析构函数定义为虚拟函数的时候,在子类的对象的首地址开始会有一块基类的虚函数表拷贝,在析构子类对象的时候会删除此虚函数表,此时会调用基类的析构函数,所以此时内存是安全的。
1.5 为什么虚函数比普通函数慢?
因为虚函数要通过查找虚函数表的方法访问。
1.6 为什么构造函数不能是虚函数?
构造函数不可以是虚函数的,这个很显然,毕竟虚函数都对应一个虚函数表,虚函数表是存在对象内存空间的,如果构造函数是虚的,就需要一个虚函数表来调用,但是类还没实例化没有内存空间就没有虚函数表,这根本就是个死循环。
1.7 内联函数、构造函数和静态成员函数可以定义为虚函数么?为什么?
内联函数是编译时展开函数体,所以在此时就需要有实体,而虚函数是运行时才有实体,所以内联函数不可以为虚函数。
静态成员函数是属于类的,不属于任何一个类的对象,可以通过作用域以及类的对象访问,本身就是一个实体,所以不能定义为虚函数。
如果构造函数定义为虚函数,则需要通过查找虚函数表来进行调用。但是构造函数是虚函数的情况下是找不到的,因为构造函数自己本身也不存在,创建不了实例,没有实例化对象,则类的成员不能被访问。
1.8 基类指针指向派生类时如何知道指向的是哪一个派生类?
派生类对象的内存范围大于基类对象的内存范围。指向派生类的指针如果指向基类,则可能访问不可预知的内存空间,也就是派生类增加的特殊属性或方法地址入口。指向基类的指针如果指向派生类,其访问空间总是在派生类的内存空间的内部,不会越界。
1.9 正确区分重载、重写和隐藏
注意三个概念的适用范围:处在同一个类中的函数才会出现重载。处在父类和子类中的函数才会出现重写和隐藏。
重载:同一类中,函数名相同,但参数列表不同。
重写:父子类中,函数名相同,参数列表相同,且有virtual修饰。
隐藏:父子类中,函数名相同,参数列表相同,但没有virtual修饰;函数名相同,参数列表不同,无论有无virtual修饰都是隐藏。
基类中:
(1) virtual void show(); //是虚函数
(2) void show(int); //不是虚函数
子类中:
(3) void show(); //是虚函数
(4) void show(int); //不是虚函数
1,2构成重载,3,4构成重载,1,3构成重写,2,4构成隐藏。另外2,3也会构成隐藏,子类对象无法访问基类的void show(int)成员方法,但是由于子类中4的存在导致了子类对象也可以直接调用void show(int)函数,不过此时调用的函数不在是基类中定义的void show(int)函数2,而是子类中的与3重载的4号函数。
1.10 如何定义一个只能在栈上或者堆上生成对象的类?
详细的解答:
链接:https://www.nowcoder.com/questionTerminal/0a584aa13f804f3ea72b442a065a7618
1. 只能在堆上生成对象:将析构函数设置为私有。(最好的方式设置为protect 然后自己定义create和destroy方法 https://www.cnblogs.com/vincently/p/4838283.html)
原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
- 只能在栈上生成对象:将new 和 delete 重载为私有。
原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。
将new操作设置为私有,那么第一阶段就无法完成,就不能够再堆上生成对象。
1.11 基类的析构函数不是虚函数,会带来什么问题?
派生类的析构函数用不上,会造成资源的泄漏。
1.12 派生类中构造函数与析构函数,调用顺序
构造函数的调用顺序总是如下:
1. 基类构造函数: 如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
2. 成员类对象构造函数: 如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。如果有的成员不是类对象,而是基本类型,则初始化顺序按照声明的顺序来确定,而不是在初始化列表中的顺序。
3. 派生类构造函数: 析构函数正好和构造函数相反.
1.13 析构函数中抛出异常时概括性总结
- C++中析构函数的执行不应该抛出异常;
- 假如析构函数中抛出了异常,那么系统将变得非常危险,也许很长时间什么错误也不会发生;但也许系统有时就会莫名奇妙地崩溃而退出了,而且什么迹象也没有;
- 当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外,即在析构函数内部写出完整的throw…catch()块。
1.14 子类析构时要调用父类的析构函数吗?
析构函数调用的次序是先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了。定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数。
1.15 面向对象有哪些特点?如何体现?
- 继承:就是保留父类的属性,开扩新的东西。通过子类可以实现继承,子类继承父类的所有状态和行为,同时添加自身的状态和行为。
- 封装:就是类的私有化。将代码及处理数据绑定在一起的一种编程机制,该机制保证程序和数据不受外部干扰。
- 多态:是允许将父对象设置成为和一个和多个它的子对象相等的技术。包括重载和重写。重载为编译时多态,重写是运行时多态。
重载与覆盖:
Overloading:重载(两个或者多个函数在同一类中,名一样,参数列表不一样)。
或者说:方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载.
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现.
Overriding(重写)=覆盖:在父类有个函数,在子类也又有一个同样名字的函数,而且在子类内中把这个功能做的跟具体化。
1.16 多态
定义:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现;
多态实现的两种方式:父类指针指向子类对象 或 将一个基类的引用类型赋值为它的派生类实例。(重要:虚函数 + 指针或引用)
构造函数、复制构造函数、析构函数、赋值运算符不能被继承。
在程序编译时多态性体现在函数和运算符的重载上;
1.17 “引用”与多态的关系?
引用是除指针外另一个实现多态的方式。这意味着,一个基类的引用可以指向它的派生类实例。例:
Class A; Class B : Class A{…};
B b; A& ref = b;
1.18 在什么时候需要使用“常引用”?
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:const 类型标识符 &引用名=目标变量名;