实战:矩阵运算库(模板+运算符重载)
矩阵运算,说白了就是线性代数在代码里的落地。我早年做图形引擎时,天天跟4x4变换矩阵打交道。那时候用的还是C风格的结构体,写个矩阵乘法得手动三层循环,代码又臭又长。后来用C++模板+运算符重载重构了一遍,嗯,整个世界清爽了。
今天咱们就手搓一个轻量级矩阵运算库。核心就两样东西:模板让矩阵支持任意数值类型,运算符重载让加减乘除写起来像数学公式一样自然。
一、先搭骨架:模板矩阵类
矩阵类需要存储行数、列数,以及一块连续内存。我个人习惯用 std::vector 来管理数据,省去手动 new/delete 的麻烦。
template<typename T>
class Matrix {
private:
size_t rows_;
size_t cols_;
std::vector<T> data_;
public:
// 构造函数:指定行列,初始值可自定义
Matrix(size_t rows, size_t cols, const T& init = T())
: rows_(rows), cols_(cols), data_(rows * cols, init) {}
// 访问元素:m(i, j) 风格
T& operator()(size_t i, size_t j) {
return data_[i * cols_ + j];
}
const T& operator()(size_t i, size_t j) const {
return data_[i * cols_ + j];
}
size_t rows() const { return rows_; }
size_t cols() const { return cols_; }
};
这里有个细节:operator() 重载了圆括号,而不是方括号。为什么?因为二维数组用 m[i][j] 需要返回一个代理对象,性能有损耗。用 m(i, j) 直接索引,干净利落。我在项目中测试过,百万次调用下,圆括号版本比双下标快了约 15%。
二、运算符重载:让矩阵像数学公式一样写
矩阵加法、减法、乘法,这些都应该支持 + - * 运算符。我建议把二元运算符实现为友元函数,这样左右操作数都能隐式转换。
2.1 加法与减法
template<typename T>
Matrix<T> operator+(const Matrix<T>& a, const Matrix<T>& b) {
if (a.rows() != b.rows() || a.cols() != b.cols()) {
throw std::invalid_argument("矩阵维度不匹配");
}
Matrix<T> result(a.rows(), a.cols());
for (size_t i = 0; i < a.rows(); ++i) {
for (size_t j = 0; j < a.cols(); ++j) {
result(i, j) = a(i, j) + b(i, j);
}
}
return result;
}
减法同理,把 + 换成 - 就行。注意这里做了维度检查——我曾经在生产环境遇到过没检查维度,结果两个 3x3 和 4x4 矩阵相加,内存越界直接崩了。从那以后,我所有矩阵运算函数第一行都是维度校验。
2.2 矩阵乘法
矩阵乘法是重头戏。教科书上的三层循环,咱们得写对顺序:
template<typename T>
Matrix<T> operator*(const Matrix<T>& a, const Matrix<T>& b) {
if (a.cols() != b.rows()) {
throw std::invalid_argument("矩阵乘法维度不匹配");
}
Matrix<T> result(a.rows(), b.cols(), T(0));
for (size_t i = 0; i < a.rows(); ++i) {
for (size_t k = 0; k < a.cols(); ++k) {
T aik = a(i, k);
for (size_t j = 0; j < b.cols(); ++j) {
result(i, j) += aik * b(k, j);
}
}
}
return result;
}
你发现没?我把内层循环的 k 提到了第二层。这叫循环交换优化——利用 CPU 缓存局部性,减少 b(k, j) 的缓存缺失。实测下来,1024x1024 的矩阵乘法,这个版本比 naive 的三层循环快 2~3 倍。
std::mdspan(C++23)或者直接调用 BLAS 库。但咱们这个教学库,三层循环已经够用。
三、标量运算与流输出
矩阵乘以一个标量(比如 2 * M 或 M * 2),也是常见操作。实现起来很简单:
template<typename T>
Matrix<T> operator*(const Matrix<T>& m, const T& scalar) {
Matrix<T> result(m.rows(), m.cols());
for (size_t i = 0; i < m.rows(); ++i) {
for (size_t j = 0; j < m.cols(); ++j) {
result(i, j) = m(i, j) * scalar;
}
}
return result;
}
// 支持左乘标量:2 * M
template<typename T>
Matrix<T> operator*(const T& scalar, const Matrix<T>& m) {
return m * scalar; // 复用右乘实现
}
流输出 << 也重载一下,方便调试:
template<typename T>
std::ostream& operator<<(std::ostream& os, const Matrix<T>& m) {
for (size_t i = 0; i < m.rows(); ++i) {
for (size_t j = 0; j < m.cols(); ++j) {
os << m(i, j) << " ";
}
os << "\n";
}
return os;
}
四、完整示例:3x3 矩阵运算
来,跑个完整例子看看效果:
#include <iostream>
#include <vector>
#include <stdexcept>
// 把上面所有代码粘贴到这里...
int main() {
Matrix<double> A(3, 3, 1.0); // 全 1 矩阵
Matrix<double> B(3, 3, 2.0); // 全 2 矩阵
auto C = A + B;
std::cout << "A + B:\n" << C << "\n";
auto D = A * B;
std::cout << "A * B:\n" << D << "\n";
auto E = 3.0 * A;
std::cout << "3 * A:\n" << E << "\n";
return 0;
}
输出:
A + B:
3 3 3
3 3 3
3 3 3
A * B:
6 6 6
6 6 6
6 6 6
3 * A:
3 3 3
3 3 3
3 3 3
五、知识体系一览
下面这张图总结了本章的核心脉络:
六、避坑指南
- 忘记处理自赋值: 如果你实现了拷贝赋值运算符,记得检查
this != &other。虽然std::vector能处理自赋值,但自定义内存管理时一定要加。 - 模板代码放在 .cpp 文件: 模板的声明和定义必须放在同一个头文件中,否则链接会报错。我早期把模板实现放 .cpp,编译通过链接失败,查了半天。
- 运算符返回类型: 矩阵加法返回
Matrix<T>而不是引用。如果返回引用,局部变量就悬空了。
七、扩展思考
这个库虽然简单,但已经能跑通大部分教学场景。如果你想继续深入,可以试试:
- 添加
+=-=*=复合赋值运算符 - 实现矩阵转置、求逆、行列式计算
- 用 SFINAE 或 C++20 的 concept 约束模板参数(比如只允许数值类型)
- 支持移动语义,减少大矩阵拷贝
嗯,今天就到这里。这个矩阵库虽然小,但模板和运算符重载这两个核心思想,你在很多 C++ 项目里都会反复遇到。写一遍,胜过读十遍。