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) 文件名。 - 流程:
- 打开输入
TFile并获取TTree指针。 - 创建输出
TFile和用于存储结果的TTree。 - 核心实例化:
tracking *tk = new tracking(ipt);将输入树挂载到类中。 - 启动循环:调用
tk->Loop(opt);开始处理数据。 - 安全关闭:处理完后务必
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 关键点解析:¶
wildcard文件自动扫描:通过$(wildcard ...)结合$(shell pwd)获取绝对路径,自动搜索src/和include/下的所有相关文件。这与本节开头设定的工程目录结构完全对应,未来新增分析源文件时,无需再手动修改 Makefile。DIR_INC路径包含规则:设定为-I$(shell pwd)/include并加入编译选项CFLAGS。这正是为什么在前面的main.cpp和tracking.cpp中,无需编写相对路径(如../include/tracking.h),只需简写为#include "tracking.h"的原因,编译器会自动到该-I指定的目录下寻找头文件。- 编译与链接的严格分离(重点):
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