2.3 编写可编译执行的分析程序 (I)¶

1. 运行环境的转变:从“解释”到“编译”¶

  • 解释执行 (.x mycode.cpp):ROOT 动态解析每一行代码。优点是方便,缺点是运行效率低,且不利于构建大规模、结构复杂的分析系统。
  • 编译执行 (Recommended):通过 g++ 将代码直接转化为机器码。ROOT 库作为外部库通过 #include 调用。这种方式运行速度极快,代码结构清晰,是进行复杂物理分析的首选。

2. 工程目录组织¶

一个标准的 C++ 分析工程应当将不同功能的文件分目录存放:

  • ./tracking/:主目录,存放 main.cpp(程序入口)和 Makefile(构建脚本)。
  • ./tracking/include/:存放头文件 (.h, .hh)。
  • ./tracking/src/:存放源文件 (.C, .cpp)。

3. 利用 MakeClass 生成基础框架¶

首先,利用 ROOT 的自动化工具生成映射 TTree 的基类:

root -l f8ppac001.root
[0] tree->MakeClass("tracking")

生成的 tracking.h 移至 include/,tracking.C 移至 src/。


4. 主程序:main.cpp¶

main 函数是整个分析程序的入口,负责调度。

  • 参数解析:使用 argc 和 argv 接收命令行参数(如运行号 run_number)。
  • 文件名自动化:利用 TString::Form() 动态生成输入 (f8ppac%03d.root) 和输出 (out%03d.root) 文件名。
  • 流程:
    1. 打开输入 TFile 并获取 TTree 指针。
    2. 创建输出 TFile 和用于存储结果的 TTree。
    3. 核心实例化:tracking *tk = new tracking(ipt); 将输入树挂载到类中。
    4. 启动循环:调用 tk->Loop(opt); 开始处理数据。
    5. 安全关闭:处理完后务必 Close() 文件,确保数据从内存完全写入磁盘。。
#include <iostream> 
#include <TFile.h>
#include <TTree.h>
#include <TString.h>
#include "tracking.h" // 包含用户定义的类

using namespace std;

int main(int argc, char* argv[]) {
    if(argc != 2) {
        cout << "Usage: ./" << argv[0] << " run_number " << endl;
        return -1;
    }
    int run_number = atoi(argv[1]);
    
    TString infile, outfile;  
    infile.Form("./f8ppac%03d.root", run_number);
    outfile.Form("./out%03d.root", run_number);

    // 1. 输入管理
    TFile *ipf = new TFile(infile);
    if(!ipf->IsOpen()) return -1;
    TTree *ipt = (TTree*)ipf->Get("tree");

    // 2. 输出管理
    TFile *opf = new TFile(outfile, "RECREATE");
    TTree *opt = new TTree("tree", "ppac tracking");

    // 3. 核心对象实例化与执行
    tracking *tk = new tracking(ipt); 
    tk->Loop(opt); // 将输出 Tree 传入 Loop 进行填充

    ipf->Close();
    opf->Close();
    delete tk;
    return 1;        
}

在编写复杂的 C++ 程序时,遵循“声明与实现分离”的原则至关重要:

  • 头文件 (.h):只写类的声明、成员变量、函数原型和宏定义。不写函数的具体实现逻辑。
  • 源文件 (.C / .cpp):” 编写函数的具体逻辑。

这种分离可以加快编译速度,并使代码逻辑更加清晰。

5. 头文件:include/tracking.h¶

头文件用于声明类、变量和方法。

在编写 ./include/tracking.h 时,必须在文件的开头和结尾加上预编译语句。这是为了防止在复杂的工程中,同一个头文件被多次 #include 而导致“重复编译”错误。

规范格式如下:

#ifndef TRACKING_H   // 如果没有定义 TRACKING_H
#define TRACKING_H   // 那么定义 TRACKING_H

// --- 你的代码声明写在这里 ---
class tracking {
    // ...
};

#endif               // 结束判断
  • 通常使用大写的文件名(如 TRACKING_H),只要确保在整个工程中唯一即可。
#ifndef tracking_h
#define tracking_h

#include <TROOT.h>
#include <TChain.h>
#include <TFile.h>
#include <TH2.h>

class tracking {
public :
   TTree          *fChain;   //!指向分析的 TTree 的指针
   // --- 原 Branch 变量 (MakeClass 生成) ---
   Float_t         PPACF8[5][5];
   Float_t         targetX, targetY;

   // --- 用户自定义变量 (User-defined) ---
   Double_t xx[3], xz[3], yy[3], yz[3]; // PPAC 位置数据
   Double_t dx[3], dy[3];              // 残差 (Residuals)
   Double_t tx, ty;                    // 靶点外推位置 (Target Pos)
   Double_t c2nx, c2ny;                // 拟合质量 Chi2/ndf

   // --- 函数声明 ---
   tracking(TTree *tree=0);
   virtual void Loop(TTree *tree);
   virtual void SetOutBranch(TTree *tree); // 关键:定义输出结构
   virtual void TrackInit();               // 关键:每粒子初始化
};
#endif

6. 源文件:src/tracking.C¶

在源文件中,我们通过 tracking:: 前缀来实现头文件中声明的方法。

注意:必须包含 #include "tracking.h"。

#define tracking_cxx
#include <TH2.h>
#include "tracking.h"

using namespace std;

void tracking::SetOutBranch(TTree *tree) {
    // 建立内存变量与输出文件 Branch 的联系
    tree->Branch("xx", &xx, "xx[3]/D");
    tree->Branch("dx", &dx, "dx[3]/D");
    tree->Branch("tx", &tx, "tx/D");
}

void tracking::TrackInit() {
    // 每粒子处理前清空变量,防止上一个 Event 的数据污染
    tx = -999; ty = -999;
    for(int i=0; i<3; i++) {
        xx[i] = PPACF8[i][0]; // 从原始数组提取
        // ... 其他位置初始化
    }
}

void tracking::Loop(TTree *tree) {
    if (fChain == 0) return;
    SetOutBranch(tree); // 循环前初始化输出分支

    Long64_t nentries = fChain->GetEntriesFast();
    for (Long64_t jentry=0; jentry<nentries; jentry++) {
        Long64_t ientry = LoadTree(jentry);
        if (ientry < 0) break;
        fChain->GetEntry(jentry); // 读取当前 Entry 数据

        TrackInit(); // 初始化用户变量

        // --- 这里编写物理算法 ---
        // 例如:调用之前讨论过的拟合函数计算 dx[i] 和 tx
        
        tree->Fill(); // 将本粒子计算结果填入输出树
    }
}

7. 自动化编译:Makefile¶

为了将上述分散在不同目录下的代码(主程序、源文件、头文件)一键编译为可执行程序,需要利用 Makefile 进行自动化构建。

Makefile 相当于一个构建剧本,告诉 make 命令如何正确地传递参数、编译源代码并链接所需的动态库。

  • 获取 ROOT 环境参数:通过 ROOT 提供的自带工具 root-config,程序可以自动适配不同电脑上的 ROOT 安装路径。
    • ROOTCFLAGS = $(shell root-config --cflags):获取 ROOT 的头文件搜索路径和预处理宏。
    • ROOTLIBS = $(shell root-config --libs):获取 ROOT 的核心基础架构库(Core, Tree, Hist 等)。
    • ROOTGLIBS = $(shell root-config --glibs):获取包含图形界面支持的更全面的库(GUI, Gpad 等)。
  • 编译与链接指令: $(GXX) $(CFLAGS) -o $@ $(MainFile) $(SourceFile) $(LIBS) 这条指令将编译(Compile)与链接(Link)合并为一步,直接将主程序、所有源文件以及 ROOT 动态库打包,生成最终的可执行文件 tracking(即 $@ 代表的目标文件)。

以下为标准 Makefile 脚本:

#############################################################################
OBJ = tracking
MainFile = main.cpp

###############################################################################

# 自动匹配 src 和 include 目录下的所有源文件和头文件
SourceFile := $(wildcard $(shell pwd)/src/*.c $(shell pwd)/src/*.cc $(shell pwd)/src/*.C $(shell pwd)/src/*.cpp $(shell pwd)/src/*.cxx)
IncludeFile := $(wildcard $(shell pwd)/include/*.h $(shell pwd)/include/*.hh $(shell pwd)/include/*.hpp)

###############################################################################

ROOTCFLAGS  = $(shell root-config --cflags)
ROOTLIBS    = $(shell root-config --libs)
ROOTGLIBS   = $(shell root-config --glibs)

GXX = g++ 

# 包含路径 (-I) 告知编译器头文件的搜索目录
DIR_INC = -I$(shell pwd)/include

# 编译选项:动态链接库决不能写在这里
CFLAGS = -Wall -O2 $(ROOTCFLAGS) $(DIR_INC)  

# 链接选项:动态链接库必须写在这里 (例如 ROOT 额外的寻峰库 -lSpectrum)
LIBS = $(ROOTGLIBS) -lSpectrum   

###############################################################################

all:$(OBJ)

# 编译与链接规则
$(OBJ): $(MainFile) $(SourceFile) $(IncludeFile)
	$(GXX) $(CFLAGS) -o $@ $(MainFile) $(SourceFile) $(LIBS)
	@echo "=============================================================="
	@echo "$@ done !"
	@echo "=============================================================="

clean:
	rm -f *.o *.d $(OBJ)

Makefile 关键点解析:¶

  1. wildcard 文件自动扫描:通过 $(wildcard ...) 结合 $(shell pwd) 获取绝对路径,自动搜索 src/ 和 include/ 下的所有相关文件。这与本节开头设定的工程目录结构完全对应,未来新增分析源文件时,无需再手动修改 Makefile。
  2. DIR_INC 路径包含规则:设定为 -I$(shell pwd)/include 并加入编译选项 CFLAGS。这正是为什么在前面的 main.cpp 和 tracking.cpp 中,无需编写相对路径(如 ../include/tracking.h),只需简写为 #include "tracking.h" 的原因,编译器会自动到该 -I 指定的目录下寻找头文件。
  3. 编译与链接的严格分离(重点):CFLAGS 用于预处理和编译阶段(包括警告提示 -Wall、优化等级 -O2 和头文件路径),而 LIBS 专门用于链接阶段。外部的动态链接库(如 ROOT 的库以及额外的 -lSpectrum 等)必须且只能放置在 LIBS 变量中,且在编译命令中必须放在源文件的后面。否则,由于现代 GCC 编译器的符号解析顺序机制,会导致编译时抛出“未定义引用 (Undefined reference)”的致命错误。

使用方法:¶

make clean

make

  • ROOT环境内直接用.x myClass.C运行时 ROOT 自动载入所用函数的库文件,因此不用显式包含库文件;make编译时要求用户在代码中显式包含库文件。

    • 如程序内使用了TGraph,则需要在头部加入
        #include <TGraph.h>
  • c++或ROOT的库文件:include 头文件用尖括号<>:
      #include <iostream>
      #include <TGraph.h> 
  • 用户编写的的头文件用双引号”“:如
      #include "mylib.h"
  
  
  • 未包含函数的库文件是root编程中最常见的错误。

  • 直接使用cout, 或cin等在 std 的 namespace 上定义的函数也会报错。

    • 加入using namespace std; 或直接使用std::cout,std::cin(推荐)
  • C++编译器编译报错给出一堆错误信息时,一般只修正出现的第一个错误信息,然后再次编译。按上述步骤依次进行修改。

  • 编译成功时输出如下的信息:


g++  -Wall -O2 -I/Users/zhli/ROOT/root61804/include  -I/Users/zhli/ana/course/data_analysis/chapt2/compile/include -I/Users/zhli/ROOT/root61804/include  -L/Users/zhli/ROOT/root61804/lib -lCore -lImt -lRIO -lNet -lHist -lGraf -lGraf3d -lGpad -lROOTVecOps -lTree -lTreePlayer -lRint -lPostscript -lMatrix -lPhysics -lMathCore -lThread -lMultiProc -lROOTDataFrame -lpthread -stdlib=libc++ -lm -ldl -lSpectrum -lXMLParser  -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -pthread -stdlib=libc++ -std=c++11 -m64 -I/Users/zhli/ROOT/root61804/include -L/Users/zhli/ROOT/root61804/lib -lCore -lImt -lRIO -lNet -lHist -lGraf -lGraf3d -lGpad -lROOTVecOps -lTree -lTreePlayer -lRint -lPostscript -lMatrix -lPhysics -lMathCore -lThread -lMultiProc -lROOTDataFrame -lpthread -stdlib=libc++ -lm -ldl -L/Users/zhli/ROOT/root61804/lib -lGui -lCore -lImt -lRIO -lNet -lHist -lGraf -lGraf3d -lGpad -lROOTVecOps -lTree -lTreePlayer -lRint -lPostscript -lMatrix -lPhysics -lMathCore -lThread -lMultiProc -lROOTDataFrame -lpthread -stdlib=libc++ -lm -ldl -o tracking main.cpp /Users/zhli/ana/course/data_analysis/chapt2/compile/src/tracking.C
==============================================================
tracking done !
==============================================================

./tracking 1

In [1]:
!ls -R
2.3_comiling_1.html	f8ppac001.root		out001.root
2.3_comiling_1.ipynb	include			src
Makefile		main.cpp		tracking
Untitled.ipynb		main.cpp~

./include:
tracking.h	tracking.h~

./src:
tracking.C	tracking.C~

In [2]:
!./tracking 1
Inputfile: ./f8ppac001.root


Outputfile: ./out001.root


processing 400000


processing 600000