ROOT 所需的 C++ 对象与指针¶
本页接续《ROOT 所需的 C++ 基础》,介绍《核物理实验方法》ROOT C++ 教程中的对象操作,适合尚未使用过 C++ 类和指针的同学。
内容包括类名、构造函数、方法、指针、引用,以及 ::、-> 等运算符。
阅读范围¶
遇到 ROOT 示例中的以下语法时,可查阅本页:
- 命名空间与
::; - 类、对象、构造函数与方法;
- 通过对象使用
.,通过指针使用->; - 取地址运算符
&、指针、new、delete和nullptr; - 函数的值传递、引用传递和指针传递;
- 将数组传给函数;
- 理解 ROOT 类型关系所需的基本继承概念。
1. 命名空间与 ::¶
大型程序库包含许多名称。命名空间(namespace)将相关名称组织在一起,减少名称冲突;C++ 标准库使用 std 命名空间。
#include <iostream>
#include <cmath>
#include <string>
#include <vector>
{
double variance = 25.0;
double sigma = std::sqrt(variance);
std::cout << "sigma = " << sigma << std::endl;
}
sigma = 5
std::sqrt 表示 std 命名空间中的 sqrt 函数。作用域运算符 :: 也出现在 ROOT 中,例如 TMath::Pi() 调用 TMath 命名空间中的 Pi 函数。
有些示例先写 using namespace std;,之后便可用 cout 代替 std::cout。本页保留明确的前缀,便于看清名称来源,也避免不同程序库之间的冲突。
2. 类与对象¶
类(class)将数据和相关操作组合为一种类型;对象(object)是该类型的一个实例。ROOT 提供 TH1F、TF1、TGraph 和 TFile 等类,分析时再根据需要创建具体对象。
下面用一个简单类表示一次探测器读数,先熟悉 C++ 语法,再进入 ROOT 的物理分析和绘图。
class DetectorReading
{
public:
DetectorReading(double energy_keV, double time_ns)
: energy_keV_(energy_keV), time_ns_(time_ns)
{
}
double energy() const
{
return energy_keV_;
}
double time() const
{
return time_ns_;
}
bool passes_threshold(double threshold_keV) const
{
return energy_keV_ >= threshold_keV;
}
private:
double energy_keV_;
double time_ns_;
};
class DetectorReading 定义一种新类型。public 部分列出外部代码可调用的操作;本例的 private 数据由类内部管理,外部通过公开方法访问。
与类同名的函数是构造函数(constructor),创建对象时自动执行。它的两个参数分别设置能量和时间。下面这种写法
: energy_keV_(energy_keV), time_ns_(time_ns)
称为构造函数的初始化列表。ROOT 类内部也采用类似方式;使用 ROOT 时通常只需按接口要求传入参数。
energy()、time() 和 passes_threshold() 后的 const 表示这些方法不修改对象。
创建对象并调用方法¶
{
DetectorReading reading(661.7, 104.2);
std::cout << "energy = " << reading.energy() << " keV" << std::endl;
std::cout << "time = " << reading.time() << " ns" << std::endl;
std::cout << "passes 100-keV threshold: "
<< reading.passes_threshold(100.0) << std::endl;
}
energy = 661.7 keV time = 104.2 ns passes 100-keV threshold: 1
DetectorReading reading(661.7, 104.2); 创建名为 reading 的对象。参数按构造函数的声明顺序,依次为能量(keV)和时间(ns)。
reading.energy() 中的点号选择对象的方法,圆括号表示调用该方法。阈值 100.0 则是传给 passes_threshold 的参数。
直接创建 ROOT 对象时,也采用相同写法:
histogram.Fill(energy);
3. 地址与指针¶
内存中的每个对象都有地址。指针(pointer)是一种保存地址的变量。
{
DetectorReading reading(344.3, 98.5);
DetectorReading* pointer_to_reading = &reading;
std::cout << "object address = " << pointer_to_reading << std::endl;
std::cout << "energy through pointer = "
<< pointer_to_reading->energy() << " keV" << std::endl;
}
object address = 0x7ffe6f469be8 energy through pointer = 344.3 keV
DetectorReading* 表示“指向 DetectorReading 对象的指针”。&reading 取得已有对象的地址,并没有创建第二个读数对象;指针指向的仍是同一个对象。
通过对象调用方法用 .,通过对象指针调用用 ->:
reading.energy(); // object
pointer_to_reading->energy(); // pointer to object
-> 后仍是普通的方法调用。ROOT 经常通过指针操作对象,因此这种写法很常见。
指针解引用¶
* 有两种相关用法:在声明中构成指针类型,如 double*;在表达式中则访问该地址处的对象,称为解引用(dereference)。
{
double energy_keV = 511.0;
double* energy_pointer = &energy_keV;
std::cout << "value through pointer = " << *energy_pointer << std::endl;
*energy_pointer = 512.0;
std::cout << "value stored in energy_keV = " << energy_keV << std::endl;
}
value through pointer = 511 value stored in energy_keV = 512
给 *energy_pointer 赋值会改变 energy_keV,因为两者访问的是同一块内存。
nullptr 与有效性检查¶
指针也可能不指向任何对象。现代 C++ 用 nullptr 表示空指针。
{
DetectorReading* selected_reading = nullptr;
if (selected_reading == nullptr) {
std::cout << "No reading has been selected." << std::endl;
}
}
No reading has been selected.
不能通过 nullptr 调用方法。ROOT 从文件或目录中查找对象时,如果没有找到,可能返回空指针;因此在可能失败的情况下,先检查指针再使用 ->。本例中简写为 if (!selected_reading) 含义相同。
4. 用 new 和 delete 管理动态对象¶
new 动态创建对象,并返回指向它的指针。许多 ROOT macro 和 notebook 都使用这种写法。
{
DetectorReading* reading = new DetectorReading(1173.2, 210.0);
std::cout << "dynamic reading energy = "
<< reading->energy() << " keV" << std::endl;
delete reading;
reading = nullptr;
}
dynamic reading energy = 1173.2 keV
new 后的表达式调用构造函数,返回的指针保存在 reading 中,因此用 -> 调用方法。
delete reading 销毁动态创建的对象并释放内存。随后将指针设为 nullptr,避免它看起来仍指向有效对象。对象删除后,不要再通过原指针访问它。
交互式 ROOT 示例常用 new 创建对象,使其在当前代码块结束后仍然存在。ROOT 还有对象所有权(ownership)规则:文件、目录、画布等容器可能负责管理对象。具体规则需结合相应示例理解,不能只凭指针语法判断。
这里不需要使用 new[] 创建动态数组。入门分析用定长数组和 std::vector 即可,也更便于管理内存。
5. 函数参数:值、引用与指针¶
C++ 函数可以通过多种方式接收数据,ROOT 接口会用到以下三种方式。
double calibrated_copy(double raw_energy_keV, double gain)
{
return raw_energy_keV * gain;
}
void apply_gain_in_place(double& energy_keV, double gain)
{
energy_keV *= gain;
}
void clear_measurement(double* energy_keV)
{
if (energy_keV != nullptr) {
*energy_keV = 0.0;
}
}
{
double measured_energy_keV = 660.0;
double copied_result_keV = calibrated_copy(measured_energy_keV, 1.0025);
std::cout << "original after value call = "
<< measured_energy_keV << " keV" << std::endl;
std::cout << "returned copy = " << copied_result_keV << " keV"
<< std::endl;
apply_gain_in_place(measured_energy_keV, 1.0025);
std::cout << "original after reference call = "
<< measured_energy_keV << " keV" << std::endl;
clear_measurement(&measured_energy_keV);
std::cout << "original after pointer call = "
<< measured_energy_keV << " keV" << std::endl;
}
original after value call = 660 keV returned copy = 661.65 keV original after reference call = 661.65 keV original after pointer call = 0 keV
calibrated_copy 接收的是数值的副本,修改形参不会改变调用者的变量;函数通过返回值给出计算结果。
apply_gain_in_place 中的 double& 是引用参数,相当于调用者变量的另一个名称,因此函数可以直接修改原变量。调用时直接写变量名,不加 &。
clear_measurement 中的 double* 是指针参数。调用者通过 &measured_energy_keV 传入地址,函数通过 *energy_keV 访问对应的值。
后续 ROOT 代码中的两种写法会用到这些概念:
rng.Sphere(x, y, z, 1.0); // x, y, and z are modified by reference
tree->Branch("energy", &energy); // the branch receives an address
ROOT 教程将介绍这些操作的具体作用。目前先理解:为什么有的实参需要写 &,有的不需要。
6. 向函数传递数组¶
当函数需要指向首元素的指针时,可以传入 C 风格数组。数组长度不会自动传入,因此还要单独提供元素个数。
void print_calibration_points(const double channels[],
const double energies_keV[],
int n_points)
{
for (int i = 0; i < n_points; ++i) {
std::cout << channels[i] << " " << energies_keV[i] << std::endl;
}
}
{
const int n = 4;
double channels[n] = {244.0, 688.0, 1326.0, 2665.0};
double energies_keV[n] = {121.8, 344.3, 661.7, 1332.5};
print_calibration_points(channels, energies_keV, n);
}
244 121.8 688 344.3 1326 661.7 2665 1332.5
调用时,channels 和 energies_keV 前面不加 &;在这里,数组名会提供首元素地址。明确传入 n,可使循环保持在合法范围内。
这也解释了 ROOT 图对象的构造方式:
TGraph graph(n, x_values, y_values);
第一个参数是点数,第二、三个参数分别是保存对应坐标的数组。
7. 长度可变的容器:std::vector¶
当元素个数在运行时发生变化,通常用 std::vector 比手动分配数组更方便,因为它会管理自身的内存。
{
std::vector<double> deposited_energies_keV = {1.2, 4.8, 2.6};
deposited_energies_keV.push_back(3.1);
for (std::size_t i = 0; i < deposited_energies_keV.size(); ++i) {
std::cout << "deposit " << i << " = "
<< deposited_energies_keV[i] << " keV" << std::endl;
}
}
deposit 0 = 1.2 keV deposit 1 = 4.8 keV deposit 2 = 2.6 keV deposit 3 = 3.1 keV
push_back 在末尾追加一个元素,size() 返回当前元素个数。std::size_t 是适合表示数组或容器大小的无符号整数类型。
vector 离开作用域时会释放自身存储,不需要手动 delete。作业 2.1 中,事例时间列表的长度取决于产生的事例数,适合使用 vector;作业 4.1 的三个探测器能量和作业 5.1 的 250 个波形采样点则使用定长数组。
8. 函数重载¶
C++ 允许参数列表不同的函数使用同一个名称,这称为重载(overload)。编译器根据调用时传入的参数选择对应版本。
void report_energy(double energy_keV)
{
std::cout << "E = " << energy_keV << " keV" << std::endl;
}
void report_energy(double energy_keV, double sigma_keV)
{
std::cout << "E = " << energy_keV
<< " +/- " << sigma_keV << " keV" << std::endl;
}
report_energy(661.7);
report_energy(661.7, 1.2);
E = 661.7 keV E = 661.7 +/- 1.2 keV
第一次调用传入一个参数,匹配第一个函数;第二次传入两个参数,匹配第二个函数。ROOT 的 Draw、Fit 和直方图构造函数也有重载,因此文档中会列出多个同名接口。
重载并不意味着可以随意传参;参数个数、顺序和类型仍要与文档中的某个接口匹配。
9. 读懂一个 ROOT 对象¶
下面借助 ROOT 直方图说明 C++ 语法。直方图的创建、bin 设置、填充、绘制和统计含义,将在 ROOT Tutorial I 中系统介绍。
#include <TH1F.h>
{
TH1F* spectrum = new TH1F(
"spectrum_cpp_bridge",
"Measured energy;Energy (keV);Counts",
100,
0.0,
1000.0
);
spectrum->Fill(661.7);
std::cout << "entries = " << spectrum->GetEntries() << std::endl;
delete spectrum;
spectrum = nullptr;
}
entries = 1
从左到右看对象创建语句:
TH1F是 ROOT 类。TH1F* spectrum声明名为spectrum的指针。new TH1F(...)创建直方图并返回其地址。"spectrum_cpp_bridge"是对象在 ROOT 中的内部名称。ROOT 用名称查找和保存对象,同一目录中的名称应避免重复。- 第二个字符串依次写图的标题、x 轴标题和 y 轴标题,用分号分隔。
100是 bin 的个数。0.0和1000.0分别给出 x 轴下限和上限,单位为 keV。
spectrum 是指针,所以用 -> 调用方法。Fill(661.7) 将一次能量测量填入对应 bin,使计数增加一;GetEntries() 返回传给 Fill 的值的总数。
10. 读懂 ROOT 中的继承关系¶
ROOT 的类具有继承关系。例如 TH1F 从更通用的 TH1 派生而来;因此,指向基类的指针也可以指向派生类对象。
#include <TH1.h>
{
TH1F histogram("inheritance_example", "", 20, 0.0, 10.0);
TH1* general_histogram = &histogram;
std::cout << "number of bins = "
<< general_histogram->GetNbinsX() << std::endl;
}
number of bins = 20
histogram 是 TH1F 对象,general_histogram 是指向同一对象的 TH1* 指针。通过基类指针,可以调用 TH1 中定义的 GetNbinsX 等方法。
11. 常见的 ROOT 类型名¶
ROOT 示例常用类型别名表示基本 C++ 类型:
Int_t event_id = 0; // integer, normally equivalent to int
Float_t signal = 0.0F; // single-precision floating point
Double_t energy = 0.0; // double-precision floating point
Bool_t accepted = false; // Boolean value
0.0F 中的后缀 F 表示该常量为 float;不加后缀的小数常量通常是 double。这些 ROOT 类型别名在旧 macro 和 TTree 示例中很常见,普通的 int、float、double 和 bool 仍然可以使用。
12. ROOT 指针用法实例¶
下面这段代码代表了后续教程中的一种常见写法:
TFile* input = TFile::Open("data.root", "READ");
if (input == nullptr || input->IsZombie()) {
std::cout << "The file could not be opened." << std::endl;
}
TFile* 声明指针,TFile::Open 通过 :: 调用类的静态函数,不需要预先创建 TFile 对象。打开成功后,input 指向文件对象,input->IsZombie() 调用其方法;若返回空指针,前面的检查可避免无效调用。
本节只解释 C++ 结构。文件打开模式、对象所有权、关闭文件和读回对象,将在 ROOT 文件读写部分介绍。
13. 综合示例:处理探测器读数对象¶
下面创建多个探测器读数对象,通过方法检查阈值,并累加通过筛选的能量,将对象操作与前一讲的循环、条件判断结合起来。
{
const int n_readings = 5;
DetectorReading readings[n_readings] = {
DetectorReading(42.0, 100.1),
DetectorReading(121.8, 101.4),
DetectorReading(344.3, 102.0),
DetectorReading(78.5, 103.2),
DetectorReading(661.7, 104.1)
};
int accepted = 0;
double energy_sum_keV = 0.0;
for (int i = 0; i < n_readings; ++i) {
if (readings[i].passes_threshold(100.0)) {
++accepted;
energy_sum_keV += readings[i].energy();
}
}
std::cout << "accepted readings = " << accepted << std::endl;
std::cout << "mean accepted energy = "
<< energy_sum_keV / accepted << " keV" << std::endl;
}
accepted readings = 3 mean accepted energy = 375.933 keV
readings[i] 是对象,用 . 调用方法;若数组中保存的是指针,则使用 ->。阈值作为参数传入方法,通过筛选的能量与前一讲一样逐个累加。
在 ROOT 中,直方图对象可以代替手工计数。例如 spectrum->Fill(readings[i].energy()) 会找到对应 bin 并增加计数。下一讲将进一步说明 Fill 的作用。
练习¶
- 创建能量为
511.0keV 的DetectorReading对象,用.输出其能量。 - 用
&取得该对象的地址,再通过指针用->输出同一能量。 - 在 ROOT 直方图示例中,将能量范围改为
0.0–1500.0keV,仍保留 100 个 bin。bin 宽度由 10 keV 变为 15 keV,填入的事例数不变。 - 说明
double& value与clear_measurement(&energy)中&的区别:前者声明引用参数,后者取得变量地址。
小结¶
现在应能区分类和对象,读懂构造函数调用,区分 . 与 ->,理解 new 返回的指针和 & 传入的地址,并理解数组接口为什么通常还需要长度参数。
掌握这些语法后,即可阅读 ROOT Tutorial I — C++,进一步学习 TF1、TGraph、随机数、直方图、文件和 TTree 的功能与应用。