ROOT 所需的 C++ 基础¶
本页介绍《核物理实验方法》ROOT C++ 教程中用到的 C++ 语法,适合尚不熟悉 C++ 的同学。
通过探测器信号、刻度数据、能量测量和计数统计,学习变量、表达式、条件判断、循环、数组和函数。
阅读说明
阅读 ROOT Tutorial I — C++ 前,可按需补充本页内容。new、&、-> 和 ROOT 对象声明的含义,见 C++ 对象与指针。
在 notebook 中运行 C++¶
选择 ROOT C++ kernel,按顺序运行单元格。它使用 C++ 解释器 Cling,可以直接执行普通 C++ 语句;前面单元格中的定义可在后面继续使用。
一个 notebook 单元格不等同于完整的独立 C++ 程序。在源文件中,这些语句通常放在 main 函数内:
#include <iostream>
int main()
{
std::cout << "Hello" << std::endl;
return 0;
}
这样的源文件可用 g++ program.cpp -o program 编译。本课程从 notebook 开始,所以下面的示例不需要写 main()。
1. 第一个 C++ 单元格¶
下面先引入标准输入输出库,再输出一行文字。
#include <iostream>
std::cout << "C++ is ready for detector analysis." << std::endl;
C++ is ready for detector analysis.
#include <iostream> 引入标准输入输出功能。以 # 开头的是预处理指令,在 C++ 代码解释执行前处理。
std::cout 是标准输出流,<< 将右侧内容送入输出流,std::endl 用于换行。前缀 std:: 表示该名称属于 C++ 标准库。
大多数 C++ 语句以分号结束,花括号 { ... } 将语句组成代码块。// 后的内容是注释,不会执行。
2. 变量与数值类型¶
变量有类型、名称和值。探测器数据中,ADC channel 通常是整数,能量通常用浮点数保存,而筛选结果是真或假。
{
int adc_channel = 1326;
double energy_keV = 661.7;
bool above_threshold = true;
std::cout << "channel = " << adc_channel << std::endl;
std::cout << "energy = " << energy_keV << " keV" << std::endl;
std::cout << "accepted = " << above_threshold << std::endl;
}
channel = 1326 energy = 661.7 keV accepted = 1
int 保存整数,double 保存双精度浮点数,通常足以满足本课程的数值精度要求;bool 保存 true 或 false。
= 将右侧的值赋给左侧变量,不是数学上的相等判断。判断相等使用后面介绍的 ==。
示例外层的花括号建立局部作用域,其中声明的变量到右花括号处就不再存在。这也便于在不同 notebook 单元格中使用相同的清晰名称,而不与此前的变量冲突。
线性能量刻度¶
设探测器给出的 channel 为 C,用简单的线性关系换算为能量:
$$E = a + bC,$$
其中 offset 对应零点偏移 a,单位为 keV;gain 对应增益 b,单位为 keV/channel。
{
const double offset_keV = -1.2;
const double gain_keV_per_channel = 0.500;
int channel = 1326;
double calibrated_energy_keV = offset_keV + gain_keV_per_channel * channel;
std::cout << "calibrated energy = "
<< calibrated_energy_keV << " keV" << std::endl;
}
calibrated energy = 661.8 keV
const 表示变量初始化后不再改变,适合用来声明刻度常数。
常用算术运算符是 +、-、* 和 /。乘法要明确写出,如 gain * channel;可用括号明确运算顺序。
整数除法与浮点除法¶
操作数的类型会影响计算结果。两个整数相除会舍去小数部分;计数均值通常需要保留小数,因此至少将一个操作数转换为 double。
{
int total_counts = 125;
int number_of_intervals = 10;
double wrong_mean = total_counts / number_of_intervals;
double mean_counts = static_cast<double>(total_counts) / number_of_intervals;
std::cout << "integer division result = " << wrong_mean << std::endl;
std::cout << "mean counts per interval = " << mean_counts << std::endl;
}
integer division result = 12 mean counts per interval = 12.5
static_cast<double>(total_counts) 显式地将整数转为浮点数,因此第二种除法保留小数部分。误用整数除法是 C++ 中常见的数值错误,程序未必会报错。
3. 比较与事例筛选¶
探测器数据分析中,常要判断测量值是否超过阈值,或是否位于感兴趣区间内。
比较运算符有 <、<=、>、>=、== 和 !=,结果为 bool。逻辑与、或、非分别写作 &&、|| 和 !。
{
double measured_energy_keV = 659.8;
const double threshold_keV = 100.0;
const double lower_edge_keV = 655.0;
const double upper_edge_keV = 668.0;
bool above_threshold = measured_energy_keV >= threshold_keV;
bool inside_peak_window =
measured_energy_keV >= lower_edge_keV &&
measured_energy_keV <= upper_edge_keV;
std::cout << "above threshold: " << above_threshold << std::endl;
std::cout << "inside peak window: " << inside_peak_window << std::endl;
}
above threshold: 1 inside peak window: 1
只有 && 两侧的比较都成立,inside_peak_window 才为真。比较运算的优先级高于 &&;适当换行和缩进有助于看清筛选条件。
if 与 else¶
if 仅在条件成立时执行对应代码块;可选的 else 用于处理条件不成立的情况。
{
double measured_energy_keV = 72.4;
const double threshold_keV = 100.0;
if (measured_energy_keV >= threshold_keV) {
std::cout << "The signal is accepted." << std::endl;
} else {
std::cout << "The signal is below threshold." << std::endl;
}
}
The signal is below threshold.
条件写在圆括号中,其后的花括号界定对应代码块。即使只有一条语句,也建议保留花括号,减少后续修改时出错的机会。
ROOT 分析中的筛选也使用同样的语法。通过筛选的值可用 h->Fill(energy) 等语句填入直方图。
4. 定长数组¶
多个同类型的值可以保存在数组中。C 风格数组在创建时确定长度。
下面两个数组保存一一对应的刻度数据:channels 的第 i 个元素,对应 reference_energies_keV 的第 i 个元素。
{
const int n_points = 5;
double channels[n_points] = {244.0, 688.0, 1326.0, 2342.0, 2665.0};
double reference_energies_keV[n_points] =
{121.8, 344.3, 661.7, 1173.2, 1332.5};
std::cout << "first channel = " << channels[0] << std::endl;
std::cout << "third reference energy = "
<< reference_energies_keV[2] << " keV" << std::endl;
}
first channel = 244 third reference energy = 661.7 keV
数组索引从 0 开始,五个元素的合法索引为 0 到 4。C++ 不会自动阻止 channels[5] 这样的越界访问;越界读写会产生未定义行为(undefined behavior)。
ROOT 中也常用成对数组。例如,构造图对象时依次传入点数、x 数组和 y 数组。
5. 用 for 循环重复计算¶
已知重复次数时,适合用 for 循环。处理探测器数据通常就是对每次测量重复同一计算。
{
const int n_points = 5;
double channels[n_points] = {244.0, 688.0, 1326.0, 2342.0, 2665.0};
double energies_keV[n_points] = {121.8, 344.3, 661.7, 1173.2, 1332.5};
for (int i = 0; i < n_points; ++i) {
std::cout << "point " << i
<< ": channel " << channels[i]
<< ", energy " << energies_keV[i] << " keV"
<< std::endl;
}
}
point 0: channel 244, energy 121.8 keV point 1: channel 688, energy 344.3 keV point 2: channel 1326, energy 661.7 keV point 3: channel 2342, energy 1173.2 keV point 4: channel 2665, energy 1332.5 keV
for 括号内包含三部分:
for (int i = 0; i < n_points; ++i)
int i = 0 在循环前执行一次,创建索引变量;每次循环前检查 i < n_points;每次循环后用 ++i 将索引加一。因此循环遍历的索引为 0、1、……、n_points - 1。
累加与求平均值¶
常见做法是先将累加变量设为零,再在每次循环中加入一个值。
{
const int n_measurements = 6;
double energies_keV[n_measurements] =
{659.8, 662.5, 661.1, 660.7, 663.0, 661.6};
double sum_keV = 0.0;
for (int i = 0; i < n_measurements; ++i) {
sum_keV += energies_keV[i];
}
double mean_keV = sum_keV / n_measurements;
std::cout << "mean measured energy = " << mean_keV << " keV" << std::endl;
}
mean measured energy = 661.45 keV
sum_keV += energies_keV[i] 是下面语句的简写:
sum_keV = sum_keV + energies_keV[i];
累加变量是 double,所以最后进行浮点除法。增加测量次数可以提高均值的估计精度,但不会改变探测器的本征分辨率。
统计通过筛选的测量次数¶
条件判断通常与循环一起使用。下面统计 661.7 keV 峰区内的测量次数。
{
const int n_measurements = 8;
double energies_keV[n_measurements] =
{90.0, 658.9, 661.3, 662.0, 410.0, 665.2, 900.0, 660.5};
int selected_count = 0;
for (int i = 0; i < n_measurements; ++i) {
if (energies_keV[i] >= 657.0 && energies_keV[i] <= 666.0) {
++selected_count;
}
}
std::cout << selected_count << " measurements are in the peak window."
<< std::endl;
}
5 measurements are in the peak window.
++selected_count 将整数计数加一。这里尚未绘制能谱,只演示事例筛选;后续在 ROOT 中,同样的判断可放在 Fill() 之前。
while 循环的常见用法¶
如果循环何时结束要在计算过程中判断,而不是预先固定次数,可以使用 while。下面依次检查一小段数据,直到找到三个满足条件的信号,或数据已读完。
{
const int n_signals = 7;
double signals_keV[n_signals] = {20.0, 145.0, 82.0, 233.0, 95.0, 180.0, 75.0};
int index = 0;
int accepted = 0;
while (index < n_signals && accepted < 3) {
if (signals_keV[index] >= 100.0) {
++accepted;
}
++index;
}
std::cout << "examined " << index << " signals to find "
<< accepted << " accepted signals" << std::endl;
}
examined 6 signals to find 3 accepted signals
while 中的两个条件都成立时,循环才继续。注意更新 index,否则条件可能一直不变,形成死循环。ROOT Tutorial I 主要使用 for,目前能读懂 while 即可。
6. 函数¶
函数为可重复使用的计算提供一个名称。例如刻度函数接收 channel、零点偏移和增益,返回刻度后的能量。
double channel_to_energy(int channel, double offset_keV,
double gain_keV_per_channel)
{
return offset_keV + gain_keV_per_channel * channel;
}
{
double energy_keV = channel_to_energy(1326, -1.2, 0.500);
std::cout << "energy = " << energy_keV << " keV" << std::endl;
}
energy = 661.8 keV
第一个 double 是返回值类型,channel_to_energy 是函数名。括号内是形参,每个形参都声明类型和局部名称;return 给出返回结果。
调用函数时:
channel_to_energy(1326, -1.2, 0.500)
三个实参按位置依次对应 channel、零点偏移和增益,交换顺序就会改变计算。ROOT 构造函数和方法也常按位置接收参数,需要留意文档中的顺序。
向函数传递数组¶
将 C 风格数组传给函数时,还要提供有效元素个数,因为数组本身不会把长度信息一并传入函数。
double mean_of_measurements(const double values[], int n_values)
{
double sum = 0.0;
for (int i = 0; i < n_values; ++i) {
sum += values[i];
}
return sum / n_values;
}
{
const int n = 4;
double energies_keV[n] = {660.2, 662.1, 661.5, 660.9};
std::cout << "mean = " << mean_of_measurements(energies_keV, n)
<< " keV" << std::endl;
}
mean = 661.175 keV
const double values[] 表示函数可读取数组,但不修改其元素。调用时同时传入 energies_keV 和 n。TGraph(n, x, y) 等 ROOT 接口也需要明确给出数组长度。
7. 综合示例:探测器信号的刻度与筛选¶
下面综合使用前面的 C++ 语法:将每个 ADC channel 换算成能量,对超过阈值的信号求平均值,并单独统计 661.7 keV 附近的信号数。
bool inside_energy_window(double energy_keV, double center_keV,
double half_width_keV)
{
return energy_keV >= center_keV - half_width_keV &&
energy_keV <= center_keV + half_width_keV;
}
{
const int n_signals = 8;
int channels[n_signals] = {180, 1320, 1325, 1330, 810, 1328, 1600, 1317};
const double offset_keV = -1.2;
const double gain_keV_per_channel = 0.500;
const double threshold_keV = 100.0;
int accepted_count = 0;
int peak_count = 0;
double accepted_energy_sum_keV = 0.0;
for (int i = 0; i < n_signals; ++i) {
double energy_keV = channel_to_energy(
channels[i], offset_keV, gain_keV_per_channel);
if (energy_keV >= threshold_keV) {
++accepted_count;
accepted_energy_sum_keV += energy_keV;
}
if (inside_energy_window(energy_keV, 661.7, 3.0)) {
++peak_count;
}
}
double accepted_mean_keV =
accepted_energy_sum_keV / accepted_count;
std::cout << "accepted signals = " << accepted_count << std::endl;
std::cout << "mean accepted energy = " << accepted_mean_keV << " keV"
<< std::endl;
std::cout << "signals in the 661.7-keV window = " << peak_count
<< std::endl;
}
accepted signals = 7 mean accepted energy = 643.8 keV signals in the 661.7-keV window = 4
本例包含刻度、阈值筛选、累加和峰区计数,不是对完整 γ 能谱的模拟。
随机涨落和直方图留到 ROOT 教程中介绍,届时结合实例说明 TRandom3 和 TH1,这里先掌握基本 C++ 语法。
8. 后续 ROOT 代码中的基本语法¶
学完本页后,下面这部分 ROOT 分析代码应当已经能够读懂:
const int n_events = 10000;
for (int i = 0; i < n_events; ++i) {
double energy = /* one simulated or measured value */;
if (energy >= 100.0) {
/* use the accepted value */
}
}
ROOT 教程会在这一结构中加入对象和方法调用。ROOT C++ 中常见的 *、new、. 和 ->,将在第二篇 C++ 基础讲义中介绍。
练习¶
- 将综合示例中的阈值从
100.0keV 改为500.0keV,先判断哪些输出量会变化,再运行检查。 - 将峰区半宽从
3.0keV 改为1.0keV。改变的是筛选范围,不是测量能量。 - 向数组添加一个 channel 读数,并同步更新
n_signals,使循环次数与数组长度一致。
这些练习对应修改 ROOT 示例时的常见操作:调整参数、改变筛选条件(cut)和更换输入数据。
小结¶
现在应能读懂 double energy = 661.7 等声明,计算算术表达式,理解 if 和循环,使用定长数组,以及调用或定义简单函数。这些是 ROOT C++ 入门所需的数值计算与流程控制基础。
接下来阅读 ROOT Tutorial I,遇到不熟悉的语法时查阅对象与指针。