面试题5

69 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

周一至周五都是有课的,所以更新会晚点 面试准备第三天———得知一个不算坏的消息,找到一个大厂的实习,就能够提前出去实习(起码是有机会出去工作不是),希望能尽快拿到我人生中的第一份大厂实习。

1.为什么析构函数必须是虚函数,而C++默认的析构函数不是虚函数?

(1)将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

(2)虚函数需要而外的虚函数表和虚表指针,会占用额外的内存。但是对于不会被继承的类来说,如果它的析构函数是虚函数,就会浪费内存

2.简述C++有几种传值方式,之间的区别是什么?

(1)值传递

形参即使在函数体内值发生变化,也不会影响实参的值;

(2)引用传递

形参在函数体内值发生变化,会影响实参的值;

(3)指针传递

在指针指向没有发生改变的前提下,形参在函数体内值会发生改变,会影响实参的值

(4)总结

值传递用于对象时,整个对象会拷贝一个副本,这样效率低;而引用传递用于对象时,不发生拷贝行为,只是绑定对象,更高效;指针传递同理,但不如引用传递安全。

3.简述以下堆和栈的区别

(1)堆栈空间分配不同。栈由操作系统自动分配释放,比如存放函数的参数值,局部变量的值等;堆一般由程序员分配释放。

(2)堆栈缓存方式不同。栈使用的是一级缓存,它们通常都是被调用时处于存储空间中,调用完毕后会立即释放;堆则是存放在二级缓存中,速度会慢点。

(3)堆栈数据结构不同。堆类似数组结构;栈类似栈结构,先进后出。

4.简述C++的内存管理

(1)内存分配方式

在C++中,内存分为五个区,它们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

堆:那些由new动态分配的内存块,一般一个new就要对应一个delete。

栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元会被自动释放。

自由存储区:那些由malloc等分配的内存块,和堆十分相似,不过它们用free来结束自己的生命

全局/静态存储区:全局变量和静态变量被分配到同一块内存中。

常量存储区:一个比较特殊的存储区,里面存放的是常量,不允许修改

(2)常见的错误以及对策

错误:

1.内存分配未成功,却使用了它;

2.内存分配成功,但没有初始化就引用它;

3.内存分配成功并且已经初始化,但操作越过了内存的边界;

4.忘记了释放内存,造成内存泄漏;

5.释放了内存却继续使用它

对策:

1.定义指针时先初始化为NULL;

2.用malloc或new申请内存后,应立即检查指针值是否为NULL,防止使用指针值为NULL的内存;

3.为数组和动态内存赋初值,防止把未被初始化的内存作为右值使用;

4.避免数组或指针的下标越界;

5.动态内存的申请与释放必须配对,防止内存泄漏;

6.用free或者delete释放了内存后,立即将指针设置为NULL,防止该指针变成野指针

7.使用智能指针(万事大吉)

(3)内存泄漏以及解决办法

什么是内存泄漏?

简单来说就是申请了一块内存空间,使用完毕后没有释放掉。(三种情况) 1.new和malloc申请资源使用后,没有delete和free释放内存;

2.子类继承父类时,父类的析构函数不是虚函数

3.Windows句柄资源使用后没有释放

解决办法

1.良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。

2.将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。

3.使用智能指针。

5.简述一下什么是面向对象

1.面向对象是一种编程思想,把一切东西都看成一个个对象,比如动物、书、背包等,它们每个都有各自的特性,比如动物是吃东西的,书是看东西的,背包是用来装东西的,把这些对象所拥有的属性变量和操作这些属性变量的函数打包成一个类来表示

2.面向过程和面向对象的区别:

面向过程:根据业务逻辑从上到下构建框架,写代码(比如建房子),先打地基,再盖楼房,最后填上房顶。

面向对象:提取事务的特征,将这些特征与函数绑定在一起,进行封装,这样能够更快速的开发程序,减少了重复代码的重写过程,也便于后期代码的维护和更新。

6.简述以下面向对象的三大特征

面向对象的三大特征是封装、继承和多态

1.封装

将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细则,仅对外公开接口来和对象进行交互。封装本质上是一种管理:我们怎么管理动物呢?比如如果什么都不管,动物很可能会被随意杀害,那么我们就要建一个生态环境保护区来封装动物,但我们并不是封装起来就不给人来观赏,所以我们开放了景区门票,可以通过买票突破封装在合理的监管机制下参观。类也是一样的,不想给人看到里面是怎样运转的,我们使用protected/private把成员封装起来,开放一些共有的成员函数对成员合理的访问。所以封装本质上就是一种管理。

2.继承

可以使用现有类的所有功能,并且无需重新编写原有的类的情况下对这些功能进行拓展。 比如:动物会吃东西,而猫会吃鱼。 在我们已经建立了动物类的情况下,我们就可以直接继承动物类,然后适当的拓展猫的特性。

三种继承方式 在这里插入图片描述

3.多态:

用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态,有两种方式,重载和重写。

7.简述C++中的重载和重写,以及它们的区别

(1)重写(动态多态)

是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有的都必须和基类中被重写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须要有virtual修饰

#include<bits/stdc++.h>
using namespace std;

class A {
public:  virtual void fun()  {   cout << "A";  } 
};

class B :public A {
public:  virtual void fun()  {   cout << "B";  } 
};

int main(void) {  A* a = new B();  a->fun();//输出B,A类中的fun在B类中重写 }

(2)重载(静态多态)

我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同。例如:交换两个数的值其中包括(int, float,char,double)这些个类型。在C语言中我们是利用不同的函数名来加以区分。这样的代码不美观而且给程序猿也带来了很多的不便。于是在C++中人们提出了用一个函数名定义多个函数,也就是所谓的函数重载。函数重载是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

#include<bits/stdc++.h>
using namespace std;
class A {
void fun() {};
void fun(int i) {};
void fun(int i, int j) {};
void fun(int i, float j) {};
void fun1(int i,int j){};
};

8.说说构造函数有几种,分别什么作用

C++中的构造函数可以分为4类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数。

(1)默认构造函数和初始化构造函数。

在定义类的对象的时候,完成对象的初始化工作。有了有参的构造了,编译器就不提供默认的构造函数。

(2)拷贝构造函数

赋值构造函数默认实现的是值拷贝(浅拷贝)。

(3)移动构造函数。

用于将其他类型的变量,隐式转换为本类对象。下面的转换构造函数,将int类型的r转换为Student类型的对象,对象的age为r,num为1004.

Student(int r) {  int num=1004;  int age= r; }

9.请问拷贝构造函数的参数是什么传递方式,为什么?

==拷贝构造函数的参数必须使用引用传递==

如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。

需要注意的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass* c_class),也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。

10.如何理解抽象类?

(1)抽象类的定义如下:

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,有虚函数的类就叫做抽象类。

(2)抽象类有如下几个特点:

1)抽象类只能用作其他类的基类,不能建立抽象类对象。

2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。

3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。