说实话,立即模式用了一年多了,还是犯了这个错误。因为很多代码例子里面都是先指定法线,再指定位置,结果添加纹理坐标的时候就变成了指定纹理坐标在最后。
错误的写法:

1
2
3
glNormal3f(n.x(), n.y(), n.z());
glVertex3f(p.x(), p.y(), p.z());
glTexCoord1f(g_p_DisFromFile->norm_dis[g_denoted_point_id][he->vertex()->tag()]);

不要以为这个bug很简单,说实话这种绘制的bug,不知道真的无从调试起来。。。这样写出现什么样子的bug,看下图吧。。。


面片是不是很恶心的块状物???我这里绘制的是三维属性场,肯定是连续的,我无论怎么改插值模式,都没有用。。。
后面才回想起以前遇到过这个bug,所以改过来了。正确的代码应该是:

1
2
3
glTexCoord1f(g_p_DisFromFile->norm_dis[g_denoted_point_id][he->vertex()->tag()]);
glNormal3f(n.x(), n.y(), n.z());
glVertex3f(p.x(), p.y(), p.z());

正确的显示结果是:


如此简单的事情,能造成这样大的差距。

首先声明下,只扯面试时候容易手写出来的代码。不写类似于stl里面实现sort的优化部分,包括递归层次太深了改用堆排序,还有当序列长度小于一定值,改用插排,等等。
这里只讲快排主函数的三种写法和partion的两种写法,加上轴元素的选择。
为了兼容stl,假设传入的参数是指针(直接可以换成迭代器了)。
所以主函数应该是这样的接口:void QSort(intbeg, int end);
主函数写法1—-分治递归,也就是常用的写法。

1
2
3
4
5
6
7
8
9
10
11
12
void QSort(int* beg, int* end)
{
if (beg >= end)
{
return;
}

int* middle = Partion(beg, end);
QSort(beg, middle);
QSort(middle + 1, end);

}

这个是最直观,最简单的写法,至于快排的思路就不介绍了。下面是将其改成循环形式的递归,也有人说这是尾递归,但是当前栈不能完全消除额。所以,到底是不是尾递归了?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void QSort(int* beg, int* end)
{
if (beg >= end)
{
return;
}

while (beg < end)
{
int* middle = Partion(beg, end);
QSort(beg, middle);
beg = middle + 1;
}
}

说下我对这种写法的理解。其实,推广一下,分治造成的多次递归调用都可以改成这种循环形式。这样的话,双支就变成单支了,至少能减少爆栈的可能性。其余的好处,应该不大吧。问题是,如何将其改成这种循环形式了。额,其实可以这样想。分治的话,是划分为多个子问题,那么循环尾递归的话,是一次处理原问题的一部分,不断减少原问题,那么代码思路就顺理成章的出来了。上面的代码[beg,end)就代表的是问题空间,每次循环一直在减小这个空间。
下面说一个更少见的写法,就是将递归改成非递归。其实了,这种写法,会的人觉得很简单,不会的人就会觉得少见。递归改循环,大家都知道是加个栈,问题是如何用栈模拟递归了。
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
31
32
33
34
35
void QSort(int* beg, int* end)
{
if (beg >= end)
{
return;
}

struct Infor
{
int* beg;
int* end;
Infor(int* b = 0, int* e = 0) : beg(b), end(e) {}
};

stack<Infor> si;
si.push(Infor(beg, end));

while (si.size())
{
Infor in = si.top();
si.pop();

int* middle = Partion(in.beg, in.end);

if (middle > in.beg)
{
si.push(Infor(in.beg, middle));
}

if (middle + 1 < in.end)
{
si.push(Infor(middle + 1, in.end));
}
}
}

从上面的代码中,能够体会到,其实在栈里面存储下递归的参数就行了。栈反正是先入后出的,递归不也是这样么?那么,我们可以采用模拟先序,中序,或者后序遍历树的方法,任何递归都能够模拟出来吧,只是代码复杂程度的问题了。只是真正理解这个,需要一点点时间而已。上面的代码,其实就是用栈模拟了先序遍历二叉树吧。
那么剩下的就是partion函数了。
先来一个教科书版本的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int* Partion(int* beg, int* end)
{
--end;
int tmp = *end;
while (beg < end)
{
while (beg < end *beg <= tmp) ++beg;
*end = *beg;

while (beg < end *end >= tmp) --end;
*beg = *end;
}
*end = tmp;

return end;
}

这个代码应该非常常见,大部分写法就是这种。首先,看到轴元素是在end处。所以了,将end处的元素暂存,所以end就空出来了。因此,先从头开始遍历到第一个大于*end的元素,然后将其放入end。剩下的一个循环就是从后面往前面遍历了。最后大循环结束时候,beg必定等于end,而且必定放的是一个重复的元素,所以了,放入轴元素就行了。
下面介绍种更简便的写法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int* Partion(int* beg, int* end)
{
--end;
int* small = beg;
while (beg < end)
{
if (*beg < *end)
{
if (beg != small)
{
swap(*beg, *small);
}

++small;
}

++beg;
}
swap(*end, *small);

return small;
}

这个代码只要一个循环,思路是[0,small)中放小于轴元素的数字,保持这个集合就行了。那么,最终轴元素就应该放在small处。理解下吧,这个代码更方面手写出来。
现在还剩个更严重的问题,如何消除快排的最坏情况出现的可能。最坏情况出现在输入数据本身有序的时候。
如果听说过随机算法,那么这个问题就知道怎么轻松解决了。在算法中,加入随机性操作吧。在partion,可以随机选择轴元素,也可以采用三点取中法选择轴元素。至于证明,参加算法导论。
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
int* Partion(int* beg, int* end)
{
int len = end - beg;

swap(beg[rand() % len], *(end - 1));//随机选取轴元素

--end;
int* small = beg;
while (beg < end)
{
if (*beg < *end)
{
if (beg != small)
{
swap(*beg, *small);
}

++small;
}

++beg;
}
swap(*end, *small);

return small;
}

三点取中法。
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
31
32
33
34
35
36
37
int* Partion(int* beg, int* end)
{
int len = end - beg;
int* middle = beg + len / 2;
--end;
if (*middle < *beg)
{
swap(*middle, *beg);
}
if (*end < *beg)//最小
{
swap(*beg, *end);
}
else if (*end > *middle)//最大
{
swap(*middle, *end);
}

int* small = beg;
while (beg < end)
{
if (*beg < *end)
{
if (beg != small)
{
swap(*beg, *small);
}

++small;
}

++beg;
}
swap(*end, *small);

return small;
}

组合这些不同的写法,确实可以出现很多个版本的快排算法了。。。

最近因为准备找工作的东西,看了不少书,感悟也挺多的。stl源码剖析,effective c++,面试宝典,剑指offer,大话设计模式,编程精粹,还有很多想看的还没看完了。。。这段时间,看完这些书后,真的发现,看书是一件很棒的事情,不仅仅是知识的学习,查漏补缺,思维的体验也是一件让人觉得满心愉悦的事情。有点明白古人雪夜读书的感觉,那时候没有电脑可以玩,没有那么多娱乐的游戏,读书确实是一件让人愉悦的事情。
很久没有更新博客了,随便扯扯吧。
所谓的top k算法,意思就是求n个数字的最大k个或者最小k个,大概也是个比较常见的面试题了。一般有两种思路,第一种是利用快排里面的partion函数,不断用轴元素分割数组,直到分割出前k个或者后k个,迭代或者递归都是很方便的;stl里面有个nth_element泛函做的就是这件事情。第二种思路,则是假定有一个容量为k的容器,存储了当前的top k元素,当处理下一个元素的时候,判断当前处理元素是否可能是top k元素,比如如果求最大的k个元素,则判断当前处理元素是否比容器里面最小元素大,如果大的话,则需要加入当前元素,并且删除容器里面最小的元素,反之亦然。现在的问题是,如何选取一个很好的容器了。能够使得这些操作尽可能快。其实大家知道的容器也就那几种类型,基本上是stl里面已经实现了的。线性?搜索树?hash?
hash的话,没法快速查找极值,线性结构的话,插入新元素,保持有序结构的话,也需要O(n)。那么,搜索树结构了,查找和删除都是log(n),stl里面有现成的红黑树实现set容器。另外,还有个更奇葩的用数组表示的完全二叉树结构,最大堆和最小堆。可以满足O(1)查找极值元素和log(n)插入删除操作,最终还可以排序元素。综上所说,堆结构是在时间和空间上最佳的满足要求的容器了。
刚好stl里面有个partial_sort函数也是用这个思路实现的,而且最后还排了一个序,所以,实际开发的时候多了解下标准库是值得投入的一件事情。
下面是用stl的heap操作实现的top k算法。

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
31
32
33
34
35
36
37
38
39
40
#include <cstdlib>
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> vi;
const int NUM = 100;

for (int i = 0; i < NUM; ++i)
{
vi.push_back(rand() % NUM);
printf("%d ", vi[i]);
}
printf("\n");

//求最小k个数字
const int K = 10;

make_heap(vi.begin(), vi.begin() + K);
for (int i = K; i < NUM; ++i)
{
if (vi[i] < vi[0])
{
pop_heap(vi.begin(), vi.begin() + K);
vi[K - 1] = vi[i];
push_heap(vi.begin(), vi.begin() + K);
}
}

sort_heap(vi.begin(), vi.begin() + K);
for (int i = 0; i < K; ++i)
{
printf("%d ", vi[i]);
}
printf("\n");

return 0;
}

这里的多态指的是动态多态,不包括函数重装和模版的静态多态内容。多态所代表的特性,才是面向对象的真正表现。具体行为其实就是用基类的指针或者引用调用虚函数,得到的结果跟指针指向对象的具体类型有关,而跟指针的静态类型无关。比如说,基类指针指向派生类A的对象,那么调用的其实是派生类A里面实现的虚函数,如果是派生类B的对象,那么则是B类里面实现的虚函数。
我们先定义一个接口。也就是一个包含纯虚函数的类,这种类不能用于构造对象。其实,接口才是我们最关心的。

1
2
3
4
5
6
7
#define interface struct
interface IAnimal
{
virtual void Eat() = 0;
virtual void Drink() = 0;
virtual void Sleep() = 0;
};

然后定义2个类继承这个接口。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class  CMonkey : public IAnimal
{
public:
CMonkey()
{
m_nSex = 0;
}

virtual void Eat()
{
cout << "Monkey Eat" << endl;
}

virtual void Drink()
{
cout << "Monkey Drink" << endl;
}

virtual void Sleep()
{
cout << "Monkey Sleep" << endl;
}

enum SEX
{
MALE = 0,
FEMALE = 1
};

int GetSex() const { return m_nSex; }
void SetSex(int nSex) { m_nSex = nSex; }

private:
int m_nSex;//0:male,1:female
};

class CMonster : public IAnimal
{
public:
CMonster()
{
m_nEvil = NO;
}

virtual void Eat()
{
cout << "Monster Eat" << endl;
}

virtual void Drink()
{
cout << "Monster Drink" << endl;
}

virtual void Sleep()
{
cout << "Monster Sleep" << endl;
}

enum EVIL
{
NO = 0,
YES = 1
};

int GetEvil() const { return m_nEvil; }
void SetEvil(int nEvil) { m_nEvil = nEvil; }

private:
int m_nEvil;//0:no,1:yes
};

再定义CPeople多重继承上面2个类。

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
class CPeople : public CMonkey, public CMonster
{
public:
virtual void Eat()
{
cout << "People Eat" << endl;
}

virtual void Drink()
{
cout << "People Drink" << endl;
}

virtual void Sleep()
{
cout << "People Sleep" << endl;
}

virtual void Name()
{
cout << "People Name" << endl;
}

const char* GetName() const { return m_szName; }
void SetName(char* pszName) { strcpy(m_szName, pszName); }

private:
char m_szName[12];
};

我们先来测试多态是如何表现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CMonkey monkey;
monkey.SetSex(CMonkey::MALE);

CMonster monster;
monster.SetEvil(CMonster::YES);

CPeople people;
people.SetName("Jack");

//下面代码用于展示多态
IAnimal* pAnimal[3];
pAnimal[0] = monkey;
pAnimal[1] = monster;
pAnimal[2] = (CMonkey*)people;
for (int i = 0; i < 3; ++i)
{
pAnimal[i]->Eat();
pAnimal[i]->Drink();
pAnimal[i]->Sleep();
}
cout << endl;

这段代码的输出如下:

从代码的输出可以看到,pAnimal[i]是根据所指向的数据类型确定该调用什么函数的。这就是多态的意思。相同的接口,不同的表现。
下面我们来推测这几个对象的内存布局。首先是monkey。
假设,对象monkey的内存布局,如图:
也就是,monkey保护2个成员,一个虚函数表指针和一个int成员。虚函数表指针指向一个表,前面3项是函数地址,第四项是NULL,第五项根据有些书籍的说法是type_info对象的地址。
我用下面的代码来验证这个内存布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef void (*FunType) ();
FunType p;
int nSex;
int* vtbl = NULL;
cout << "sizeof(monkey): " << sizeof(monkey) << endl;
vtbl = (int*)(*(int*)monkey);//虚函数指针
cout << "vtbl address:" << (int)vtbl << endl;
p = (FunType)vtbl[0];
p();
p = (FunType)vtbl[1];
p();
p = (FunType)vtbl[2];
p();
cout << "vtbl[3]:" << vtbl[3] << endl;
cout << "vtbl[4]:" << vtbl[4] << endl;
nSex = *((int*)monkey + 1);
cout << "nSex: " << nSex << endl << endl;

输出如同:
从输出可以看到,monkey大小为8字节,刚好是一个指针和一个int的大小。而且,monkey的地址转换为int*之后,再取值就可以得到虚函数表的地址。用vtbl[0]到vtbl[2]调用刚好是调用接口里面定义的三个虚函数。vtbl[3]为0,vtbl[4]为一个地址。为什么,这里推断vtbl里面含有5项了,而不是4项了。下面再说明。
monster的内存布局类似,就不说明了。
最关键的是people的内存布局。推测如下图所示:
用下面的代码,来验证这个假设。

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
31
32
cout << "sizeof(people): " << sizeof(people) << endl;
vtbl = (int*)(*(int*)people);//虚函数指针
cout << "vtbl address:" << (int)vtbl << endl;
p = (FunType)vtbl[0];
p();
p = (FunType)vtbl[1];
p();
p = (FunType)vtbl[2];
p();
p = (FunType)vtbl[3];
p();
cout << "vtbl[4]:" << vtbl[4] << endl;
cout << "vtbl[5]:" << vtbl[5] << endl;

nSex = *((int*)people + 1);
cout << "nSex: " << nSex << endl << endl;

vtbl = (int*)(*((int*)people + 2));//虚函数指针
cout << "vtbl address:" << (int)vtbl << endl;
p = (FunType)vtbl[0];
p();
p = (FunType)vtbl[1];
p();
p = (FunType)vtbl[2];
p();
cout << "vtbl[3]:" << vtbl[3] << endl;
cout << "vtbl[4]:" << vtbl[4] << endl;
nEvil = *((int*)people + 3);
cout << "nEvil: " << nEvil << endl << endl;

char* pszName = (char*)((int*)people + 4);
cout << pszName << endl;

输出如同:
people大小为28字节,符合假设。第一个成员为继承自CMonkey的虚函数表指针,符合假设。虚函数表的前三项,是在CPeople类中实现的接口里面虚函数地址,接下来CPeolple里面定义的虚函数Name的地址,最后是NULL和type_info obj的地址。people的第三个成员为继承自CMonster的虚函数指针,指向虚函数表的第七项。从第七项开始,可以看作继承自CMonster的虚函数表。
从输出可以看出,CPeople里面新定义的虚函数,放到第一个虚函数指针所指向的虚表里面了。并且,由于一个类只有只有一张虚表,所以不同的虚函数指针所指向的其实是虚表的不同部分。这些表可以看作是独立的,也可以看作是地址上面连续的。
前面说过,为什么猜测虚函数表里面还有一个NULL项了,然后才跟随一个type_info obj的地址?由于,在上面的输出中,2个虚函数指针相差是24。所以,我猜测第一个虚表里面含有6项。经过代码测试,第五项也是0。
至于为什么猜测内存布局里面先是虚函数表的指针,再是数据成员,这个是我通过代码测试得来的。环境是vs2013。

我们经常会看到头文件里面会有下列的结构。

1
2
3
4
5
6
7
#ifdef __cplusplus
extern "C" {
#endif
/*...*/
#ifdef __cplusplus
}
#endif

而且我们也大都知道这是为了和C语言兼容。但是更具体的事情了?
实际上,这段代码是为了保证用C语言调用C++函数或者用C++调用C语言实现的函数,都能够顺利进行。不仅仅是单向的左右。如果没有加#ifdef cplusplus判断就是单向的吧。
大家都知道C++支持函数重载,而C语言是不支持的。所以,类似函数int add(int,int)很可能在链接的时候,符号是_add_int_int。而在C语言里面,还是_add。既然符号不一样,那就会找不到吧。这就发生在用C语言去调用C++实现的函数的时候。如果在C++头文件里面加上extern “C”声明,那么就会按照C语言的方式生成函数,自然能和C语言兼容了。
如下列代码,就能顺利运行。
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
31
32
33
34
//add.h
#ifndef ADD_H
#define ADD_H

//int add(int x, int y);

#ifdef __cplusplus
extern "C" {
#endif
int add(int x, int y);
#ifdef __cplusplus
}
#endif

#endif

//add.cpp
#include "add.h"

int add(int x, int y)
{
return x + y;
}

//main.c
#include <stdio.h>
#include "add.h"

int main()
{
add(1, 2);

return 0;
}

如果把头文件定义改成,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//add.h
#ifndef ADD_H
#define ADD_H

int add(int x, int y);

/*#ifdef __cplusplus
extern "C" {
#endif
int add(int x, int y);
#ifdef __cplusplus
}
#endif*/

#endif

main.c就无法链接到函数_add上面。提示:main.obj : error LNK2019: 无法解析的外部符号 _add,该符号在函数 _main 中被引用。
现在再反过来,用c语言实现add,c++实现main。代码如下,
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
31
32
33
34
//add.h
#ifndef ADD_H
#define ADD_H

int add(int x, int y);

/*#ifdef __cplusplus
extern "C" {
#endif
int add(int x, int y);
#ifdef __cplusplus
}
#endif*/

#endif

//add.c
#include "add.h"

int add(int x, int y)
{
return x + y;
}

//main.cpp
#include <stdio.h>
#include "add.h"

int main()
{
add(1, 2);

return 0;
}

结果还是无法链接,提示: error LNK2019: 无法解析的外部符号 “int
cdecl add(int,int)” (?add@@YAHHH@Z),该符号在函数 _main 中被引用。这是因为main.cpp用C++的方式去查找函数符号,_add_int_int,而add.c里面生成的符号是_add。因此无法链接上去。那么,能不能直接加上extern “C”就行了?
由于C语言里面不支持extern “C”,那么作为C语言的头文件就不能这么加,应该加上#ifdef __cplusplus的判断,再将所有的函数定义前加上extern “C”,就能保证该头文件同时兼容C语言和C++了。

以前在群里面发现有人说,引用可以被赋值,然后其推断出引用其实是可以重新指向其它对象的,而不是只能在初始化时确定所指对象,以后不能改变。如果是这样的话,引用和指针还有什么区别了。存在引用的好处就是,引用一经过定义就确定所指对象,以后不用测试其是否指向无效对象,也不用担心所指对象会改变。
下面来看看这段造成误解的代码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

int main()
{
int nA = 10;
int nRef = nA;//必须初始化

cout << "nA:" << nA << ' ' << "nRef:" << nRef << endl;

int nB = 11;
nRef = nB;//难道引用可以重新指向nB?
cout << "nB:" << nB << endl;
cout << "nA:" << nA << ' ' << "nRef:" << nRef << endl;

nRef = 12;
cout << "nB:" << nB << endl;
cout << "nA:" << nA << ' ' << "nRef:" << nRef << endl;

return 0;
}

运行结果:
从结果可以看到,用nB对nRef进行赋值后,nA的值也变成11了,由此可见nRef仍然指向nA。而再对nRef赋值12,只有nA变成12,而nB仍然是11。这些都可以很清楚的说明,引用所指的对象是没有变化的,一经初始化就不会改变。
下面再来看看,注释掉输出语句后的反汇编代码。

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
31
32
33
34
35
36
37
38
int main()
{
00EC48F0 push ebp
00EC48F1 mov ebp,esp
00EC48F3 sub esp,0E4h
00EC48F9 push ebx
00EC48FA push esi
00EC48FB push edi
00EC48FC lea edi,[ebp-0E4h]
00EC4902 mov ecx,39h
00EC4907 mov eax,0CCCCCCCCh
00EC490C rep stos dword ptr es:[edi]
int nA = 10;
00EC490E mov dword ptr [nA],0Ah
int nRef = nA;//必须初始化
00EC4915 lea eax,[nA]
00EC4918 mov dword ptr [nRef],eax

//cout << "nA:" << nA << ' ' << "nRef:" << nRef << endl;

int nB = 11;
00EC491B mov dword ptr [nB],0Bh
nRef = nB;//难道引用可以重新指向nB?
00EC4922 mov eax,dword ptr [nRef]
00EC4925 mov ecx,dword ptr [nB]
00EC4928 mov dword ptr [eax],ecx
//cout << "nB:" << nB << endl;
//cout << "nA:" << nA << ' ' << "nRef:" << nRef << endl;

nRef = 12;
00EC492A mov eax,dword ptr [nRef]
00EC492D mov dword ptr [eax],0Ch
//cout << "nB:" << nB << endl;
//cout << "nA:" << nA << ' ' << "nRef:" << nRef << endl;

return 0;
00EC4933 xor eax,eax
}

从以下几句能看到nRef实际上存储的是nA的地址。lea是取有效地址的意思。

1
2
3
int nRef = nA;//必须初始化
00EC4915 lea eax,[nA]
00EC4918 mov dword ptr [nRef],eax

从给引用赋值的汇编代码可以看出,实际上的动作是把nRef代表的地址放入一个寄存器,再取nB的值放入一个寄存器,然后将nB的值赋给nRef指向的内存。

1
2
3
4
nRef = nB;//难道引用可以重新指向nB?
00EC4922 mov eax,dword ptr [nRef]
00EC4925 mov ecx,dword ptr [nB]
00EC4928 mov dword ptr [eax],ecx

通常,我们知道,一个程序执行起来就是一个进程,一个进程里面至少包含一个线程。那么什么是模块了,被加载内存中的dll或者exe都是模块。
据说,windows有个数据结构叫做Module Database(MDB),专门代表模块。可执行程序(exe或者dll),包括其代码、资源等被加载到内存中,windows大概就用这个结果管理它吧。这个数据结构其实代表的就是一个PE文件的表头。
那么进程了,代表的又是什么了,既然模块表示的是加载到内存的pe文件。进程应该代表的是资源拥有者吧。比如说,地址空间、申请的内存、打开的文件、所有的线程、以及模块(加载的dll,本身的exe)。windows也有一个叫做Process Database(PDB)的数据结构负责管理它。这样看,进程是各种所有权的集合吧。
那么线程了。执行的线程表示的是模块中一段正在被执行的代码吧。线程是被cpu调度的基本单位,因此真正占有cpu时间片的是线程,而不是进程。也有一个叫做Thread Database(TDB)的数据结构来代表线程,里面会记录执行线程区域储存空间(Thread Local Storage,TLS)、讯息队列、handle表格等等。其实,只有在多cpu的机器上面才能实现真正的并行。在单cpu上面,只是在硬件计时器的通知下不断的切换线程。

上一篇文章里面讲到用allocator预分配空间已经在需要的时候才构造对象,其实还有其它的办法实现这一功能。operator new表达式也可以只分配内存而不初始化,但是返回的是void指针,因此没有allocator类型化的优点。同样placement new表达式只负责调用指定的构造函数初始化内存,不申请内存,与allocator的construct相比,这个表达式更加方便,可以使用任意类型的构造函数参数列表。但是,construct只能使用复制构造函数。
同样的,可以使用operator delete代替allocator的deallocate释放内存。使用析构函数代替allocator的destroy清理对象。下面我把原来使用allocator的代码改写成使用这些表达式的。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <memory>
#include <algorithm>

template <typename T>
class Vector
{
public:
Vector() : elements(0), first_free(0), end(0) {}
~Vector()
{
for (T* p = first_free; p != elements;)
{
//allocator.destroy(--p);
--p;
p->~T();
}

if (elements)
{
//allocator.deallocate(elements, end - elements);
operator delete[](elements);
}
}

void push_back(const T t)
{
if (first_free == end)
{
reallocate();
}

//allocator.construct(first_free, t);//复制构造新对象
new (first_free) T(t);
++first_free;
}

void pop_back()
{
if (first_free != elements)
{
//allocator.destroy(--first_free);
--first_free;
first_free->~T();
}
}

private:
//std::allocator<T> allocator;
void reallocate()
{
std::ptrdiff_t size = first_free - elements;
std::ptrdiff_t newcapacity = 2 * std::max(size, 1);

//T* newelements = allocator.allocate(newcapacity);//申请新内存
T* newelements = static_cast<T*>(operator new[](newcapacity * sizeof(T)));

//newelements处直接复制构造
std::uninitialized_copy(elements, first_free, newelements);

for (T* p = first_free; p != elements;)
{
//allocator.destroy(--p);//析构原对象
--p;
p->~T();
}

if (elements)
{
//allocator.deallocate(elements, end - elements);//释放内存
operator delete[](elements);
}

elements = newelements;
first_free = elements + size;
end = elements + newcapacity;
}

T* elements;
T* first_free;
T* end;
};

//template <typename T> std::allocator<T> Vector<T>::allocator;

placement new表达式的一般形式是new (地址) 类型名(初始化列表),由于使用初始化列表直接构造对象,因此不用拷贝。而且比只能用复制构造函数构造的allocator的construct函数更方便。
比如,allocator alloc;string* sp = alloc.allocate(2);如果用定位new表达式,new (sp) string(b, e);(b,e是用2个迭代器构造string类型)。但是,用construct的话,则是alloc.construct(sp+1,string(b,e));可以清楚的看到,需要用迭代器构造个临时的string对象,再用这个临时对象调用复制构造函数。因此,会有细微的性能损失。最终要的是,有些类根本没有可以调用的复制构造函数,比如该函数是私有的,那么就必须使用placement new表达式了。
operator new和operator delete的使用方式类似于new和delete,具体的可以参看代码。

相信大家都用过vector,而且用多了之后会发现怎么还有一个我们基本上不会使用的模版参数Allocator。查阅msdn能够看到,vector的类型是template >class vector。那么这个Allocator到底是用来做什么的了。实际上,Allocator是stl里面用到的内存管理工具,因为这个时候new和delete已经不能满足要求了。
比如说,vector需要预分配内存,需要添加元素。如果使用new分配内存的话,那么在预分配内存的时候就会调用一次构造函数,添加元素的时候会调用一次赋值操作符。其实,这些操作都是冗余的,会降低效率。这也是stl和手写容器的最大区别。如果使用allocator,那么可以把分配内存和构造操作分开,在预分配内存的时候单纯分配空间,不构造,而在添加新元素的时候,调用复制构造函数。
要达到这样的要求,需要allocator提供对应的接口。实际上,std::allocator有allocate和deallocate负责内存申请释放,construct和destroy负责对象构造和析构。下面来解析相关模拟的代码。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <memory>
#include <algorithm>

template <typename T>
class Vector
{
public:
Vector() : elements(0), first_free(0), end(0) {}
~Vector()
{
for (T* p = first_free; p != elements;)
{
allocator.destroy(--p);
}

if (elements)
{
allocator.deallocate(elements, end - elements);
}
}

void push_back(const T t)
{
if (first_free == end)
{
reallocate();
}

allocator.construct(first_free, t);//复制构造新对象
++first_free;
}

void pop_back()
{
if (first_free != elements)
{
allocator.destroy(--first_free);
}
}

private:
static std::allocator<T> allocator;
void reallocate()
{
std::ptrdiff_t size = first_free - elements;
std::ptrdiff_t newcapacity = 2 * std::max(size, 1);

T* newelements = allocator.allocate(newcapacity);//申请新内存
//newelements处直接复制构造
std::uninitialized_copy(elements, first_free, newelements);

for (T* p = first_free; p != elements;)
{
allocator.destroy(--p);//析构原对象
}

if (elements)
{
allocator.deallocate(elements, end - elements);//释放内存
}

elements = newelements;
first_free = elements + size;
end = elements + newcapacity;
}

T* elements;
T* first_free;
T* end;
};

template <typename T> std::allocator<T> Vector<T>::allocator;

在push_back里面,使用allocator.construct(first_free, t)初始化内容,相当于调用复制构造函数。如果空间已满,调用reallocate。在这里面用allocator.allocate(newcapacity)申请原来2倍大小的空间,这个操作不会调用构造函数。有意思的是接下来调用uninitialized_copy,这个函数直接在新地址上面使用原来的值构造对象,而不是赋值。然后,循环析构原来对象,释放原来内存。如果清楚了解复制构造函数和赋值操作的区别,那么理解allocator的意义是很容易的。关于这个,可以阅读我另外一篇文章
另外一个关于模版类静态成员的初始化,大家可以看到我直接写在头文件的最后一句了。如果写在源文件里面,那么特例化模版才能定义静态成员。而这样直接包含头文件就可以使用了,不信大家可以试试。我的环境是vs2013。

我们都知道平面可以表示为Ax+By+Cz+d=0,但是很多人不知道这个表达式隐含的意义。
从另一个角度,点法式来看平面,假设法线n=(a,b,c),平面上一点p0(x0,y0,z0),平面上任意一点p为(x,y,z)。那么平面可以表示为n
(p-p0)=0,即(a,b,c)(x-x0,y-y0,z-z0)=0,可以化简为ax+by+cz-ax0-by0-cz0=0。
上面两个式子一对比,就能发现a=kA,b=kB,c=kC,-a
x0-by0-cz0=kD。取k=1,就可以得到系数相等。所以,看到平面的一般式就能够得到法线为n=(A,B,C)。
根据上面的结论,进一步推广,我们可以把一般式改成点积的形式(A,B,C)(x,y,z)+D=0。这个就是平面的向量表示,即**nP+d=0。那么这个向量表示法有什么意义了。首先,在程序实现中,我们只需要保存法线n和系数d就可以了。
其次,d还代表从原点到平面上任意点的向量在法线上的投影长度。所以,利用这个信息,可以方便的求出任意点到平面的距离。
假设n为单位法线,假设任意点为q,q在平面上的投影为q’,那么向量
(q-q’)=kn。意思是向量(q-q’)肯定和法线共线,长度是其k倍。在上式两边乘以n,可以得到(q-q’)n=knn,即(qn-q’n)=k,即k=q*n-q’n。由于q’是平面上的点,所以q’n=-d,那么,k=qn+d。k即是点q到平面的长度,其中q,n,d都是已知的。运用这个式子的时候,必须得先归一化n**。
现在可以很清晰的看到,如果我们将平面表示为向量形式,而且n是归一化的,那么计算任意点到平面的距离是非常方便的。