Bingo, Computer Graphics & Game Developer
Unreal为每个平台的底层都封装了一致的高层代码,本文讲述的是Windows下Unreal的源代码细节表现
###1. 使用Mutex来保证当前程序是当前系统中唯一开启的
Unreal源代码
bool MakeNamedMutex( const TCHAR* CmdLine )
{
bool bIsFirstInstance = false;
TCHAR MutexName[MAX_SPRINTF] = TEXT( "" );
FCString::Strcpy( MutexName, MAX_SPRINTF, TEXT( "UnrealEngine4" ) );
GNamedMutex = CreateMutex( NULL, true, MutexName );
if( GNamedMutex && GetLastError() != ERROR_ALREADY_EXISTS && !FParse::Param( CmdLine, TEXT( "NEVERFIRST" ) ) )
{
// We're the first instance!
bIsFirstInstance = true;
}
else
{
// Still need to release it in this case, because it gave us a valid copy
ReleaseNamedMutex();
// There is already another instance of the game running.
bIsFirstInstance = false;
}
return( bIsFirstInstance );
}
其中使得程序不会启动两次的关键在于互斥锁CreateMutex()。我参考了这篇文章CreateMutex,其中这一段讲解清晰了原理
进程在启动时首先检查当前系统是否已经存在有此进程的实例,如果没有,进程将成功创建并设置标识实例已经存在的标记。此后再创建进程时将会通过该标记而知晓其实例已经存在,从而保证进程在系统中只能存在一个实例。
如有同名程序运行,则通过 GetLastError()得到错误代码 ERROR_ALREADY_EXIST。
###2. do{ some_code }while(false);
代码块实现模板switch
Unreal源代码
#define checkCode( Code ) do { Code } while ( false );
大致含义就是代码块中一旦出现错误或者指定情况下可以直接跳过(break)后部代码,实现类似goto的逻辑代码。说的直白一些,我个人更倾向于认为这像是”模板Switch”的实现。
// 若任何类型的switch存在 就会像下面这样
switch(condition)
{
case condition1:
break;
case condition2:
break;
// ...
}
因为switch只能满足标量类型,而搭配此,就无需在每步中额外提供一些标示符来表示执行到的位置。其优点就相当于switch(int)和一堆的if/else组合对比下的优势。就像下面这样,阅读感受很糟糕。
// if/else
// condition1
if ( data == & array[o1])
operation = O1;
// condition2
else if ( data == & array[o2])
operation = O2;
// conditionxxx
else if
//...
// conditionn
else if ( data == & array[on])
operation = ON;
Log( "operation:",operation);
相对比之下,checkCode()的优势很明显
// macro switch
checkCode(
if(!condition1)
break;
if(!condition2)
break;
// ...
)
除了我个人理解的switch以外,它也有一定的错误代码跳转能力。以下某种情况下,some_code
和some_further_code
是有可能不被执行的,实现了类似goto的逻辑。
// template switch
do
{
if (!condition1) break;
some_code;
if (!condition2) break;
some_further_code;
// …
} while(false);
###3. class后部用于方便导出到dll的”关键字”
Unreal源代码
class RHI_API FDynamicRHI
{
public:
// ...
}
找了很久,在WindowsPlatfrom.h
下找到类似定义
#define DLLEXPORT __declspec(dllexport)
#define DLLIMPORT __declspec(dllimport)
因为我只使用过一次dll导出函数使用,同时也没有过导出类的经验,因此遇到这个宏时一无所知。参考与以下文章CSDN, C++编写DLL的方法, 其中第二篇更为精确一点。
这个宏出现最核心的原因就在于,客户端和DLL本身共用同一份头文件
。
如若是客户端,那么需要在class后声明_declspec(dllimport)
用以导入该类给自己,DLL则是class后声明_declspec(dllexport)
,问题就出在这里,要共用一份头文件,但class却有两个写法,岂不是无法起到共用的效果?
解决方案如下
.h
#ifdef DLL_API
#else
#define DLL_API _declspec(dllimport)
#endif
.cpp
#define DLL_API _declspec(dllexport)
#include "DLLTest.h"
此时客户端在导入这个类时,使用_declspec(dllimport)
,DLL使用时已定义DLL_API,因此依旧是class被认定为导出_declspec(dllexport)
。
下次打算自己亲手试一试Windows平台下的dll导出。这样印象更深一点。
基本上初步入门都是在这里正则表达式入门教程学到的,当然没有30分钟那么夸张。
许多时候如若不进行实际测试很难知道自己的正则表达式正确与否,一般我就在这里正则表达式在线测试或者Rebular直接在线测试,其实道理都一样。
从开始学习,到实际能够解读正则表达式,到真正运用正则表达式在CSS文件的解析上,我起码花了整整三天时间,甚至更多
首先说说我自己的感想,正则表达式难度不在于其各个元字符的搭配很难,而是解析的这一步很难。所以我自己结合了教程总结了如下经验。
案例1
^(\*?[-#/\*\w]+(\[[0-9a-z_-]+\])?)\s*
乍一看非常难以分解,不过我一般会这样做
^ #字符串开始
(
\*? #可选的*
[-#/\*\w]+ #"-""#""\""*""字母数字下划线或汉字"出现一次或多次
(
\[[0-9a-z_-]+\] #"["+0~9|a~z|_~空格+"]"
)? #上述字符串是可选的
)
\s* #任意多空格
上述代码不会对正则表达式产生任何的影响,因为(?= #xxx)内部的内容表明为注释,这样的缩进可以更明显的看出来内容,我个人更偏向于下面这样的用法。
// ^ 字符串开始
// (
// \*? 可选的任意出现零次或者多次的字符
// [-#/\*\w]+ "-""#""\""*""字母数字下划线或汉字"出现一次或多次
// (\[[0-9a-z_-]+\])? 可选的"["+0~9|a~z|_~空格+"]"
// )
// \s* 任意多空格
static const std::regex _RE0_ = RE(R"raw(^(\*?[-#/\*\w]+(\[[0-9a-z_-]+\])?)\s*)raw");
这样就有一个好处,在实际使用此正则表达式时,不用在表达式内部修改样式,有的OS例如Windows,会要求在每行的最后加上\
类似于这样
string test2 =
"body{\
background-color: #d0e4fe;\
}\
\
h1{\
color: orange;\
text-align: center;\
}\
\
p{\
font-family: \"Times New Roman\";\
font-size: 20px;\
}";
这对有强迫症的人来说简直是致命的,但又没有什么好的办法。所以我个人偏向于将正则表达式的注释写到对应语言的注释中。
对于元字符其实没有什么太多能够讲述的
反义
代码 | 说明 |
---|---|
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
重复
代码 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
元字符
代码 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
要说说的就是这个反义,^这个字符如若不在[]中,那么它的意思一般就是表示从字符串开始的位置,当然如果前面没有转义字符的话。一旦跑到了[]中,那么它就代表了反义。
案例2
^([^\{]+)
// ^ 字符串开始处
// (
// [^\{]+ 除{以外其他的一个或者多个字符
// )
这里的两个^就展现了不同的含义,一开始我在这里费了不少功夫。
删除注释
有的正则表达式可以匹配得到多门语言当中各型各色的注释,以便于直接找到删除。删除注释大多数的语言注释的正则表达式都可以在这里找到。C/C++/Java因为语言相通所以注释匹配也是可以通用的。
在实际应用中,我使用/\*[^\*]*\*+([^/\*][^\*]*\*+)*/
来匹配CSS的注释,读者可以自行测试。
在实际的CSS解析中最难以理解的表达式如下^((?:'(?:\\'|.)*?'|\"(?:\\\"|.)*?\"|\([^\)]*?\)|[^\};])+)
// ^
// (
// (
// ?: 可选的:
// '(?:\\'|.)*?' 单引号围起来的内容或者任意字符 若前方有',那么需要以'结尾
// |
// \"(?:\\\"|.)*?\" 双引号围起来的内容或者任意字符 若前方有",那么需要以"结尾
// |
// \([^\)]*?\) (开始 匹配除了)以外的任意字符 字符可以出现零次或者多次[^x] 可选的)
// |
// [^\};] 匹配除了} ;以外的任意字符
// )
// + 可以有零个或者多个上述值出现
// )
这里的单引号的确是难以理解,后来才发现其实单引号不需要转义就可以直接表达自身的意思,可以对比下一行的双引号。因为有的CSS property: value;里的value是字符串/单一字母/xxx(xxx)/等等形式 如
background-color: rgb(0, 10, 20);
background-color: #123456;
background-color: red;
很种类繁多,因此需要一并写出。
我将我尝试的心路历程都记录在了这里。
这里的快速实质上是代表了代码书写的高效率。在重造数据结构里线性表的轮子的时候,遇到了需要构造NxM的矩阵需求,因为NxM可以动态构建出来,这没什么。
如下,可以非常方便的造一个简单的NxM的矩阵轮子出来
class t3Matrix
{
public:
// 构造函数参数里给出行列即可
t3Matrix(int row, int col):col(col), row(row)
{
m = new int*[row]();
for(int i = 0; i < row; i++)
m[i] = new int[col]();
}
~t3Matrix()
{
for(int i = 0; i < row; i++)
delete[] m[i];
delete[] m;
}
int **m;
int col, row;
};
问题来了,如何赋值,倘若是以往的2x2, 3x3事实上都是好解决的。
void t3Matrix2x2::create(int a00, int a01, int a10, int a11)
{
// do something...
}
void t3Matrix3x3::create(int a00, int a01, int a02, int a10, int a11, int a12)
{
// do something...
}
// etc...
实际使用的时候这样的方法也不免为一个好决策。这里我颇受Eigen的影响, 他的矩阵初始化办法颇为简便易读,他是这样的。
void main()
{
// NxM
t3Matrix m(2, 3);
// core!
m << 1, 2,
3, 4,
5, 6;
}
这才像话,立马动手抄,但是Eigen过于庞大,也只能依样画葫芦尝试着去实现。
尝试1
移位运算符与stream联系非常迫切,兴许有效。这里我尝试着这样去试着使用istream,ostream配合着完成这样的初始化。
class t3Matrix
{
public:
t3Matrix();
~t3Matrix();
};
ostream& operator<<(ostream& os, t3Matrix& m);
istream& operator>>(istream& is, t3Matrix& m);
思来想去,我最多能够做到使用cin,cout为矩阵初始化和输出,并不能在代码中实现初始化动作。
void fuck()
{
t3Matrix m(n, m);
// 标准输入初始化矩阵
cin >> m;
// 抛却m.print()方式标准输出
cout << m;
}
然而这并没有什么卵用,期间我甚至尝试着让t3Matrix继承
std::ostream。注意!OSX下XCode不会对以下代码报错
class A:public std::ostream
{
public:
// 如若不为ostream构造,那么你将面临两次调用matrix析构函数的问题(Windows上VS编译无法通过)
// 解决办法是写为ostream(NULL)提供一个streambuf参数即可,当然也可以给NULL
A(){}
// 会被二次调用
~A(){}
};
class A:public std::ostream
{
public:
// 运行正常
A():std::ostream(NULL){}
~A(){}
};
尝试2
这回另辟蹊径,想起了重载逗号运算符。重载这个运算符实在是太少见了,以至于我和同学说起时,惊讶的反应:这**玩意也要重载?
其实需求很明显只要m << 1, 2, 3, 4;
能够一路正确的读取即可,<<
的优先级高于,
,加上从左到右的运算顺序。
所以只要满足m << 1
的返回值能够与重载,
的运算符左参数相符,那么其实就是t3Matrix自身了。
以下写法已证实可行
class t3Matrix
{
public:
// ...
t3Matrix& operator,(int value)
{
// 赋该value到正确的位置
// do something...
return *this;
}
t3Matrix& operator<<(int value)
{
// 赋该value到正确的位置
// do something...
return *this;
}
};
void main()
{
t3Matrix m(2, 3);
m << 1, 2,
3, 4,
5, 6;
}
其实难度不在于”<<”的重载,而是想到”,”的重载才是
已更新至t3DataStructures
P74页中倒数第二行的else if
if(next_row == EXIT_ROW && next_col == EXIT_COL)
found = TRUE;
else if(!maze[next_row][next_col] && !mark[nextRow][next_col]){
// do something...
}
此处的else if我认为可以更正为if,那么最后一个结点也可以入栈,或者为了性能起见,将stack.push()的在第一步if中添加上也可。个人更倾向于第二种做法。
即(t3DataStructures.t3Maze中就是这样做的)
if(next_row == EXIT_ROW && next_col == EXIT_COL){
stack.push(row, col, dir);
found = TRUE;
}
else if(!maze[next_row][next_col] && !mark[next_row][next_col]){
// do something...
}
大体就是这样了,本工序基本上是按照《数据结构-C语言》来重现出一个大致的数据结构基本内容的轮廓了的。
本身代码的难度不是很大,除了几个较难理解的算法重新实现以外,抑郁的基本上是对自己基本功的一次完整的复习,希望可以抛砖引玉。
一旦重现70%书中内容之后,打算接《深入剖析STL》来对照着看具体的差距在哪里。因为已经有不少跨平台工程的经验,所以本数据结构应该可以做到多平台的编译运行(Windows下也徐汇遇到文件格式问题,只需要重新保存成Unicode即可,OSX和Win的平台差异没办法似乎难以解决)
工程Github链接t3DataStructures,欢迎Star。
本文是对 数据结构–C语言版 书中代码的详解与个人理解
上一节我只讲述了一下代码层面的理解,如果有算法背景不了解的可以去开篇的两篇教程中寻找答案。这里只阐述自己对于算法中较为难以理解部分的想法
。
为了能清晰的讲述KMP总共大概花费了一礼拜的时间; 这里我将书上的failure数组称为next数组以节省篇幅; s代表代求解字符串,p代表模式字符串
这一段代码我认为是我个人最难以理解的。
i = failure[i];
首先这里的背景是,在求next数组某一循环时发现p[j]!=p[i+1],且i>=0。换言之p0,p1, … pi != pj-1, pj-i+1, …pj,因此我希望能够在p0,p1, … pi中找到一个位置x(x < p),使得p0,p1,…px = pj-x, pj-x+1,…pj。
简而言之,就是希望能够找到一条稍微短一些的公共缀。可以参考
v_JULY_v
教程里的图
而算法的答案是:你可以试试x = next[i]
,说不定可行。注意,这里x = next[i]
不是一次就可以找到,因此也就是while存在的价值。
难点依然存在,为什么x = next[i]
就可以快速的找到这个x呢?这也是我这么多天一直困扰的地方。
while(pat[j] != pat[i+1] && i>0)
i = failure[i];
总结了一下,其实难点在于几个指针的意义不明确。这里j很好理解,指向当前需要求解的位置,i才是重点。
注意,这里的指针概念全部都引用自
KMP心得-1
里的代码实现
这里我用一句话概括求解next数组的中心思想:让模式串p与自身匹配
知晓了此中心思想,理解算法就游刃有余了。
假定现在已知模式串为,我将结果和序号一一标示在下边
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
A | B | A | C | A | B | A | B |
-1 | -1 | 0 | -1 | 0 | 1 | 2 | ? |
此时i=2(i=next[j-1]),为求解B的next值,根据算法比对前缀ABAC
与后缀ABAB
(pat[j]!=pat[i],B!=C
),不匹配,这个时候就是关键点了。
直接从字面上寻找,当然很快就能找到s[1] = s[7]的事实,也就是next[7] = 1。
如果这个过程改为p于自身匹配,这个时候就很好理解了。
A B A C A B A B
- - - - A B A C A B A B
模拟pmatch的过程,现前循环是这样的。发现B!=C,此时不是从头开始再次匹配,而是回溯到某一个位置重新比对
,也就是next[i](next[2]=0),这就是为什么i可以通过赋值next[i]来迭代寻找的核心原因!
A B A C A B A B
- - - - - - A B A C A B A B
此时s[7]==p[1],next[7]=1!因为谁也不能保证p[next[i]]=p[j],因此这里用while循环。其实和pmatch里的while是一个道理,我并不清楚回溯之后string[i]和pat[j]
是否相等,所以while里j的值一直迭代。
总结一下,其实求解next数组受益于求解前后缀,其实质就是在匹配自身字符串,大致的核心思想都是相同的。