面经-计算机基础
本文最后更新于:2024年9月1日 晚上
C++
关键字
incline
inline是先将内联函数编译完成⽣成了函数体直接插⼊被调⽤的地⽅,减少了压栈,跳转和返回的操作。没有普通函数调⽤时的额外开销;
内联函数是⼀种特殊的函数,会进行类型检查;对编译器的⼀种请求,编译器有可能拒绝这种请求;
C++中inline编译限制:
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞⼤
- 内联函数声明必须在调⽤语句之前
static
- 修饰变量
修改了变量的作用域和生命周期,存储在静态区域。生命周期和程序相同,作用域分为全局变量和局部变量。局部变量仅在函数内可用,全局变量仅在当前源文件中可用。 - 修饰函数
表明函数的作用域仅在当前源文件中。 - 修饰成员变量
静态成员变量为全局类对象所共享,仅有一份拷贝。类中声明,类外定义和初始化。 - 修饰成员函数
静态成员函数为全局类对象所共享。没有this指针,仅能访问静态成员变量和函数,虚函数不能为静态成员函数。【虚函数运行时绑定,静态成员函数编译时绑定】
extern
修饰变量:变量声明,表明变量在此处引用,在其他源文件中定义。
修饰函数:表明函数在其他源文件中定义。
extern “C”:编译器用C的命名规范去编译函数,链接器用C的命名规范进行链接。因为C++支持函数重载,而C不支持。
例如,假设某个函数的原型为:void
foo( int x, int y);该函数被C编译器编译后在符号库中的名字为 _ foo,而C++编译器则会产生像_foo_int_int之类的名字。这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。
constexpr 和 const
constexpr:告诉编译器我可以是编译期间可知的,尽情的优化我吧。
const:告诉程序员没人动得了我,放心的把我传出去;或者放心的把变量交给我,我啥也不动就瞅瞅。
修饰对象的时候两者之间最基本的区别是:
- const修饰一个对象表示它是常量。这暗示对象一经初始化就不会再变动了,并且允许编译器使用这个特点优化程序。这也防止程序员修改了本不应该修改的对象。
- constexpr是修饰一个常量表达式。但请注意constexpr不是修饰常量表达式的唯一途径。
修饰函数的时候两者之间最基本的区别是:
- const只能用于非静态成员的函数而不是所有函数。它保证成员函数不修改任何非静态数据。
- constexpr可以用于含参和无参函数。constexpr函数适用于常量表达式,只有在下面的情况下编译器才会接受constexpr函数:
- 1.函数体必须足够简单,除了typedef和静态元素,只允许有return语句。如构造函数只能有初始化列表,typedef和静态元素 (实际上在C++14标准中已经允许定义语句存在于constexpr函数体内了) 2.参数和返回值必须是字面值类
sizeof
sizeof计算的是在栈中分配的内存大小。
(1) sizeof不计算static变量占的内存;
(2) 32位系统的指针的大小是4个字节,64位系统的指针是8字节,而不用管指针类型;
(3) char型占1个字节,int占4个字节,short int占2个字节
long int占4个字节,float占4字节,double占8字节,string占4字节
一个空类占1个字节,单一继承的空类占1个字节,虚继承涉及到虚指针所以占4个字节
(4) 数组的长度:
若指定了数组长度,则不看元素个数,总字节数=数组长度*sizeof(元素类型)
若没有指定长度,则按实际元素个数类确定
Ps:若是字符数组,则应考虑末尾的空字符。
(5) 结构体对象的长度
在默认情况下,为方便对结构体内元素的访问和管理,当结构体内元素长度小于处理器位数的时候,便以结构体内最长的数据元素的长度为对齐单位,即为其整数倍。若结构体内元素长度大于处理器位数则以处理器位数为单位对齐。
(6) unsigned影响的只是最高位的意义,数据长度不会改变,所以sizeof(unsigned int)=4
(7) 自定义类型的sizeof取值等于它的类型原型取sizeof
(8) 对函数使用sizeof,在编译阶段会被函数的返回值的类型代替
(9) sizeof后如果是类型名则必须加括号,如果是变量名可以不加括号,这是因为sizeof是运算符
(10) 当使用结构类型或者变量时,sizeof返回实际的大小。当使用静态数组时返回数组的全部大小,sizeof不能返回动态数组或者外部数组的尺寸
为什么空类大小不是0?
为了确保两个不同对象的地址不同,必须如此。
类的实例化是在内存中分配⼀块地址,每个实例在内存中都有独⼀⽆⼆的地址。
同样,空类也会实例化,所以编译器会给空类隐含的添加⼀个字节,这样空类实例化后就有独⼀⽆⼆的地址了。
所以,空类的sizeof为1,⽽不是0。
反射
C++如何实现反射?
引用
引用和指针的区别?从底层角度考虑
引用必须初始化,不能改变引用的指向,从汇编的角度来看,引用就是一个const指针
智能指针
C++中的智能指针有哪些,各自有什么作用?
智能指针主要解决一个内存泄露的问题,它可以自动地释放内存空间。因为它本身是一个类,当函数结束的时候会调用析构函数,并由析构函数释放内存空间。智能指针分为共享指针(shared_ptr), 独占指针(unique_ptr)和弱指针(weak_ptr):
(1)shared_ptr ,多个共享指针可以指向相同的对象,采用了引用计数的机制,当最后一个引用销毁时,释放内存空间;
(2)unique_ptr,保证同一时间段内只有一个智能指针能指向该对象(可通过move操作来传递unique_ptr);
(3)weak_ptr,用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
shared_ptr的实现原理是什么?构造函数、拷贝构造函数和赋值运算符怎么写?
(1)shared_ptr是通过引用计数机制实现的,引用计数存储着有几个shared_ptr指向相同的对象,当引用计数下降至0时就会自动销毁这个对象;
(2)具体实现:
1)构造函数:将指针指向该对象,引用计数置为1;
2)拷贝构造函数:将指针指向该对象,引用计数++;
3)赋值运算符:=号左边的shared_ptr的引用计数-1,右边的shared_ptr的引用计数+1,如果左边的引用技术降为0,还要销毁shared_ptr指向对象,释放内存空间。
shareptr引用计数数据类型
long
多态
多态的原理
静态多态和动态多态的区别?
何为静态多态?
又称编译期多态,即在系统编译期间就可以确定程序将要执行哪个函数。例如:函数重载,通过类成员运算符指定的运算。
何为动态多态?
动态多态是利用虚函数实现运行时的多态,即在系统编译的时候并不知道程序将要调用哪一个函数,只有在运行到这里的时候才能确定接下来会跳转到哪一个函数。
动态多态是在虚函数的基础上实现的,而实现的条件有:
(1) 在类中声明为虚函数
(2) 函数的函数名,返回值,函数参数个数,参数类型,全都与基类的所声明的虚函数相同(否则是函数重载的条件)
(3) 将子类对象的指针(或以引用形式)赋值给父类对象的指针(或引用),再用该指向父类对象的指针(或引用)调用虚函数
如此,便可以实现动态多态,程序会按照实际对象类型来选择要实行的函数具体时哪一个。
虚函数
虚函数表
每个子类会生成一个虚函数表,根据这个子类有无重写父类的虚函数,重写了会覆盖对应的内存空间
一个继承一个比较好理解,一个继承了多个可以看下面的图
每一个类会有一个虚函数表,然后这个类的多个对象都会共享这一张虚函数表,新创建的对象会保存虚函数指针。
纯虚函数和抽象类
纯虚函数是指在基类中定义的没有实现的虚函数。使用纯虚函数可以使该函数只有函数原型,而没有具体的实现。注:这里的“=0”表示该函数为纯虚函数。
纯虚函数的作用是让子类必须实现该函数,并且不能直接创建该类对象(即该类为抽象类)。
抽象类是包含纯虚函数的类,它们不能被实例化,只能被继承。抽象类只能用作其他类的基类。如果一个类继承了抽象类,则必须实现所有的纯虚函数,否则该类也会成为抽象类。
哪些函数不能声明成虚函数?
非成员函数
非成员函数只能被重载(overload),不能被继承(override),而虚函数主要的作用是在继承中实现动态多态,非成员函数早在编译期间就已经绑定函数了,无法实现动态多态,那声明成虚函数还有什么意义呢?
构造函数
要想调用虚函数必须要通过“虚函数表”来进行的,但虚函数表是要在对象实例化之后才能够进行调用。而在构造函数运行期间,还没有为虚函数表分配空间,自然就没法调用虚函数了。
友元函数
静态成员函数
静态成员函数对于每个类来说只有一份,所有的对象都共享这一份代码,它是属于类的而不是属于对象。虚函数必须根据对象类型才能知道调用哪一个虚函数,故虚函数是一定要在对象的基础上才可以的,两者一个是与实例相关,一个是与类相关。
内联成员函数
内联函数是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,并且inline函数在编译时被展开,虚函数在运行时才能动态地绑定函数。
虚析构函数有什么作用?
在Effective C++ 中,Scott Meyers在《条款07:为多态基类声明virtual析构函数》中提到,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没有被销毁。也就是说,如果派生类继承了父类的情况下,如果父类的析构函数不是虚函数,而在使用中用了多态的写法,就会导致没有调用到派生类的析构函数,导致资源没有释放,造成泄漏。
总的来说虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。
(1)如果父类的析构函数不加virtual关键字
当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。
(2)如果父类的析构函数加virtual关键字
当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。
内存
内存空间有哪些分类?
(1)堆,使用malloc、free动态分配和释放空间,能分配较大的内存;
(2)栈,为函数的局部变量分配内存,能分配较小的内存;
(3)全局/静态存储区,用于存储全局变量和静态变量;
(4)常量存储区,专门用来存放常量;
(5)自由存储区:通过new和delete分配和释放空间的内存,具体实现可能是堆或者内存池。
malloc和new有什么区别?
(1)new分配内存空间无需指定分配内存大小,malloc需要;
(2)new返回类型指针,类型安全,malloc返回void*,再强制转换成所需要的类型;
(3)new是从自由存储区获得内存,malloc从堆中获取内存;
(4)对于类对象,new会调用构造函数和析构函数,malloc不会(核心)。
函数
构造函数分类
默认构造函数(Default Constructor):没有参数的构造函数。如果在类中没有定义构造函数,编译器会自动生成一个默认构造函数。默认构造函数用于创建对象时进行默认的初始化操作。
参数化构造函数(Parameterized Constructor):带有参数的构造函数。参数化构造函数可以接受不同的参数,并根据参数的值来初始化对象的数据成员。
拷贝构造函数:使用一个对象初始化另一个对象。拷贝构造函数的参数为同类对象的引用。它将被复制的对象的数据成员值复制给新创建的对象。
移动构造函数(Move Constructor):C++11引入的特性,用于实现对象的移动语义。移动构造函数通过接管另一个对象的资源而避免进行深拷贝,提高了性能。
移动构造函数
传入右值,直接浅拷贝,右值用std::move来生成
STL
stl底层实现
vector:数组
Dequeue(双端队列):二维数组
List:环状双向链表
set(集合):平衡的红黑树
multiset:红黑树
map:平衡二叉树
unordered_map:散列表(哈希表)
而C++ STL 标准库中,不仅是 unordered_map 容器,所有无序容器的底层实现都采用的是哈希表存储结构。更准确地说,是用“链地址法”(又称“开链法”)解决数据存储位置发生冲突的哈希表。
哈希表原理
首先是哈希函数,就是把一个长的二级制数据转换成一个短的二进制数据的函数;然后就是解决哈希冲突的方法,常见的有开放寻址法和链表法,前者是通过探测并占用下一个可用的存储位置,后者是在冲突的位置用链表记录多个值。
编译问题
编译过程
(1)预处理阶段处理头文件包含关系,对预编译命令进行替换,生成预编译文件;
(2)编译阶段将预编译文件编译,生成汇编文件(编译的过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码);
(3)汇编阶段将汇编文件转换成机器码,生成可重定位目标文件(.obj文件)(汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可);
(4)链接阶段,将多个目标文件和所需要的库连接成可执行文件(.exe文件)
#include<file.h> 与 #include “file.h”的区别?
前者从标准库查找寻找和引用file.h,后者从当前路径寻找和引用
main函数执行之前会执行什么?执行之后还能执行代码吗?
(1)全局对象的构造函数会在main函数之前执行;
(2)可以,可以用_onexit 注册一个函数,它会在main 之后执行;
如果你需要加入一段在main退出后执行的代码,可以使用atexit()函数,注册一个函数。
比如全局变量的初始化,就不是由main函数引起的
举例: class A{};
A a; //a的构造函数限执行
int main() {}
动态库和静态库优缺点
静态库
优点
- 代码装载速度快,执行速度比动态链接库略快
- 只需要开发者有lib就行,不需要考虑用户电脑上有无lib。
缺点
生成的体积较大,包含相同的公共代码,造成浪费
动态库
优点
- 节省内存
- dll和exe独立,更换dll就可以改变函数内容,提高可维护性和可拓展性
- 不同编程语言只要按照函数调用约定可以用同一个dll
- 耦合度小,开发过程独立
缺点
用户电脑里面需要有dll
C#
关键字
unsafe
在C#中,unsafe 关键字用于标识包含不安全代码块的上下文,允许直接使用指针和执行不安全的操作。
优点:
更高的性能: 使用指针直接操作内存可以提高性能,特别是在处理大量数据或需要高效访问内存的场景下。
与非托管代码交互: 允许与非托管代码进行更直接的交互,例如调用 Windows API 或者使用一些底层的系统功能。
灵活性: 可以执行一些 C# 中无法直接实现的操作,如访问特定的内存地址或进行底层的位操作。
缺点:
安全性风险: 使用 unsafe 可能导致程序出现潜在的安全漏洞,因为绕过了 C# 的类型安全检查和边界检查。
可读性下降: 使用指针和不安全的操作会增加代码的复杂性,并且降低代码的可读性和可维护性。
难以调试: 不安全的代码可能更难调试和定位错误,因为涉及到直接操作内存的技术细节。
const和readonly有什么区别?
都可以标识一个常量。主要有以下区别:
1、初始化位置不同。const必须在声明的同时赋值;readonly即可以在声明处赋值;
2、修饰对象不同。const即可以修饰类的字段,也可以修饰局部变量;readonly只能修饰类的字段
3、const是编译时常量,在编译时确定该值;readonly是运行时常量,在运行时确定该值。
4、const默认是静态的;而readonly如果设置成静态需要显示声明
5、修饰引用类型时不同,const只能修饰string或值为null的其他引用类型;readonly可以是任何类型。
反射和特性
要用到特性就必须要用反射,比如说你要序列化一个类,如果直接写接口来实现,你不知道这个类有哪些属性,而且还要写很多不同的接口,但是用反射可以很优雅地实现,如下。
1 |
|
装箱和拆箱
引用类型和值类型
C#中定义的值类型包括原类型(Sbyte、Byte、Short、Ushort、Int、Uint、Long、Ulong、Char、Float、Double、Bool、Decimal)、枚举(enum)、结构(struct),引用类型包括:类、数组、接口、委托、字符串等,引用型是在堆中分配内存,初始化为null,引用型是需要GARBAGE COLLECTION来回收内存的,值型不用,超出了作用范围,系统就会自动释放!
结构体和类区别
OS
线程和进程
线程和进程和协程
(1)进程是运行时的程序,是系统进行资源分配和调度的基本单位,它实现了系统的并发;
(2)线程是进程的子单位,也称为轻量级进程,它是CPU进行分配和调度的基本单位,也是独立运行的基本单位,它实现了进程内部的并发;
(3)一个程序至少拥有一个进程,一个进程至少拥有一个线程,线程依赖于进程而存在;
(4)进程拥有独立的内存空间,而线程是共享进程的内存空间的,自己不占用资源;
(5)线程的优势:线程之间的信息共享和通讯比较方便,不需要资源的切换等.
每一个进程都独立拥有自己的指令和数据,所以称为资源分配的基本单位。其中数据又分布在内存的不同区域,我们在C语言课程中学习过内存四区的概念,一个运行中的进程所占有的内存大体可以分为四个区域:栈区、堆区、数据区、代码区。其中代码区存储指令,另外三个区存储数据。
线程是处理器调度和执行的基本单位,一个线程往往和一个函数调用栈绑定,一个进程有多个线程,每个线程拥有自己的函数调用栈,同时共用进程的堆区,数据区,代码区。操作系统会不停地在不同线程之间切换来营造出一个并行的效果,这个策略称为时间片轮转法。
那么协程在其中又处于什么地位呢? 一切用户自己实现的,类似于线程的轮子,都可以称之为是协程。
线程的独占资源和共享资源
独占资源
- 线程就是函数的运行,所以运行时候的信息都是独占的,包括返回值,局部变量,寄存器信息等,每个进程有自己独占的栈区。
- 每个线程有自己独立的线程id,独立的调度优先级和错误返回码。
共享资源
- 共享进程的代码区
- 共享进程的数据区,即全局变量和静态变量。
- 共享进程的堆区。
- 动态链接库。
- 文件,打开的文件信息。
- 共享当前工作目录,以及用户id和组id。
线程安全
“线程安全”也不是指线程的安全,而是指内存的安全,在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。所以线程安全指的是,在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。
如何避免?
私有化内存
栈内存
使用多进程与多线程的区别?
(1)线程执行开销小,但不利于资源管理和保护;进程则相反,进程可跨越机器迁移。
(2)多进程时每个进程都有自己的内存空间,而多线程间共享内存空间;
(3)线程产生的速度快,线程间通信快、切换快;
(4)线程的资源利用率比较好;
(5)线程使用公共变量或者资源时需要同步机制。
操作系统如何保证每个进程都有独立的空间?
通过虚拟内存来实现,
首先是虚拟内存分页,
然后是页表映射,给每个进程维护一个页表,记录了虚拟地址和物理地址的映射关系
之后是内存保护,操作系统会给分配的进程页表有一些额外的标志,用于控制进程对内存的访问权限。
之后是上下文切换,当操作系统切换到一个新的进程时,它会保存当前进程的页表以及其他的上下文信息,并加载下一个进程的页表。这样,每个进程在运行时拥有自己独立的虚拟地址空间,与其他进程的内存空间相隔离。
通过虚拟内存机制,操作系统能够为每个进程提供独立的内存空间,无论是代码、数据还是堆栈,每个进程都认为自己独占系统的整个内存空间。这种内存隔离保证了每个进程的数据安全和保密性,并且允许操作系统有效地管理和保护进城间的内存使用
线程同步的方法
同步指的是按一定的顺序依次执行
互斥锁
读写锁
条件变量
信号量
计网
TCP
tcp和udp的区别
(1)TCP是传输控制协议,UDP是用户数据报协议;
(2)TCP是面向连接的,可靠的数据传输协议,它要通过三次握手来建立连接,UDP是无连接的,不可靠的数据传输协议,采取尽力而为的策略,不保证接收方一定能收到正确的数据;
(3)TCP面向的是字节流,UDP面向的是数据报;
(4)TCP只支持点对点,UDP支持一对一,一对多和多对多;
(5)TCP有拥塞控制机制,UDP没有。
tcp三次握手的过程
三次握手的本质是确认通信双方收发数据的能力
首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的。
于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以。
然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我最后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的。
HTTP和HTTPS
https为什么更加安全?
彻底搞懂HTTPS的加密原理 - 顾伊凡 YGY的文章 - 知乎
http是明文传输,对称加密虽然性能好但有密钥泄漏的风险,非对称加密(2组公钥+2私钥双向传输)安全但性能低下,因此考虑用非对称加密来传输对称加密所需的密钥,然后进行对称加密,但是为了防止非对称过程产生的中间人攻击,需要对服务器公钥和服务器身份进行配对的数字认证,然后引入了CA数字签名+数字证书验证的方式!
https基本采用以下流程,即非对称+对称
某网站拥有用于非对称加密的公钥A、私钥A’。
浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器。
浏览器随机生成一个用于对称加密的密钥X,用公钥A加密后传给服务器。
服务器拿到后用私钥A’解密得到密钥X。
这样双方就都拥有密钥X了,且别人无法知道它。之后双方所有数据都通过密钥X加密解密即可。
但是可能会遭受中间人攻击,即在传输过程中把明文的公钥替换,那么如何保证浏览器收到的公钥就是服务器的公钥?所以就需要CA证书,CA证书本身也需要加密生成一个签名来保证没有被掉包。
而且也不用每次传输都传输密钥,服务器会为每个浏览器(或客户端软件)维护一个session ID,在TLS握手阶段传给浏览器,浏览器生成好密钥传给服务器后,服务器会把该密钥存到相应的session ID下,之后浏览器每次请求都会携带session ID,服务器会根据session ID找到相应的密钥并进行解密加密操作,这样就不必要每次重新制作、传输密钥了!
渲染流程
(1)应用程序阶段,该阶段主要是在软件层面上执行的一些工作,包括空间加速算法、视锥剔除、碰撞检测、动画物理模拟等。大体逻辑是:执行视锥剔除,查询出可能需要绘制的图元并生成渲染数据,设置渲染状态和绑定各种Shader参数,调用DrawCall,进入到下一个阶段,GPU渲染管线。
(2)几何阶段,包含顶点着色、投影变换、裁剪和屏幕映射阶段。
a. 顶点处理阶段:这个阶段会执行顶点变换和顶点着色的工作。通过模型矩阵、观察矩阵和投影矩阵(也就是MVP矩阵)计算出顶点在裁剪空间下的位置(clip space),以便后续阶段转化为标准化设备坐标系(NDC)下的位置。也可能会计算出顶点的法线(需要有法线变换矩阵)和纹理坐标等。同时,在这个阶段也可能会进行顶点的着色计算,如平面着色 (Flat Shading)和高洛德着色 (Gouraud Shading)都是在顶点着色器中进行着色计算。因为这个阶段是完全可控制的,因此执行什么样的操作由程序员来决定。(此外,在顶点处理阶段的末尾,还有一些可选的阶段,包括曲面细分(tessellation)、几何着色(geometry shading)和流输出(stream output),此处不详细描述)
b. 裁剪阶段:简单来说就是两次裁剪的粒度不同,前者是在物体对象层面的,一般对对象的包围盒做剔除,剔除掉不在视锥体内的物体,NDC裁剪是在三角形层面做的,裁剪掉不在屏幕内的像素。
c. 屏幕映射阶段:主要目的是将之前步骤得到的坐标映射到对应的屏幕坐标系上。
(3)光栅化阶段,包含三角形设置和三角形遍历阶段。
a. 三角形设置(图元装配),计算出三角形的一些重要数据(如三条边的方程、深度值等)以供三角形遍历阶段使用,这些数据同样可用于各种着色数据的插值。
b. 三角形遍历,找到哪些像素被三角形所覆盖,并对这些像素的属性值进行插值。通过判断像素的中心采样点是否被三角形覆盖来决定该像素是否要生成片段。通过三角形三个顶点的属性数据,插值得到每个像素的属性值。此外透视校正插值也在这个阶段执行。
这两个阶段是完全硬件控制的,不可进行任何操作。
(4)像素处理阶段,包括像素着色和测试合并。
a. 像素着色,进行光照计算和阴影处理,决定屏幕像素的最终颜色。各种复杂的着色模型、光照计算都是在这个阶段完成。
b. 测试合并,包括各种测试和混合操作,如裁剪测试、透明测试、模板测试、深度测试以及色彩混合等。经过了测试合并阶段,并存到帧缓冲的像素值,才是最终呈现在屏幕上的图像。
参考资料
面经:https://zhuanlan.zhihu.com/p/417640759
设计模式:https://zhuanlan.zhihu.com/p/23821422
C++面经:https://github.com/huihut/interview?tab=readme-ov-file
https://github.com/guaguaupup/cpp_interview?tab=readme-ov-file