实战:矩阵运算库(模板+运算符重载)

矩阵运算,说白了就是线性代数在代码里的落地。我早年做图形引擎时,天天跟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
核心收获: 模板让矩阵支持 int、double、complex 等任意类型;运算符重载让代码读起来像数学公式。这两样东西组合起来,就是 C++ 泛型编程的典型应用。

五、知识体系一览

下面这张图总结了本章的核心脉络:

矩阵运算库 模板 (Template) 支持任意数值类型 int / double / complex 运算符重载 + - * 二元运算 标量乘法 2*M 流输出 << 实现细节 维度校验 循环交换优化 连续内存存储 模板 + 运算符重载 = 优雅的矩阵运算库

六、避坑指南

我曾经踩过的坑:
  • 忘记处理自赋值: 如果你实现了拷贝赋值运算符,记得检查 this != &other。虽然 std::vector 能处理自赋值,但自定义内存管理时一定要加。
  • 模板代码放在 .cpp 文件: 模板的声明和定义必须放在同一个头文件中,否则链接会报错。我早期把模板实现放 .cpp,编译通过链接失败,查了半天。
  • 运算符返回类型: 矩阵加法返回 Matrix<T> 而不是引用。如果返回引用,局部变量就悬空了。

七、扩展思考

这个库虽然简单,但已经能跑通大部分教学场景。如果你想继续深入,可以试试:

  • 添加 += -= *= 复合赋值运算符
  • 实现矩阵转置、求逆、行列式计算
  • 用 SFINAE 或 C++20 的 concept 约束模板参数(比如只允许数值类型)
  • 支持移动语义,减少大矩阵拷贝

嗯,今天就到这里。这个矩阵库虽然小,但模板和运算符重载这两个核心思想,你在很多 C++ 项目里都会反复遇到。写一遍,胜过读十遍。


公众号:蓝海资料掘金营,微信deep3321