作者 略游 q群 512 001 256
标准(standard):C++语言标准,在代码世界里,我们假设与公理等价。
结论:由标准推导出的事实。
规定:便于讨论,我们设定的一些规则。
类型(type):同一类型,它们在C++内存布局一致,使用同一代码段。用符号T表示。
实例(instance):基于类型的不同实例对象,在内存中拥有不同的值。
容器(container):容纳0到N个同类型的实例。用符号C表示,它也是一种类型。
函数(function):执行后,改变程序的内存状态,间接改变外部状态。用F表示。
命名:常见概念我们以特定名字表示。
标准:内存最小单元是位(bit),1字节(byte)为8位。
标准:内存的大小是有限的。
规定:计算机在运行过程中,外部条件可能会导致电源断开、硬件错误、比特翻转。但我们规定代码世界不受理论之外事物的影响。
标准:
[std::numeric_limits::min(), std::numeric_limits::max()]为类型范围。
标准:float与double是不精确的、有限的、不满足交换律,类似实数的类型,称为浮点数。两个浮点数最小差值为:
std::numeric_limits::epsilon()
标准:整型的表示是准确的,有以下类型表示不同大小:
int8_t、int16_t、int32_t、int64_t
uint8_t、uint16_t、uint32_t、uint64_t
以上类型,C++标准规定并不一定存在,取决于编译器,但我们假设(规定)它们一定存在。
int 表示非定长整数
unsigned 表示非定长自然数(非负整数)
标准:unsigned int 与 unsigned等价。
结论:int是有限整数。
结论:unsigned是有限自然数。
float本意是浮点数、double是双精度浮点数。我们使用一个新的类型表示它们任意一个,叫实数(real)是不恰当的,因为它们表示的并不是数学上的实数,只是一种近似模拟。
命名:使用Float表示float或double。
标准: 长度为N的 std::array、std::vector,下标访问区间为[0, C.size())。
命名: 长度为size,下标为index。
定义:下标使用size_t表示,它属于无符号整型。
定义:长度使用size_t表示,它属于无符号整型。
定义:一维点使用int表示,它属于有符号整型。
标准: std::array
标准: std::vector
定义: 二维点为std::array
命名: 我们使用以下命名表示通用类型,默认设定为2维的一个原因是,显示器为二维坐标系:
using Point = std::array;
using Position = std::array;
其中Point为整型点、Position为浮点数点。
命名: 通用类型默认为2维、有符号整型。若为x维度,则加上x后缀。无符号则加上U后缀。得出以下名字:
using Point3U = std::array;
using Position4 = std::array;
当然,也会有以下定义:
using Point1 = std::array;
using Position1 = std::array;
using Point0 = std::array;
using Position0 = std::array;
其中Point1与int基本等价,之所以定义Point1,是由于写容器算法时,容器的长度可以是[0, N]。故标准库也规定std::array
另外在现实世界中,二次方程被三次方程求根公式兼容;圆是椭圆的特例;正方形是矩形的特例。前者均有更简单的公式算法,所以在程序中使用int表示一维,而不是Point1。比如二维向量与三维向量叉乘均有简洁的写法,4x4矩阵为了效率考虑不会使用NxN维矩阵的算法。
从另一个角度讲计算机的性能是有限的,并不能保证编译器可以将Point1优化为int。
另外在写代码层面,在访问Point1时,需要使用下标0访问,比如写x[0]。相比直接写x会麻烦很多。
标准:std::begin(C)为容器首元素。
标准:std::end(C)为容器尾元素之后的元素。
命名:迭代器为iter。
标准:迭代器自增指向容器下一个元素,最多到end。
结论:容器的所有元素在[std:begin(C), std::end(C))的区间内。
规定:从最普遍、最简易的角度,我们使用如下遍历方法:
for(auto& iter : c)
{
}
规定:通用类型由于长度固定,我们使用下标遍历。在需要使用下标时,也可以如下遍历:
T a;
for(std::size_t i = 0; i < std::size(a); ++i)
{
a[i];
}
在容器为固定类型时,可以使用 c.size()。如果 array
template
constexpr void Zero(const std::array& a)
{
for(std::size_t i = 0; i < N; ++i)
a[i] = 0;
}
以加法举例,有两个操作符,“+”和“+=”,以类型T举例,有如下写法:
T& operator+=(T& a, const T& b)
{
a.x += b.x;
a.y += b.y;
return a;
}
T operator+(const T& a, const T& b)
{
T ret = a;
return ret += b;
}
“+”的运算返回前操作数a的引用,而“+=”运算返回新的对象,为了避免重复代码出现,“+=”实现依赖于“+”。
命名:返回值变量为ret。
由上推广到通用类型,有以下运算:
//------------------------------N op N------------------------
template
constexpr std::array& operator+=(std::array& a, const std::array& b)
{
for (std::size_t i = 0; i < N; ++i)
a[i] += b[i];
return a;
}
template
constexpr std::array operator+(const std::array& a, const std::array& b)
{
std::array ret = a;
return ret += b;
}
除了加法,还有减法、乘法、除法。实现以后我们便可如下计算:
constexpr Point a{100, 100};
constexpr Point b{200, 200};
constexpr Point c = a + b; // c is 300, 300
以上为N op N的操作,当b长度为1时,即N op 1的操作,如下:
//------------------------------N op 1------------------------
template
constexpr std::array& operator+=(std::array& a, const T& b)
{
for (std::size_t i = 0; i < N; ++i)
a[i] += b;
return a;
}
template
constexpr std::array operator+(const std::array& a, const T& b)
{
std::array ret = a;
return ret += b;
}
实现以后支持如下计算:
constexpr Point a{100, 100};
constexpr Point b = a + 50; // c is 150, 150
对自身取负号,同理可得(为了写法统一,也实现取正号):
//------------------------------取负号------------------------
template
constexpr std::array operator-(const std::array& a)
{
std::array ret = a;
for (std::size_t i = 0; i < N; ++i)
ret[i] = -ret[i];
return ret;
}
template
constexpr std::array operator+(const std::array& a)
{
std::array ret = a;
for (std::size_t i = 0; i < N; ++i)
ret[i] = +ret[i];
return ret;
}
以数字255举例,有以下字面量写法:
int a{ 255 };
unsigned b{ 255 };
double c { 255.0 };
float c {255.0f };
由于我们使用 Float表示 float和 double,则需要自定义字面量f,由于避免和标准库冲突(即使标准库不存在f后缀),使用_f,故有以下定义:
inline constexpr Float operator""_f(long double r)
{
return static_cast(r);
}
inline constexpr Float operator""_f(unsigned long long r)
{
return static_cast(r);
}
至此,我们可以使用字面量 100_f初始化一个 Float。
命名:时间使用FloatTime表示,基于float或double。
命名:大小使用Size表示,它没有负数,所以基于unsigned。
命名:矩形使用Rect表示。常用与窗口、图像区域,所以基于int。
命名:三角形使用Triangle表示,与绘图相关,基于Position。
命名:四边形使用Quad表示,与精灵相关,基于Position。
命名:颜色使用Color表示,顺序为TODO,float精度足够。
命令:分数使用Frac表示。需要存放分子分母,基于Point。
以上均可以使用通用类型表示,使用using语法,如下:
using FloatTime = float;
using Size = PointU;
using Rect = Point4;
using Triangle = std::array;
using Quad = std::array;
using Color = std::array;
using Frac = Point;
这时体现出定义通用类型的意义:
1、所有算法通用,只需要写一遍。
2、定义新类型,只需要一个using。
3、命名不同的类型,但实际类型可能一致,避免转换操作。
首先假设我们身处0维空间,此时整个世界是一个点,方向没有意义。
如果我们身处1维空间,世界是一条线。以当前位置观察,产生两个方向,比如常见的我们用左右或上下来描述它。一般情况我们规定右为正,但当我们在南半球头朝下时,所规定的右变成了左。身处在1维世界,如果自身有朝向,则有前后的概念,则可规定前方即为正方向。
规定:使用右手坐标系。
规定:x、y、z、w为4维坐标轴简称。
规定:2维坐标系,x正轴向右,y正轴向下( 基于显示器和书写习惯)。
重回一维空间,规定自身点为原点(origin)。则自身位置可定义为0,正方向为+1,负方向为-1。数学上的欧氏空间有无穷远,往正轴走为正无穷,往负轴走负无穷。而代码世界的一切都是有限的,所以我们将坐标轴两端连起来,与0相对的位置称为max,如下图所示:
命名:方向(direction)简写为dir。
规定:dir类型为int。
规定:dir取值为0、1、-1、std::numeric_limits
如此,要计算位移,我们有以下公式:
标准:std::byte表示字节类型。
命名:缓冲区(Buffer)表示一段连续的、长度固定的内存。
程序本身就像一张纸条,程序执行就像不断的操作纸条。内存就是纸条,使用std::array我们可以定义一块连续的内存。如果内存在堆上,那么就必须使用指针(pointer)指向它的首地址。在delete和free时我们不需要传入内存的长度(操作系统有记录长度),但平时我们需要使用长度时则需要手动记录。表示缓冲区我们经常会看到如下几种:
首地址 | 长度 |
char* | int |
unsigned char* | unsigned |
void* | size_t |
uint8_t* |
上面两两组合就已经有12种表示方法。再加上std::string、std::vector
using Buffer = std::vector;
using BufferRef = std::span;
using BufferView = std::span;
命名:Ref表示引用,可读可写。
命名:View表示视图,只读。
说到图像,一般人都会想到它是2d的。
规定:Image表示图像,2维。
内存不存在2维的概念,但显示器显示2维,甚至3维,均是通过模拟的方式。所以我们应该如何实现2维内存块?如下:
首先内存必须连续,以便获得较好的缓存(cache)命中率、减少内存碎片。
有一种做法是,如下计算索引:
但这种方法需要一次乘法,所以我们额外记录行指针(row pointer),以快速访问。
对于固定宽高的内存块,基于std::array,命名为Array2,实现如下:
template
class Array2
{
private:
std::array _data;
std::array _ptrRow;
}
对于变化宽高的内存块,基于std::vector,命名为Vector2,实现如下:
template
class Vector2
{
std::vector _data;
std::size_t _w;// 宽
std::vector _ptrRow; // 行指针
};
由于32位颜色为图像格式主流,无论是8位灰度还是24位RGB,我们加载到内存后都统一为32位。所以有如下Image定义:
using Image = Vector2;
C++标准是一种人为规定的理性。虽然非常的理性,但远远比不上数学和历史。比如1+1很难失效,发生过的事似乎永远不会逆转。但C++标准在不断发生变化,不过只有少数标准被废弃。
C++代码会被编译器(compiler)编译为机器码,最终在CPU上执行。
目前主流编译器有msvc、clang、gcc,主流CPU架构有x86、arm,操作系统有windows、linux、macos。所以事实上的C++标准的实现,是有多份的。在不同操作系统上,还提供了独自的api。这就使C++跨平台的说法成为了一种理论,实际上C++的跨平台就是相同的接口,在不同平台各写一份。基于这一点,我们需要一些实用的宏来判断操作系统,如下:
#define DL_PLATFORM_ERROR 0
#define DL_PLATFORM_WIN32 1
#define DL_PLATFORM_LINUX 2
#define DL_PLATFORM DL_PLATFORM_ERROR
#ifdef _WIN32
#undef DL_PLATFORM
#define DL_PLATFORM DL_PLATFORM_WIN32
#endif
#ifdef __linux__
#undef DL_PLATFORM
#define DL_PLATFORM DL_PLATFORM_LINUX
#endif
判断当前程序是否64位:
//! 是否64位
consteval bool is_x64()
{
return sizeof(void*) == 8;
}
判断字节序是否小端:
//! 字节序是否小端
consteval bool is_little_endian()
{
union
{
int a;
char b;
}num;
num.a = 1;
return !num.b;
}
Color
ColorRef
对容器每一个元素做同一操作:
namespace Every
{
template
void Do(const C& c, F func)
{
for(auto& iter : c)
func(iter);
}
}
for(a
for(a
sort
clamp
min
max
stable_sort
基础