CATEGORY / Development

用 前置声明,还是用 #include?

为了能让编译速度快一点,明智之举是使用前置声明,而不是 #include 整个头文件。

那声明时候可以用前置声明,而什么时候必须 #include 头文件呢?简单的说:

  1. 当不需要用到类型的具体实现时,包括构造器赋值运算符成员函数等,只需要前置声明就可以了
  2. 当需要用到类型的以上方法时,就不得不 #include 整个头文件

对该技巧的具体分析可以参考这里这里


征服 Visual Studio 的 Editor

  • 块选中
  • 类似 UltraEdit 里的“列模式”,按住 Alt 键再移动光标,就可以选择一块范围内的内容,而不必被束缚在只能一行接一行的选中方式了。

  • 整行剪切 / 复制 / 粘贴
  • 平时我都是用鼠标移到一行最左边的行号区去选中一行,然后执行复制或拖动等操作,其实,只要光标在某一行,直接按 Ctrl + X / Ctrl + C,就等于剪切/复制整行了,非常方便。

  • Clipboard Ring
  • 一直以来,当需要在多个文件中复制粘贴多个内容时,我都很笨地挨个 Ctrl + C / Ctrl + V,从来也懒得想是不是该搞个 multi-clipboard 工具来提高效率 -,-|||

    其实,从 VS2003 开始,VS 就已经内置了多重剪贴板的功能,虽然只支持当前 VS 进程内多文件间的复制粘贴,这就是 Clipboard Ring。Clipboard Ring 采用 LIFO(后进先出)的方式组织,即最后被复制或粘贴的内容排在环的最前面,最大支持 10 块剪贴板。用法很简单,对需要复制的多个内容块按 Ctrl + C,然后到需要粘贴的地方按 Ctrl + Shift + V,选择需要粘贴的内容就 Ok 了。每个 VS 版本的 Clipboard Ring 操作略有不同,在此不赘述了。

  • 代码重构
  • 待续……


遍历 CTreeCtrl

CTreeCtrl 的 GetNextItem 成员函数很诡异,nCode 设为 TVGN_NEXT | TVGN_CHILD 会一直返回传进去的 hItem 值,而不是返回下一个兄弟 item “或”第一个 child item。

所以,只好自己写遍历函数,没有采用递归的做法,用了一个 STL List 容器来保存下一个兄弟 item 和第一个 child item,遍历返回的依据是 item 的 lParam 值等于给定的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
HTREEITEM CModelNodeTreePane::_GetTreeItemByID(const unsigned int uiItemID)
{
    HTREEITEM   hCurrItem = m_kModelNodeTreeCtrl.GetRootItem(),
                hItem;
    std::list kItemList;
 
    kItemList.push_back(hCurrItem);
 
    while (kItemList.size() > 0)
    {
        hCurrItem = kItemList.front();
        kItemList.pop_front();
 
        if ((unsigned int)m_kModelNodeTreeCtrl.GetItemData(hCurrItem) == uiItemID)
        {
            return hCurrItem;
        }
 
        if ((hItem = m_kModelNodeTreeCtrl.GetChildItem(hCurrItem)) != NULL)
        {
            kItemList.push_back(hItem);
        }
        if ((hItem = m_kModelNodeTreeCtrl.GetNextSiblingItem(hCurrItem)) != NULL)
        {
            kItemList.push_back(hItem);
        }
    }
 
    return NULL;
}

动态创建的 CTreeCtrl 实例的消息响应

由于程序里的 CTreeCtrl 控件实例是通过 CTreeCtr::Create() 来创建的,无法通过 VS 的 Properties 面板里的 Control Events 工具来生成消息映射函数,但控件又需要响应鼠标点击事件,这时最简单的办法就是重载 CTreeCtr 实例的 owner 的 OnNotify() 成员虚函数(这个 owner 也必然是 CWnd 的子类):

protected:
    virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BOOL CTreePane::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
    NMHDR* pNMHDR = (NMHDR*)lParam;
    ASSERT(pNMHDR != NULL);
 
    switch (pNMHDR->code)
    {
    case TVN_SELCHANGED:
        _OnTreeCtrlSelChanged(wParam, lParam, pResult);
        break;
 
    default:
        break;
    }
 
    return CWnd::OnNotify(wParam, lParam, pResult);
}

IE8 引发 VS 2005/2008 向导出错的解决方案

Internet Explorer 8 正式版会导致  Visual Studio 2005 / 2008 里的部分 VC++ 向导出错,包括:

  • Add Function
  • Add Variable
  • Smart Device – New Project Creation
  • Smart Device – Add Class

VC++ 团队给出了解决方案:

  1. 运行注册表编辑器 regedit(64 位系统请使用 32 位版本
  2. 定位到“HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet
    Settings\Zones
    ”下
  3. 新建一个名为 1000 的项
  4. 在 1000 这个项下,新建一个 DWORD 值,名为 1207,值为 0x000000

然后重启 VS 即可。

注意:VS 2005 必须装上 SP1

想深究这个问题原因,可以参看原文


3D Coordinates

最近碰到 Gamebryo3DS MAX 的坐标系转换问题,顺便花了几分钟研究了下各 3D 坐标系,真是比当下全球形势还混乱:

除了微软当年为了跟 OpenGL 划清界限,特立独行搞出左手坐标系的 DX 外,其他都是右手系(应该都是受 OGL 这个工业标准的影响),不过 Z 轴朝什么方向的都有,哭死了。


WoW Model Viewer 的编译问题

这段时间要搞角色换装系统的改进,参考魔兽世界的换装系统机制,便先拿了 WoW Model Viewer 的代码来参考。

先拷来一个 0.48b 版本的代码,按照这里说的做后编译是通过了,但运行总报错,跟了一下没找到问题,索性去下了 0.5.08 。该版本使用 wxWidgets 2.8.0、CxImage 5.99c、GLEW 1.3.3,其中 wxWidgets 2.8.0 需要自行下载、编译。

然后打开 wowmodelview.sln,打开工程属性,修改 C++ | Additional Include Directories、Linker | Additional Library Directories 中关于 wxWidgets 的路径,然后编译,这时可能会报一些 build error,基本都是类型没有显式强转的问题,改掉即可。

编译完成后,需要修改配置文件 Config.ini,把 [Locale] 下的“Path=”和“MPQFiles=”后的路径都改为 .mpq 文件所在文件夹根目录,就可以运行了。


看,给这位大老粗穿上粉红色紧身T恤是不是很性感呀?


应用工厂模式解决实际问题

我前段时间去面试时的一道题目,问题如下:

有一个脚本文件,每行有一句指令或空行,指令格式:

Command[,Param[,Value]]

其中Parameter 和 Value 非必须。设计一套解析指令的类,高效且易于扩展(尽可能降低代码内部耦合性)。

当时虽然都想到了,不过满脑混沌,没有完整明白地表达出来。本来想去公司再看看代码是怎么实现的,昨天在网上闲逛的时候忽然看到了这篇笔记,那就顺便也整理了下自己的思路,结合实际温故理论。

假设有“移动(Move)”、“攻击(Attack)”等几个指令;建立一个工厂类,并将所有指令类预先注册到工厂中,由工厂调用每个指令类的静态成员函数 CreateInstance() 来实现指令类实例的创建:

[ more... ]


判断一个点是否在 2D 三角形内

这是我拿到公司 offer 时美国老大给我的面试题,对于当时我这种文盲来说,还是杀死了不少脑细胞。最近闲来无事(嗯。。。被危机了),又拿出来琢磨了一下各算法。

设一在在 2D 空间中的三角形 △ABC ,三个顶点向量 A(ax, ay)、B(bx, by)、C(cx, cy),三条有向边 AB、BC、CA,有一点 P(px, py)。

  1. 叉乘法
  2. 原理:

    沿 △ABC 各有向边按一定方向走(顺时针或逆时针),判断点 P 是否在该边的某侧(右侧或左侧),若点 P 在三条边的同侧,则点 P 在 △ABC 内。

    实现:

    分别计算向量 AB、BC、CA 与向量 AP、BP、CP 的向量积(叉乘),若三个结果均同号(正或负,为零表示 P 在边上),则可得点 P 在 △ABC 内。其中AB = B - A,AB×AP = AB.x*AP.y - AB.y*AP.x。

    该算法只需要做 3 次叉乘(6 次普通数值乘法),效率高,且没有浮点误差。
    这是我当时面试想的算法,Azure 等人也用的类似算法。

  3. 面积法
  4. 原理:

    若点 P 在 △ABC 内,则 △ABP、△BCP、△CAP 的面积之和应等于 △ABC 的面积。

    实现:

    利用两个向量叉积的几何意义为该两个向量所围三角形面积的 2 倍,分别计算 AB×BP、BC×CP、CA×AP、AB×BC,若 |AB×BP| + |BC×CP| + |CA×AP| = |AB×BC|,则得点 P 在 △ABC 内。

    这个算法用得比较普遍,需要做 4 次叉乘(8 次普通数值乘法),效率和叉乘法差不多,同时避免了用海伦公式计算面积的低效和精度问题(数值除法和开方运算)。

我昨天想的一个算法有点类似这篇文章中的方法 3,比它简单一点,但同样需要对向量做归一化处理,效率不高,故放弃了。另外的算法还包括划线交点法、解方程组法、复数法等,但计算量都较大,不再赘述。


The IEEE 754 Floating Point Format

最近碰到用十六进制数表示浮点小数的问题,本科没学好,现在是补习时间。

FP32(单精度浮点数)

  • 第 31 bit 为符号位,0 表示正数,反之为负数,其读数值用 s 表示;
  • 第 30~23 bit 共 8 bits 为指数,其读数值用 e 表示;
  • 第 22~0 bit 共 23 bits 为分数,视为二进制纯小数,假定该小数的十进制值为 f;
 S EEEEEEEE FFFFFFFFFFFFFFFFFFFFFFF
31 30    23 22                    0

v = (-1)s × 2e-127 × 1.f

十进制浮点小数转 IEEE 754 格式的方法,以 −118.625 为例:

  • 由于是负数,得 s = 1;
  • 去符号后,将小数转为二进制数表示(整数部分除以 2 取整,小数部分乘以 2 取整)得 1110110.101;
  • 将该二进制小数正规化,即将小数点向左(为正)或向右(为负)移动,直至形成 1.f 格式的数,得1.110110101×26,补齐 23 bit 从而得到 f = 11011010100000000000000;移动的位数加 127 即为 e,得 e = 133 = 10000101;
  • 最后得到 IEEE 754 格式:1 10000101 11011010100000000000000 = 0xC2ED4000。

[ more... ]


Page 2 of 3123