Course home · Installation · Next: Objects and pointers

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++ 基础讲义中介绍。

练习¶

  1. 将综合示例中的阈值从 100.0 keV 改为 500.0 keV,先判断哪些输出量会变化,再运行检查。
  2. 将峰区半宽从 3.0 keV 改为 1.0 keV。改变的是筛选范围,不是测量能量。
  3. 向数组添加一个 channel 读数,并同步更新 n_signals,使循环次数与数组长度一致。

这些练习对应修改 ROOT 示例时的常见操作:调整参数、改变筛选条件(cut)和更换输入数据。

小结¶

现在应能读懂 double energy = 661.7 等声明,计算算术表达式,理解 if 和循环,使用定长数组,以及调用或定义简单函数。这些是 ROOT C++ 入门所需的数值计算与流程控制基础。

接下来阅读 ROOT Tutorial I,遇到不熟悉的语法时查阅对象与指针。