简介:Eigen-3.4.0 是一份面向 Windows 平台 C++ 开发者的线性代数模板库源码包,适用于需要在 Visual Studio 2022 中进行矩阵与矢量运算、数值分析及相关算法实现的工程人员与算法学习者。资源共包含 1785 个文件,以 795 个 cpp 源文件、528 个 h 头文件为主,辅以 cmake 构建脚本、dox 文档、hh 与 cu 等扩展文件,覆盖核心运算、稀疏矩阵、几何变换、非线性优化、FFT、张量等模块,压缩包约 6.84MB,体积轻量却结构完整。目前已有 1639 人学习下载,说明其在 C++ 数值计算领域具备较高参考价值。使用者可借助该源码包快速完成项目属性中 Eigen 包含目录的配置,直接调用矩阵分解、特征值求解、稀疏线性系统等成熟实现,同时通过内置测试与基准脚本理解模板元编程与表达式模板机制,为科学计算、机器人、计算机视觉等场景提供稳定底层支撑。
1. 为什么一个 C++ 矩阵库能让我在 Windows 上少写 300 行胶水代码
如果你在 Windows 上用 C++ 做过矩阵运算、坐标变换或者最小二乘拟合,大概率经历过这样的场景:手写一个 4x4 矩阵乘法,调了三个小时发现转置写反了;想做个特征值分解,翻遍手头工具发现要么依赖庞大的第三方库,要么接口难用到想砸键盘。Eigen-3.4.0 就是来解决这类问题的——它是一个纯头文件的 C++ 模板库,专注线性代数、矩阵和向量运算、数值求解,在 Windows 上不需要编译安装,解压、加 include 路径就能用。适合做机器人运动学、点云处理、SLAM 后端优化、图像几何变换的开发者,也适合任何需要在 C++ 里做矩阵运算但不想引入重型依赖的人。它不依赖 BLAS/LAPACK 也能跑,当然你想接 MKL 或 OpenBLAS 加速也留了口子。
2. Eigen-3.4.0 的获取与 Windows 工程接入:从解压到第一个可编译工程
2.1 为什么选 Eigen 而不是手写或别的库
先说选型逻辑。手写矩阵类的问题在于,你写的第一版通常只支持固定尺寸,等你要动态尺寸时发现内存管理要重写;等你把内存管理写好,又发现表达式模板没做,A = B + C + D会生成一堆临时对象,性能直接崩。Eigen 用表达式模板把这类开销在编译期消掉,同时支持固定尺寸和动态尺寸,接口统一。
和别的库比:Armadillo 接口友好但依赖 LAPACK/BLAS,Windows 上配起来多一层;GLM 偏图形学,只有小矩阵和向量,没有分解和求解;xtensor 更偏多维数组,线性代数分解能力不如 Eigen 全。Eigen 的定位很明确——纯头文件、零依赖起步、需要性能时再挂后端。
Eigen-3.4.0 这个版本值得注意的点:对 C++14 的支持更完整,constexpr相关的矩阵操作有增强,稀疏矩阵模块修了不少边界问题,对 AVX512 的支持也在这一版里更稳。如果你之前用的是 3.3.x,升级到 3.4.0 在 Windows + MSVC 环境下编译报错会少一些,尤其是涉及std::complex和混合标量类型的场景。
2.2 Windows 下的目录结构与工程配置
Eigen 是头文件库,所谓“安装”就是把目录放到编译器能找到的地方。解压后你会看到Eigen/和unsupported/两个主要目录。Eigen/里是核心模块:Dense、Core、Geometry、LU、Cholesky、SVD、QR、Sparse等。unsupported/里是一些实验性模块,比如Eigen::FFT、Eigen::PolynomialSolver、Eigen::NonLinearOptimization,这些不在官方稳定 API 保证范围内,但实际项目里用的人不少。
在 Visual Studio 里接入,最直接的方式是在项目属性里加附加包含目录。假设你把 Eigen 解压到了D:\libs\eigen-3.4.0,那么包含目录就填D:\libs\eigen-3.4.0。注意不是填到Eigen子目录,因为代码里写的是#include <Eigen/Dense>,编译器会从你给的根目录往下找Eigen/Dense。
如果你用 CMake,可以这样写:
cmake_minimum_required(VERSION 3.15) project(eigen_demo LANGUAGES CXX) # 设置 C++ 标准,Eigen 3.4 推荐 C++14 及以上 set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 方式一:直接把 Eigen 根目录加为 include 目录 target_include_directories(eigen_demo PRIVATE "D:/libs/eigen-3.4.0") # 方式二:如果 Eigen 提供了 CMake 配置文件,可以用 find_package # find_package(Eigen3 3.4 REQUIRED) # target_link_libraries(eigen_demo PRIVATE Eigen3::Eigen) add_executable(eigen_demo main.cpp)方式一适合你手动管理依赖,方式二适合用包管理器(比如 vcpkg)装好的场景。vcpkg 装 Eigen 的命令是vcpkg install eigen3:x64-windows,装完后 CMake 里用find_package就能找到。手动解压的好处是版本完全可控,不会因为包管理器更新导致 API 行为变化。
注意:Eigen 的路径里尽量不要有中文和空格,MSVC 在某些版本下对含空格路径的 include 处理会有玄学问题,虽然不常见,但踩过一次就会长记性。
2.3 第一个可编译的矩阵程序
配置好包含路径后,写一个最小验证程序:
#include <iostream> #include <Eigen/Dense> // 稠密矩阵核心模块,包含 Matrix、Vector 和基本运算 int main() { // 声明一个 3x3 的 double 类型矩阵,固定尺寸 Eigen::Matrix3d m; m << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0; // 逗号初始化,按行填充 // 声明一个 3 维列向量 Eigen::Vector3d v(1.0, 2.0, 3.0); // 矩阵乘向量 Eigen::Vector3d result = m * v; std::cout << "m * v = \n" << result << std::endl; // 求矩阵的逆(仅用于演示,实际项目中慎用直接求逆) Eigen::Matrix3d inv_m = m.inverse(); std::cout << "m.inverse() = \n" << inv_m << std::endl; // 验证 m * inv_m 是否接近单位阵 std::cout << "m * inv_m = \n" << m * inv_m << std::endl; return 0; }这段代码里几个关键点:Matrix3d是Matrix<double, 3, 3>的 typedef,固定尺寸矩阵在栈上分配,没有堆内存开销。<<逗号初始化是 Eigen 特有的语法,按行优先填充,写的时候注意元素个数要和矩阵尺寸匹配,少一个多一个都会在运行时报断言错误。inverse()对 3x3 这种小矩阵没问题,但后面会讲到为什么大矩阵不该直接求逆。
编译命令如果用 MSVC 命令行:
cl /EHsc /I "D:\libs\eigen-3.4.0" main.cpp /Fe:eigen_demo.exe/EHsc是启用标准 C++ 异常处理,Eigen 在某些分解失败时会抛异常,不开这个选项行为不确定。/I指定包含目录。如果你用 MinGW:
g++ -std=c++14 -I "D:/libs/eigen-3.4.0" main.cpp -o eigen_demo.exe跑通这个程序,说明环境没问题了。接下来才是真正要花时间的地方——理解 Eigen 的表达式模板和内存对齐规则,这两个东西决定了你的代码是跑得飞快还是莫名其妙崩溃。
3. 矩阵运算的核心接口与表达式模板:把 API 用对,把性能吃满
3.1 动态尺寸与固定尺寸的选择策略
Eigen 支持两种尺寸模式:固定尺寸(编译期已知)和动态尺寸(运行期确定)。固定尺寸的矩阵对象直接持有数据数组,没有堆分配,适合小矩阵(一般 4x4 以下,最多到 16x16 左右)。动态尺寸的矩阵内部有一个指针指向堆内存,适合大矩阵或尺寸在运行期才能确定的场景。
选择逻辑不复杂:尺寸小于等于 4 且编译期已知,用固定尺寸;尺寸较大或运行期确定,用动态尺寸。但有一个容易被忽略的点——固定尺寸矩阵作为函数参数时,如果尺寸太大,栈拷贝开销会很明显。我一般建议超过 4x4 就用动态尺寸或者传引用。
// 固定尺寸:编译期已知,栈上分配 Eigen::Matrix4d T; // 4x4 变换矩阵,机器人里最常见的尺寸 Eigen::Vector3d p; // 3D 点 // 动态尺寸:运行期确定,堆上分配 Eigen::MatrixXd A(100, 100); // 100x100 动态矩阵 Eigen::VectorXd b(100); // 100 维动态向量 // 固定尺寸的最大尺寸版本:栈上预留空间,但尺寸可变 Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, 0, 16, 16> small_dyn; // 最多 16x16,超出会断言失败,适合尺寸不大但不确定的场景最后那种写法在实际项目里用得不多,但知道有这个东西,在看别人代码时不会懵。
3.2 表达式模板:为什么A = B + C + D不会产生临时矩阵
Eigen 最核心的设计是表达式模板。普通实现里,B + C会生成一个临时矩阵,+ D再生成一个,最后赋值给A再拷贝一次。Eigen 把整个表达式在编译期展开成一个循环,直接算到A里,中间没有临时对象。
但这带来一个坑:auto和表达式模板一起用会出问题。
Eigen::MatrixXd A = Eigen::MatrixXd::Random(100, 100); Eigen::MatrixXd B = Eigen::MatrixXd::Random(100, 100); Eigen::MatrixXd C = Eigen::MatrixXd::Random(100, 100); // 危险写法:auto 推导出的是表达式类型,不是矩阵 auto D = A + B + C; // D 是一个表达式对象,持有 A、B、C 的引用 // 如果之后修改了 A,D 的结果会变 A(0, 0) = 999.0; // 此时 D 的值已经不是你期望的 A+B+C 了 // 正确写法:显式声明为矩阵类型,强制求值 Eigen::MatrixXd E = A + B + C; // 立即计算并存储结果这个坑我见过不止一次。有人写了一个函数返回auto,调用方拿到表达式对象后原始矩阵已经析构了,结果就是随机崩溃或者读到脏数据。血泪经验:在 Eigen 里,除非你明确知道自己在做什么,否则不要用auto接收矩阵表达式的结果,直接用Eigen::MatrixXd或对应的具体类型。
3.3 常用分解的接口与选型
Eigen 提供了多种矩阵分解,选哪个取决于你的矩阵性质和问题类型。下面这张表是我自己整理的选择依据:
| 分解方法 | 适用矩阵 | 典型用途 | 复杂度 |
|---|---|---|---|
PartialPivLU | 可逆方阵 | 解线性方程组 | O(n³) |
FullPivLU | 任意方阵 | 求秩、解方程 | O(n³) |
HouseholderQR | 任意矩阵 | 最小二乘 | O(mn²) |
ColPivHouseholderQR | 任意矩阵 | 最小二乘(带列主元) | O(mn²) |
LLT | 正定对称 | 解方程、Cholesky | O(n³/3) |
LDLT | 半正定对称 | 解方程 | O(n³/3) |
BDCSVD | 任意矩阵 | SVD、求伪逆 | O(mn²) |
JacobiSVD | 小矩阵 | SVD、精度要求高 | O(mn²) |
实际项目里,解线性方程组优先用PartialPivLU,正定系统用LLT,最小二乘用ColPivHouseholderQR,需要伪逆或秩亏处理用BDCSVD。JacobiSVD精度高但慢,只在小矩阵上用。
#include <Eigen/Dense> #include <iostream> int main() { // 构造一个正定对称矩阵 Eigen::Matrix3d A; A << 4.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0; Eigen::Vector3d b(1.0, 2.0, 3.0); // LLT 分解解方程 Eigen::Vector3d x = A.llt().solve(b); std::cout << "LLT solution: " << x.transpose() << std::endl; // 验证残差 std::cout << "residual: " << (A * x - b).norm() << std::endl; // 最小二乘:超定方程 Eigen::MatrixXd M(5, 3); M << 1, 2, 3, 4, 5, 6, 7, 8, 10, 1, 1, 1, 2, 3, 4; Eigen::VectorXd rhs(5); rhs << 1, 2, 3, 4, 5; Eigen::VectorXd ls = M.colPivHouseholderQr().solve(rhs); std::cout << "least squares: " << ls.transpose() << std::endl; std::cout << "residual norm: " << (M * ls - rhs).norm() << std::endl; return 0; }llt().solve(b)返回的是解向量,llt()本身只做分解,solve做前代和回代。如果你要对同一个矩阵解多个右端项,应该把分解结果存下来复用:
Eigen::LLT<Eigen::Matrix3d> llt(A); Eigen::Vector3d x1 = llt.solve(b1); Eigen::Vector3d x2 = llt.solve(b2);这样分解只做一次,后面每次 solve 都是 O(n²)。直接写A.llt().solve(b1)和A.llt().solve(b2)会做两次分解,浪费计算。
3.4 内存对齐:Eigen 在 Windows 上最容易被忽略的崩溃源
Eigen 为了支持 SIMD 指令(SSE、AVX),对固定尺寸且尺寸是 16 字节倍数的矩阵类型要求内存对齐。Matrix4d、Vector4d、Matrix2d这些都在对齐要求范围内。如果你把这些类型放到std::vector里,或者用new分配,默认的对齐可能不满足要求,结果就是程序在运行时崩溃,报错信息通常是ASSERT失败或者直接段错误。
// 危险:std::vector 默认分配器不保证 16 字节对齐 std::vector<Eigen::Vector4d> points; points.push_back(Eigen::Vector4d(1, 2, 3, 4)); // 可能崩溃 // 正确:使用 Eigen 提供的对齐分配器 std::vector<Eigen::Vector4d, Eigen::aligned_allocator<Eigen::Vector4d>> points; points.push_back(Eigen::Vector4d(1, 2, 3, 4)); // 安全 // 如果是自定义结构体,需要加宏 struct AlignedStruct { Eigen::Vector4d v; EIGEN_MAKE_ALIGNED_OPERATOR_NEW // 重载 new 保证对齐 };在 C++17 及以上,std::vector对过度对齐类型的支持已经改善,但如果你用的是 C++14 或者更早的标准,这个问题必须手动处理。MSVC 下还有一个编译选项/arch:AVX2会影响对齐要求,开了之后Vector4d的对齐要求从 16 字节变成 32 字节,如果你之前用aligned_allocator没问题,但手动malloc的地方就会出问题。
提示:如果你在 Windows 上遇到 Eigen 相关的崩溃,第一反应应该是检查对齐。把
EIGEN_MAKE_ALIGNED_OPERATOR_NEW加上,把std::vector的分配器换成aligned_allocator,大部分玄学崩溃都能解决。
4. 避坑与排查:Eigen 在 Windows 上的五个真实翻车现场
4.1 现象:编译报错Eigen::Matrix相关模板实例化失败,错误信息几千行
原因通常是包含路径不对,或者只包含了Eigen/Core却用了Eigen/Dense里的功能。另一个常见原因是 MSVC 的/permissive-选项和 Eigen 的某些模板代码不兼容。
解决:先确认#include <Eigen/Dense>能正常找到,路径里不要有中文。如果开了/permissive-,试试关掉或者升级到 Eigen 3.4.0 之后的版本,3.4.0 对 MSVC 的严格模式兼容性比 3.3.x 好很多。实在不行,在包含 Eigen 头文件之前加#define EIGEN_PERMISSIVE_MSVC 1。
4.2 现象:程序在 Debug 下正常,Release 下崩溃或结果错误
原因通常是 Debug 和 Release 的对齐宏定义不一致,或者NDEBUG导致 Eigen 的断言被关掉,一些本该报错的非法操作变成了未定义行为。
解决:检查 Debug 和 Release 的预处理器定义是否一致,特别是EIGEN_MAKE_ALIGNED_OPERATOR_NEW相关的宏。另外,Eigen 的断言在 Release 下默认关闭,如果你依赖断言来捕获尺寸不匹配,需要在 Release 下也定义EIGEN_NO_DEBUG之外的东西——更好的做法是在代码里显式检查尺寸,不要依赖断言。
4.3 现象:A.inverse() * b结果和A.lu().solve(b)不一致,且大矩阵时inverse()极慢
原因:inverse()是显式求逆,数值稳定性差,复杂度 O(n³) 且常数大。lu().solve()是分解后求解,数值稳定性好,复杂度相同但常数小很多。
解决:永远不要用inverse()解方程。需要求逆矩阵本身时(比如协方差传播),才用inverse(),而且优先用llt().solve(Identity)或lu().solve(Identity)来求逆。对于正定矩阵,llt().solve(Identity)比inverse()快且稳。
4.4 现象:稀疏矩阵求解时内存暴涨或求解失败
原因:Eigen 的稀疏矩阵模块对填充(fill-in)的处理依赖排序。默认的SparseLU在某些矩阵上会产生大量填充,导致内存爆炸。SimplicialLDLT只适用于自伴随正定矩阵,用错类型会失败。
解决:稀疏 Cholesky 用SimplicialLDLT,稀疏 LU 用SparseLU,但都要配合AMDOrdering或COLAMDOrdering来减少填充。如果矩阵是对称正定的,优先用SimplicialLDLT,它比SparseLU快且省内存。求解失败时检查矩阵是否真的满足分解方法的前提条件。
4.5 现象:多线程下 Eigen 运算结果随机错误
原因:Eigen 本身是线程安全的,前提是你不在多个线程里同时写同一个矩阵。但如果你用了Eigen::setNbThreads()或者链接了 OpenMP,而你的代码里又有共享的矩阵对象被多个线程读写,就会出问题。
解决:每个线程用自己的矩阵对象,不要共享可写矩阵。如果必须共享,加锁。另外,Eigen::setNbThreads()设置的是 Eigen 内部并行化的线程数,不是你的程序线程数,别搞混。在 Windows 上如果链接了 OpenMP,注意 MSVC 的 OpenMP 版本和 Eigen 的兼容性,必要时用/openmp显式开启。
5. 进阶技巧:用 Eigen 的几何模块做坐标变换与一个验证习惯
Eigen 的Geometry模块提供了Transform、Quaternion、AngleAxis等类型,做机器人运动学或点云配准时比手写矩阵乘法省事得多。但这里有一个容易翻车的点:Transform的乘法顺序和Isometry的构造方式。
#include <Eigen/Dense> #include <Eigen/Geometry> #include <iostream> int main() { // 构造一个旋转:绕 Z 轴 45 度 Eigen::Matrix3d R = Eigen::AngleAxisd(M_PI / 4, Eigen::Vector3d::UnitZ()).toRotationMatrix(); // 构造平移 Eigen::Vector3d t(1.0, 2.0, 3.0); // 用 Isometry 构造变换矩阵 Eigen::Isometry3d T = Eigen::Isometry3d::Identity(); T.linear() = R; // 设置旋转部分 T.translation() = t; // 设置平移部分 // 变换一个点 Eigen::Vector3d p(1.0, 0.0, 0.0); Eigen::Vector3d p_transformed = T * p; std::cout << "transformed point: " << p_transformed.transpose() << std::endl; // 逆变换 Eigen::Vector3d p_back = T.inverse() * p_transformed; std::cout << "back to original: " << p_back.transpose() << std::endl; // 四元数转旋转矩阵 Eigen::Quaterniond q(Eigen::AngleAxisd(M_PI / 4, Eigen::Vector3d::UnitZ())); Eigen::Matrix3d R_from_q = q.toRotationMatrix(); std::cout << "R from quaternion:\n" << R_from_q << std::endl; // 验证旋转矩阵正交性:R * R^T 应该接近单位阵 std::cout << "orthogonality check:\n" << R * R.transpose() << std::endl; return 0; }Isometry3d是 4x4 变换矩阵的轻量封装,内部存储是 3x3 旋转加 3 维平移,比完整的Matrix4d省内存,而且inverse()有解析解,比通用矩阵求逆快。T.linear()返回旋转部分的引用,T.translation()返回平移部分的引用,直接赋值即可。
四元数这边,Quaterniond的构造函数接受AngleAxis,toRotationMatrix()转成旋转矩阵。注意四元数的乘法顺序和旋转矩阵一致,但q1 * q2表示先应用q2再应用q1,和矩阵乘法R1 * R2对向量的作用顺序相同。
我自己的习惯是:每次写完一个变换相关的函数,都强制走一遍验证——用T * T.inverse()看是否接近单位阵,用R * R.transpose()看正交性,用(T * p - p).norm()看平移量是否符合预期。这个习惯帮我省了很多调试时间,因为变换矩阵的 bug 往往不会立刻崩溃,而是让结果慢慢偏掉,等到发现时已经很难定位了。
还有一个技巧:Eigen 的JacobiSVD在求伪逆时,可以通过设置阈值来控制数值秩。svd.setThreshold(1e-10)比默认阈值更严格,适合精度要求高的场景。但阈值设太小会导致数值不稳定,设太大会丢失有效信息,一般从1e-12到1e-8之间试,看残差和条件数。
希望帮到你。
本文还有配套的精品资源,点击获取