校招八股文(拼凑版)
Dionysen

计算机基础知识.

C/C++

基础语法

1. define宏定义和const的区别?以及static的用法和作用?const关键字的作用有哪些?

define只是在预处理时生效,const是在编译和运行时生效。

前者没有类型检查,容易出错,只是将名字替换

define不需要分配内存空间;const需要分配内存空间

2. volatile、mutable和explicit关键字的用法?

volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,如操作系统、硬件或其他线程;编译器不再优化此变量从而提供稳定的访问;使用时系统总是重新从它所在的内存读取数据(而非寄存器);多线程中被几个任务共享的变量需要定义为volatile。作用:防止编译器优化使变量从内存装入寄存器中。

mutable可以用来在const函数里面修改一些跟类状态无关的数据成员

explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显式的方式进行类型转换。

  • explicit关键字只能用于类内部的构造函数声明
  • explicit修饰的构造函数的类,不能发生相应的隐式类型转换

3. 什么是内联函数?和宏定义的区别?

内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。

区别:内联函数有参数类型检测、语法判断等功能,而宏没有

4. 值传递、指针传递、引用传递的区别和效率?

值传递:向函数所属的栈拷贝数据,如果对象较大,将耗费一定的时间和空间

指针传递:同样向函数所属的栈拷贝数据,但拷贝的数据是一个指针,固定大小(4字节)。

引用传递:依然是传递地址,只是取了一个别名

指针和引用传递效率更高,引用传递使代码逻辑更清晰。

5. 什么时候使用extern”C”?

欲正确地在C++代码中调用C语言。

(1)C++代码中调用C语言代码;

(2)在C++中的头文件中使用;

(3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;

6. 静态变量什么时候初始化?静态变量与全局变量的区别?

静态变量与全局变量都放在全局区主程序之前就已经为其分配好了内存;

一般情况分配好内存后就会进行初始化,所以在C中无法使用变量对静态变量初始化,而C++标准定为全局或静态对象是有首次用到时才会进行构造,可以使用局部变量对其初始化。

7. 形参与实参的区别?

形参被调用时才有内存,且只存在与函数内部;

实参先具有确定的值然后传递给形参,因此调用时如果是输入一个变量则会产生一个临时变量

形参实参数量、类型和顺序必须一样,传递是单向的,改变形参的值不会影响到实参的值。

8. 变量的声明和定义?函数的声明和定义?全局变量和局部变量有什么区别?

声明只是把位置和类型提供给编译器,定义要分配内存;相同变量可以多处声明,但只能定义一次

全局变量与局部变量的生命周期不同,全局变量跟随主程序创建和销毁,而局部变量只在其所在的函数或循环体内部存在。全局变量放置在全局区,局部变量在堆栈区

9. 数组首地址相关?二维数组的参数传递?

数组名并不是普通的指针,而是常量指针(指向首地址),没有自增自减的操作,但作为参数传递后,就变成了一般指针,有自增自减,sizeof运算无法得到原数组的大小

形参为二维数组、指针数组或二级指针

10. 重载运算符?程序中的函数重载,匹配原则和顺序是什么?

只能重载已有的运算符,优先级和结合律与内置一样,不能改变操作数的个数

成员运算符和非成员运算符,前者少一个参数下标和箭头运算符必须是成员运算符,重载的箭头运算符必须返回类的指针。

引入的原因是为了实现多态

成员运算符重载时,this指针绑定到左侧对象。

匹配顺序:

  • 名字查找
  • 确定候选函数
  • 寻找最佳匹配

11. C++模板是什么,底层怎么实现的?

编译器从函数模板通过具体类型产生不同的函数,因此有两次编译,一次再声明的地方对模板代码进行编译,一次在调用的地方进行参数替换后编译。

使用函数模板时,如果包含有声明的头文件,但没有定义,就会发生连接错误,因为无法实例化模板函数。

12. 模板函数和模板类的特例化?

模板函数对所有类型生效,且功能一样,如果想对某一种类型设置不同的功能,就要引入特例化。

特例化的本质是实例化一个模板,而非重载它。

模板及其特例化版本应该声明在同一个头文件中,且所有同名模板的声明应该放在前面,后面放特例化版本。

13. 模板和实现可不可以不写在一个文件里面?为什么?

因为在编译时模板并不能生成真正的二进制代码,而是在编译调用模板类或函数的CPP文件才会去找对应的模板声明和实现,在这种情况下编译器是不知道实现模板类或函数的CPP文件的存在,所以它只能找到模板类或函数的声明而找不到实现,而只好创建一个符号寄希望于链接程序找地址。但模板类或函数的实现并不能被编译成二进制代码,结果链接程序找不到地址只好报错了。

所以一般总是在头文件中放置全部的模板声明和定义

内存管理

1. C++内存管理模型?堆和栈的区别?哪个更快?

五个分区:(由高到低,地址十六进制数的大小从小到大)
栈:执行函数时,函数中的局部变量都在栈上创建,函数结束时自动释放;栈中的内存分配运算内置于cpu中,效率很高,但容量有限。

堆:由程序员自己申请分配的内存(用new和delete),编译器不会管它的释放,由程序员自己释放,如果程序结束没有释放操作系统自动回收

全局存储区/静态: 存放全局变量和静态变量的内存区域,在此定义的变量如果没有初始化会自动初始化

常量区:存放常量,不能修改

代码区:存放函数的二进制代码

自由存储区:堆是操作系统维护的一块内存,是一个物理概念,而自由存储是C++中通过new与delete动态分配和释放的对象的存储区,是一个逻辑概念。

2. 简述对智能指针的了解,如何使用?底层实现的原理?

智能指针是一个,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象防止内存泄漏。该类的生命周期结束,会自动调用析构函数释放资源。

**shared_ptr:**使用引用计数器,允许多个指针指向同一个对象,每当多一个指针时,计数器加一,减少一个计数器减一,计数器为零时自动释放资源。

创建一个对象时,引用计数器设置为1,拷贝构造的时候引用加1,赋值构造的时候,左边减1(减到0则删除对象),右边加1.

unique_ptr: 独享资源,非空的unique_ptr总是拥有它所指向的资源,转移一个unique_ptr,原指针会置空。所以unique_ptr不支持普通的拷贝和赋值操作。

weak_ptr: 弱引用,为了配合shared_ptr而引入的,它指向一个shared_ptr而不拥有这个资源,没有引用计数器,当shared_ptr析构后,不管有没有weak_ptr,内存都会被释放,因此weak_ptr不保证它的引用一定有效,使用时要用lock()函数检查是否为空。

auto_ptr: 为了解决有异常抛出时发生内存泄漏的问题,发生异常导致无法正常释放内存。autoptr析构时会自动销毁所绑定的动态对象。

实现: 拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。

3. 什么是悬垂指针、野指针、哑指针和内存泄露?如何检测和避免内存泄漏?

野指针即未初始化的指针(不是nullptr)。

悬垂指针是指所指向的资源已经被释放。

哑指针是指普通指针。

内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制;

只发生一次小的内存泄漏可能不被注意,但泄漏大量内存的程序将会出现各种证照:性能下降到内存逐渐用完,导致另一个程序失败;

更好的内存管理或者智能指针可以避免。

4. new/delete与malloc/free的异同?new和delete是如何实现的以及存在的必要性?C++中有几种类型的new?delete是如何知道释放内存的大小的?delete p、delete [] p、allocator都有什么作用?

new自动计算空间大小(类型安全),malloc手动计算(不是类型安全)。

new封装了malloc,直接free不会报错,但会释放内存,不会析构对象(delete才可以)。

-—-

malloc和free是标准库函数,支持覆盖;new和delete是运算符,支持重载。

malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。

malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。

-—–

malloc/free和new/delete都是用来申请内存和回收内存的。

在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候要执行析构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给malloc/free,所以new/delete是必不可少的。

-——

C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

5. malloc与free的实现原理?malloc申请的存储空间能用delete释放吗? malloc、realloc、calloc的区别?

太难,不背了。

calloc省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;

realloc给动态分配的空间分配额外的空间,用于扩充容量。

6. 什么是内存池,如何实现?

内存池(Memory Pool) 是一种内存分配方式。通常我们习惯直接使用new、malloc 等申请内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块, 若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。

7. 在成员函数中调用delete this会出现什么问题?对象还可以使用吗?

在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

8. 如果在类的析构函数中调用delete this,会发生什么?

会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

9. 你知道空类的大小是多少吗?

C++空类的大小不为0,不同编译器设置不一样,vs设置为1;

C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;

带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定;

C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。

10. this指针调用成员变量时,堆栈会发生什么变化?

当在类的非静态成员函数访问类的非静态成员时,编译器会自动将对象的地址传给作为隐含参数传递给函数,这个隐含参数就是this指针。

即使你并没有写this指针,编译器在链接时也会加上this的,对各成员的访问都是通过this的。

例如你建立了类的多个对象时,在调用类的成员函数时,你并不知道具体是哪个对象在调用,此时你可以通过查看this指针来查看具体是哪个对象在调用。This指针首先入栈,然后成员函数的参数从右向左进行入栈,最后函数返回地址入栈。

面向对象

基础

1. 介绍面向对象的三大特性,并且举例说明?

封装:把客观事物封装成抽象的类,数据和方法给定权限,避免外界干扰和不确定性的访问。

继承:可以继承现有类的功能,并加以扩展

多态:同一事物表现出不同的能力,允许子类类型的指针赋值给父类类型的指针;实现方式有覆盖(虚函数)重载(函数重载)

2. 类成员初始化方式?构造函数的执行顺序?为什么用成员初始化列表会快一些?

赋值初始化:通过在函数体内进行赋值初始化,所有数据成员分配好内存后进行,相当于赋值

列表初始化:在冒号后使用初始化列表进行初始化,给数据成员分配空间时就进行,所以更快。

构造函数执行顺序

· 虚拟基类构造函数

· 基类构造函数

· 类的成员对象的构造函数

· 派生类自己的构造函数

3. 有哪些情况必须用到成员列表初始化?作用是什么?

· 初始化一个引用成员

· 初始化常量成员

· 调用基类的构造函数有参数

· 调用一个成员的构造函数有参数

编译器操作初始化时,根据的是成员声明的顺序而不是初始化列表的顺序。

4. 什么是类的继承?多态的实现机制?

子类继承父类,子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用;

基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。

虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表,该表是一个一维数组,保存了虚函数的入口地址

虚表指针:含有虚函数的类实例化对象对象地址的前四个字节存储的指向所属类虚表的指针,即vptr;构造时根据对象的类型初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数.

具体过程:

  • 编译器发现基类中有虚函数,为每一个含有虚函数的类生成虚表(一个数组,保存了虚函数入口地址)
  • 每个对象前四个字节保存一个虚表指针,指向该类的虚表,构造时根据对象的类型初始化虚表指针使其指向正确的虚表(调用虚函数则能找到正确的函数)
  • 派生类定义对象时程序自动调用构造函数并在构造函数中创建虚表且加以初始化
  • 派生类不重写基类的虚函数,派生类的虚表指针指向基类虚表重写后,指向自身的虚表;派生类有自己的虚函数时,虚函数地址加在虚表后面。

5. 类的对象如何储存?this指针?

· 非静态成员的数据类型大小之和

· 编译器额外加入的成员变量(如虚表指针);

· 为了边缘对齐优化加入的padding。

6. 什么是深浅拷贝?

浅拷贝:只拷贝指针,不开辟新的地址,原指针指向的资源释放时,再释放浅拷贝指针指向的资源就会出错。

深拷贝:不仅拷贝指针,还开辟新的空间存放新的对象,原对象析构后,不会影响拷贝的对象

类中有指针成员时,需要深拷贝来防止新的对象与旧对象产生关联。

7. 初始化和赋值的区别?拷贝初始化和直接初始化?

简单类型没有区别。

对于类的对象,直接初始化会调用与实参匹配的构造函数,拷贝初始化会调用拷贝构造函数。*拷贝初始化首先用指定的构造函数创建一个临时对象,然后用拷贝构造函数将临时对象拷贝到正在创建的对象中。*

8. C++中的重载、重写(覆盖)和隐藏的区别?

同名函数不同参数为重载(overload)。

派生类重写虚函数为覆盖(override)。

子类与父类函数参数相同,但不是虚函数,父类的函数会被隐藏(hide)

9. 一个空类会添加哪些函数?

C++空类的大小不为0,不同编译器设置不一样,vs设置为1;

C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;

带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定

C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。

10. public,protected和private访问和继承权限/public/protected/private的区别?

  • public:类内类外子类都可以访问
  • protected:类内和子类可以访问
  • private:类内可以访问

继承权限可以调整子类对继承来的成员的权限:

  • public公共继承:私有保持私有,保护和公有保持
  • protected保护继承:私有保持私有,公有和保护均为保护
  • private私有继承:所有成员都为私有,无法再往下继承

11. final和override关键字?

override指定了子类的虚函数是重写父类的。

类名和虚函数后添加final关键字,无法被继承和重写

12. 静态类型和动态类型,静态绑定和动态绑定的介绍?引用是否能实现动态绑定,为什么可以实现?

静态类型:对象在声明时的类型,编译期已经确定

动态类型:通常是一个指针或引用所指对象的类型,运行时才确定。

静态绑定:绑定的对象是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译器

动态绑定:绑定的对象是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行时

如非虚函数是静态绑定,虚函数是动态绑定,因此能实现多态。

在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定。

13. 静态成员与普通成员的区别?

生命周期:静态成员是类被加载到卸载,普通成员是类的对象被创建和销毁。

共享方式:静态成员是全类共享,普通成员是单独的对象。

定义位置:静态成员变量储存在静态全局区,普通成员变量储存在堆或栈中。

初始化位置:静态成员变量在类外初始化,普通成员变量在类内初始化。

默认实参:可以是用静态成员变量作为默认实参。

14. 类如何实现只能静态分配和只能动态分配?

静态创建一个类,由编译器在栈空间中分配内存。

动态创建一个类,使用new运算符,为对象在堆空间中分配内存。

只有使用new运算符,对象才会被创建在堆上,因此限制new的使用,如将new设为私有。

15. 继承机制中对象之间如何转换?指针和引用之间如何转换?

将子类的指针或引用转换成父类的指针或引用成为向上类型转换自动进行且安全。

反之为向下类型转换不会自动进行,因为不知道要转换成哪一个子类,所以需要用动态类型识别技术,dynamic_cast。

16. C++中的组合?与继承相比有什么优缺点?

继承的缺点:①父类内部子类可见②继承在编译期确定下来,运行时无法改变继承关系③父类修改时子类必须跟着修改,高耦合。

组合的优点:①使用对象,对象内部不可见低耦合③可以在运行时动态绑定所包含的对象,可以通过set方法给对象赋值。

组合的缺点:①容易产生过多的对象②为了组合多个对象,接口需要仔细定义。

17. 如何设计一个计算仅单个子类的对象个数?

为类设计一个static的变量count用作计数器,类定义结束后初始化count,构造函数count++,拷贝构造函数count++,赋值构造函数措施虐++,析构函数count–。

18.如何阻止一个类被实例化?有哪些方法?

将类定义为抽象类,或构造函数权限为private。

18. 如何禁止程序自动生成拷贝构造函数?

手动重写并定义为private

定义一个base类,拷贝构造函数和赋值构造函数设置成private,这样继承的类就不会自动生成这两个函数,子类将组织编译器执行。

19. 友元函数和友元类?

友元函数:定义在类外,不属于任何类,可以访问其他类的私有成员在其它类中声明它为友元函数即可;一个函数可以是多个类的友元函数,只需在类中声明friend即可。

友元类:一个类的所有成员函数都是另一个类的友元函数,可以访问另一个类的所有成员,在另一个类中声明即可。

  • 友元关系不可被继承
  • 友元是单向的,不可交换
  • 友元不具有传递性

20. 多继承的优缺点,作为一个开发者怎么看待多继承?什么是虚拟继承?

一个子类有多个父类,此为多继承

优点:可以调用多个父类的接口

缺点:如果子类的多个父类继承自同一个基类,对用祖先类时可能出现二义性;容易让代码逻辑复杂、思路混乱

优化方法:加上全局符来确定调用的是哪一份拷贝;使用虚拟继承,使得多继承类只拥有一份虚基类

虚拟继承:为了解决菱形继承的问题;虚拟继承的子类只有一个基类实例,因此不会出现二义性;是通过虚基类指针实现的,子类可以访问和共享相同的虚基类数据(只保留一份)。

21. 什么是纯虚函数,与虚函数的区别?抽象基类为什么不能创建对象?

纯虚函数首先是虚函数,虚函数用=0定义就成为了纯虚函数,纯虚函数没有具体的函数体,虚表中的值为0,只是一个接口。

一个类中有纯虚函数,即为抽象类,抽象类不能用于实例化对象,子类继承后必须重写纯虚函数才能实例化对象。

22. 哪些函数不能是虚函数?

构造函数:子类必须知道基类干了什么才能构造;虚表指针是在构造函数中初始化的,因此构造函数不能是虚函数。

内联函数:内联函数在编译阶段进行替换操作,而虚函数是在运行时确定类型

静态函数:静态函数没有this指针,因此静态函数设为虚函数没有意义。

友元函数/普通函数:友元函数和普通函数不是类的成员,不能被继承。

构造与析构

1. 构造函数能否声明为虚函数或者纯虚函数,析构函数呢?为什么析构函数一般写成虚函数?

构造函数中不要调用虚函数,因为基类构造时,虚函数还不是虚函数,可能发生未定义行为。

析构函数可以声明为虚函数,并且一般情况下析构函数应该定义为虚函数,只有这样当对用delete时才能准确地调用子类地析构函数

析构函数也能是纯虚函数,子类重写即可。

2. 基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间?

虚表(有虚函数的类)全局共享的,储存的位置根据编译器不同会有所不同,在常量区(或可执行文件的只读数据段)

虚表指针(有虚函数或继承于有虚函数的类)类初始化时初始化的,存放在对象内存的最前面

3. 构造函数、析构函数、虚函数可否声明为内联函数?

构造函数和析构函数声明为内联函数是没有意义的,构造和析构中编译器会添加额外的操作,使其比较复杂,就不会内联。

类中的函数默认是内联的,编译器是选择性地内联。

虚函数为内联时,子类调用虚函数不会内联展开,对象本身调用虚函数可能会内联展开。

4. 构造函数和析构函数的作用是什么?执行顺序是什么?它们内部可以调用虚函数吗?为什么?它们可否抛出异常?

构造函数是初始化值用的。析构函数是撤销一些任务如释放内存空间,析构函数不能重载,撤销对象时自动调用。

两者在用户没有手动定义时都会由编译器自动生成默认的。

构造执行顺序:基类构造函数(如果多个基类,要看基类在派生类中出现的顺序)-> 成员类对象的构造函数(依然是看被声明的顺序)-> 派生类的构造函数

析构执行顺序派生类的析构函数 -> 调用成员类对象的析构函数 -> 基类的析构函数

不建议在它们内部调用虚函数,一般不能达成目的。

5. 类什么时候会析构?

  • 对象生命周期结束
  • delete指向对象的指针或指向对象基类类型的指针而其基类的析构函数是虚函数时;
  • 对象所属的对象析构

6. C++有哪几种的构造函数?说说移动构造函数?什么情况下会调用拷贝构造函数?

默认构造函数(编译器自动生成,或手动定义的无参构造)。

初始化构造函数(有参数)。

拷贝构造函数(使用已存在的对象初始化新的对象),注意自己定义拷贝构造函数实现深拷贝(对于指针,申请新的内存空间存放新的对象)。

移动构造函数:浅拷贝之所以危险,是因为两个指针同时指向一个对象,其中一个释放时,另一个就不合法了;将第一个不用的指针赋值为nullptr,析构时判断指针是否为nullptr再进行释放;拷贝构造函数的参数是一个左值引用,而移动构造函数的参数是右值引用,可以使用move语句将左值变成右值然后即可调用移动构造函数。

**std::move()**:将一个左值引用变成右值引用,达到转移资产所属权的作用。

委托构造函数:多个构造函数可以委托给一个构造函数,这个构造函数的初始化列表要包含所有要委托的构造函数。

转换构造函数:当一个类作为参数时,传入此类的构造函数的参数,会使用实参临时构建一个对象传入。

7. 构造函数的几种关键字?构造函数、拷贝构造函数和赋值操作符的区别?拷贝构造函数和赋值运算符重载的区别?

default:显式地要求编译器生成合成构造函数。

delete:删除构造函数,赋值运算符函数(可以删除new)。

0:声明为纯虚函数(类内部)。

8. 什么情况会自动生成默认构造函数?什么时候需要合成拷贝构造函数呢?

默认构造函数:

  1. 类没有构造函数,但有成员
  2. 基类有默认构造函数派生类没有则会合成一个构造函数来调用上层基类地构造函数
  3. 带有虚函数虚基类的类

合成构造函数中,只有基类子对象和成员类对象会被初始化,其他的非静态成员都不会被初始化。

拷贝构造函数

  1. 用一个对象做显式初始化
  2. 对象当作参数交给某个函数
  3. 函数返回一个类的对象
  4. 一个类没有拷贝构造函数,但是含有另一个类类型的成员(有拷贝构造函数),或继承自含有拷贝构造函数的类,或声明/*继承了虚函数,或*含有虚基类,都会自动生成拷贝构造函数。

9. 为什么拷贝构造函数必须传引用不能传值?

如果拷贝构造函数的参数不是引用,那么调用时直接把一个对象传入,这本身就是一次拷贝,会再次调用拷贝构造函数,这样就陷入了死循环

为什么是const引用:拷贝构造不会也不需要改变原对象的内容;而且是const时传入的参数是不是const都可。

10. 静态函数能定义为虚函数吗?常函数呢?

静态函数不属于任何对象或类实例,因此加上virtual没有意义

静态与非静态成员唯一的区别时是静态成员没有this指针。

常函数:只能调用常函数,且不能修改成员变量除非变量被mutable修饰。原理:非静态成员函数都有一个this指针(一个指向该函数的调用对象的指针常量),A *const this,而在常函数中,this指针为const A *const this,即指针指向的内容也不可修改。常对象(const A a;)只能调用常函数。

11. 虚函数的代价是什么?

每一个含有虚函数的类都会有一个虚表,增大存放类需要的空间;

带有虚函数的类的对象都有一个虚表指针,也会增大存放所需要的空间;

虚函数不能是内联函数

进阶知识

1. C++的异常处理的方法?如何处理多个异常?

主要使用trythrowcatch关键字。

先执行try包裹的语句,如果没有异常发生,则不会进入catch包裹的语句,如果发生异常,使用throw抛出异常,再使用catch捕捉异常。

throw可以抛出任何数据类型的信息,可以自定义class;throw的可以同时抛出多个异常,以列表的形式写在throw的参数中。

异常类:

bad_typeid: 使用typeid运算符,其操作数是一个多态类的指针,指针为NULL时,抛出此异常。

bad_cast: 使用dynamic_cast进行从多态基类到派生类的强制类型转换时,如果是不安全的,抛出此异常。

bad_alloc: 使用new运算符进行动态分配内存时,如果内存不够,抛出此异常。

out_of_range: vector或string的at成员函数根据下标访问时,越界则抛出此异常。

2. 指针和引用区别?参数传递时的区别?引用的底层实现?使用场景?

指针是变量,可以初始化后再赋值,可以为,可以改变指向;而引用是取别名,必须初始化为另一个变量,指向不能为空,且不能改变指向,sizeof得到的是变量的大小。

引用底层是通过指针实现的。

参数传递时建议使用引用,可以使代码逻辑更清晰可读。

返回局部变量的内存时使用指针递归函数时使用引用,因为引用不创建临时变量,开销小,类对象作为参数时使用引用(深浅拷贝)

3. 如何用代码判断大小端存储?

使用强制类型转换,int和char的长度不同,创建一个int,将其强转成char,只会留下低地址的部分,看此char是高字节还是低字节。

4. 对象复用的了解,零拷贝的了解?

将对象存放到对象池中,避免多次创建和销毁对象。

零拷贝:避免将一块内存中的数据拷贝到另一块内存中,减少开销。如使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高。

5. C++函数调用的(压栈)过程?

当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、mian函数中的变量、进行依次压栈;

当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;

当func()调用f()的时候,编译器此时会将func()函数的运行状态进行压栈,再将的返回地址、f()函数的参数从右到左、f()定义变量依次压栈

从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依次弹出,最后主函数返回。

6. 写C++代码时有一类错误是coredump,你遇到过吗?怎么调试这个错误?

编译时加上-g参数,使用gdb调试生成的core文件。

7. C++中将临时变量作为返回值时的处理过程?

临时变量的返回值放在寄存器中,跟堆和栈没有关系,需要的话赋值取之。

8. 如何获得结构成员相对于结构开头的字节偏移量?

使用<stddef.h>头文件中的,offsetof宏。

加上 #pragma pack(4) 指定4字节对齐方式就可以了。

9. 指针加减计算要注意什么?

指针加减本质上是指针的移动,步长与指针的类型有关,需要明确指针每移动一位,它实际跨越的内存间隔是指针类型的长度,建议都转成10进制计算,计算结果除以类型长度取得结果。

10. 怎样判断两个浮点数是否相等?

相减取绝对值,与预先设定的精度比较。(即使是浮点数与0比较)

11. 内存对齐以及原因?

分配内存的顺序按照声明顺序。

每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。

最后整个结构体的大小必须是里面变量类型最大值的整数倍。

添加了#pragma pack(n)后规则就变成了下面这样:

1、 偏移量要是n和当前变量大小中较小值的整数倍

2、 整体大小要是n和最大变量大小中较小值的整数倍

3、 n值必须为1,2,4,8…,为其他值时就按照默认的分配规则

12. 函数指针?

函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。

一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是该函数的指针(注意:大部分情况下,可以这么认为,但这种说法并不很严格)。

声明方法:返回值 + *函数名(参数类型)。

函数也有地址,可以通过函数指针来调用函数,函数指针允许将函数作为变元传递给其他函数,回调函数用的多。

13. #ifdef、#ifndef、#define和#endif代表什么?有何作用?

条件编译,只编译特定的内容。

14. 隐式转换?如何消除?

编译器自动完成的转换,用户没有干预且可能不知道。如多态中,子类可以隐式转换成父类进行返回,在构造函数前加上explicit可以禁止隐式转换。

15. strcpy和memcpy的区别是什么?sprintf和strncpy又是什么?

strcpy只能复制字符串,不需要指定长度,直到’\0’结束,所以容易溢出;memcpy可以复制任何内容,需要指定复制的来源去向和长度。

sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串,主要实现其他数据类型格式到字符串的转化。

strncpy用来复制源字符串的前n个字符,src 和 dest 所指的内存区域不能重叠,且 dest 必须有足够的空间放置n个字符。

16. Debug和Release的区别?

debug不进行任何优化,包含调试信息,额外生成调试文件;Release则进行优化,只生成可执行文件。

17. 回调函数吗?作用?

回调函数是一个通过函数指针调用的函数,用于中断处理。

18. 什么是一致性哈希?

一致性哈希是一种哈希算法,就是在移除或者增加一个结点时,能够尽可能小的改变已存在key的映射关系。

19. C++从代码到可执行程序经历了什么?

预编译-编译-汇编-链接。

(1)预编译

主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:

删除所有的#define,展开所有的宏定义。

处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。

处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他 文件。

删除所有的注释,“//”和“/**/”。

保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重 复引用。

添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是 能够显示行号。

(2)编译

把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应 的汇编代码文件。

词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分 割成一系列的记号。

语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的 语法树是一种以表达式为节点的树。

语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进 行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定 的语义。

优化:源代码级别的一个优化过程。

目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言 表示。

目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移 来替代乘法运算、删除多余的指令等。

(3)汇编

将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没 有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过 来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Linux 下)、xxx.obj(Window下)。

(4)链接

将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:

静态链接

函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库 中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。

空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个 目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;

更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

动态链接

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;

更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运 行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

20. 几种典型的锁?

读写锁:多个读者同时读;写者互斥;写者优先于读者。

互斥锁:一次只能有一个线程拥有互斥锁,其他线程必须等待。抢锁失败的情况下主动放弃CPU进入睡眠状态知道锁的状态发生改变再唤醒,为了实现锁的状态改变时能唤醒阻塞线程或进程,需要把锁交给操作系统管理,所以互斥锁加锁操作涉及上下文切换。

条件变量:通过允许阻塞和等待另一个线程发送信号,一旦一个线程改变了条件变量,它会通知相应的条件变量唤醒一个或多个被此条件变量阻塞的线程。

自旋锁:进程无法取得锁,不会立即放弃CPU时间片,而是一直循环尝试获取锁,知道获取到为止。如果别的线程长时间占有锁,就会浪费CPU做无用功,所以一般应用在加锁时间很短的场景下。

总的来说,锁是互斥机制,条件变量是同步机制。

新的标准

1. C++的四种强制转换reinterpret_cast/const_cast/static_cast /dynamic_cast?

2. string,它与C语言中的 char *有什么区别吗?它是如何实现的?

string继承自basic_string,其实是对char进行了封装,封装的string包含了char数组,容量,长度等等属性。

string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2*n),然后将原字符串拷贝过去,并加上新增的内容。

3. C11添加了一些有关多线程的内容,是什么?

4. NULL和nullptr区别?

在C语言中,NULL被定义为(void*)0,而在C++语言中,NULL则被定义为整数0。

那么在传入NULL参数时,会把NULL当做整数0来看,如果我们想调用参数是指针的函数,该怎么办呢?。nullptr在C++11被引入用于解决这一问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。

5. lambda函数的全部知识?

利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;

每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。

lambda表达式的语法定义如下:

[capture] (parameters) mutable ->return-type {statement};

lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;

6. auto_ptr的作用?智能指针的循环引用?如何解决?

auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题;抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄漏;

auto_ptr构造时取得某个对象的控制权,在析构时释放该对象。我们实际上是创建一个auto_ptr类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放,所以不会有内存泄漏;

auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;

由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针;

Auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组;

auto_ptr支持所拥有的指针类型之间的隐式类型转换。

可以通过*和->运算符对auto_ptr所有用的指针进行提领操作;

T* get(),获得auto_ptr所拥有的指针;T* release(),释放auto_ptr的所有权,并将所有用的指针返回。

7. 手写实现智能指针类需要实现哪些函数?

智能指针是一个数据类型,一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer<T*>对象的引用计数,一旦T类型对象的引用计数为0,就释放该对象。

除了指针对象外,我们还需要一个引用计数的指针设定对象的值,并将引用计数计为1,需要一个构造函数。新增对象还需要一个构造函数,析构函数负责引用计数减少和释放内存。

通过覆写赋值运算符,才能将一个旧的智能指针赋值给另一个指针,同时旧的引用计数减1,新的引用计数加1

一个构造函数、拷贝构造函数、复制构造函数、析构函数、移动函数;

标准模板库

1. 使用智能指针管理内存资源,RAII是怎么回事?

RAII是资源获取即初始化,在构造函数中申请内存,析构函数中释放。

智能指针实现自动内存管理

2. 迭代器:++it、it++哪个好,为什么?

++前置返回引用,不会产生临时对象。

++后置返回一个对象,临时对象会导致效率降低。

3. C++左值引用和右值引用?

有名字的、能取地址的是左值,反之为右值,c11中右值又分为纯右值和将亡值。左值引用就是对一个左值引用,右值引用就是对右值引用,右值引用必须被绑定到右值

左值常引用相当于是万能型引用,也可以绑定到右值。

std::move可以把一个左值强制转化成右值。

应用场景:临时对象拷贝到函数中的对象后,临时对象就销毁了,不如直接把临时对象拿来使用,可以使用std::move把临时对象转移到所用的对象。

4. 简单说一下traits技法?

5. 常见容器性质总结?每种容器对应的迭代器?

6. STL中迭代器失效的情况有哪些?

对于deque和vector:

插入元素:size < capacity时,首迭代器不失效,尾迭代器失效(未重新分配空间),当size = capacity时,所有迭代器失效。

删除元素:删除尾元素,尾迭代器失效;删除中间元素,删除元素后面的所有迭代器失效。

双向链表list每一个结点内存不连续,删除结点仅当前迭代器失效,erase返回下一个有效迭代器。

map/set等关联式容器底层是红黑树,删除结点不影响其他结点的迭代器,使用递增方法获取下一个迭代器即可。

7. vector与list的区别与应用?怎么找某vector或者list的倒数第二个元素?

8. STL 中vector删除其中的元素,迭代器如何变化?为什么是两倍(或1.5倍)扩容?释放空间?vector如何释放空间?

9. 容器内部删除一个元素?

10. STL迭代器如何实现?

11. 红黑树概念?map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器?为什么使用红黑树?

12. map插入方式有哪几种?与unordered_map(hash_map)的区别,hash_map如何解决冲突以及扩容?map中[]与find的区别?

13. vector越界访问下标,map越界访问下标?vector删除元素时会不会释放空间?

14. STL中list、vector与queue之间的区别?deque的内部实现?stack、queue和heap又是如何实现的?

数据结构

1. 用过哪些常见的数据结构?

数组、链表、树、图。

链表有单向和双向;

树常见的有二叉树、平衡二叉树、二叉搜索树。

2. 什么是深度优先搜索(DFS),什么是广度优先搜索(BFS)?应用场景?

深度优先搜索一直访问到最深的节点然后返回到父节点遍历另一条路。

应用场景:n的阶乘、斐波那契数列第n项、给定n输出其全排列。剪枝算法是剪掉不可能有解的树枝。

广度优先搜索是先将所有邻节点访问完毕再访问下一层的节点。

3. 如何判断一个链表内部是否有循环?

(1)遍历链表,用哈希表储存所有遍历过的节点,如果有重复说明链表存在循环;如果遍历结束没有重复,则说明没有循环。

(2)快慢指针

4. 常见的排序有哪些?举例说明原理?冒泡?二分?快排?归并?

冒泡排序:遍历,比较相邻元素,如果是逆序,交换两个元素,之后最后一个元素会是最大(最小的数),也即已经排序成功;对未排序成功的元素重复上述步骤直到所有元素都排序成功。时间O(n2),空间O(1),稳定排序(也即相同数据顺序不变)。

快速排序:选择第一个数作为基准,小于此数的放到前面,大于此数的放到后面,然后对左右区间重复以上操作,直到各区间只剩一个数。

选择排序:从未排序的序列中找出最小的放到前面,重复至全部排序完成。是不稳定排序,时间O(n2),空间O(1);

插入排序:未排序序列取一个数在已排序序列中比较,找到它应该在的位置插入进去。时间O(n2),空间O(1),稳定排序;

归并排序:将一个序列分成两个子序列,分别排序然后合并,有序序列合并很简单,递归地拆分子序列,直到子序列大小为1;分而治之地典型应用。

堆排序:二叉堆的堆顶元素是最大或最小的元素,将此元素与最后一个元素交换,然后调整堆(从上到下调整)。

5. 什么是堆?如何创建堆?堆中如何插入数据和删除数据?堆排序?

堆是一个完全二叉树,任意节点优于它的子节点;

如果任意节点大于它的所有孩子,则为大根堆;反之为小根堆。一般用数组来表示堆,下标为 i 的结点的父结点下标为(i-1)/2;其左右子结点分别为 (2i + 1)、(2i + 2)。

创建:对于一个完全二叉树,从最末尾的父节点开始逆序调整,注意调整的时候需要考虑所有子孙节点。

插入:把新的元素放在堆的末尾,然后不断向上提升直到满足堆的结构;

删除:每次只能删除堆顶元素,然后将末尾元素赋值给堆顶,对堆顶从上到下调整,每次优先选择大(或小)的。

堆排序:利用堆这种数据结构所设计的排序算法。即取出堆顶元素(将堆顶元素与末尾元素交换),然后从上到下调整,如此反复,最终得到有序排列。

6. 对二叉树的了解?概念:满二叉树、完全二叉树、深度与节点数的关系?

二叉树是树的一种,每个节点最多有两个孩子,且区分左右子树。

度是孩子的个数。

满二叉树就是所有节点都有两个孩子且叶子的深度都一样。

完全二叉树是满二叉树最后一行从右边缺失若干个。

深度为k的满二叉树节点个数为2k-1.

7. 二叉树的三种遍历方式?递归和非递归版本?深度优先和广度优先?

分别是先序遍历、中序遍历和后序遍历,区别在于树根的位置

三种遍历方式都属于深度优先遍历。

递归版本比较简单,只需要改变递归调用的顺序(层级太深会栈溢出);

非递归版本,使用数据结构-栈,如果是先序遍历,先把根节点压栈,然后开始循环,出栈,然后先将右节点压栈,再将左节点压栈.

广度优先遍历:层序遍历,从上到下逐层遍历,使用队列实现。先将根节点入队,然后开始循环,出队同时将左右子节点依次入队

8. 什么是AVL树(平衡二叉树)?什么是平衡查找树?红黑树?

平衡二叉树:左右子树高度相差不超过1;平衡的树查找效率最高。

平衡二叉查找树:可以为空树,如果不为空,任意结点的左右子树都是平衡二叉树。

平衡因子:左右子树高度差,只能为-1、0、1,为左子树高度减去右子树高度。

插入新元素时,从插入元素的结点向上查找,第一关平衡因子绝对值大于1的结点为根的子树为最小失衡子树。

失衡调整主要通过旋转最小失衡子树来实现。旋转的目的是减小树的高度。

AVL树查找、插入和删除在平均和最坏情况下都是O(logn)。

红黑树:

红黑树是一个二叉查找树,但是每个节点都增加了一个记录结点颜色的属性,红或黑。

  1. 结点是红色或黑色
  2. 根节点是黑色
  3. 叶子节点(包括空结点)是黑色
  4. 红色的子节点是黑色,父节点也是黑色,根节点到叶子结点路径上不能有连续两个红色
  5. 从任意结点到根节点的所有路径都包含相同数量的黑色结点

红黑树的查找,插入和删除操作,时间复杂度都是O(logN)。

搜索:O(logn)

添加:O(logn),O(1) 次的旋转操作

删除:O(logn),O(1) 次的旋转操作

9. 什么是B 树和B+ 树?

操作系统

1. 线程与进程之间的关系?线程的必要性?并发和并行?什么时候该用多线程,什么时候该用多进程?

进程:是实现某个独立功能的程序,它是操作系统(如windows 系统)进行资源分配和调度的一个独立单位,也是可以独立运行的一段程序。

线程:是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使⽤用多线程对进⾏行行运算提速。比如,如果⼀个线程完成⼀个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。

1、线程启动速度快,轻量级

2、线程的系统开销小

3、线程使用有一定难度,需要处理数据一致性问题

4、同一线程共享的有堆、全局变量、静态变量、指针,引用、文件等,而独自占有栈

并发是指宏观上在一段时间内同时运行多个程序,并行是指同一时刻运行多个指令并行需要硬件支持。

场景:

频繁创建和销毁优先使用线程,因为进程的创建和销毁消耗较大;

线程的切换速度快,因此需要频繁切换的情况优先使用线程;

多个机器情况下优先使用进程。

2. 多线程之间的同步如何实现,有哪几种方法?分别是什么?

互斥锁:多个线程可以访问同一个互斥量,一个线程获取锁后,其他线程抢锁失败会主动放弃CPU进入睡眠状态直到锁的状态改变再唤醒,也因此锁必须交给操作系统管理。

条件变量:它允许线程阻塞并等待另一个线程发送信号,也即其他线程改变了条件变量,它会通知相应条件变量唤醒一个或多个被此条件变量阻塞的线程。

互斥锁是线程间的互斥机制,条件变量是同步机制

读写锁:读没有限制,写必须互斥,不能读写同时进行,写优先于读。

信号量:是一个计数器,一般是的机制,用控制共享资源的访问,如果信号量只能取01,就是互斥量了。

3. 进程间通信的方式有几种,有什么特点?孤儿进程与僵尸进程?

a) 信号:可以通知接收进程某个事件已经发生,比如按下Ctrl+C。

b) 信号量:是一个计数器,一般是的机制,用控制共享资源的访问,同步或者互斥。

c) 管道:数据单向流动的通信方式。

d) 消息队列:有消息的链表,存放在内核中,可以传递更多消息。

e) 共享内存:一段内存可以被多个进程访问,一般要配合信号量来使用。

f) Socket:通用的进程通信方式,也可以实现不同机器中进程的通信。

孤儿进程:父进程退出,而子进程还没有退出,就会变成孤儿进程,被init进程收养。

僵尸进程:子进程退出,父进程还没退出,那么子进程必须等待父进程获取到子进程的退出状态才能真正结束,否则子进程就成了僵尸进程。

4. 死锁是什么?如何防止?

死锁是多个线程争夺资源相互等待的状态,不解锁就永远等待下去,造成程序卡死。

产生条件,缺一不可:

互斥条件:所求资源必须有排他性;

不剥夺条件:进程获取的资源必须由自己释放;

请求和保持条件:当前进程请求其他资源不会改变已持有的资源占有;

循环等待条件:存在一个资源等待链环,链中每一个进程已获得资源同时被下一个进程请求。

破坏以上条件可以防止死锁。

计算机网络

网络分层结构(七层)及其作用?优点?

应用层:各种应用软件;

表示层:数据格式标识,压缩加密功能;

会话层:控制应用程序之间的会话;如不同软件数据分发;

传输层:端到端数据传输的基本功能;如TCP、UDP;报文段

网络层:定义IP,路由等;如不同设备的数据转发;

数据链路层:定义数据的基本格式,如何传输如何标识,如网卡,MAC地址;

物理层:数据传输,网线;比特流

TCP头部报文字段介绍几个?各自的功能?

TCP三次握手?四次挥手?(为什么需要三次握手,两次不行吗?)可以携带数据吗?

三次握手:

初始状态客户端是closed状态,服务端是listen状态。

  1. 客户端向服务端发送SYN报文,指定客户端的初始化序列号ISN,此时客户端处于SYN_SEND的状态;
  2. 服务端如果同意连接,将自己的同步序列号SYN、初始化序列号seq和确认序列号ack=x+1以及确认号ACK=1发送给客户端,服务器状态为SYN_Receive.
  3. 客户端收到SYN+ACK后,发送同步序列号ack=y+1,数据包序列号seq=x+1以及确认号ACK=1作为应答,状态为established。

两次不行,因为第一次客户端验证了自身的发送能力,服务端验证了自身的接受能力和客户端的发送能力,第二次服务端发送,客户端接收,服务端可以验证自身的发送能力,客户端可以验证自身的接收能力,但服务端还不能验证客户端的接收能力,因此要第三次握手让服务端直到客户端接收到了自己的报文。

第三次才可以携带数据,因为这时候客户端的连接已经建立,可以发送数据,服务端接收到后建立连接,然后验证数据即可。

ISN(Initial Sequence Number)是固定的吗?SYN攻击是什么?

对HTTP协议的了解?一次完整的HTTP请求过程包括哪些内容?HTTP请求方法有?

GET 和 POST 的区别,你知道哪些?

一个TCP连接可以对应几个HTTP请求?一个 TCP 连接中 HTTP 请求发送可以一起发送么(比如一起发三个请求,再三个响应一起接收)?

DNS是什么?DNS的工作原理?

框架

OpenGL

1. OpenGL 是按照什么架构设计的?

C/S模式。

在CPU上运行的代码相当于Client,OpenGL图形渲染管线相当于Server,渲染过程就是不断地从CPU传输渲染指令到GPU,简介操作GPU。

2. 什么是渲染上下文(Context)?

OpenGL是一个巨大的状态机,所谓Context可以理解为其状态,认为改变状态,然后OpenGL根据状态进行渲染。状态是全局的。

3. 什么是离屏渲染?为什么离屏渲染会造成性能损耗?

GPU渲染机制:CPU计算好显示的内容,提交到GPU,GPU渲染完成后将结果放入帧缓冲区,随后屏幕控制器按照Vsync信号逐行读取缓冲区的数据,经过数模转换显示到屏幕上。

当前屏幕渲染就是GPU的渲染操作用于当前屏幕的显示;

离屏渲染指的是在当前屏幕缓冲区外开辟一个缓冲区进行渲染。

离屏渲染会有性能损耗,主要是屏幕缓冲区的切换很耗性能。

4. 简述图形渲染管线?为什么说 OpenGL 渲染管线中的着色器(Shader)是可编程管线?

因为我们可以自己定义Shader来替换默认地着色器,从而更加细致地控制渲染过程中特定地部分。

5. 什么是 VBO、EBO 和 VAO?

OpenGL中处理数据地三大缓冲对象。

VBO:顶点缓冲对象,用于存放顶点数据(位置、颜色、纹理坐标、法线坐标等);

EBO:元素索引缓冲对象,为了更高效地利用数据,储存索引来减少重复数据;

VAO:顶点数组对象,用以管理VBO或EBO,可以方便地切换顶点数组,更改要渲染地内容。

6. 纹理的本质?纹理储存的位置?如何创建一个纹理(2D或3D)?什么是mipmap?

纹理本质上来说是一张图片,是一种由数据元素构成的二维数组,它存储了二维图像数据。

储存在GPU中。

纹理不依赖于分辨率,所以过滤选项很重要,常见的有Nearest(临近过滤:就近选择颜色)和Linear(线性过滤:基于附近的纹理像素,插值近似);

创建一个2D纹理:

  1. 生成纹理,glGenTexture,参数为纹理个数和纹理ID
  2. 绑定创建的纹理,指定纹理类型和纹理ID
  3. 加载图片,使用加载的数据创建纹理,glTexImage2D,函数需要指定图片的格式,纹理的格式
  4. glGenerateMipmap创建多级渐变纹理。

创建一个3D纹理:

  1. 生成纹理,glGenTexture,参数为纹理个数和纹理ID
  2. 绑定创建的纹理,指定类型为cubemap
  3. 加载图片,创建6个2D纹理分别对应六个面

创建skybox:使用六张图片创建一个3D纹理,渲染时禁用深度写入,将观察矩阵转换成3维矩阵后再转换成4维矩阵,这样将移除一个维度即位移部分,这样摄像机的移动不会影响到skybox。

Mipmap:多级渐变纹理,远近不同的物体如果使用相同的纹理会浪费内存,而且太远的物体会很小,OpenGL很难获取正确的颜色值,Mipmap是纹理处理成一系列不同等级的纹理,后一个是前一个二分之一,根据物体的远近使用最合适的那一个。缺点是内存占用更大,因为要储存更多的纹理。

7. 光照的实现在什么着色器中?有几种实现?原理分别是什么?

早期在顶点着色器中实现光照,但由于顶点的数量是有限的(性能好),实现的效果往往不是不太逼真;现在一般实现在片段着色器中,光照更加逼真,但性能损耗更多。

现在有两种实现:

冯氏着色(phong)光照模型:

环境光是一个常数,所有物体都会发出微弱的光,这让场景看起来不是纯黑的;

漫反射光:两个单位向量的夹角越小,它们点乘的结果越倾向于1,因此计算光照的方向(即从片段到光源的向量,也即入射光)与法线的点积,两者夹角越小漫反射强度越大;

镜面光:镜面光也取决于光照方向和法线方向,但同时也取决于观察方向,根据从法线方向翻折入射方向得到反射方向,计算反射方向与视线方向的点积,夹角越小,作用越大,点积也就越大。

Blinn-Phong光照模型:

为了解决冯氏模型的问题,冯氏光照模型的镜面高光区域会出现明显的断层,因为观察向量和反射向量的夹角不能大于90度,如果点积的结果小于零,镜面光分量就为0了,为了解决这个问题,引入了Blinn-Phong模型,其他相同,不同的在于它是通过计算半程向量与法向量的夹角来决定镜面光分量的,半程向量是入射向量与观察向量标准化后相加得来的

8. 什么是帧缓冲?为什么要使用帧缓冲?

颜色缓冲、深度缓冲和模板缓冲结合起来就叫帧缓冲,帧缓冲储存在显存中,每一帧的渲染,帧缓冲中的数据都会被读取然后显示到屏幕上。

OpenGL允许我们自定义帧缓冲,以此可以实现很多炫酷的效果。正常情况下所有的渲染都是在默认帧缓冲中进行的,当我们创建并绑定一个帧缓冲后,再进行的缓冲就都会绘制到这个帧缓冲中,也即离屏渲染;对此结果进行一系列的处理,再放到默认缓冲中,再完成一次渲染操作显示到屏幕上,就可以得到后处理之后的渲染结果。

后处理实现步骤

创建并绑定一个帧缓冲,此时读取和写入帧缓冲的所有操作都会影响到当前绑定的帧缓冲;

创建一个纹理附件,不填充数据;将其附加到帧缓冲上;

或者创建并绑定一个渲染缓冲对象附件,将其附加到帧缓冲上;(渲染缓冲对象直接把渲染数据储存在它的缓冲中,而不会针对纹理格式作任何转换,从而变成一个更快的可写介质;通常只写,可以使用glReadPixels读取,但比较慢)

两个阶段,第一阶段绑定创建的帧缓冲,将场景渲染到纹理上,第二阶段,绑定会默认缓冲,使用纹理,将场景渲染到屏幕上。

9. 什么是深度缓冲?深度测试是什么?阴影贴图是什么?实时阴影是如何实现的?

深度缓冲即Z缓冲(Z-buffer),深度值会作为z值(深度值)储存在每一个片段中,当片段想要输出颜色时,OpenGL会将它的深度值与深度缓冲对比,如果当前片段在其他片段后面,就将会被丢弃,否则会覆盖。这个过程为深度测试。glEnable(GL_DEPTH_TEST);可以开启深度测试,开启深度测试后,每一次渲染都要清除深度缓冲。

阴影映射(Shadow Mapping):从光的视角渲染,能看到的东西应该是亮的,看不到的东西就是在阴影之中的。从光源的透视图渲染场景,将深度值储存到纹理中(即深度贴图/Depth Map),通过深度值可以找到从光源发射出的射线所遇到的诸多点中距离光源最近的点,用以决定片段是否在阴影中。

实现步骤:

  1. 为要渲染的深度贴图创建帧缓冲对象
  2. 创建一个空的2D纹理,纹理格式指定为GL_DEPTH_COMPONENT,高和宽设置成1024*1024或2048*2048(深度贴图的分辨率)
  3. 把深度纹理设置为帧缓冲的深度缓冲glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);然后将颜色缓冲设置为GL_NONE
  4. 渲染深度贴图,视口变换到贴图的分辨率,绑定帧缓冲对象,创建正交投影矩阵(因为是平行光)和lookat矩阵进行空间变换,渲染,解绑
  5. 绑定深度贴图到当前默认帧缓冲,渲染

10. 什么是阴影失真?如何改进?什么是PCF(百分比近似过滤)?

阴影贴图受限于分辨率,在距离光源较远的情况下,多个片段从同一个深度值进行采样,光线斜射到表面时,一个像素所覆盖的片段中一部分被被认为在阴影之中,另一部分不在,这样就产生了明暗的条纹。

阴影偏移:对表面的深度应用一个偏移量,使得所有的片段都被认为在阴影中。偏移量可以根据表面朝向光线的角度max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);但是使用阴影偏移之后会出现悬浮的阴影失真,渲染深度贴图的时候使用正面剔除可以解决大部分的问题。(OpenGL默认是背面剔除)。

PCF:

11. 点光源阴影?万向阴影贴图?(待加工)

点光源时,普通的2D深度贴图无法工作,使用cubemap即可,将六个面的深度值渲染到立方体深度贴图中。

12. 什么是lookAt函数?是如何变换的?

三个相互垂直的轴定义一个坐标空间,外加一个平移向量创建一个矩阵,可以使用这个矩阵乘以任意向量来使其变换到那个坐标空间。

需要一个位置、目标和上向量;位置减去目标得到从位置指向目标的向量,此为z轴,上向量是x轴,两轴叉乘得到的是y轴,所有向量都要归一化(单位化就可以)。平移向量根据位置设置,摄像机的话是反方向,取负即可。

13. 写过什么着色器,效果是什么?如何实现的?

光照、阴影、地形曲面细分。

14. 如何渲染透明物体和半透明物体?透明物体和不透明物体渲染顺序?

全透明物体可以直接根据纹理的透明度选择丢弃片段,在片段着色器中调用discard。

如果要渲染半透明物体可以使用混合(blend),使用混合方程计算最终的颜色,即颜色乘以其因子数相加

深度测试与混合同时启用时,深度缓冲不会检查片段是否是透明的,所以透明的部分也和其他值一样写入深度缓冲中,因此看不到半透明后面的半透明物体。解决办法是严格按照确定的顺序来绘制:

1. 绘制所有不透明物体

2. 将透明物体按照距离进行排序

3. 按照顺序绘制所有透明物体

排序方法:从观察者视角获取物体的距离(摄像机位置和物体的位置),储存到map中,会自动排序,然后从远到近绘制。

15. 几何着色器的作用,顶点着色器传输给几何着色器什么数据?

输入是一个图元(点、线、三角形)的一组顶点,几何着色器可以变换这些顶点或者生成更多顶点,甚至转换成完全不同的图元。

16. 描述OpenGL中由顶点数据输入到绘制出一幅图像的具体过程?

  1. 首先创建和绑定VBO、VAO,需要的话创建绑定EBO,缓冲数据传递给vertex shader,通过固定的锚点解析数据;
  2. 顶点着色器中进行坐标变换(mvp矩阵),将坐标转换到裁剪坐标系,这里可以进行顶点着色(但一般情况效果不太好)
  3. 下一阶段是曲面细分着色器,这里可以
  4. 几何着色器
  5. 裁剪
  6. ndc空间
  7. 屏幕空间
  8. 光栅化
  9. 片段着色器的输入是屏幕空间
  10. 测试与混合

17. 介绍一下渲染管线,三维坐标如何变成屏幕坐标,有哪些五个空间和变换?

局部空间:物体自身的空间,用来描述物体的形状。

世界空间:经过模型矩阵,可以进行位移、旋转、缩放,从局部空间变换到世界空间。变换矩阵前三列代表缩放和旋转,最后一列是平移

观察空间:世界空间中的坐标转化为用户视野前方的坐标,也就是摄像机观察到的空间,往往需要一系列的位移和旋转,这些变换存储在观察矩阵中。

裁剪空间:在屏幕上显示时,应该只能看到屏幕范围内的坐标,范围之外的应该裁剪掉,因此从摄像机的位置为原点定义一个视锥体,创建投影矩阵将坐标转换为裁剪空间中的坐标,裁剪空间之外的坐标都会被裁剪掉。

定义一个透视投影矩阵,需要确定远平面近平面视野大小。远平面是超过一定距离就裁剪掉,不会显示出来,近平面是太近也不会显示,因为事实上眼睛并不位于身体的几何中心,视野大小决定了观察空间的角度大小,一般情况45是比较真实的效果。

屏幕空间:OpenGL对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中是一个800x600的屏幕)。这个过程称为视口变换

NDC:归一化的设备坐标标准化设备坐标,经过透视投影和透视除法之后,视锥体变为CVV,此时采用的坐标系叫做NDC。

18. 旋转有哪几种方式?欧拉角会有什么问题?讲一下四元数?

欧拉角表示3D空间中可以任意旋转的三个值,俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll)。对于摄像机来说一般不考虑滚转角,而是考虑将俯仰角和偏航角转化为用来自由旋转视角的摄像机的3维方向向量,鼠标输入获取偏航角和俯仰角,计算方向向量。

19. 如何优化shader计算量?

尽量使用内建函数,尽量少用逻辑判断(if语句可以使用step函数代替),尽量使用低精度。

20. 什么是G-buffer?

是对所有用来储存光照相关的数据,并在最后光照处理阶段中使用的纹理数据的总称。本质上是一个帧缓冲对象。

更多问题

i++和++i的区别?

i++ 是先用临时对象保存原来的对象,然后对原对象自增,再返回临时对象,不能作为左值;++i 是直接对于原对象进行自增,然后返回原对象的引用,可以作为左值。由于要生成临时对象,i++ 需要调用两次拷贝构造函数与析构函数(将原对象赋给临时对象一次,临时对象以值传递方式返回一次);

++i 由于不用生成临时变量,且以引用方式返回,故没有构造与析构的开销,效率更高。

>> 和 << 是什么?

正则表达式?

sizeof和strlen区别

Linux 查看进程、线程、 CPU 核数命令

TCP拥塞控制?

显示评论