类与对象
需要预先学习: 变量与常量 函数 类与对象 前面学习的 int、double 等内置类型只能表示单个数据。当你想描述一个学生——有姓名、学号、成绩,还会做自我介绍——散落的几个变量无法把"数据"和"操作"组织在一起。C++ 为此提供了类(class)机制,让你可以定义自己的类型。 类(class):一种用户自定义类型,描述一类事物共有的特征(数据)和行为(操作)。 对象(object):类的具体实例(instance),每个对象都拥有自己的一份数据。 比如"人"是一个类——所有人都有名字、年龄,都会打招呼,这些共性是"人"这个类描述的;而张三、李四则是两个具体的对象,他们拥有同样的结构,但名字、年龄各不相同。 类与对象的关系,就像图纸和按图纸造出来的实物:图纸只有一份,不占实际空间;每按图纸造出一个实物,都要占用真实的空间。对应到程序中:类本身不占内存,每实例化一个对象,就会分配一块独立的内存。 类的定义 用 class 关键字定义类: #include <iostr...
封装
需要预先学习: 类与对象 封装 封装(encapsulation)有两层含义: 把数据(成员变量)和操作数据的行为(成员函数)打包到类中 通过访问权限限定符控制谁能访问这些成员,把实现细节藏起来 在 类与对象 里,成员统一用了 public,任何代码都能直接读写成员变量。这带来一个问题:如果有人给 m_age 赋了 -1,程序照单全收,对象就处于“年龄是负数”的非法状态。封装要做的,就是收起成员的直接访问权,只通过类提供的成员函数来操作数据,由类自己守住数据的合法性。 访问权限限定符 C++ 提供三种访问权限限定符,用它们把类体分成几个区域: 限定符 类内部 类外部 public 可访问 可访问 private 可访问 不可访问 protected 可访问 不可访问 private 与 protected 在"类外部不可访问"上是一致的,区别在于继承时派生类能否访问(详见 继承)。本节统一用 private 表示私有成员。 #include <iostream> #include <s...
继承
需要预先学习: 类与对象 封装 继承 继承(inheritance)让一个新类可以获得另一个类的成员,从而复用代码,并表达"是一种"(is-a)的关系。 学生、教师都是人,都有姓名、年龄,都会打招呼。把这些共性提取到 Person 类里,让 Student、Teacher 从它继承,各自只需写独有的部分——不必把姓名、年龄在每个类里重复一遍。 基类(base class,也叫父类):被继承的类 派生类(derived class,也叫子类):继承别人的类 “学生是一个人”——这种 is-a 关系是继承的核心:派生类对象可以当作基类对象来用。 继承的语法 继承写在类名后面的冒号处: class Student : public Person // Student 公开继承 Person { // 这里只需写 Student 独有的成员 }; #include <iostream> #include <string> class Person { public: st...
多态
需要预先学习: 类与对象 继承 多态 多态(polymorphism)的字面意思是"多种形态":同一个调用形式,根据对象的实际类型表现出不同的行为。 C++ 中的多态分为两类: 静态多态(编译期多态):函数重载、模板,调用哪个版本在编译期就确定 动态多态(运行期多态):虚函数,调用哪个版本在程序运行时才确定 静态多态 函数重载是静态多态的典型:同一个函数名,参数列表不同,编译时根据实参类型决定调用哪个版本。 #include <iostream> void print(int x) { std::cout << "整数:" << x << std::endl; } void print(double x) { std::cout << "浮点数:" << x << std::endl; } int main() { print(10); // 编译时确定调用 print(int...
类的内存占用
需要预先了解: 类与对象 内存基础 多态 类的实例的内存占用 类的实例(对象)的内存占用,指的是 sizeof(类名) 的结果——它只统计数据成员和编译器为支持语言特性而插入的隐藏成员(如虚表指针),不包含静态成员变量和成员函数。先看一组真实测量(64 位平台): #include <cstdio> class Empty {}; // 空类 class WithInt { public: int x; }; class WithVirt { public: virtual void f() {} int x; }; class WithStatic { public: static int s; // 静态成员变量 int x; }; int WithStatic::s = 0; int main() { prin...
剔除总结
需要预先学习: 着色与渲染管线 深度缓冲 视图(摄像机)与投影 剔除解决什么问题 真实场景的三角形动辄百万千万,但屏幕只有几百万像素,一帧里真正可见的几何只占极小一部分。剔除(culling)就是在管线的不同阶段尽早把"反正看不见"的几何丢掉,让 GPU 不做无用功。剔除发生得越早,省下的越多:一个物体在 CPU 侧被剔除,顶点变换、光栅化、片元着色全省;一个片元在着色前被剔除,至少省下最贵的片元着色。 按发生的位置,剔除技术可以分层: 层次 执行者 粒度 代表技术 应用层 CPU 物体 / 实例 视锥剔除、遮挡剔除、小物体剔除 图形管线固定阶段 GPU 硬件 三角形 / 片元 裁剪、背面剔除、Early-Z GPU 计算阶段(现代) compute shader 物体 / 实例 / 三角形 Hi-Z 遮挡剔除、GPU 驱动管线 每一层的输入都是上一层筛剩下的,层层过滤。这篇笔记按"从物体到片元"的顺序把这些技术串起来。 视锥剔除(Frustum Culling) 视锥剔除(fru...
抗锯齿算法
需要预先学习: 光栅化 抗锯齿的分类 光栅化 的结尾简单列过几种反走样技术的名字,这篇笔记把它们逐个展开,讲清每种算法在管线的哪个位置工作、付出什么代价、换来什么样的边缘质量。 先回到问题的根源。几何边缘之所以产生锯齿,是因为"三角形边界"在数学上是一个无限陡的阶跃信号(step signal,从一个值瞬间跳到另一个值)——非黑即白,频率无穷高。任何有限的采样率都追不上它(奈奎斯特定理,见 光栅化),所以抗锯齿算法没有谁能"完全消除"锯齿,它们都在做同一件事:近似边缘在像素内的覆盖率。一个像素 70% 被三角形覆盖,理想的输出就是 70% 的三角形颜色混合 30% 的背景色,各种算法的差别只在"怎么知道是 70%"。 按"额外的采样从哪里来",主流算法分三类: 类别 思路 代表算法 超采样(supersampling) 增加每个像素的采样点数 SSAA、MSAA 后处理(post-process) 从渲染好的图像里检测并修复边缘 FXAA、MLAA、SM...
层次包围结构
需要预先学习: 光线追踪 剔除总结 层次包围结构 两个看似无关的问题,其实是同一件事: 剔除总结 里的视锥剔除,每帧要拿每个物体的包围体测六个平面——物体一多,CPU 逐个测试本身就成为开销; 光线追踪 里每条光线要与每个三角形求交——三角形数乘以光线数,直接相乘完全不可接受。 两者的共同点:绝大多数测试的结果都是"没有命中",但每一次"没有"都要花一次测试的代价换回来。层次包围结构(hierarchical bounding structure)的思路是让"没有"变得成片:把物体按空间位置组织成一棵树,每个节点用包围体框住它负责的所有内容;光线或视锥与某个节点的包围体不相交,整棵子树一次排除。单次查询的复杂度从 O(n)O(n)O(n) 降到 O(logn)O(\log n)O(logn)。 按"切的东西"不同,层次结构分两大族: 族类 切的是什么 代表结构 空间划分(spatial subdivision) 空间本身 均匀网格、八叉树、BS...
extern、static 与 inline 关键字总结
需要预先了解: 变量与常量 函数 内存基础 extern、static、inline 修饰变量时的区别 三个关键字修饰变量时,本质都在操纵声明与定义的关系和链接属性(linkage)。要理解它们的区别,先要弄清楚链接是怎么回事。 翻译单元与链接 一个 C++ 程序通常由多个 .cpp 文件组成。每个 .cpp 文件连同它 #include 的所有头文件,构成一个翻译单元(translation unit)。编译器逐个翻译单元独立编译,产出目标文件(.o),链接器(linker)再把所有目标文件合并成可执行文件。 每个翻译单元中定义的全局变量和函数,都会在目标文件里生成一个符号(symbol)——它是链接器用来识别"哪个名字对应哪块地址"的索引。链接器需要回答一个关键问题:a.cpp 里用到了 global,该去哪里找它的定义?答案取决于符号的链接属性: 外部链接(external linkage):符号对其他翻译单元可见,链接器可以在整个程序范围内解析它 内部链接(internal linkage):符号只在本翻译单元内可见,...
类型转换
需要预先了解: 变量与常量 指针 类与对象 多态 隐式类型转换 隐式类型转换(implicit conversion)由编译器自动完成,不需要程序员写出任何转换语法。它发生在赋值、传参、表达式求值等场景中,正确使用时十分自然,但某些情况下会悄悄引入错误。 数值转换与整型提升 小整型参与算术运算时,会先提升为 int,这叫整型提升(integral promotion): char c = 'A'; int n = c; // char 提升为 int,n = 65 short s = 100; int sum = s + 1; // s 先提升为 int 再参与加法 从窄到宽的数值转换是安全的:int 转 long、float 转 double 都不丢失信息。但从宽到窄的转换会截断或丢失精度,比如 double 转 int 直接丢掉小数部分: double d = 3.99; int x = d; // x = 3,小数部分被丢弃 浮点转整数的截断方向是向零取整(truncation),不是四舍五...


