Graphics Software面试准备
个人针对芯片公司的Graphics Software岗位的面试笔记,主要分为4部分:c++基础,图形学基础,图形API
c++基础
此部分综合了网络上的c++面经、笔记以及个人在面试中遇到的问题。
Part1:class相关
- 纯虚函数可以有实现吗?
答:语法层面来讲可以有,但是一个类只要含有纯虚函数就变成了抽象类,为纯虚函数提供实现也无法实例化。纯虚函数由继承该类的子类实现,子类可以实例化。 - 定义一个空类,有哪些默认的函数?
默认构造函数
默认拷贝构造函数
默认析构函数
默认重载赋值运算符函数
默认重载取址运算符函数
默认重载取址运算符const函数
默认移动构造函数(C++11)
默认重载移动赋值操作符函数(C++11)。 - 析构函数为什么要设置成虚函数?构造函数可以是虚函数吗?
构造函数不能是虚函数,编译会报错。每个对象的虚函数表是在构造函数中初始化的,因此构造函数不能是虚函数。析构函数一般都是虚函数,当一个基类指针指派生类对象时,通过该指针调用对象时就可以调用到子类的析构函数,从而释放所有资源。 - 虚函数的实现原理?
虚函数通过虚函数表实现,有以下几个注意点:
1)虚函数表是编译器在编译时期为我们创建好的, 只存在一份。
2)定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表。 3)vfptr是指向函数指针数组的指针,const void**,指针指向的内容不可更改。
4)如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。 5)编译器生成虚表-(指针数组)》开辟内存-》写入虚表(吧虚表写入到内存中)-》构造函数-》调用虚函数。 6)构造函数内部调用虚函数,语法层面是允许的,但调用虚函数只能调用该类的虚函数,无法实现多态。 - 内联函数可以是虚函数吗?
虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。 - 为什么类的成员模版函数不能是虚函数?
因为模板函数可以有无数个实例,所以编译器在编译时无法为其生成一个确定的虚函数表条目,无法确定需要调用哪个特定的模板实例。 - 隐藏是什么?
派生类的函数屏蔽了基类的同名函数,只需要函数名字相同,无论参数列表是否相同,其基类函数都会被隐藏。 - 空类的大小是?
1字节。要求每个对象必须具有独一无二的内存地址。 - 在成员函数中调用delete this会发生什么
在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。对象不可以使用。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。 - 在类的析构函数中调用delete this会发生什么?
栈溢出。delete关键字会调用类的析构函数,从而造成栈溢出。 - 为什么成员初始化列表会快一些?
对于类型,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次调用。而对于内置数据类型则没有差别。
Part2:语言基础
-
引用的本质是什么?引用和指针的区别是什么?
可以将引用理解为一个对象的别名,引用在底层是通过指针实现的,const ptr*。引用在创建时必须被初始化,不能为NULL,在初始化之后不能进行更改。 -
volatile关键字
volatile 关键字声明的变量,每次访问时都必须从内存中取出值,告诉编译器不应对这样的对象进行优化。const 可以是 volatile (如只读的状态寄存器)。 -
sizeof
数组作为参数传入函数后会退化为指针,sizeof的结果是指针的大小。当数组直接作为 sizeof 的参数时,返回的是数组占据内存空间的大小。 -
c++字节序列
大端:高位字节在低地址,低位字节在高地址。
小端:低位低地址,高位高地址 -
左值和右值
左值是可以取地址的,而右值不可以。
右值引用和move语句结合使用可以避免拷贝。 -
引用折叠
T& & -> T&
T& && -> T&
T&& & -> T&
T&& && -> T&&
万能引用
template <typename T>
void MyFunc(T&& value) {
}
-
c++内存模型
1)堆
2)栈
3)自由存储区 new/delete
4)常量存储区
5)全局/静态存储区 -
typedef vs #define
- 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义 常量,以及书写复杂使用频繁的宏。
- 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
- 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用 都是正确的。
- 对指针的操作不同:typedef 和define 定义的指针时有很大的区别。
- 静态链接库和动态链接库
- 在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
- 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。
STL相关
-
std::vector
1)vector底层通过动态数组实现,空间不够时会进行扩容,将空间扩大为原来的1.5倍,将原有数据拷贝到新的空间中,释放原来占据的空间。
2)reserve和resize:reserve只改变capacity,但是不改变size。resize(n)将size变为n,若原有的capacity<n,则改变capacity。
3)迭代器:插入和删除元素都可能导致迭代器失效(开辟新的空间,导致原地址失效)。使用迭代器删除元素时,it = vec.erase(it);
-
红黑树和AVL树
stl中的map和set都是基于红黑树实现,每个节点是红色或者黑色,根结点和叶子结点都是黑的,如果一个节点是红色的,那么它的两个子节点都是黑色的。
AVL树是一棵二叉搜索树,每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。
AVL 树比红黑树更加平衡,但AVL树在插入和删除的时候也会存在大量的旋转操作。所以当你的应用涉及到频繁的插入和删除操作,切记放弃AVL树,选择性能更好的红黑树;当然,如果你的应用中涉及的插入和删除操作并不频繁,而是查找操作相对更频繁,那么就优先选择 AVL 树进行实现。
图形学基础
芯片公司的graphics software岗位要求对图形学相关的基础知识有一定了解。下面总结一些常见的图形学问题。
- 渲染管线
- ShadowMap原理、常见的问题以及解决方案
- PBR
- Anti-Aliasing
- Culling
更多内容可参考图形学面经,总结的很全面。
图形API相关
本人主要学习了Vulkan API,在这里总结一下常见的面试问题。
- Vulkan画一个三角形需要哪些步骤?
考察Vulkan中的基础概念。
- VkInstance
- VkDevice
- VkQueue
- VkSwapchain
- VkCommandPool & VkCommandbuffer
- VkRenderPass & VkFramebuffer
- VkPipeline & VkPiplineLayout & PSO & VkDescriptorSet VkDescriptorSetLayout VkDescriptorPool
- VkSemaphore VkFence Barrier
- Vulkan中的多线程渲染
Secondary commandbuffer, 多线程 - Vulkan PSO
构建Pipeline需要Pipeline State Objects,包括fixed function的设置、shader设置、pipelineLayout设置,重复利用PSO可以提升性能。 - Vulkan Performance Optimization
- Variable rate shading
- RayTracing API