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 无需做任何修改,这体现了软件工程思维在核物理数据分析中的应用价值。

In [ ]: