2.4 编写可编译执行的分析程序 (II):¶
在 2.3 节中,介绍了使用 ROOT 的 tree->MakeClass("tracking") 自动生成读取数据代码(tracking.h 和 tracking.C)的方法,并将自定义的物理分析代码直接编写在了 tracking.C 的 Loop() 函数中。
然而,在核物理实验数据分析中,探测器的数量、TTree 中的 Branch 经常会发生变动。一旦数据结构改变,通常需要重新运行 MakeClass。此时,ROOT 自动生成的新文件会直接覆盖掉原有的物理分析代码。
解决方案:面向对象的“继承(Inheritance)”与“职责分离”
- 保留自动生成的代码:由
MakeClass生成的tracking类只负责底层的数据结构映射,应避免手动修改该文件。 - 类的继承(Inheritance):建立自定义的分析类
ana并继承tracking。这样ana天然具备了读取探测器原始数据的能力,只需在ana中专注编写物理算法。 - 主程序的职责划分(I/O 解耦):
ana类中应避免对物理文件进行创建操作(如new TFile)。所有文件的打开、创建,以及相关参数的解析,应当全部交由主程序main.cpp来调度,并在ana类初始化时,将所需的参数和树指针直接传入。
1. 文件和目录的组织¶
为了实现上述解耦,工程目录严格划分为“自动生成”与“用户编写”两部分:
主目录: ./tracking
├── main.cpp (主程序,负责流程控制)
├── Makefile (编译脚本,负责自动化构建)
├── include/
│ ├── tracking.h (MakeClass 生成的头文件)
│ └── ana.h (用户分析头文件)
└── src/
├── tracking.C (MakeClass 生成的源文件)
└── ana.cpp (用户分析源文件)
2. 代码完整实现方案(自顶向下结构)¶
2.1 ./tracking/main.cpp¶
作为程序的主入口,该文件负责解析终端输入的参数,动态生成文件名,创建读写流,并在初始化时将输入/输出树直接传入分析类。
(注:代码中的头文件引用采用 #include "ana.h",相应的路径搜索将由 Makefile 统一管理。)
#include <iostream>
#include <TString.h>
#include <TFile.h>
#include <TTree.h>
#include "ana.h" // 头文件路径已由 Makefile 统一管理
int main(int argc, char** argv) {
// 1. 解析命令行参数,获取 Run Number
if (argc < 2) {
std::cerr << "用法: ./tracking <run_number>" << std::endl;
return -1;
}
int run_num = atoi(argv[1]);
// 2. 利用 TString 动态拼接输入/输出文件名
TString inFileName = TString::Form("f8ppac%03d.root", run_num);
TString outFileName = TString::Form("out%03d.root", run_num);
// 3. 打开输入文件与获取原始树
TFile *f_in = new TFile(inFileName, "READ");
if (!f_in->IsOpen()) {
std::cerr << "无法打开输入文件: " << inFileName << std::endl;
return -1;
}
TTree *tree_in = (TTree*)f_in->Get("tree"); // 假设原始 TTree 名为 tree
// 4. 创建输出文件与输出树
TFile *f_out = new TFile(outFileName, "RECREATE");
TTree *tree_out = new TTree("phys_tree", "Analyzed Physical Data");
// 5. 实例化分析类 (在构造时注入各项依赖:Run号、输入树、输出树)
ana *myAna = new ana(run_num, tree_in, tree_out);
// 配置输出变量映射
myAna->SetOutBranch();
// 6. 执行核心分析循环
myAna->Analysis();
// 7. 写入数据并安全关闭所有文件 (统一管理 I/O 的闭环)
f_out->cd();
tree_out->Write();
f_out->Close();
f_in->Close();
// 8. 清理内存
delete myAna;
delete f_out;
delete f_in;
std::cout << ">>> Run " << run_num << " 分析完毕,结果保存在 " << outFileName << std::endl;
return 0;
}
2.2 ./tracking/include/ana.h¶
在该头文件中,声明继承关系、物理变量以及存储输出树的指针。
在实现继承时,子类的构造函数通过“初始化列表(Initializer List)”(即冒号 : 后面的部分)完成变量分配与数据交接:
tracking(tree_in):将输入树直接传递给父类tracking的构造函数,完成原始数据读取的准备。
#ifndef ana_h
#define ana_h
#include "tracking.h"
#include <TTree.h>
class ana : public tracking // 从 tracking 类继承,获取所有原始 Branch 变量
{
public:
int run; // 存储由 main 传入的 Run Number
TTree *fOutTree; // 存储由 main 传入的输出树指针
Double_t c2nx, c2ny; // 自定义的物理变量
Double_t my_tx;
// 构造函数:利用初始化列表,配置父类与子类的成员变量
ana(int run_number, TTree* tree_in, TTree* tree_out)
: tracking(tree_in), run(run_number), fOutTree(tree_out) {}
virtual ~ana() {};
// 接口 1:设置输出 TTree 的 Branch
virtual void SetOutBranch();
// 接口 2:核心分析循环
virtual void Analysis();
};
#endif
“初始化列表(Initializer List)”
1. tracking(tree_in):调用父类构造函数(传递参数)¶
- 作用:因为
ana继承自tracking,在创建子类ana的对象时,必须先将父类tracking建立起来。 - 机制:这里的操作是“传递参数”。它相当于显式调用了父类的构造函数,把
tree_in这个指针交给了父类,让父类去完成底层 ROOT 树分支(Branch)的绑定工作。
2. run(run_number) 与 fOutTree(tree_out):成员变量初始化(功能等同于赋值)¶
- 作用:用来设定子类
ana自身拥有的成员变量(run和fOutTree)的初始状态。 - 机制(初始化 vs 赋值):虽然它的实际效果和在大括号里面写
run = run_number;一样,但在 C++ 中,写在冒号后面的这部分叫做“初始化(Initialization)”,而写在大括号{}内部的叫做“赋值(Assignment)”。
为什么推荐用这种写法而不是在大括号里赋值? 如果写成赋值的形式:
ana(int run_number, TTree* tree_in, TTree* tree_out) : tracking(tree_in) {
run = run_number; // 这是赋值
fOutTree = tree_out; // 这是赋值
}
编译器会先给 run 和 fOutTree 分配内存并赋予一个默认的随机值(完成初始化),然后才执行大括号里的“赋值”操作将它们覆盖。
而使用初始化列表 run(run_number), fOutTree(tree_out) 时,编译器在分配内存的瞬间就直接把值放进去了。这不仅是 C++ 的标准规范,对于复杂对象来说也能提高程序的运行效率。
2.3 用户分析具体实现:./tracking/src/ana.cpp¶
此处仅编写纯粹的物理逻辑。由于 fOutTree 已在对象初始化时保存为成员变量,内部函数可直接对其进行操作 (Branch 和 Fill)。
#include "ana.h"
#include <iostream>
// 配置输出变量
void ana::SetOutBranch() {
if (!fOutTree) return; // 确保输出树指针有效
// 将计算得出的物理变量的内存地址,映射为新 TTree 中的 Branch
fOutTree->Branch("tx", &my_tx, "tx/D");
fOutTree->Branch("c2nx", &c2nx, "c2nx/D");
}
// 核心分析代码
void ana::Analysis() {
if (fChain == 0 || fOutTree == 0) return; // 确保输入输出流均有效
Long64_t nentries = fChain->GetEntriesFast();
std::cout << "Run Number: " << run << " | 正在处理 Event 数量: " << nentries << std::endl;
for (Long64_t jentry = 0; jentry < nentries; jentry++) {
Long64_t ientry = LoadTree(jentry); // 父类方法:加载当前 entry
if (ientry < 0) break;
fChain->GetEntry(jentry); // 父类方法:读取数据到内存
// --- 物理算法区域 ---
my_tx = -999; // 每次循环初始化为非物理值,防止上一事件数据残留
c2nx = -999;
// 执行物理计算...
double offset = (run > 100) ? 1.5 : 0;
my_tx = 12.34 + offset; // 示例赋值
c2nx = 0.5;
// 将本 Event 计算出的结果填入输出树
fOutTree->Fill();
}
}
分离架构的优势¶
当 I/O 和参数解析剥离到 main,编译工作交由 Makefile 统筹后,程序即具备了自动化批处理(Batch Processing)的能力。
在实验数据处理期间,只需编写一个简单的 Bash 脚本:
#!/bin/bash
for run in {1..100}; do
./tracking $run
done
即可自动处理海量数据。而核心物理分析代码 ana.cpp 无需做任何修改,这体现了软件工程思维在核物理数据分析中的应用价值。