第一次面试(C++)
in 代码随笔 with 3 commentsand 627 read

第一次面试(C++)

in 代码随笔 with 3 commentsand 628 read

“黄博士你好,我叫刘腾遥!”
“喔,刘腾遥是吧,请坐。”

这是我第一次面试,对面端坐着一位文质彬彬的中年博士,面露和善的微笑,让我有些恍惚,忘却了这是一场面试。
第一次面试,我排在名单的最后,这是一个最十分尴尬与难受的位置,看着前面的一个个和我一样抱有满腔热血的年轻人,踌躇满志的进去,然后各有面色的出来。或喜,或忧,或眉目舒展,或川纹紧锁。直到叫到我的时候,一直紧绷的我才缓缓释然。

你英语怎么样?

这是黄博士问我的第一个问题,被问到这个问题,我倒是不是很慌,做IT英语是语言基础,因为过了四级,所以我有恃无恐,也向他表达了即将来到的CET考试有想过六级的打算。

你了解数据库索引么?为什么要有数据库的索引?

在学校写MIS系统的的时候,关键就是数据库的设计,数据库设计的优劣就能预见之后的程序的可发展性,在大数据的背景下,数据库的设计查询优化,也就尤为重要,索引(Index)是帮助SQL高效获取数据的数据结构。提取句子主干,就可以得到索引的本质:索引是数据结构。索引是什么样的数据结构呢?最常见的就是B-Tree(平衡二叉树)和B+-Tree

那你在说说什么是二叉树?什么又是平衡二叉树?

黄博问问题是循循善诱,逐步深入的,说到二叉树,就讲到了数据结构部分,当时我回答的是每个节点都最多只有两个结点的树就是二叉树,平衡二叉树是左右子树的高度差不大于一,这其中考虑到了数据结构里树的基础概念,大一下学期学的这个,大三来已经记得不多,所以解释了一下,就转移的问题。

在二叉树中基本概念还有满二叉树和完全二叉树,概念如下

  1. 完全二叉树——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
  2. 满二叉树——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
  3. 平衡二叉树——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

说到二叉树又不得不拓展说下,二叉树的遍历

数据库设计里的范式了解么,谈谈你对第三范式的理解?

数据库设计有三大范式:

所以3NF中:首先是 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。

左外连接知道么?

这样问的就明显比较细节了,也比较深入,很可惜,数据库已经扔了一两年,没怎么复习,也没怎么使用这么全,看着我既懵又很着急的样子,黄博笑了笑,说,听说过吧。我连忙应承道,听过听过,只是很久没用,一时半会儿想不起来。尴尬的笑笑后,就跳过了这一题。回来之后一看,很难受,这么简单的问题,居然放过了,很可惜,暴露了底子不扎实。

SQL里面的连接一般用FROM和WHERE指定,一般分成内连接和外连接。

内连接(典型的连接运算,使用像=或<>之类的比较运算符)。包括相等连接和自然连接。内连接使用比较运算符根据每个表共有的列的值匹配两个表中的行。例如,检索students和courses表中学生标识号相同的所有行。

外连接。外连接可以是左向外连接、右向外连接或完整外部连接。

在FROM子句中指定外连接时,可以由下列几组关键字中的一组指定:
LEFT JOIN或LEFT OUTER JOIN
左外连接的结果集包括LEFT OUTER子句中指定的左表的所有行,而不仅仅是连接列所匹配的行。如果左表的某行在右表中没有匹配行,则在相关联的结果集行中右表的所有选择列表列均为空值。
RIGHT JOIN或RIGHT OUTER JOIN
右外连接是左外连接的反向连接。将返回右表的所有行。如果右表的某行在左表中没有匹配行,则将为左表返回空值。
FULL JOIN或 FULL OUTER JOIN
完整外部连接返回左表和右表中的所有行。当某行在另一个表中没有匹配行时,则另一个表的选择列表列包含空值。如果表之间有匹配行,则整个结果集行包含基表的数据值。

交叉连接。交叉连接返回左表中的所有行,左表中的每一行与右表中的所有行组合。交叉连接也称作笛卡尔积。

给变量开辟内存的方式有哪些?他们之间有什么区别

这次问题就回到了编程语言的基础上了,在C++中给变量分配内存有两种,一种是基于C库函数的malloc,一种是就是new;
1.申请的位置:
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
2.返回类型安全性:
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void ,需要通过强制类型转换将void指针转换成我们需要的类型。
3.内存分配失败时的返回值
new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
4.是否需要指定内存大小
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
5.是否调用构造函数/析构函数
使用new操作符来分配对象内存时会经历三个步骤:
第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
第三部:对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存时会经历两个步骤:
第一步:调用对象的析构函数。
第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会。
6.能够直观地重新分配内存
使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。new没有这样直观的配套设施来扩充内存。
new和malloc的区别.png

在C++中捕获异常的方法?如果有个函数,不管它抛不抛异常都要运行,怎么做?

讲真在C++中还没有写到有异常处理的地方,但是我很了解Java中的异常处理,心想可能会是一样的,所以,我再回答前一个问题的时候,我说:使用try…catch()来捕获异常。看着黄博士若无其事的点头,我觉的我可能蒙对了。对于黄博士第二个深入的问题,我隐约记得在Java中有一个在异常处理的使用final关键字修饰函数,但是我不敢确定,是否在C++中也同样试用。可能就在这一下子,就暴露了我CPP的底子太薄。没对语言进行全面的了解。

说一下static的用法。

越短的问题是越难回答的,可能我本次面试我回答的最失败的地方,可能我对C++中的static理解不深或者没有理解,我记忆里都还是停留在C里面的静态全局变量那里,这个问题我回答的磕磕巴巴,说的都没到点子上。

面向过程里,在全局变量前,加上关键字static,该变量就被定义成为一个静态全局变量。静态全局变量有以下特点:

  1. 首先是在全局数据区分配内存;
  2. 未经初始化的静态全局变量会被程序自动初始化为0(在函数体内声明的自动变量的值是随机的,除非它被显式初始化,而在函数体外被声明的自动变量也会被初始化为0);静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;
  3. 静态变量都在全局数据区分配内存,包括后面将要提到的静态局部变量。对于一个完整的程序,在内存中的分布情况如下
    代码区 //low address->全局数据区->堆区->栈区 //high address
  4. 一般程序把新产生的动态数据存放在堆区,函数内部的自动变量存放在栈区。自动变量一般会随着函数的退出而释放空间,静态数据(即使是函数内部的静态局部变量)也存放在全局数据区。全局数据区的数据并不会因为函数的退出而释放空间。

静态局部变量也是一样:

通常,在函数体内定义了一个变量,每当程序运行到该语句时都会给该局部变量分配栈内存。但随着程序退出函数体,系统就会收回栈内存,局部变量也相应失效。

但有时候我们需要在两次调用之间对变量的值进行保存。通常的想法是定义一个全局变量来实现。但这样一来,变量已经不再属于函数本身了,不再仅受函数的控制,给程序的维护带来不便。

静态局部变量正好可以解决这个问题。静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。

静态局部变量有以下特点:

面向对象里,在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。

对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;

静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。
静态数据成员和普通数据成员一样遵从public,protected,private访问规则;
因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它;
静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:
<数据类型><类名>::<静态数据成员名>=<值>
类的静态数据成员有两种访问形式:
<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ;

静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这 有两个好处,第一,不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,所以节省存储空间。第二,一旦利息需要改变时,只要改变一次, 则所有存款类对象的利息全改变过来了;

同全局变量相比,使用静态数据成员有两个优势:

  1. 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
  2. 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;

静态成员函数

与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部 实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this 是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指 针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。

那你说一说const吧!

黄博士问出这个问题的时候,我就知道上一题的static我的理解就狭义了,或者说我把satic的用法和const搞混了,
对于const我也回答到,定义常量和类型检查的基本用法,下面是我查到的相关资料
const.png

C++里struct 和class有什么区别?

这个问题很基础,一般在学习C++的时候了解到OOP的时候引入类,就会和面向过程中的解决办法(struct)比较一下,结构体和类本质上是没有区别的,在C++中二者唯一的区别就是其默认的继承访问权限,struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。当然只是默认,自定义的就另当别论了。

谈一谈Public,Private,Protected的区别

黄博问起这个问题,我倒是有所准备。
访问范围,是面向对象里的一个基本保障
类的一个特征就是封装,public和private作用就是实现这一目的。所以用户代码(类外)可以访问public成员而不能访问private成员;private成员只能由类成员(类内)和友元访问。
类的另一个特征就是继承,protected的作用就是实现这一目的。所以protected成员可以被派生类对象访问,不能被用户代码(类外)访问。
有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。

  1. public继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:public, protected, private
  2. protected继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:protected, protected, private
  3. private继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:private, private, private

但无论哪种继承方式,上面两点都没有改变:

  1. private成员只能被本类成员(类内)和友元访问,不能被派生类访问;
  2. protected成员可以被派生类访问。

之后黄博士就问了下之前笔试做的题目问题,基本出入不大,失败的想法和解决办法以及改进的可行性都描述的一下,和博士级别的人聊天真的很舒服,让我彻底忘记了这是一场面试,反而是一种探讨了,这点让我找回了之前基础不牢打击的自信心。

当我讲完我的思路之后,黄博依旧面带微笑,指出了我其中错误的要点,并要我下去再想一下,然后问我如果没有什么问题,本次面试就可以结束了!
我起身说了句谢谢,走出面试教室,捏了捏手,才发现我手心已经一手汗了。

评论
  1. 太深入了,10年前的程序员会斤斤计较资源的分配,底层原理这些。现在,大多数电脑性能高,根本不必像10年前的那种一个细节斤斤计较啊的写法。可能我太菜了吧,我写C#就不会管这些,目的是把东西写出来能运行,如果内存遭不住,运行速度太慢我会优化。写python爬虫更是,我能爬下来数据就行了,什么原理我也不知道

    回复
  2. 挺有意思的哈!

    回复
    1. @ACG资源

      过奖过奖,弄着玩玩

      回复