《EDA前端软件开发工程师面试指南》

2023届EDA领域校招总结,完结撒花!!!


目录

前言

一、EDA公司介绍

二、项目面试

1.自我介绍

2.项目深入

3.专业经验

4.成果和技能

5.对面试官有什么问题

三、C++面试

1、高频考点

2、其他知识点

3、算法题

四、逻辑综合面试

1.逻辑综合知识详解

2.开源逻辑综合ABC

五、简历制作

总结


前言

  • 2022/08/26:

本人2023年6月毕业,于2022年7-10月参加秋招,面试总结纯属个人经验,仅供参考

面试的是EDA前端软件开发岗位,也会掺杂一些EDA其他流程的面试

在面试过程中发现自己准备的很乱,没有一个清晰的思路,现在把自己面试的所有经历和题型整理出来,在这里做一个小的总结,不仅帮助自己整理思路,也给大家做个参考

  • 2022/08/27:

对于基本算法而言,推荐去LeetCode力扣进行刷题学习,主要分为easy/medium/hard三种难度系数,保证自己在medium难度下可以做出题目即可。

  • 2022/09/05:

自9月份来看,2022年国内EDA行业的就业形势是没有2021年好的。

主要原因有二,一是22年互联网寒冬,导致很多计算机专业的学生选择到EDA这个工资高、跟互联网差不多的行业就业,竞争加剧。

二是因为20年美国发布EDA禁令,今年正好是20届入学的研究生的择业年

目前的形式是22年的最高工资没有21年的平均工资高,且竞争压力极其卷。

本人目前收到2家公司的offer,在工资层面分别比去年低了20%和15%,在竞争层面是与985的学生进行竞争,<211的基本都很难。

可以预见的是,23年校招和24年校招将会一年比一年难。

  • 2022/09/18

陆陆续续接到好几家公司的offer,观察了今年的局势,个人理解是找一家具有良好人员体系培养的公司是最关键的。

首先需要明确,国产EDA的路至少还有10年,才能逐渐追赶上S/C这些国际大厂。

如果我们以每三年为界,至少需要在第一份合同结束前,变成可以独挡一面的R&D,才能在未来的EDA市场中站住脚。

在收到offer后,一定要明确了解到该公司是否具有良好的人员培养体系,而不是一味地追求高薪水和好公司履历,毕竟EDA市场是以实力为准的。


一、EDA公司介绍

自2020年起,国内的EDA公司犹如雨后春笋般出现,各路公司争相在市场上抢夺人才,并购、对抗等不计其数,因此对就业者来说,选择一家适合自己的且有发展前景的公司是首要的。

据可靠消息称,到2025年左右,国内EDA行业的就业形式依旧是一片良好,工资可以随便开到比互联网大厂还要高的水平,但是再往后看,似乎就不一定了,面试最重要的就是对自己未来发展有一个良好的规划。

在这里,首先介绍一些我熟知的、目前比较火热的国内EDA公司,给大家做个不成熟的行业介绍。

1.合见工软(上海)

公司目前融资30亿左右,简单来说就是财大气粗,同时吸引了很多Synopsys和Cadence的专家,可以学到很多东西,工资也是目前开的很高的之一,其主营业务目前有simulation工具和prototype工具。

本人面试的是prototyping工具下emulation frontend组,做的是FPGA原型验证,将ASIC的RTL设计转换成FPGA的RTL,通过EDA工具生成多块FPGA板,已达到验证ASIC设计正确性的目的。

2.华大九天(上海)

刚上市的EDA公司,首日股价暴涨3倍,足以看出业界对他的信心。前身是国内第一个EDA工具——熊猫系统,也算是国企,目前的模拟电路设计EDA是行业领先的水平,公司正在开展数字电路设计EDA的全流程开发。

目前是全国最火,最火的EDA公司,无限被拔高,面试人都挤破脑袋想往公司进。

本人面试的是SYN组和STA组,做的是ASIC电路的逻辑综合。

3.鸿芯微纳(上海)

国内较早开始的EDA公司,有前端和后端两大产品线的ASIC EDA公司,其中后端产品Aguda是基于国外某EDA巨头的布局布线产品前身开发的,属于行业领先水平。目前尚未融资过多,因此工资开的不属于最高的那批,公司秉持着先拿产品说话,再搞资本运作的理念。

个人评价——国内目前最干实事的公司,员工基本都是科研型,俗称EDA黄埔军校,本人面试的是前端综合组,以ABC为基准进行开发。

4.芯华章(南京)

亿元俱乐部的亿元,为数不多的在南京的EDA公司,可见与南京市政府等关系十分紧密。主要产品是验证工具,包括simulation和emulation等,据传其产品背后有某国际EDA公司支持,因此实力十分雄厚。

  • 2022/09/27
    最近芯华章刚刚收购了瞬曜(Emulation)公司,实力又进一步。

5.安路(上海)

国内目前最大的FPGA上市公司,以国产化替代Xilinx为目标,主要做硬件设计,辅以相应的EDA软件,目前在低端市场可以有一席之地,生存下去是没问题的,工资是开的十分的高,属于会眼红的状态。

本人面试的是Synthesis组,即设计EDA工具的前端(逻辑综合),可以适用于安路自己生产的FPGA上。

6.立芯(上海)

复旦大学陈建利教授创立的公司,其在ICCAD Contest上多次获得PR 1st的成绩,其主要做ASIC芯片布局布线的EDA工具,同时也在开展前端EDA工具的设计。

本人面试的是Syn组,其考验程度可以称为极高,需要在线做LeetCode多题,并且还有众多C++细节以及逻辑综合的面试内容。

7.中微亿芯(无锡)

中国电子旗下的FPGA公司,简称国企。在江苏无锡,小城市福利很不错。主要做的是基于FPGA(Xilinx)开发EDA工具,团队人很好,因为是国企,没有什么压力。

本人面试的是EDA综合组,面试不算很难。

8.Synopsys(新思)(上海)

虽说是EDA三巨头的头把交椅,实际上就是全世界最“牛逼”的EDA公司,没有之一。产品线覆盖EDA全流程且效率极其高,目前国内的技术大牛基本都是从S出来的。

目前S的前端工具DC就是TOP1,但是后端工具ICC2似乎不是很给力。

由于国内S很少招聘EDA前端的岗位,本人面试的是验证岗位,面试更多与Verilog相关的知识点。

9.Cadence(楷登)(上海)

EDA三巨头之一,万年老二,但是同S一样依旧覆盖EDA全流程,且效率极高。

C的后端工具Innovus有反超S的ICC2的架势,且C较S少了很多傲慢,毕竟是追赶者的角色。

同样,国内C也是很少招聘EDA前端的岗位,本人依旧是面试的验证岗位,但是面试官的和善度稍微比S高一些些(S轻喷,纯纯个人意见,虽然但是,能去S的还是一定去S的)。


二、项目面试

1.自我介绍

问: 请简单的介绍一下你自己和你在学校里研究的项目

// 虽说是简单介绍,但不是随便介绍,需要用少的语言(1~2分钟)概括自己简历上的全部内容

答:个人条件+研究课题及成果+专业知识和技能

  • 个人条件:毕业于某大学电子科学与技术专业,目前的研究方向是EDA前端设计,主要研究内容是逻辑综合。

  • 研究生研究课题和成果:我的课题是基于布尔可满足性求解器(SAT)的精确逻辑综合,已知一个待实现电路,对其约束条件进行SAT求解,并得到对应最优电路结构,实现了精确综合,相关内容共发表了5篇论文,其中在CCFDAC2021会议上得到了大会唯一最佳论文奖。

  • 专业知识和技能:目前在合见工软实习,熟悉EDA前端的流程,熟练使用C++开发,较熟悉脚本语言、Linux系统、gdb调试以及基本Verilog。

2.项目深入

问: 能仔细说说你课题具体做了什么吗?

// !!!将科研内容以最通俗易懂的语言进行全方面的讲解,记住三字口诀(是什么、为什么、怎么做

答: 主要分为三部分回答:

1、是什么:做的课题叫什么?主要实现了什么应用?

  • 最重要的是,一定要强调跟面试的公司的项目组有哪些共性问题
  • 面试官可能并不知道你做的课题是什么,他需要对你有一个评估,其标准就是你做的东西难不难,你做的东西是不是跟公司的业务有联系,即你是否有相应的背景。
  • 同时,可以适当的做一些功课,了解其公司业务,在相关领域看看一些文献或实际工具(逻辑综合强烈推荐要提前看看ABC),这是一个大的加分项!!

2、为什么:为什么要开展这个研究?

  • 介绍做这个项目的意义和实际应用,突出自己解决一个问题时的思路
  • 重点对比有这个项目和没有这个研究的区别!

3、怎么做:你是怎么实现的?C++代码!!!!

  • EDA公司目前无非是C++写代码、verilog写case、tcl写自动化脚本、GDB进行Debug
  • 需要强调自己在实现过程中,是怎么用到这些技能的,这是基本盘,也是你在面试官眼里的第一印象。

3.专业经验

问: 我看到你这里有一些实习经验,可以简单说说你干了什么吗?

//如果没有实习,这个问题也可以改成“你在学校里参与了什么课题,可以展开说说吗?”

//这个问题主要是考验你如何从0到1解决一个问题,或者看你在一个项目中的参与度。

//在公司中是需要解决实际问题的,如果有类似实践经验,会上手很快,这是加分项!

答:类似问题可以总结为三类经验,导师课题(面上项目、纵向课题、企业横向课题)/ 研究生科研项目(省市级项目、校科研项目、比赛项目)/ 实习

1、导师课题:突出你负责的内容,划水的项目不如不写!

2、研究生科研项目:以本人为负责人的项目

  • 课题内容简介(参考项目经验)
  • 如何把课题从0到1完成,如何分配你的任务给项目成员(考验你对一个问题如何进行划分和实现

3、实习:具体做了哪些事情,参与了什么/负责了什么要写清楚

  • 参与:浅浅的在一个project中做了什么事
  • 负责:需要一五一十的全部讲清楚

4.成果和技能

1、尽量有体系的写明自己的成果

  • 从GPA到奖学金 = 学习能力
  • 科研项目-比赛 = 实践能力
  • 论文 = 科研能力

2、依次从熟练-熟悉-较熟悉-了解说明自己的技能,在这里列举个人认为在EDA前端需要掌握技能的熟练程度

  • 熟练:C++/C, 算法(LeetCode)
  • 熟悉:基本Verilog语法, GDB调试(Debug能力), 前端设计流程, 综合常用算法
  • 较熟悉:脚本语言(不限于TCL/Shell/Python)
  • 了解:Git/gerrit等版本控制系统

5.对面试官有什么问题

问: 那你对我们公司和部分有什么想问的吗?

//看似是没话找话,但如果回答的好,实际上让面试官对你情商以及态度的印象很好

//!!!!千万不要问“你们公司是做什么的?”等此类问题,实际上这种问题在面试前可以在网上自己获得的,很容易让现场变得尴尬,尤其是技术面的时候,面试官是公司的研发大佬,你问这种低级问题,至少不会加分了。

答: 这里我分类成提问三部曲,结合各位自己的实际情况,供大家参考

1、“我在入职贵公司之后,可能会负责的项目大概会是哪个方向?我可以提前学习一下。”

      “请问,假如我入职了,您对我未来几年的规划是什么?”

  • 这个问题实际上是给面试官一个信号,你对未来的职业规划有一个清晰的认识,同时有一个希望解决的问题的态度。
  • 同时,如果面试官告诉你具体的方向的话,你也可以评估自己是否喜欢这部分内容,或直接开始学习这部分知识,以为入职后打算。

2、“冒昧的问一句,我这次面试下来,有哪些不足和缺点?”

  • 首先,你会给面试官一个态度,表决心也好、突出自己的上进心也罢,这是一个积极的信号!
  • 其次,可以旁敲侧击出公司对你这种岗位和水平的人有什么要求,比如EDA前端软开就必备C++、算法和GDB等基础知识,可以了解到面试官更重视哪方面,可以针对性的进行学习。
  • 哪怕这次面试失败了,从外人的角度看出很多问题,发现自己还有哪些不足,可以快速提升自己,以备下次面试。

3、“感谢您的面试,这次对我收获很大,对于我的不足我也会尽力去补足,也希望可以通过面试加入贵公司!”

  • 简单来说,最后一句话就可以总结成一个字 —— “”。
  • 但是要“舔”的有深度,“舔”的含蓄,“舔”出风格~
  • 以下步骤仅供参考,如有雷同或冒犯,纯属巧合
  1. 第一步,降低自己的姿态
  2. 第二步,提高面试官的地位
  3. 第三步,以退为进,说自己可能进不了,但是下次在努力学习后,还会再来面试
  4. 第四步,感谢话术
  5. 第五步,展现自己锲而不舍、撞破南墙的,要加入公司的态度

三、C++面试

1、高频考点

首先,总结一些高频考点,下述问题都是必须要懂的,且要了解的很透彻。

1.智能指针

  • 避免申请的空间在函数结束时忘记释放 —— 可以减少野指针(wild)和悬空指针(dangling),即内存泄漏的产生
  • 野指针:没有被初始化的指针
  • 悬空指针:指针最初指向的内存已经被释放的指针
     
  • unique_ptr:保证同一时间只有一个智能指针可以指向该对象,避免空间泄露
  • shared_ptr:强引用。多个智能指针可以指向相同对象,但该对象仅在“最后一个引用被销毁时”释放
  • weak_ptr:弱引用,用的不多。协助shared_ptr工作,其构造和析构不会引起引用计数的增加或减少,防止两个shared_ptr互相引用导致的死锁问题(资源永远不释放)
     
  • 因为智能指针是一个,类会自动调用析构函数,自动释放内存。

2.参数传递

  • 形参:在函数定义时出现的参数
  • 实参:函数实际调用的参数
     
  • 指针:形参为指向实参地址的指针。对指针进行地址操作时,会改变实参。编译时将“指针变量名-指针变量的地址”添加到符号表中
  • 引用:形参是实参的别名不可修改必须初始化。在栈中开辟形参的空间,存放的是实参的地址。编译时将“引用变量名-引用对象的地址”添加到符号表中
  • 值:形参是实参的拷贝。在中开辟了空间,将实参复制给形参,改变形参不会修改实参
     
  • 指针参数传递&引用参数传递:指针传参的本质是值传递,传递地址值 / 引用传参是地址传递,传递引用变量
  • 编译的角度:指针在符号表上对应的地址值为指针变量的地址(可修改指向的对象),引用在符号表上对应的地址值为引用对象的地址(不可修改被引用的对象)

例:int &ref= var;

引用:ref作var的别名,不单独占用内存空间,即ref和var的地址是相同的

指针:程序将开辟一块内存空间,存储“ref指向var”这一信息

但仔细思考,如果没有内存空间存储“ref指向var”,计算机是怎么知道ref是var的别名?

解:在部分编译器中,ref在内存中是占用了一块空间的。
但编译器处理后,使计算机认为ref不单独占用内存空间,且取ref地址时直接取到var的地址。

在内存空间中,ref本身也占用一块内存,里面存储着var的地址,实际上大小与指针相同,只是经过编译器处理后,访问这块ref内存时将直接转而访问var的内存。

综上:引用实际上是占用内存空间的,是在程序运行过程中声明的,但程序把它按照不占用内存空间来处理。

3.const关键字

  • 基本数据类型:修饰符为const的变量定义为常量
  • const应用到函数的参数中:
    1、参数const:调用函数时对参数const初始化const,在函数体中保护了对应对象的属性(通常用于参数为指针或引用)
    2、返回值const:返回值分为引用返回(返回引用对象)和非引用返回(返回对象的副本)
    对于引用返回,需要使用const增加其常量属性,则不能被修改,保护对象的属性。
  • const应用到类(class)中:
    1、const成员变量:在对象的生命周期中是const,但类可以创建多个对象,具有多个const值。
    不能在类的声明中初始化const常量,只能在构造函数的初始化列表中初始化
    2、const成员函数:防止成员函数修改成员变量的内容
    使用mutable可以强行修改某个变量,static const成员变量可以建立整个类都恒定的常量
    3、const类对象:定义常量对象,只能调用常量函数。
    非常量对象既可以调用const函数,也可以调用非const函数

4.class的定义和基本概念

C++是面向对象的程序设计。类(class)是C++的核心特性,包含了数据表示法(类成员对象)和处理数据的方法(类成员函数)。类似于C的struct,但比struct更丰富。

类的公共数据成员可以使用直接成员访问运算符 . 实现

创建对象: 类名称 对象 (如 Name A)

  • 类对象

对象有三个访问修饰符:public, private, protected,默认访问修饰符是private。

  1. public(公有):在类的外部是可访问的
  2. private(私有):在类的外部是不可访问的,甚至是不可查看的
  3. protected(受保护):与私有比较类似,但protected还可以在派生类中被访问
  • 构造函数(默认是无参构造函数

在每次创建类的新对象时执行

构造函数的名称和类的名称是完全相同的,并且不会返回任何类型,也不会返回void。

  • 一般构造函数:构造函数可以带参数,用于为某些成员变量设置初始值,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同
  • 拷贝构造函数:函数参数为对象本身的引用(在创建对象时使用同一类中之前创建的对象来初始化新创建的对象)
    使用场景:通过使用另一个同类型的对象(本类型的一个引用变量)来初始化新创建的对象,为深拷贝  
  1. 对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象
  2. 对象以值传递的方式从函数体返回,需要拷贝构造函数创建一个临时对象
  3. 对象需要另一个对象进行初始化
  • 类型转换构造函数:一般构造函数的一种,根据一个指定类的对象创造一个本类的对象(不允许默认转换的话,提前声明explict
  • 执行顺序:已知存在多态,与析构顺序相反
  1. 基类构造函数,若多个基类,则以在类派生表中出现的顺序进行构造
  2. 成员类对象构造函数,若多个成员类对象,则以对象在类中被声明的顺序进行构造
  3. 派生类构造函数
  • 析构函数

类似于构造函数,在每次删除所创建的对象时执行

析构函数的名称和类名称完全相同,只是在前面加了一个波浪号(~)作为前缀, 它也不会返回任何值,也不能带有任何参数。析构函数有助于在调出程序(如关闭文件,释放资源等)前释放资源

  • 执行顺序:已知存在多态,与构造顺序相反
  1. 派生类构造函数
  2. 成员类对象构造函数,若多个成员类对象,则以对象在类中被声明的顺序进行析构
  3. 基类构造函数,若多个基类,则以在类派生表中出现的顺序进行析构
  • 友元函数

友元函数定义在类的外部,其特点是有权访问类的所有私有(private)成员和保护(protected)成员。友元既可以是一个函数又可以是一个类。友元类中,整个类及其所有的成员都是友元。定义友元函数需要在函数原型前使用关键字friend。

  • 内联函数

通常与类一起使用。定义内联函数需要在函数名的前面加关键字 inline,与宏类似,但更高级。

特点:编译器在编译时会把函数的代码副本放置在每个调用该函数的地方。在类中定义的函数都是内联函数,可以不使用inline限定。

5.类的三大特性

  • 封装:把对象的属性和动作封装成抽象的类

封装性是通过访问控制来实现的(public,protected,默认模式private

  • 继承:让某个类的对象可以获得另一个类的对象的属性(多态的前提)

继承性是实现软件可重用性的一种手段,即A类被B类继承,A类为父类,B类为子类,B类继承A类的所有public和protected成员数据和成员函数

子类可以重新定义父类某些属性,重写父类的某些方法,即覆盖父类的某些属性和方法,使其获得与父类不同的功能(重写 override)

  • 多态:不同对象对同一个信息产生不同的行为(同一个接口,不同实现)

动态多态:基于封装和继承的实现,多个子类对继承于一个父类的虚函数进行重写
静态多态:在同一个作用域对函数进行重载(函数名相同,函数参数列表不同) / 对函数进行模板化,忽略数据类型强调数据操作

体现:父类引用或指针指向子类对象 / 父类的引用或指针可以接受子类对象

前提:存在父类与子类的继承关系/父类中必须有虚函数/子类必须重写父类的虚函数/父类引用或指针指向子类对象

6.虚函数

  • 虚函数:在基类的函数前加virtual关键字,并在一个或多个派生类中重新定义的成员函数,实现同一个接口不同的实现,即多态
  • 作用:实现动态联编 / 实现统一接口但不同执行过程
  • 虚函数主要通过虚函数表来实现,其保存该类中虚函数的地址,并且为派生类对象生成一个虚函数指针,指向该类型的虚函数表(可解决继承、覆盖的问题,虚函数表就像一个地图一样,可指明实际所应该调用的函数
    每一个有虚函数的类都有一个虚函数表,虚函数是整个类所共有的,虚函数表存储在对象内存最开始的位置。如果子类继承了多个父类,并且父类有虚函数,则子类要存储多个虚函数指针。如图所示,如果继承了n个父类,并且每个父类都有虚函数,那子类会有n个虚函数表指针。

《EDA前端软件开发工程师面试指南》_第1张图片 虚函数表
  • 联编:将函数名和函数体的程序连接到一起
    静态联编(静态绑定,早绑定) :在编译的时候就确定了函数的地址(效率高

    动态联编(动态绑定,晚绑定) :首先取对象的首地址,再解引用取到虚函数表的首地址,再加上偏移量才能找到要调的虚函数

  • 虚函数的意义

    1、同一个类的多个对象的虚函数表是同一个,可以节省空间

    2、一个类自己的虚函数和继承的虚函数还有重写父类的虚函数都会存在自己的虚函数表

    3、虚函数表本质是一个地图导航,可以告诉想要操作子类的父类指针到底该使用哪个函数

  • 析构函数为什么是虚函数:在用基类操作派生类时,为了防止执行基类的析构函数,不执行派生类的析构函数。因为这样的删除只能够删除基类对象,而不能删除子类对象,会造成内存泄漏
  • 构造函数为什么不是虚函数:创建一个对象需要知道对象的完成信息,虚函数调用仅需要函数的接口,不需要知道对象的具体类型,因此不能实例化。
  • 纯虚函数:在成员函数的形参后面加 = 0 (virtual 函数类型 函数名 (参数表列) = 0)1、为了让派生类只继承函数的接口,不能实例化

    2、一个基类包含一个以上纯虚函数,就是抽象基类。抽象基类不能也没必要定义对象。 
    3、在类的层次结构中,顶层或最上面几层可以是抽象基类。抽象基类体现了本类族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明。 

    4、抽象基类是本类族的共公共接口,即就是从同一基类中派生出的多个类有同一接口

  • 总结
  1. 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同
  2. 在类内用virtual声明后,类外实现虚函数时,不必再加virtual
  3. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性
  4. 只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数
  5. 如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加 
  6. 构造函数、内联函数、静态函数、友元函数、普通函数不能定义为虚函数
  7. 不要在构造函数和析构函数中调用虚函数,可能会出现未定义的行为
  8. 最好将基类的析构函数声明为虚函数

7.深拷贝&浅拷贝

  • 浅拷贝:系统默认的拷贝函数
  • 深拷贝:在堆内存中另外申请空间来存储数据

当数据成员中有指针时,必须用深拷贝,否则会造成野指针等空间泄露的问题。

8.强制转换

  • static_cast:静态转换。主要执行非多态的转换操作。
  • dynamic_cast:动态转换。专门用于派生类之间的转换操作。

静态转换中,上行转换(子类->父类)安全,下行转换(父类->子类)不安全,因为当类型不一致时,转换的是错误意义的指针,容易造成非法访问等现象。

动态转换会检查转换类型,可以实现下行转换(父类->子类)转换

  • const_cast:常量转换。用于const属性的转换。
  • reinterpret_cast:强制转换(高危操作,慎用!!!)。从底层对数据进行重新解释,肆无忌惮的转换。

9.内存泄漏

  • 申请了一块内存空间,使用完毕后没有释放
  • 使用智能指针可以有效避免内存泄漏

10.C++11的新特性

  • 空指针nullptr:替代NULL(0),类型为nullptr_t,隐式转换为任何指针或成员指针的类型
  • Lambda表达式:类似匿名函数(需要函数体,不需要函数名),可以理解为简易版函数
    基本格式: [捕获方式] (参数) -> 返回值类型 {函数体}(可以忽略返回类型,lambda自动推断出返回类型)
    [ ]:闭包形式(closure type),即定义一个lambda表达式后,编译器会自动生成一个重载()运算符的匿名类。优势在于可以通过传值或引用的方式捕获其封装作用域内的变量(即捕获方式中存在的变量)
    捕获方式
  1. [ ]:不捕获任何变量
  2. [=]:值捕获所有变量
  3. [&]:引用捕获所有变量
  4. [x]:值捕获x变量,其他变量不捕获
  5. [&x]:引用捕获x变量,其他变量不捕获
  6. [=,&x]:以值捕获所有变量,除x用引用捕获
  7. [&,x]:以引用捕获所有变量,除x用值捕获
  8. [this]:引用捕获当前对象(复制指针)
  9. [*this]:传值方式捕获当前对象
  • 右值引用:从右值中直接拿数据初始化或修改左值,不需要重新构造左值后在析构右值
    如:int a(左值) = hanshu(B);(右值)
  • 常量表达式泛化:变量初始化后,可以以常量使用。
    如:int n = 5; int arr[n];
  • 初始化列表:{变量1,...,变量n}。 如int a{1};
  • 类型推导:auto关键字和decltype关键字
    auto:静态推导出任意类型
  • decltype:不对表达式进行求值,就可以获取其类型
  • 构造函数委托:在同一个类中,一个构造函数可以调用另一个构造函数
  • final和override:final禁止虚函数被重写或禁止类被继承 / override重写虚函数
  • 哈希表:设置哈希函数F,输入一个key,通过哈希函数F制造独一无二的key1,然后找到存在哈希表key1对应的val。

11.STL容器

序列式容器:元素在容器中的位置顺序存储和访问(array/vector/list/deque)

vector:高效的随机存取

list:大量的插入和删除

deque:即要求高效的随机存取,有需要数据两端的插入和删除

vector:连续空间的容器

  • 迭代器:数据的头(start)、数据的尾(finish)和数组的尾(end)
  • 属性获取
    1、begin() = 数据的头(start)、end() = 数据的尾(finish)
    2、front() = *start、back() = *(finish-1)
    3、size() = 数组元素的个数   max_size() = 数组最大能存储的元素个数  capacity() = 数组的实际大小(容量)
    4、empty() = 判断vector是否为空
  • 数据操作
    1、push_back():首先检查是否还有备用空间,有,则调用迭代器finish,向尾部插入元素。没有,则扩充空间(复制2倍大小的vector ->复制数据 -> 释放原空间),再向其尾部插入元素。
    2、pop_back():放弃尾部元素
    3、erase(iterator first, iterator last):删除first到last之间的元素,实际上是将last后面的元素向前移动
    4、insert():插入元素。实际分为三种情况,
    备用空间够且插入点后面的元素多于新增元素:先构造出finish-n的空间在后面 -> 将插入点后的元素拷贝到备用空间 -> 从插入元素位置插入新值;
    备用空间够且插入点后面的元素少于等于新增元素:先判断是否还有备用空间 -> 在finish后构造一个元素finish1 -> 将原先插入点后的元素移到finish1后 -> 在插入元素位置插入新值;
    备用空间不够:先复制两倍大小的vector -> 分段拷贝元素(插入点前、插入点后) -> 填充新值;
  • 优缺点在频率较高的插入和删除时尽量不使用
    1、优点:在内存的一段连续空间中操作,动态扩容 / 随机访问,支持下标访问 / 节省空间
    2、缺点:​插入删除元素的时间复杂度为O(n) / 只能在末端进行push和pop / 长度太长后,需要重新分配、拷贝和释放空间​​​​​​

list:双向链表

  • 数据结构:存储了前后指针的双向链表
  • 属性获取
    1、begin() = 返回指向头的指针  end() = 返回最后一个元素的后一个地址
    2、rbegin() = 返回最后一个地址    rend() = 返回第一个地址
    3、empty() = 是否为空链表
    4、size() = 链表长度 max_size() = 链表最大长度
    5、front() = 第一个元素的值  back() = 最后一个元素的值
  • 数据操作:list是循环的双向链表,必须确认是在头或者尾进行删除或插入(插入是在指定位置前一个地址进行插入的)
    内部提供transfer()函数,将某连续范围的元素迁移到某个指定位置之前
    1、push_front():在头插入    push_back():在尾插入
    2、pop_front():在头删除    pop_back():在尾删除
    3、erase():先保留前驱和后继节点,删除后,再调整指针位置
    4、splice():将两个链表合并,调用transfer()函数
    5、merge():将传入的list链表x与原链表按从小到大合并到原链表中(前提是两个链表已经是排序过的,调用的也是transfer()函数)
    6、reverse():链表反转,将每个元素一个一个插入到begin之前
    7、sort():排序,调用的是merge()函数
    8、赋值:原链表大(复制完后删除原链表多余的元素)、原链表小(复制完后将被复制链表的剩余元素插入到原链表中)
    9、resize():重新修改list大小,如果原链表长度大于新长度,则删除后面多余的节点
    10、clear():清除所有节点(遍历每个节点,析构并释放)
    11、remove():清除指定值的元素(遍历每个节点,找到就删除)
    12、unique():清除数值相同的连续元素(连续且相同)
  • 优缺点:
    1、优点:不适合连续内存完成动态操作 / 在内部方便进行插入和删除 / 两端push和pop
    2、缺点:不支持随机访问 / 相对于vector内存多

deque:双向队列(list+vector ≈ deque)

  1. duque允许在常数时间内对头部或尾部进行元素的插入和删除
  2. deque没有容量的概念,动态地以分段连续空间组合而成。随时可以增加一块新的空间拼接起来——在缓冲区
  3. 提供随机访问的迭代器(没vector好用),却极其复杂
  • 元素操作:finish是指向最后一个元素的后一个地址,但first是指向第一个元素的地址
    1、push_back():先执行构造再移动节点
    2、push_front():先移动节点再进行构造
  • 优缺点:
    1、优点:随机访问方便,支持下标和at() / 在内部方便进行插入和删除操作 / 两端进行push和pop  
    2、缺点:采用分段连续空间,占用内存多

关联式容器:通过键值(key)存储和读取元素

红黑树:set/map/multiset/multimap

哈希表:unordered_set/unordered_map/unordered_multiset/unordered_multimap

带unordered = 无序,带multi = 允许键(key)重复

insert_equal:允许键值重复

insert_unique:不允许键值重复

set:所有元素都会根据元素的键值自动被排序

  • 元素就是键值(key),不允许有两个元素具有相同的key
  • 不允许通过迭代器改变set的元素值
  • 元素操作:

    1、begin():返回set容器的第一个元素

    2、end():返回set容器的最后一个元素

    3、clear():删除set容器中的所有的元素

    4、empty():判断set容器是否为空

    5、max_size():返回set容器可能包含的元素最大个数

    6、size():返回当前set容器中的元素个数

    7、rbegin:返回的值和end()相同

    8、rend():返回的值和rbegin()相同
    9、count():用来查找set中某个key出现的次数,即判断某一key是否出现过
    10、equal_range() :返回一对迭代器(pair),分别表示第一个大于或等于给定key的元素和第一个大于给定key的元素,如果返回失败则返回end()

    11、erase(iterator) :删除迭代器iterator指向的值

    erase(first,second):删除迭代器first和second之间的值

    erase(key_value):删除键值key的值
    12、find() :返回给定key的迭代器,没找到则返回end()

    13、insert(key_value):将key插入到set中 ,返回值是pair::iterator,bool>,bool标志着插入是否成功,而iterator代表插入的位置,若key已经在set中,则iterator表示的key在set中的位置。

    inset(first,second):将迭代器first到second之间的元素插入到set中

    14、lower_bound(key_value) :返回第一个大于等于key的迭代器

    15、upper_bound(key_value):返回最后一个大于等于key的迭代器

multiset与set完全一致,除了允许key重复

unordered_set基于哈希表,是关键字和key相同的容器

性质 set multiset unordered_set unordered_multiset
底层实现 红黑树 红黑树 哈希表 哈希表
key 不重复 可重复 不重复 可重复
插入元素 insert_unique insert_equal insert_unique insert_equal
元素顺序 有序 有序 无序 无序
是否支持[]
迭代器性质 const_iterator const_iterator const_iterator const_iterator

map:以pair为基础的关联容器 = key + value

multimap允许key重复,且不支持[]运算符

unordered_map基于哈希表

性质 map multimap unordered_map unordered_multimap
底层实现 红黑树 红黑树 哈希表 哈希表
key 不重复 可重复 不重复 可重复
插入元素 insert_unique insert_equal insert_unique insert_equal
元素顺序 有序 有序 无序 无序
是否支持[] 支持 支持
迭代器性质 非const_iterator 非const_iterator 非const_iterator 非const_iterator

12.排序算法

排序算法 平均时间复杂度 最好情况 最坏情况 空间复杂度 排序方式 稳定性
冒泡 O(n^2) O(n) O(n^2) O(1) 原址 稳定
选择 O(n^2) O(n^2) O(n^2) O(1) 原址 不稳定
插入 O(n^2) O(n) O(n^2) O(1) 原址 稳定
希尔 O(n logn) O(nlog^2 n) O(nlog^2 n) O(1) 原址 不稳定
归并 O(n logn) O(n logn) O(n logn) O(n) 额外空间 稳定
快速 O(n logn) O(n logn) O(n^2) O(logn) 原址 不稳定
O(n logn) O(n logn) O(n logn) O(1) 原址 不稳定
计数 O(n+k) O(n+k) O(n+k) O(k) 额外空间 稳定
O(n+k) O(n+k) O(n^2) O(n+k) 额外空间 稳定
基数 O(n \times k) O(n \times k) O(n \times k) O(n+k) 额外空间 稳定
  • 冒泡排序:选择相邻的元素A/B,如果A>B,则交换。从0-n -> 1-n -> 2-n -> ... -> n-1-n进行循环
    void BubbleSort(int arr[], int n)
    {
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) { // 相邻元素比较
                    int temp = arr[j]; 
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;  //交换两个元素
                }
            }
        }
    }
  • 选择排序:分为已排序和未排序区间。遍历未排序区间找到最小元素,将其放到已排序区间。同时由于乱序了,选择排序是不稳定的(相同大小的元素在排序后顺序可能会改变)。
  • 插入排序:同样分为已排序和未排序区间。对未排序区间的每一个元素,将其插入到已排序区间的合适位置。
    void SelectSort(int arr[], int n)
    {
    	int i, j, min; //min = 最小元素
    	int t = 1;
    	for (i = 0; i < m - 1; i++) { //共有m个元素,只需要比m-1次
    		min = i;
    		for (j = i + 1; j < m; j++) {
    			if (arr[j] < arr[min])
    				min = j; //下标变化,一直是比较后目前最小的数的下标
    		}
    		if (min != i) { //如果最小数下标改变了,进行数的交换
    			t = arr[min];
    			arr[min] = arr[i];
    			arr[i] = t;
    		}
    	}
    }
    
    
    void InsertSort(int arr[], int n)
    {
    	int i, j;
    	int low, high, mid; // 对已排序空间进行二分法进行查找
    	for (i = 2; i <= n; i++) {
    		arr[0] = arr[i];
    		low = 1;
    		high = i - 1;
    		while (low <= high) {
    			mid = (low + high) / 2;
    			if (A[mid] > A[0])
    				high = mid - 1;
    			else
    				low = mid + 1; 
    		}
    		for (j = i - 1; j >= low; j--)
    			A[j + 1] = A[j];
    		A[low] = A[0];
    	}
    }
    
  • 归并排序:分治算法。
    1、首先递归地将数组分成两段(如果数组长度是奇数时,则一半长一半短),直到分成长度为1的 n 个数列,即n个数
    2、将数列两两合并,每次合并时进行比较和排序,直到完成排序
    void MergeSort(int arr[], int left, int right)
    {
    	if (left == right)
    		return;
    	int mid = left + (right - left) / 2;
    	MergeSort(arr, left, mid);						 //左侧排序
    	MergeSort(arr, mid + 1, right);				 //右侧排序
    	int *help = new int[right - left + 1]; //开辟一个新的数组空间O(N)
    	int i = 0;
    	int p1 = left;									 //左侧的指针
    	int p2 = mid + 1;								 //右侧的指针
    	while (p1 <= mid && p2 <= right) //数组的左侧和右侧的元素不断进行对比,较小的元素赋值进数组
    		help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
    	while (p2 <= right) //将两侧剩余的较大的元素全部赋值进去
    		help[i++] = arr[p2++];
    	while (p1 <= mid) //和上一个while循环一样,这两个while循环只能发生一个
    		help[i++] = arr[p1++];
    	for (int i = 0; i < right - left + 1; i++) //将help数组赋值给arr数组
    		arr[left + i] = help[i];
    	delete[] help; // c++需要手动删除,进行内存管理
    }
    
  • 快速排序:是目前最快的排序算法
    找到一个参照对象,比这个参照对象小的排左边,比这个参照对象大的排右边。对两部分递归快速排序,直到有序。
    选择左端(Left),右端(Right),中间值(Center)的中值为参照对象,效率最好
    void QuickSort(int arr[], int left, int right)
    {
    	if (right >= left + 3) //至少存在3个值
    	{
    		int center = (left + right) / 2;
    		if (arr[left] > arr[center])
    			std::swap(arr[left], arr[center]);
    		if (arr[left] > arr[right])
    			std::swap(arr[left], arr[right]);
    		if (arr[center] > arr[right])
    			std::swap(arr[center], arr[right]);
    		std::swap(arr[center], arr[right - 1]);
    		int pivot = arr[right - 1]; //确定找到参照对象
    		auto i = left;
    		auto j = right - 1;
    		while (true) {
    			while (arr[++i] < pivot)
    				;
    			while (arr[--j] > pivot)
    				;
    			if (i < j) //与参照对象进行比较
    				std::swap(arr[i], arr[j]); 
    			else
    				break;
    		}
    		std::swap(arr[i], arr[right - 1]);
    		QuickSort(arr, left, i - 1);  //分别对两个子序列递归
    		QuickSort(arr, i + 1, right);
    	}
    	else
    		InsertSort(arr, right - left + 1); //若小于3个值,直接插入排序
    }
  • ​​​堆排序:利用“堆”数据结构(子节点的key总小于父节点——大堆顶)进行排序,同样可以用小堆顶进行排序(子节点的key总大于父节点)
    1、通过数组创建堆,输出堆顶元素(最大/小)
    2、以最后一个元素代替堆顶,重新生成堆,输出堆顶元素(第二大/小)
    3、递归,直到堆的大小=1
    void HeapGenerate(int arr[], int i, int length) //堆生成
    {														
    	int leftChild = 2 * i + 1; //定义左右子节点
    	int rightChild = 2 * i + 2;
    	int max = i; //初始化,假设左右孩子的双亲节点就是最大值
    	if (leftChild < length && arr[leftChild] > arr[max])
    		max = leftChild;
    	if (rightChild < length && arr[rightChild] > arr[max])
    		max = rightChild;
    	if (max != i) { //若最大值不是双亲节点,则交换值
    		std::swap(arr[max], arr[i]);
    		HeapGenerate(arr, max, length); //递归,使其子树也为堆
    	}
    }
    void HeapSort(int arr[], int length) //堆排序
    { 
    	for (int i = length / 2 - 1; i >= 0; i--) //从最后一个非叶子节点开始向上遍历,建立堆
    		HeapGenerate(arr, i, length);
    	for (int j = length - 1; j > 0; j--) { //调整堆 ,此处不需要j=0
    		std::swap(arr[0], arr[j]);
    		HeapGenerate(arr, 0, j); //因为每交换一次之后,就把最大值拿出(不再参与调整堆),第三个参数应该写j而不是length
    	}
    }
    
  • 希尔排序:将比较的元素分为几个区域,提升插入排序的性能
  • 计数排序:待排序的数作为计数数组的下标,统计每个数字的个数,然后输出(仅针对一定范围内的整数)
  • 桶排序:把整型数组分到有限数量的桶里,再对每个桶进行排序
  • 基数排序:需要位比较,进行多次桶排序

2、其他知识点

  • C++内存分配情况:栈(编译器管理,局部变量和函数参数)/堆(程序员管理,new/malloc/delete/free)/全局(静态)存储区(全局变量和静态变量)/常量存储区(常量)/代码区
    栈由编译器管理,自动分配空间,程序出错经常为数据出栈;
    堆由程序员管理,手动分配空间(malloc/free/new/delete),内存泄漏常发生在此。
  • static:修饰局部变量(修改生命周期,为静态变量)/修饰全局变量(修改作用域范围,仅本文件可见)/修饰函数(同修饰全局变量)/修饰类函数(该函数属于一个类而不属于此类的任何特定对象)/修饰类变量(该变量在存储空间仅有一个副本,!类内声明类外定义!
  • 重载:overload,具有不同参数列表的同名函数
  • 重写:override,子类中重定义父类中除函数体外完全相同的虚函数
  • 重定义(隐藏):子类中重定义父类中相同名字的非虚函数
  • malloc/free:库函数。malloc(分配未初始化的内存空间)/free(回收内存空间)
  • new/delete:操作符。new(malloc -> 构造函数)/ delete (析构函数 -> free)
  • 内存对齐:CPU是2^n个字节为单位来存取内存,将所有对象的最小公倍数设置为内存存取粒度,以下是32位编译器的内存大小:
    char(1)/short(2)/int(4)/指针(4)/float(4)/long(4)/double(8)/long long(8)

    内存对齐之后,CPU的内存访问速度大大提升。
  • 二叉树:二叉树、二叉搜索树、平衡二叉树、高度平衡二叉树(AVL)
  1. 二叉树:任何节点最多只允许有两个子节点
  2. 二叉搜索树:任何节点的key一定大于其左子树的任意节点key,并小于其右子树的任意节点key。可以实现O(logn)的元素访问和插入
  3. 平衡二叉树:没有一个节点过深
  4. AVL:任何节点的左右子树高度相差最多为1的平衡二叉树(使用旋转进行插入)
  • 红黑树:每个节点是红/黑、根节点为黑、叶子节点(最低的节点)为黑、每层节点红黑交替、任意一结点到每个叶子节点的路径都包含数量相同的黑节点

3、算法题

可以参考我的另一篇博客:算法教程,里面有很详细的说明。


四、逻辑综合面试

1.逻辑综合知识详解

  1. 入门版本:参考我的另一篇博客《逻辑综合(logic synthesis)入门指南》
  2. 深入版本:参考该博客逻辑综合知识点总结,里面详细的讲解了逻辑综合相关知识

2.开源逻辑综合ABC

https://github.com/berkeley-abc/abc

读取一个门级网表(例如AIG),进行优化,映射后输出带单元库信息的门级网表

术语:

  • AIG(AND-Inverter-Graph,与非图):ABC的基本数据结构

  • BLIF: Berkeley Logic Interchange Format, a format traditionally used by SIS, VIS, and MVSIS to represent logic networks, Berkeley逻辑交换格式,是SIS、VIS和MVSIS用来表示逻辑网络的一种传统格式

  • BDD: Binary Decision Diagram, a canonical graph-based representation of Boolean functions, 二元决策图,布尔函数的一种基于规范图的表示

  • CEC: Combinational equivalence checking, 组合等价性检查

  • BAF: Binary Aig Format, 二进制AIG格式

  • CI: Primary input and latch outputs

  • CO: Primary output and latch inputs

  • FRAIG: (a) Functionally Reduced AIG and (b) AIG package with on-the-fly detection of functionally equivalent AIG nodes, 功能简化AIG和功能等效AIG节点动态检测的AIG包

  • FRAIGing: Transforming an AIG into a Functionally Reduced AIG (using the FRAIG package)

  • IWLS: International Workshop on Logic and Synthesis, and annual research-oriented workshop

  • LI: Latch input

  • LO: Latch output

  • LUT: Look-up table, a programmable logic component that can implement an arbitrary Boolean function up to a fixed number of inputs

  • PI: Primary input

  • PO: Primary output

  • SAT: Boolean satisfiability

  • SEC: Sequential equivalence checking

  • SOP: Sum-Of-Products, a non-canonical representation of Boolean functions

  • TFI: Transitive fanin

  • TFO: Transitive fanout

  • CNF: conjunctive normal form

概念:

  • CUT:割集
  • boolean-matching(布尔匹配):对于网络中的每个节点或对于每个CUT,从库中选择一个适当的门
  • MFFC(maximum fanout free cone,最大扇出自由锥):一个节点n的MFFC是指该节点n的扇入CONE的一个子集,该子集中包含了一些节点,这些节点满足的条件是每一条经过这些节点到最终输出PO的路径都需要经过该节点n,这样一个FI CONE的子集才能被称作为MFFC

《EDA前端软件开发工程师面试指南》_第2张图片

  • structural hashing:结构化哈希,确保所有常量都被传播,消除冗余项
  • NPN class:NPN类。N(Negative,输入取反)-P(Permutation,对输入进行置换)-N(Negative,对输出取反),若已知一个函数,经过以上三种操作得到的全部函数可以分为一个NPN类
  • choice:“记住”在综合流中看到的每个网络,并在工艺映射过程中选择每个网络中最好的部分

组合逻辑优化指令(Command):

  • rewrite:一种快速的贪婪算法,通过迭代选择一个节点上的AIG子图,并用更小的预先计算的子图替换它们,以最小化AIG节点的数量,同时保留节点的功能。
  • refactor:对使用10-20个输入的逻辑锥进行折叠和重构
  • resubstitution :对于当前节点,重新使用网络中已经出现过的节点进行表示(真值值相同),如果在某些方面,例如节点数或者depth取得更好的效果,就会被接受。
  • balance:得到的AIG是通过对原始AIG中包含的多输入和门进行代数平衡得到的。根据拓扑顺序进行平衡,选择每个多输入与门的最小延迟树分解。平衡考虑了主要输入的到达时间
  • multi:将两输入aig改为多输入与门
  • strash:使用结构化哈希将初始SOP逻辑网络转换为AIG
  • renode:根据AIG重新创建SOP

时序逻辑优化指令:

时序综合通过修改当前网络的逻辑以及存储元素(锁存器或触发器)来变换当前网络。

与原始网络相比,得到的网络可能具有不同的状态编码和可达状态空间,但这两个网络是时序等效的(即从初始状态开始,对于相同的输入向量序列,它们产生相同的输出向量序列)

  • retiming:保持网络结构不变,但移动锁存器,使得每个PI/PO路径和每个循环上的锁存器数量不变。更复杂的时序变换会修改锁存器的逻辑结构和位置。集成时序优化在时序变换中占有特殊的位置,它通过一系列简单的局部变换,如局部重构和retiming,就能使电路的延时达到全局最优。
  • if -s:集成时序优化命令,通过探索逻辑综合过程中看到的所有逻辑结构的组合空间、所有可能的工艺映射和所有可能的retiming,来查找电路的最小延迟
  • cycle:模拟随机输入的顺序网络,并更新其当前状态
  • init:重置当前网络所有锁存器的初始状态(请注意一个有用的命令print_latch,可用于查看当前网络中所有锁存器的初始状态)
  • retime
    • 在不增加寄存器个数的前提下,通过改变寄存器的位置来优化关键路径

    • 实现多种类型的retiming:最前向、最后向、最小寄存器、启发式最小延迟

    • 当电路从时序AIG转换为逻辑网络时,锁存器在扇出梗之间得到最佳共享。将重定时后的初始状态计算归结为SAT问题,利用MiniSat算法进行求解


五、简历制作

可以参考这个视频制作CV,

超级简历


总结

校招面试是一个很玄妙的东西,原因有二:

1.你很难在有限的时间内,将你所有的能力表达出来

2.面试官很难在有限的时间内,充分了解你的能力

因此,适当的包装和准备会大大提高自己面试成功的概率,这里并不是强调我们要进行“诈骗”,而是尽可能的展现自己的实力,毕竟面试都没通过,连被“拆穿”的可能都没有!

本文主要结合作者自己的亲身经历,给出EDA前端软件开发面试必备的一些知识,所以,不论你是刚毕业,刚培训完,或者是想去大厂,都可以浪费几分钟,看看我对面试这件事的心得。

所以说,程序员技术能力是一切的前提,不论你是刚要找工作,还是要面试大厂,如果失败太多次,你一定要先找自己的问题,沉下心来,多学习,等到实力够了,属于你的“大厂”自然会给你发来offer。

最后,祝大家都能找到一个nice的领导,心仪的公司,迎娶白富美!

你可能感兴趣的:(面试,c++,面试)