最常用方法是用std::acos和点积公式计算夹角,需检查零向量、裁剪点积至[-1,1]、转弧度为角度;高精度场景推荐atan2(|a×b|, a·b);使用Eigen时应直接调用angle()成员函数。

用 std::acos 和点积公式直接算夹角(最常用)
三维向量夹角本质是数学问题:已知向量 a 和 b,夹角 θ 满足 cosθ = (a·b) / (|a| |b|)。C++ 中只需用 std::acos 对归一化点积取反余弦,再转为角度(或保留弧度)。
关键点:
- 必须先检查
|a|和|b|是否为 0,否则除零未定义; - 点积结果可能略超 [-1, 1](因浮点误差),传给
std::acos前要裁剪,否则返回NaN; -
std::acos返回弧度,乘180.0 / M_PI得角度; - 推荐用
double运算,float在点积较大时易失精度。
double angle_between(const std::array<double, 3>& a, const std::array<double, 3>& b) {
double dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
double norm_a = std::sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2]);
double norm_b = std::sqrt(b[0]*b[0] + b[1]*b[1] + b[2]*b[2]);
if (norm_a == 0 || norm_b == 0) return 0.0; // 或抛异常
double cos_theta = dot / (norm_a * norm_b);
cos_theta = std::clamp(cos_theta, -1.0, 1.0); // C++17;若无,用 std::max(-1.0, std::min(1.0, cos_theta))
return std::acos(cos_theta) * 180.0 / M_PI; // 角度制
}用 atan2 避开 acos 的精度陷阱(高精度场景)
当两向量几乎同向或反向时,acos 在端点附近导数极大,微小误差会导致角度偏差显著。此时改用 std::atan2(std::sqrt(1 - cos²), cos) 等价于 asin + acos 组合,但更稳——实际中更推荐基于叉积模长的公式:θ = atan2(|a × b|, a·b)。
好处:
立即学习“C++免费学习笔记(深入)”;
-
atan2(y, x)自动处理象限,且在x ≈ 0时比acos(x)更鲁棒; - 叉积模长
|a × b| = |a||b|sinθ,和点积一起构成完整三角信息; - 仍需裁剪点积,但对叉积模长的浮点误差不敏感。
double angle_between_stable(const std::array<double, 3>& a, const std::array<double, 3>& b) {
double dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
double norm_a = std::sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2]);
double norm_b = std::sqrt(b[0]*b[0] + b[1]*b[1] + b[2]*b[2]);
if (norm_a == 0 || norm_b == 0) return 0.0;
double cos_theta = dot / (norm_a * norm_b);
cos_theta = std::clamp(cos_theta, -1.0, 1.0);
double sin_theta = std::sqrt(std::max(0.0, 1.0 - cos_theta*cos_theta)); // 防负数
// 更优:直接算叉积模长
double cx = a[1]*b[2] - a[2]*b[1];
double cy = a[2]*b[0] - a[0]*b[2];
double cz = a[0]*b[1] - a[1]*b[0];
double cross_norm = std::sqrt(cx*cx + cy*cy + cz*cz);
return std::atan2(cross_norm, dot) * 180.0 / M_PI;
}使用 Eigen 库时直接调用 AngleBetween(工程优先)
如果项目已用 Eigen,别手写——它提供高度优化且健壮的向量运算。注意 Eigen::Vector3d 的 angle() 成员函数返回弧度,且内部已处理零向量、裁剪、atan2 稳定性等细节。
常见误区:
- 误用
.normalized().dot()再套acos,绕过库的保护逻辑; - 忘记链接
-lEigen3或正确设置find_package(Eigen3); -
angle()对零向量返回NaN,仍需外部判空。
Eigen::Vector3d a(1, 0, 0), b(0, 1, 0);
if (a.norm() == 0 || b.norm() == 0) {
// 处理退化情况
} else {
double rad = a.angle(b); // 弧度
double deg = rad * 180.0 / M_PI;
}容易被忽略的边界:零向量、共线、跨平台 M_PI
实际代码里最常崩在三个地方:传入零向量没检查、浮点裁剪漏掉、M_PI 在某些标准下未定义(如严格 C++11)。这些不是“理论问题”,而是上线后真实 crash 点。
对策:
- 零向量检测不能只写
== 0,要用norm() ; - 裁剪必须做,哪怕你“确定”不会超范围——编译器优化或 FMA 指令可能改变中间结果;
-
M_PI不是 C++ 标准常量,跨平台应定义为#define _USE_MATH_DEFINES(MSVC)或手动写constexpr double PI = 3.14159265358979323846;; - 若需方向(而不仅是夹角大小),得结合叉积符号判断左右手系,单纯
angle()总返回 [0, π]。
夹角计算看着简单,但浮点、边界、可移植性三者叠加,手写函数第一版能跑通不等于能进生产环境。


















