Vector3类用来存储3D向量的x,y,z分量 这个类应该具有的功能有: 1.存取向量的各分量(x,y,z) 2.向量间的赋值操作 3.比较两向量是否相同 4.将向量置为零向量 5.向量求负 6.求向量的模 7.向量与标量的乘除法 8.向量标准化 9.向量加减法 10.计算两点(点用向量表示)间距离 11.向量点乘 12.向量叉乘

Vector3.h:

#ifndef __VECTOR3_H_INCLUDED__
#define __VECTOR3_H_INCLUDED__

#include <math.h>

class Vector3
{
public:
float x, y, z;

// 默认构造函数,不执行任何操作
Vector3() { }

// 复制构造函数
Vector3(const Vector3 &a) : x(a.x), y(a.y), z(a.z) { }

// 带参数的构造函数,用三个值完成初始化
Vector3(float nx, float ny, float nz) : x(nx), y(ny), z(nz) { }


// 标准对象操作
// 重载赋值运算符,并返回引用,以实现左值
Vector3 &operator =(const Vector3 &a) {
    x = a.x; y = a.y; z = a.z;
    return \*this;
}

//重载“==”操作符
bool operator ==(const Vector3 &a) const {
return x==a.x && y==a.y && z==a.z;
}

bool operator !=(const Vector3 &a) const {
    return x!=a.x || y!=a.y || z!=a.z;
}

// 向量运算
// 置为零向量
void zero() { x = y = z = 0.0f; }

// 重载一元“-”运算符
Vector3 operator -() const { return Vector3(-x,-y,-z); }

// 重载二元“+”和“-”运算符
Vector3 operator +(const Vector3 &a) const {
    return Vector3(x + a.x, y + a.y, z + a.z);
}

Vector3 operator -(const Vector3 &a) const {
    return Vector3(x - a.x, y - a.y, z - a.z);
}    

// 与标量的乘法,除法
Vector3 operator \*(float a) const {
    return Vector3(x\* a, y\* a, z\* a);
}
Vector3 operator /(float a) const {
    float oneOverA = 1.0f / a; // 注意,这里不对除零进行检查
    return Vector3(x\* oneOverA, y\* oneOverA, z\* oneOverA);
}

// 重载自反运算符
Vector3 &operator +=(const Vector3 &a)

{
x += a.x; y += a.y; z += a.z;
return *this;
}
Vector3 &operator -=(const Vector3 &a)
{
x -= a.x; y -= a.y; z -= a.z;
return *this;
}
Vector3 &operator *=(float a)
{
x *= a; y *= a; z *= a;
return *this;
}
Vector3 &operator /=(float a)
{
float oneOverA = 1.0f / a;
x *= oneOverA; y *= oneOverA; z *= oneOverA;
return *this;
}

// 向量标量化
void normalize()
{
float magSq = x * x + y * y + z * z;
if (magSq > 0.0f)
{ // 检查除零
float oneOverMag = 1.0f / sqrt(magSq);
x *= oneOverMag;
y *= oneOverMag;
z *= oneOverMag;
}
}

//向量点乘,重载标准的乘法运算符
float operator *(const Vector3 &a) const {
return x* a.x + y* a.y + z* a.z;
}
};

/////////////////////////////////////////////////////////////////////////////
//
// 非成员函数
//
/////////////////////////////////////////////////////////////////////////////
// 求向量模
inline float vectorMag(const Vector3 &a)
{
return sqrt(a.x * a.x + a.y * a.y + a.z * a.z);
}

//计算两向量的叉乘(切记不要重载叉乘)
inline Vector3 crossProduct(const Vector3 &a, const Vector3 &b)
{
return Vector3(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
);
}

//实现标量左乘
inline Vector3 operator *(float k, const Vector3 &v)
{
return Vector3(k * v.x, k * v.y, k * v.z);
}

//计算两点之间的距离
inline float distance(const Vector3 &a, const Vector3 &b)
{
float dx = a.x - b.x;
float dy = a.y - b.y;
float dz = a.z - b.z;
return sqrt(dx * dx + dy * dy + dz * dz);
}

/////////////////////////////////////////////////////////////////////////////
//
// 全局变量
//
/////////////////////////////////////////////////////////////////////////////
// 提供一个全局零向量

extern const Vector3 kZeroVector;

#endif

设计细节:

1.float与double,类采用了float,是因为float的精度够用。相比于使用double,float可以节省可观的内存资源并且还能获得更好的性能。 2.避免处理标量的函数。例如,您可能认为一个接收标量k并使三个分量都等于k的构造函数会非常有用。但在创建这个很少会用到的函数时,引入了各种突然将标量转换到向量的可能性。 3.有些代码会通过重载数组运算符([])或转化成float*来访问分量,但是对于一个应用在几何问题上的向量类,我们一般用名字(x,y,z)访问分量,而不是下标。 4.不要重载叉乘。C语言中,只有“*”表示乘,C语言中也不存在叉号运算符,所以重载运算符并不比直接调用叉乘函数(如:crossProduct())更“优雅”。 5.应该尽可能的使用const成员函数。const成员函数是一种方法,让函数对调用者承诺“不会修改对象”,而编译器确保这个承诺。这是一个保证代码没有副作用的好方法,不会在任何您不知道的情况下改变对象。 6.使用const引用参数。除了使用const成员函数外,所有以向量为参数的函数都接受向量的const引用(const&)。以传值方式传参会调用一次构造函数,传const引用在形式上是传值,而实际上是传址(传引用),避免了调用构造函数,这有助于提高效率。另外,如果函数不是内联的,传值方式比传址方式需要更多的堆栈空间和更长的参数压栈时间。 当把vector的变量作为实参,传递给接收const引用的函数时,实参的地址被传递。当把vector的表达式作为实参,传递给接受const引用的函数时,编译器产生临时代码来计算表达式,并把结果保存到临时变量中,接着将临时变量的地址传递给函数。这样我们就分别得到了传址与传值的优点。形式上是传值,使我们能够传递相量表达式,让编译器去创建临时变量。实际上传递的还是地址,提高了速度。 7.某些操作被设计成了非成员函数,而不是成员函数。成员函数在类定义中声明,作为类的成员被调用,它包含一个隐式的this指针作为形参。(例如,zero()就是一个成员函数)。非成员函数是不包含隐式this指针的普通函数,(例如,vectorMag()函数)。那些只接收一个vector实参的操作,既可以被设计为成员函数,也可以被设计为非成员函数,此时我们一般使用非成员函数,因为在vector表达式中使用这种操作时,非成员函数更易懂。 非成员函数:求模,向量叉乘,求两点见距离 成员函数(不包括运算符重载):zero(),normalize,因为我们不会在vector表达式中调用这些函数。 8.不要使用虚函数,原因如下: 第一,“自定义”向量操作没有太大意义。点乘就是点乘,它永远是点乘。 第二,Vector3是一个严格要求速度的类,如果使用了虚函数,优化器通常不能产生成员函数的内联代码。 第三,虚函数需要指向虚函数表的指针,响亮定义时该指针必须被初始化,并使对象大小增加25%。存储包含向量的大数组是一种普遍需求,在这种情况下,虚函数表指针占用的空间大部分被浪费掉了。 9.全局常量:零。它用来向函数传递零向量,使我们不必每次都调用构造函数Vector3(0,0,0)来创建零向量。 10.不存在Point3类。如果决定对点和向量使用不同的类,那么将面临两种选择: 一,提供两个版本的函数,一个接受Vector参数,另一个接收Point3参数; 二,函数仅接收Vector3参数,当Point3类型传到函数时将它转换成Vector3类型(或者是相反的方法) 这两种选择都是不好的。来看下第二个选择,Vector3和Point3之间的转换可以是显示的也可以是隐式的。设想显示转换,这将使得向量和点之间的转换满天飞,而如果是隐式转换(可以定义一个构造函数或转换符),将可以自由地转换点和向量,但这样将两个类分开的好处就大部分丧失了,不值得这么做。 结论是使用同一个类保存“点”和“向量”。 11.关于优化。如果真要对向量类进行优化,也仅能加速那些本应该用汇编写的代码,但还达不到真正汇编代码的速度。所以,不值得为如此小的回报增加向量类的复杂性。 有两种特别的代码需要仔细讨论。一个是定点数,另一个是返回临时变量的引用。 在旧时代,浮点数运算比整数慢得多。特别明显的是,浮点乘法非常慢。因此程序员使用定点数,企图绕过这个问题。定点数的基本思想就是用定点数保存小数位,例如,有8位小数位,意味着这个数应当乘以256再保存。所以3.25应该存为3.25×256=832。在过去,处理一些特殊场合,定点数是一种优化技术。当今的处理器不仅能以处理整数相同的周期处理浮点数,还会试图以向量处理器来执行浮点向量运算。 我们的向量类的许多向量运算都被写成返回实际的Vector3型变量。不同的编译器能以不同的方法实现这种返回操作。但返回类对象将导致至少一次构造函数调用(根据C++标准)。关于此优化的结论:保持类的简单性,仅在很少的情况下,构造函数或类似的调用会造成明显的问题。可以手动调整C++代码或者使用汇编。当然,也可以让函数接收一个指针参数,返回值存放在那里,而不是真正返回一个对象。不管怎么样,不要为2%的优化付出100%的代码复杂性。 参考书籍:《游戏引擎架构》 《3D Math Primer for graphics and game development》