简介:基于C++的量子计算编程源码包,面向具备一定C++基础、希望进入量子计算领域的开发者和科研学习者。包内共720个文件,压缩包大小约12.94MB,文件构成以cpp源文件、h头文件、obj目标文件、exe可执行程序为主,同时包含dsp/dsw/vcproj等工程配置文件,方便直接打开工程编译运行或逐模块阅读源码。资源内容围绕量子位、量子叠加、量子纠缠等量子计算核心概念展开,涉及Hadamard门、CNOT门、T门等常见量子逻辑门操作,并覆盖Shor质因数分解、Grover搜索等典型量子算法的实现与测试代码。通过分析这些C++程序,读者可以理解量子线路的设计方式,学习如何在经典计算机上模拟量子过程,以及如何借助Qiskit、Cirq等主流框架或基于C++自行搭建量子计算框架。目前该资源已有129人学习,适合作为量子编程入门与进阶的参考素材,帮助读者从代码层面建立对量子算法的直观认识。
1. 量子框架包入手:先搞清楚你拿到的是模拟器还是真机接口
做量子计算方向的开发者,多半都遇到过这种尴尬:兴致勃勃下了一个“量子框架”压缩包,解压一看,要么是纯数学演示代码,要么是挂着量子名字的数组操作,跑不出实际结果。这个 Cpp.zip 属于前者里质量较高的一类,它把一个完整的 C++ 量子态矢量模拟器打成了独立压缩包,包含门操作、态演化、测量抽样三块核心能力,不依赖任何云平台或真实量子硬件,纯 CPU 就能跑通整个算法流程。适合正在学量子计算、需要验证自己在论文里看到的电路设计是否正确的人,也适合想在传统 C++ 工程里嵌入量子模拟模块的从业者。这一章先说清楚它的边界,后面再逐层拆代码。
2. 态矢量模拟原理与代码骨架:复数数组怎么变成量子态
2.1 态矢量的数学表示与内存布局
量子态矢量这个词听起来玄,落地到内存里就是一个 std::complex 数组。n 个量子比特的纯态,用 2^n 个复数振幅表示,数组下标就是计算基态编号。比如两个比特,下标 0、1、2、3 分别对应 |00⟩、|01⟩、|10⟩、|11⟩。框架本质上是把线性代数的矩阵乘法翻译成对数组的遍历操作,这是态矢量模拟器的通用做法。
我打开压缩包后先看了一眼核心头文件,结构比想象中清晰,没有套太多抽象层。类的核心成员就是一个复数动态数组,加一个记录比特数的整数。构造函数按 2^n 分配内存,初始化为 |0...0⟩,逻辑和教科书上的定义完全一致。
// state_vector.hpp 核心骨架(已做简化) #include <complex> #include <vector> class StateVector { public: explicit StateVector(size_t num_qubits) : n_qubits_(num_qubits), n_states_(1ull << num_qubits) { amp_.resize(n_states_, {0.0, 0.0}); amp_[0] = {1.0, 0.0}; // 初始化为 |00...0> } size_t dimension() const { return n_states_; } std::complex<double>& amplitude(size_t index) { return amp_[index]; } const std::complex<double>& amplitude(size_t index) const { return amp_[index]; } private: size_t n_qubits_; size_t n_states_; // 等于 2^n std::vector<std::complex<double>> amp_; };这段代码逻辑不复杂:n_qubits_ 存量子比特数,n_states_ 用移位运算算出总振幅数,构造函数把下标 0 的振幅置为 1,对应全零基态。需要注意 1ull << num_qubits 用的是无符号长整型,当比特数达到 20 以上时,1 << 32 这类写法在 32 位系统上会翻车,这里换成 1ull 能规避整型溢出。还有一点,框架里没有做内存对齐优化,30 比特以上会明显吃内存,后面避坑章节会说。
2.2 单比特门作用到多比特态:索引跳转是核心难点
单比特门是一个 2×2 酉矩阵,作用到 n 比特态空间时,不能直接用普通矩阵乘法。关键是要找出目标比特变化时,振幅数组哪些下标会跟着变化。常见做法是枚举除目标比特外的所有比特组合,每个组合对应两个下标:目标比特为 0 的那个,和目标比特为 1 的那个。
// 单比特门应用,target 从 0 开始计数 void applySingleBit(const std::complex<double> gate[2][2], size_t target) { size_t shift = target; size_t step = 1ull << shift; size_t half = 1ull << (shift + 1); for (size_t base = 0; base < n_states_; base += (half)) { for (size_t offset = 0; offset < step; ++offset) { size_t i0 = base + offset; // 目标比特为 0 size_t i1 = base + offset + step; // 目标比特为 1 auto a0 = amp_[i0]; auto a1 = amp_[i1]; amp_[i0] = gate[0][0] * a0 + gate[0][1] * a1; amp_[i1] = gate[1][0] * a0 + gate[1][1] * a1; } } }这段代码是态矢量模拟器最容易写错的地方。shift 表示目标比特的位权,step 是相邻两个索引的差,half 是一个分组周期。外层循环按 half 分组跳跃,内层循环处理每个分组中的连续段。这样设计的意义在于:目标比特为 0 和 1 的两个振幅,索引差恰好是 1 << target,而非连续排列,很多初写者在这里直接做相邻两两变换,算出来结果永远是错的。建议拿到这份资源后,先跑一个单比特 X 门用例,如果输出的振幅分布不符合预期,优先检查这块索引逻辑。
2.3 双比特门与受控逻辑:CNOT 的矩阵块还原
双比特门比单比特门多一层嵌套。拿 CNOT 举例,控制比特为 c、目标比特为 t,语义是当控制比特为 1 时翻转目标比特。落到态空间,需要遍历所有满足控制比特为 1 的基态,然后交换目标比特 0、1 两个振幅。
// CNOT 门:控制比特为 ctrl,目标比特为 target void applyCNOT(size_t ctrl, size_t target) { size_t shift_c = 1ull << ctrl; size_t shift_t = 1ull << target; for (size_t i = 0; i < n_states_; ++i) { // 控制比特为 1,且目标比特为 0 时,交换到目标比特为 1 的位置 if ((i & shift_c) && !(i & shift_t)) { size_t j = i | shift_t; std::swap(amp_[i], amp_[j]); } } }这个实现的巧妙之处在于:按位与判断控制位,按位或计算交换目标。遍历所有下标一次,符合条件就交换,逻辑上不会漏也不会重。需要注意 ctrl 和 target 不能相等,否则会自我交换导致态被破坏。框架自带的示例代码里没有对这个边界做防御,自己动手改参数的时候要把这条记在脑子里。还有一点,受控门的实现通常比这个复杂——如果要实现受控 U 门(比如受控 Z 或受控相位),不能简单地交换,而是要把目标比特的 2×2 子块替换成 U 矩阵的四个元素,这也是模板里常见的扩展点。
3. 量子门与电路构建:从单比特变换到两比特纠缠
3.1 常用门的矩阵定义与参数约定
框架头文件里预置了 H、X、Y、Z、S、T 和相位门 P(θ) 的矩阵定义,全部按酉矩阵标准写在静态数组里。H 门是阿达玛门,作用是把 |0⟩ 变成 (|0⟩+|1⟩)/√2,矩阵元素都是 1/√2;X 门是量子非门,交换 |0⟩ 和 |1⟩;Z 门只在 |1⟩ 上乘 -1。这些门的参数有一个容易忽略的约定:框架默认矩阵按行主序存储,作用在振幅数组上时,相当于左乘列向量,顺序是新的振幅 = 矩阵 × 原振幅。
在 C++ 里定义这些门时还有一个细节——复数常量尽量提前算好,不要在每个门函数里反复重建。比如 e^{-iπ/4} 这种值,用 static const auto 声明一次,后面所有相位门共用,能减少一部分构造耗时。框架源码用的是 double 数组加显式复数构造,反而更容易读懂。我自己在实际工程里会包一层 constexpr 函数,但那是项目后期的优化,拿到这套资源后先按原样跑通是最重要的。
3.2 构建贝尔态电路:把 H 门和 CNOT 拼起来
贝尔态是量子纠缠的最简单例子,两比特电路只需要两步:先对第一个比特做 H 门,再用第一个比特控制第二个比特做 CNOT。输入状态是 |00⟩,输出应当是 (|00⟩+|11⟩)/√2,概率各 0.5。这个电路是所有量子通信协议、量子密钥分发算法的基础,验证它是否正确,等于验证整个模拟器的门操作链路是否通畅。
// 构建两比特贝尔态 int main() { StateVector sv(2); // H 门作用到比特 0 std::complex<double> H[2][2] = { { {1.0/std::sqrt(2.0), 0.0}, {1.0/std::sqrt(2.0), 0.0} }, { {1.0/std::sqrt(2.0), 0.0}, {-1.0/std::sqrt(2.0), 0.0} } }; sv.applySingleBit(H, 0); // CNOT:控制比特 0,目标比特 1 sv.applyCNOT(0, 1); // 打印振幅 for (size_t i = 0; i < sv.dimension(); ++i) { std::cout << "|" << i << "> amplitude = " << sv.amplitude(i).real() << " + " << sv.amplitude(i).imag() << "i\n"; } return 0; }跑完预期输出是 |0⟩ 和 |3⟩ 的振幅约等于 0.7071,|1⟩、|2⟩ 为 0。如果看到别的分布,优先检查 H 门矩阵是否写成了列主序,或者 CNOT 的比特编号是否反了。框架默认的编号规则是 bit0 对应最低位,很多从 Qiskit 转过来的开发者会踩这个坑——Qiskit 的比特顺序和这个框架刚好相反,前者用高位在前,后者用低位在前。这种语义差异会在多比特电路里放大,后面专门有一节讲怎么排查。
3.3 电路封装与可复用性设计
压缩包里自带的示例 demo 是把整个电路写死在 main 函数里,复用性很差。想在实际项目里反复调整电路参数,建议在此基础上加一层电路抽象,把「当前门列表」和「执行方法」分开。常见做法是定义一个 CircuitBuilder,内部维护 vector 存储门类型和目标比特,执行时循环调用 StateVector 的对应方法。
// 电路封装的最小可运行版本 struct QuantumGate { enum Type { H, X, CNOT, P } type; size_t target; size_t control; // 仅 CNOT 有效 double theta; // 仅相位门有效 }; class QuantumCircuit { public: void addH(size_t t) { gates_.push_back({QuantumGate::H, t, 0, 0.0}); } void addCNOT(size_t c, size_t t) { gates_.push_back({QuantumGate::CNOT, t, c, 0.0}); } void addPhase(size_t t, double theta) { gates_.push_back({QuantumGate::P, t, 0, theta}); } void run(StateVector& sv) const { for (const auto& g : gates_) { switch (g.type) { case QuantumGate::H: sv.applySingleBit(H_gate, g.target); break; case QuantumGate::CNOT: sv.applyCNOT(g.control, g.target); break; case QuantumGate::P: sv.applyPhase(g.theta, g.target); break; default: break; } } } private: std::vector<QuantumGate> gates_; };这个封装不影响原本框架的运行方式,只是把门序列变成可追加、可清空、可重跑的数据结构。实际调试量子算法时会频繁改动门顺序,用这种结构比逐个手写 main 里的调用方便很多。Phase 门对应的是 P(θ) = diag(1, e^{iθ}),是很多量子算法里的基础构件,原框架已包含,但我在抽成这种结构时额外加了 theta 字段,方便后续实现量子傅里叶变换。
4. 测量与统计:把概率幅变成可信的观测量
4.1 测量概率与随机抽样:从振幅到累积分布函数
态矢量模拟的最后一步是测量。测量的数学依据是玻恩定则:某个基态 |j⟩ 出现的概率等于对应振幅模长的平方。模拟器没法直接模拟「坍缩」的物理本质,只能按概率分布随机采样一个下标。实现时先遍历所有振幅计算概率,再做累积分布,最后生成 [0,1) 均匀随机数,查表找到对应下标。
// 测量函数:返回测量到的基态下标,并将态坍缩到该基态 #include <random> size_t measure(StateVector& sv) { static std::mt19937 rng(std::random_device{}()); std::vector<double> probs(sv.dimension()); double sum = 0.0; for (size_t i = 0; i < sv.dimension(); ++i) { double p = std::norm(sv.amplitude(i)); probs[i] = p; sum += p; } // 归一化累积分布 std::uniform_real_distribution<double> dist(0.0, 1.0); double r = dist(rng) * sum; double cumulative = 0.0; size_t result = 0; for (size_t i = 0; i < probs.size(); ++i) { cumulative += probs[i]; if (r <= cumulative) { result = i; break; } } // 坍缩:选中下标振幅保留,其余清零并归一化 for (size_t i = 0; i < sv.dimension(); ++i) { sv.amplitude(i) = (i == result) ? std::complex<double>(1.0, 0.0) : std::complex<double>(0.0, 0.0); } return result; }注意测量后的坍缩处理我写成了强制归一到 |result⟩,这和物理上「测量后状态为测量到的基态」一致,但实际测量过程中,振幅的相位信息会丢失,严格做法是保留原振幅再归一化。这个框架的处理方式是直接重置,好处是后续电路迭代时状态确定,坏处是无法模拟带相位的部分测量场景。如果你要做的算法需要测量后继续演化,建议把最后两步改成保留幅度相位再归一化,那里是这类模拟器的另一处经典翻车点。
4.2 多次采样与频率统计:验证门矩阵是否正确的最直观手段
单次测量结果没有统计意义,教科书电路的正确性必须靠多次运行验证。比如贝尔态电路理论概率是各 0.5,跑一万次统计频率应在 0.5 附近波动。这个验证方法比单次打印振幅更贴近真实实验,也更容易暴露随机数生成器和概率归一化的问题。
// 统计测量频率 int main() { StateVector sv(2); // 构建贝尔态电路 std::complex<double> H[2][2] = { /* 略,同上 */ }; sv.applySingleBit(H, 0); sv.applyCNOT(0, 1); // 重复测量一万次 const int shots = 10000; std::array<size_t, 4> counts = {0, 0, 0, 0}; for (int s = 0; s < shots; ++s) { StateVector sv_copy = sv; // 每次测量前复制一份,避免态被破坏 size_t outcome = measure(sv_copy); counts[outcome] += 1; } std::cout << "P(00) = " << (double)counts[0] / shots << "\n"; std::cout << "P(11) = " << (double)counts[3] / shots << "\n"; }这里有一个必须强调的细节:测量会破坏态,所以每次抽样前要复制一份 StateVector,或者对 StateVector 做一次深拷贝。原框架没有实现复制构造函数,如果直接用默认拷贝,复制的只是指针,两个对象共享同一块内存,第一次测量后原状态就被污染了。我在这段代码里用 sv_copy = sv 的方式避免了这个坑,但在实际使用中,给 StateVector 补一个显式深拷贝构造器才是治本之策。频率统计结果如果严重偏离理论值,例如 P(00) 跑到 0.9 以上,说明某个门没有执行或索引写错,需要回退到上一章的单门验证。
5. 常见问题与避坑:振幅、相位与内存的三个深水区
5.1 概率和不等于 1:检查门矩阵和归一化
现象:跑完一个电路后,把所有振幅的模平方求和,结果不是 1,而是 0.95 或 1.05。
原因:多数情况是某个门矩阵写错,比如 H 门少了 1/√2 系数;或者是在 applySingleBit 的旋转计算里,gate[0][1] 和 gate[1][0] 的符号写反,破坏了酉性。
解决:先写一个单比特验证函数,输入 |0⟩ 态分别作用 X、H、Z 门,打印结果,和手算对照。X 门作用于 |0⟩ 应得到 |1⟩,H 门作用于 |0⟩ 应得到两个各 0.5 的振幅。这一关过了再往多比特电路推。还有一种情况是复数运算浮点误差累积,在小规模下(比特数小于 15)误差通常小于 1e-12,如果差到 0.05 级别,基本可以断定是逻辑错误而不是精度问题。
5.2 CNOT 的输出顺序和论文不一致:比特编号语义要认清
现象:两比特电路按论文步骤搭完,理论输出是 |11⟩ 的概率最高,实测却是 |10⟩。
原因:不同框架对 bit0 的定义不同。这个框架中 bit0 是数组最低位,|2⟩ 对应的是第一位为 1、第二位为 0;而在某些论文和 Python 框架里,输出字符串从左到右是从高位到低位。同一个下标 2,在不同语义下解读出的物理比特组完全不同。
解决:统一约定并在代码注释里写明。我一般会在电路执行入口加一行注释:// 注:框架采用低位在前,输出顺序为 |bit1 bit0⟩。对比论文结果时,把输出下标转换成二进制,再倒序排列才是论文语义。这条坑在多比特算法(比如 QFT)里最致命,建议一开始就固定一种表示方法,不要中途切换。
5.3 测量后继续跑电路结果全乱:态坍缩处理错了
现象:同一份态矢量先测量再跑另一段电路,输出振幅全是 NaN 或概率分布完全对不上。
原因:测量函数在坍缩时没有处理未选中振幅的复数清零,而是把它们设成了 0,归一化时除的是 0;或者复制构造时浅拷贝导致原态被共享修改。
解决:测量后要么把所有振幅重置为选中基态的标准基向量,要么保留选中振幅并重新归一化。推荐后者,因为它在物理上更接近真实测量,也方便后续做条件分支。另外给 StateVector 写一个真正的拷贝构造,用 std::vector 的拷贝逻辑而不是默认赋值。这两件事做完,测量相关的诡异现象会少一大半。
5.4 编译报错找不到头文件或链接失败:CMake 配置与 C++ 标准
现象:按压缩包内的 README 执行编译,报错头文件路径不对,或者提示 std::complex 相关模板无法实例化。
原因:源码假设你的构建环境包含正确的 include 路径,但实际工程里头文件在子目录,没有用 target_include_directories 把当前目录加进搜索路径;另一个原因是编译器默认标准是 C++14,但源码用了 C++17 的某些特性。
解决:在项目根目录新建 CMakeLists.txt,显式声明 C++17 标准,并把包含目录指向源码文件夹。常见写法是set(CMAKE_CXX_STANDARD 17)和target_include_directories(qsim PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)。这样无论源码在哪个子目录,都能被正确解析,也避免了手动维护 -I 参数。
5.5 单比特门作用后只改了一半的振幅:分组遍历的周期算错
现象:对 3 比特态的比特 2 做 H 门,输出的振幅只有一半发生了变换,另一半原封不动。
原因:applySingleBit 的外层循环步长 half 应当是 1 << (target+1),但如果 target 较大时用了 1 << target 导致周期减半,部分基态跳出了循环范围。
解决:逐步 debug,打印每次循环的 base、offset、i0、i1 下标,和手算的期望下标比对。这类索引错误是所有模拟器里最高频的 bug,模板参数越复杂越容易错。建议为 StateVector 增加一个验证方法:作用任意单比特门后,检查变换涉及的下标数量和状态向量维度是否匹配。多写这层断言,能省掉大量 debug 时间。
6. 进阶玩法:用量子傅里叶变换验证框架的边界
如果想确认这套框架能撑起真实算法而不只是跑个教学 demo,我建议手动实现一个 3 比特量子傅里叶变换(QFT),并用逆变换还原初始态来验证。QFT 的电路包含 H 门和受控相位门,涉及比特序、相位累积、多比特控制三个最容易出错的点,比 Bell 态能暴露更多问题。
实现思路分三路:第一步对每个比特做 H 门,第二步按二进制位施加受控相位门——第 j 个比特控制第 i 个比特时,相位角度是 π/2^(j-i);第三步把所有输出比特的顺序反转。第三步在量子电路里可以用 SWAP 门实现,在模拟器层面直接交换振幅数组下标更省事。
// 3 比特 QFT 实现(核心逻辑) void applyQFT(StateVector& sv) { size_t n = 3; for (size_t i = 0; i < n; ++i) { sv.applySingleBit(H_gate, i); for (size_t j = i + 1; j < n; ++j) { double theta = M_PI / (1ull << (j - i)); sv.applyControlledPhase(ctrl=i, target=j, theta); } } // 比特反转 for (size_t a = 0; a < (1ull << n); ++a) { size_t b = 0; for (size_t k = 0; k < n; ++k) if (a & (1ull << k)) b |= (1ull << (n - 1 - k)); if (b > a) std::swap(sv.amplitude(a), sv.amplitude(b)); } }这里需要自行实现 applyControlledPhase:条件是控制比特为 1 时,对目标比特作用相位门 diag(1, e^{iθ})。注意受控相位门不像 CNOT 那样交换振幅,而是只改目标比特为 1 时的那个振幅,做乘法不交换,写的时候容易顺手复用 CNOT 的代码造成振幅错乱。验证方法是把输入初始化为 |1⟩,跑一遍 QFT 再跑逆 QFT(把相位取负、比特反转回来),最后应回到 |1⟩,振幅误差小于 1e-6 才算通过。这套流程跑通后,框架的实用性就有底了。
我在验证过程中踩过最深的坑是反转顺序写错:只反转了下标但没同步反转相位编码,导致逆变换后结果在 |1⟩ 和 |4⟩ 之间跳。最后是打印中间态的完整振幅表,一行一行比对才发现。从那以后我每次改完门函数,都会用 QFT 往返测试作为基本回归检查,顺便把振幅表的打印函数也写进公共工具里。希望这篇拆解能帮你少走几步弯路,拿到这份资源后按上面的路径验证一遍,心里就有数了。
本文还有配套的精品资源,点击获取