1 条题解

  • 0
    @ 2026-10-21 18:08:39

    GESP 2024年09月认证 C++ 4级真题 —— NOI 金牌讲师逐题深度剖析与题解

    金牌讲师寄语: GESP 4级是 C++ 从语法基础迈向算法与底层数据结构的关键台阶。本套真题围绕 函数与参数传递机制、变量作用域与遮蔽规则、指针基础与内存模型、结构体与二维数组、排序算法原理与稳定性、异常处理与 I/O 重定向 等核心考点展开。 学习编程不仅要“知其然”,更要从内存布局、编译器行为和算法运行逻辑上“知其所以然”。希望同学们通过本篇题解,夯实编程内功,建立清晰的底层思维!


    快速查对表

    单项选择题(每题 2 分,共 30 分)

    题号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
    答案 A B A B D A B D A B C A C

    判断题(每题 2 分,共 20 分)

    题号 16 17 18 19 20 21 22 23 24 25
    答案 √ × √ × √

    一. 单选题逐题深度解析 (每题 2 分,共 30 分)

    第 1 题

    【题目重现】
    在 C++ 中,( )正确定义了⼀个返回整数值并接受两个整数参数的函数。
    A. int add(int a, int b) { return a + b; }
    B. void add(int a, int b) { return a + b; }
    C. int add(a, b) { return a + b; }
    D. void add(int a, int b) { return a - b; }

    • 【标准答案】:A
    • 【考点定位】:C++ 函数声明与定义的语法规范(返回值类型与形参列表)。
    • 【深度解析】:
      1. C++ 属于强类型静态语言,函数定义格式为:返回值类型 函数名(形参类型1 形参名1, 形参类型2 形参名2, ...) { 函数体 }。
      2. 题干要求“返回整数值”,因此函数的返回值类型必须是 int,不能是 void(B、D 选项返回值类型为 void,语法错误)。
      3. C 选项中形参列表中省略了形参类型 (a, b),在 C++ 中属于语法错误(C++ 必须明确指定参数类型)。
      4. A 选项指定返回值类型为 int,形参声明为 (int a, int b),完全符合规范。故选 A。

    第 2 题

    【题目重现】
    在 C++ 中,形参与实参的关系描述正确的是( )。
    A. 形参在函数调用时指定,实参在函数定义时传递
    B. 形参在函数定义时指定,实参在函数调用时传递
    C. 形参和实参可以互换
    D. 形参和实参必须是完全相同的类型,不能有任何差异。

    • 【标准答案】:B
    • 【考点定位】:形式参数(形参)与实际参数(实参)的概念辨析与传递机制。
    • 【深度解析】:
      • 形参(Parameter):全称形式参数,是在函数定义/声明时指定的变量占位符,用于接收外界传入的数据,仅在函数被调用时分配临时内存空间。
      • 实参(Argument):全称实际参数,是在函数调用时实际传入的具体值、变量或表达式。
      • A 选项将定义与调用的时机完全颠倒;
      • C 选项概念不可随意互换;
      • D 选项不准确:当实参类型与形参类型存在隐式类型转换(如实参为 int,形参为 double)时,编译器会自动进行类型转换,不必完全一致。
      • 故表述完全正确的为 B。

    第 3 题

    【题目重现】
    运行以下代码,屏幕上将输出( )。 `cpp #include using namespace std;

    int var = 100;

    void function() { int var = 200;

    cout << var << \ \;
    cout << ::var << \ \;
    

    }

    int main() { cout << var << \ ;

    function();
    
    var += 100;
    cout << var << \ \;
    return 0;
    

    } ` A. 100 200 100 200
    B. 100 200 100 300
    C. 100 200 200 200
    D. 100 200 200 300

    • 【标准答案】:A(100 200 100 200)
    • 【考点定位】:全局变量与局部变量作用域、变量遮蔽(Shadowing)、作用域解析运算符(::)。
    • 【深度解析】:
      1. 第一步(main 开始):
        • cout << var << \ ;:首先输出全局变量 ar(初始值为 100),输出:100 。
      2. 第二步(调用 unction()):
        • 函数内部定义了局部变量 int var = 200;,该局部变量遮蔽了同名的全局变量。
        • cout << var << \ ;:就近原则输出局部变量 ar 的值,输出:200 。
        • cout << ::var << \ ;:使用全局作用域解析运算符 ::,强制访问全局变量 ar,输出:100 。
        • 函数执行结束,局部变量 ar 随函数栈帧销毁。
      3. 第三步(返回 main() 继续执行):
        • 执行 ar += 100;:此时所在作用域为 main,操作的是全局变量 ar。由于 unction() 内部的局部变量对全局变量无任何影响,全局变量的值依然为 100。自增后,全局变量变为 100 + 100 = 200。
        • cout << var << \ ;:输出修改后的全局变量值,输出:200 。
      4. 综合输出结果: 屏幕依序输出 100 200 100 200,严格对应 选项 A。
    • 【陷阱分析】: 选项 B(末尾为 300)是命题人设置的典型诱错选项。初学者容易误以为 unction() 里的赋值改变了全局变量,从而错误地计算出 200 + 100 = 300。实际上局部变量绝不会影响全局同名变量。

    第 4 题

    【题目重现】
    运行下面代码,屏幕上输出是( )。

    int arr[3] = {24, 9, 7};
    int* p = arr;
    p++;
    cout << *p << endl;
    

    A. 24
    B. 9
    C. 7
    D. 不确定

    • 【标准答案】:B
    • 【考点定位】:指针的算术运算(指针自增 p++)与解引用(*p)。
    • 【深度解析】:
      1. 数组名 arr 在隐式转换为指针时指向数组的首元素,即 &arr[0]。
      2. int* p = arr; 执行后,指针 p 指向元素 arr[0](值为 24)。
      3. 执行 p++;:指针向前移动一个 int 元素的跨度(步长为 sizeof(int)),此时 p 指向数组的下一个元素 arr[1]。
      4. *p 对当前指针所指地址解引用,获取到 arr[1] 的值,即 9。
      5. 故输出为 9,选 B。

    第 5 题

    【题目重现】
    运行下面代码片段的结果是( )。

    int x = 20;
    int y = 24;
    int* p = &x;
    int* q = &y;
    
    p = q;
    

    A. 将 xx 赋值为 24
    B. 将 yy 赋值为 20
    C. 将 qq 指向 xx 的地址
    D. 将 pp 指向 yy 的地址

    • 【标准答案】:D
    • 【考点定位】:指针变量本身的赋值与指针指向的改变。
    • 【深度解析】:
      1. 初始时:p 存放变量 x 的地址(p 指向 x);q 存放变量 y 的地址(q 指向 y)。
      2. 执行 p = q;:是将指针变量 q 中存储的地址值赋值给指针变量 p。
      3. 赋值完成后,p 也保存了 y 的地址,即 将 pp 指向 yy 的地址。
      4. 该操作没有对指针解引用(未出现 *p 或 *q),因此变量 xx 和 yy 本身的值保持不变。故选 D。

    第 6 题

    【题目重现】
    在 C++ 中,( )正确定义一个名为 student 的结构体,其中包含一个 name 字符数组和一个 age 整数?
    A. struct student { char name[20]; int age; };
    B. student struct { char name[20]; int age; };
    C. student struct { string name; int age; };
    D. struct student { char[20] name; int age; };

    • 【标准答案】:A
    • 【考点定位】:C++ 结构体(struct)的定义语法与成员声明。
    • 【深度解析】:
      1. C++ 结构体定义的标准语法为:struct 结构体名 { 成员类型 成员名; ... };(末尾必须有分号)。
      2. B、C 选项中关键字 struct 位于结构体名之后,语法错误;
      3. D 选项中数组声明语法写成了 char[20] name,属于 Java/C# 等语言风格,在 C++ 中应声明为 char name[20],语法错误。
      4. A 选项语法完全标准正确,选 A。

    第 7 题

    【题目重现】
    在 C++ 中,( )正确声明了一个 3 行 4 列的二维数组。
    A. int arr[3, 4];
    B. int arr[3][4];
    C. int arr[4][3];
    D. int arr(3, 4);

    • 【标准答案】:B
    • 【考点定位】:二维数组的声明语法。
    • 【深度解析】:
      1. C++ 中多维数组的声明采用多个中括号依次指定各维长度,格式为 类型 数组名[第一维大小][第二维大小]。
      2. 第一维表示行数,第二维表示列数。题干要求“3 行 4 列”,因此第一维大小为 3,第二维大小为 4,即 int arr[3][4];。
      3. A 选项中 [3, 4] 使用逗号运算符,语法错误;C 选项为 4 行 3 列;D 选项使用小括号属于非法声明。故选 B。

    第 8 题

    【题目重现】
    一个二维数组定义为 int arr[3][4]; (假设一个 int 变量占 4 个字节),则 int arr[0] 占用( )个字节的内存。
    A. 3
    B. 4
    C. 12
    D. 16

    • 【标准答案】:D
    • 【考点定位】:二维数组的内存构成与子数组大小计算。
    • 【深度解析】:
      1. 数组定义为 int arr[3][4];,表示包含 3 个一维数组,每个一维数组包含 4 个 int 型元素。
      2. arr[0] 代表二维数组的第 0 行,它本质上是一个拥有 4 个整型元素的一维数组(int[4])。
      3. 每个 int 占 4 个字节,因此 arr[0] 占用的总内存大小为:Size=4×4=16 字节\text{Size} = 4 \times 4 = 16\text{ 字节}
      4. 故选 D。

    第 9 题

    【题目重现】
    下面代码采用递推算法来实现整数 nn 的阶乘(n!=n×(n−1)×⋯×2×1n! = n \times (n-1) \times \dots \times 2 \times 1),则横线上应填写( )。

    int factorial(int n) {
        int result = 1;
        for (int i = 2; i <= n; i++) {
            ________________________________     // 在此处填入代码
        }
        return result;
    }
    

    A. result *= i;
    B. result += i;
    C. result *= result;
    D. result += result;

    • 【标准答案】:A
    • 【考点定位】:递推思想、累乘算法与复合赋值运算符。
    • 【深度解析】:
      1. 阶乘的数学定义为 n!=(n−1)!×nn! = (n-1)! \times n。
      2. 循环变量 ii 从 2 递增遍历到 nn,每一步需要将当前因子 ii 累乘到总积 result 中,即 result = result * i;。
      3. 用复合赋值运算符简写为 result *= i;。故选 A。

    第 10 题

    【题目重现】
    在排序算法中,稳定性指的是( )。
    A. 排序后数据不会丢失
    B. 排序后相同元素的相对顺序保持不变
    C. 排序后数据不会被修改
    D. 排序后数据的时间复杂度不变

    • 【标准答案】:B
    • 【考点定位】:排序算法的核心评价指标——稳定性(Stability)。
    • 【深度解析】:
      1. 稳定性的标准定义:在待排序序列中,若存在多个关键字相同的元素(如 A1=A2A_1 = A_2 且 A1A_1 排在 A2A_2 前面),排序后如果 A1A_1 依然必须排在 A2A_2 前面,则称该排序算法是稳定的。
      2. 换言之,稳定性即“排序后相同元素的相对顺序保持不变”。
      3. 常见稳定排序:冒泡排序、插入排序、归并排序、基数排序。
      4. 常见不稳定排序:选择排序、快速排序、堆排序、希尔排序。
      5. 故选 B。

    第 11 题

    【题目重现】
    下面代码实现了冒泡排序函数,则横线上应填写( )。

    //交换数组arr的第i个元素和第j个元素
    void swap(vector<int> &arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
    
    int bubble_sort(vector<int> &arr) {
        for (int i = arr.size() - 1; i > 0; i--) {
            bool flag = false; // 标志位
            ________________________________ {    // 在此处填入代码
    
                if (arr[j] > arr[j + 1]) {
                    swap(arr, i, j);
                    flag = true;
                }
            }
            if (!flag)
                break; // 此轮“冒泡”未交换任何元素
        }
    }
    

    A. for (int j = 0; j < arr.size() - 1; j++)
    B. for (int j = arr.size() - 1; j > 0; j--)
    C. for (int j = 0; j < i; j++)
    D. for (int j = i-1; j <=0; j--)

    • 【标准答案】:C
    • 【考点定位】:冒泡排序的区间收缩与内层循环控制。
    • 【深度解析】:
      1. 外层循环 for (int i = arr.size() - 1; i > 0; i--) 中,变量 i 代表当前未排序区间的右边界(即每轮冒泡将最大元素沉底到下标 i 处)。
      2. 内层循环需要从数组开头(下标 0)开始,两两比较相邻元素 arr[j] 与 arr[j + 1]。
      3. 因为比较的是 arr[j] 和 arr[j + 1],为了保证 j + 1 最大到达 i,内层循环变量 jj 的取值范围应为 0≤j<i0 \le j < i(即 j 最大为 i−1i - 1,j+1j + 1 最大为 ii)。
      4. 因此内层循环必须为 for (int j = 0; j < i; j++)。故选 C。

    第 12 题

    【题目重现】
    上一题算法的时间复杂度为( )。
    A. O(n2)O(n^2)
    B. O(2n)O(2^n)
    C. O(1)O(1)
    D. O(n)O(n)

    • 【标准答案】:A
    • 【考点定位】:冒泡排序的时间复杂度分析。
    • 【深度解析】:
      1. 数组长度为 nn。外层循环执行约 nn 轮;
      2. 第 kk 轮内层循环需要比较 n−kn - k 次,总比较次数为:∑k=1n−1k=n(n−1)2=O(n2)\sum_{k=1}^{n-1} k = \frac{n(n-1)}{2} = O(n^2)
      3. 虽然代码中添加了 flag 提前退出优化(最好情况下已排序数组为 O(n)O(n)),但算法的平均及最坏时间复杂度均为 O(n2)O(n^2)。故选 A。

    第 13 题

    【题目重现】
    下面代码实现了插入排序函数(升序),则横线上应填写( )。

    void insertion_sort(vector<int> &nums) {
        for (int i = 1; i < nums.size(); i++) {
            int base = nums[i], j = i - 1;
            ________________________________ {    // 在此处填入代码
                nums[j + 1] = nums[j];
                j--;
            }
            nums[j + 1] = base;
        }
    }
    

    A. while (j >= 0 && nums[j] > base)
    B. while (j > 0 && nums[j] > base)
    C. while (j >= 0 && nums[j] < base)
    D. while (j > 0 && nums[j] < base)

    • 【标准答案】:A
    • 【考点定位】:直接插入排序的比较与元素后移条件、边界保护。
    • 【深度解析】:
      1. 插入排序的核心思想:将未排序元素 base = nums[i] 插入到前面已经排好序的子序列 nums[0 ... i-1] 中的合适位置。
      2. 指针 j 从 i−1i - 1 开始向左扫描:
        • 若 nums[j] > base,说明 nums[j] 比待插入元素大,必须将 nums[j] 后移一位(nums[j + 1] = nums[j]);
        • 下标保护:指针 jj 递减,最低可以到达 0,因此边界条件必须为 j >= 0(若写 j > 0 会遗漏下标 0 处的元素比较)。
      3. 综合条件,循环继续的条件为 while (j >= 0 && nums[j] > base)。故选 A。

    第 14 题

    【题目重现】
    小杨用文件重定向实现在 log.txt 文件中输出日志,则下面横线上应填写( )。

    #include <iostream>
    #include <fstream>
    #include <string>
    using namespace std;
    
    int main() {
        ofstream log_file("log.txt");
        streambuf* original_cout = cout.rdbuf();
        cout.rdbuf(log_file.rdbuf());
        ___________________________________     // 在此处填入代码
        cout.rdbuf(original_cout);  // 恢复原始的标准输出缓冲区
        return 0;
    }
    

    A. cout << "This output will go to the log file." << endl;
    B. log_file << "This output will go to the log file." << endl;
    C. cout >> "This output will go to the log file." >> endl;
    D. log_file >> "This output will go to the log file." >> endl;

    • 【标准答案】:A
    • 【考点定位】:C++ 标准流缓冲区重定向(rdbuf())。
    • 【深度解析】:
      1. 代码关键步骤:cout.rdbuf(log_file.rdbuf()); 将标准输出流 cout 底层的流缓冲区指针重定向到了文件输出流 log_file 的缓冲区。
      2. 此时,程序员无需改变写代码的习惯,直接向 cout 写入内容,该内容就会被自动底层重定向写入到 log.txt 文件中。
      3. C、D 选项使用的流提取运算符 >>(用于输入),语法错误;B 选项是直接向文件流输出,失去了“文件重定向 cout”的考查意义。故标准做法选 A。

    第 15 题

    【题目重现】
    运行下面的代码,屏幕上将输出( )。

    #include <bits/stdc++.h>
    using namespace std;
    
    int divide(int a, int b) {
        if (b == 0) {
            throw runtime_error("division by zero error ");
        }
        return a / b;
    }
    
    int main() {
        int x = 10;
        int y = 0;  // 设为 0 会导致除零错误
    
        try {
            int result = divide(x, y);
            cout << "result: " << result << endl;
        } catch (const runtime_error& e) {
            cout << "caught an exception: " << e.what() << endl;
        }
    
        return 0;
    }
    

    A. division by zero error result: caught an exception:
    B. result: caught an exception: division by zero error
    C. caught an exception: division by zero error
    D. division by zero error caught an exception: division by zero error

    • 【标准答案】:C
    • 【考点定位】:C++ 异常处理机制(try-catch 与 throw)。
    • 【深度解析】:
      1. main 中调用 divide(10, 0)。
      2. 在 divide 函数中检测到 b == 0,执行 throw runtime_error("division by zero error "); 抛出异常。
      3. 一旦抛出异常,divide 函数立即终止,try 块内后续语句 int result = divide(x, y); 及其下方的 cout << "result: ..." 均不会被执行。
      4. 控制流直接跳转到匹配的 catch (const runtime_error& e) 块中:
        • e.what() 返回抛出异常时设置的字符串内容 "division by zero error ";
        • 输出语句拼接输出:caught an exception: division by zero error 。
      5. 故选 C。

    二. 判断题逐题深度解析 (每题 2 分,共 20 分)

    第 16 题

    【题目重现】
    代码 int a = 10; int* p = &a; 可以正确定义指针和初始化指针。

    • 【判断结果】:正确(√)
    • 【考点定位】:指针变量的声明、取地址运算符 & 与指针初始化。
    • 【深度解析】: int a = 10; 声明了整型变量 a;&a 获取 a 的内存地址;int* p = &a; 声明了一个指向 int 类型的指针变量 p 并将 a 的地址赋给它。语法完全合法,表述正确。

    第 17 题

    【题目重现】
    在 C++ 中,引用传递允许函数修改传递给它的参数的值。

    • 【判断结果】:正确(√)
    • 【考点定位】:C++ 函数参数的引用传递(Pass by Reference)。
    • 【深度解析】: 引用是变量的别名(Alias)。当函数的形参声明为非常量引用(如 void func(int &x))时,形参和调用处的实参共享同一内存空间,函数内对 x 的任何修改都会直接同步改变实参的值。表述正确。

    第 18 题

    【题目重现】
    指针的大小与其所指向的变量的数据类型的大小相同。

    • 【判断结果】:错误(×)
    • 【考点定位】:指针大小的本质决定因素(计算机寻址体系架构)。
    • 【深度解析】:
      1. 指针存储的是内存地址,其大小由操作系统的寻址位宽决定:
        • 在 32 位操作系统下,任何指针的大小均为 4 字节;
        • 在 64 位操作系统下,任何指针的大小均为 8 字节。
      2. 指针的大小与它所指向的对象类型(如 char 占 1 字节,double 占 8 字节,大型结构体占成百上千字节)毫无关系。sizeof(char*) 和 sizeof(double*) 在同一平台上完全相等。题干断言相同,错误。

    第 19 题

    【题目重现】
    二维数组的行的大小必须在定义时确定,列的大小可以动态变化。

    • 【判断结果】:错误(×)
    • 【考点定位】:C++ 二维数组内存连续性与维度声明规则。
    • 【深度解析】:
      1. C++ 内建的静态二维数组在编译时所有维度的大小都必须是固定的常量(不支持动态调整大小)。
      2. 在函数形参传递或初始化推导时,反而是列数(第二维)绝对不能省略,行数可以根据元素个数省略推导(即与题干说法恰好相反)。
      3. 二维数组定义后,列的大小根本无法动态变化。判断错误。

    第 20 题

    【题目重现】
    递推算法通过逐步求解当前状态和前一个或几个状态之间的关系来解决问题。

    • 【判断结果】:正确(√)
    • 【考点定位】:递推算法(Recurrence / Iteration)的基本概念。
    • 【深度解析】: 递推算法基于明确的初值(边界条件)和状态转移关系式,自底向上由已知的局部状态一步步推导出全局目标状态(如斐波那契数列、数塔、阶乘等)。表述准确,判断正确。

    第 21 题

    【题目重现】
    选择排序是稳定的排序算法。

    • 【判断结果】:错误(×)
    • 【考点定位】:选择排序(Selection Sort)的稳定性分析。
    • 【深度解析】: 选择排序每一轮在未排序区间找到全局最小元素,并与未排序区间的首元素进行跨越式交换。
      • 反例:序列 [5a, 5b, 2]。
      • 第一轮找到最小元素 2,与首位置的 5a 交换,序列变为 [2, 5b, 5a]。
      • 原本处于前方的 5a 跑到了 5b 的后方,破坏了相同元素的原始相对顺序。
      • 因此选择排序是不稳定的。判断错误。

    第 22 题

    【题目重现】
    插入排序的时间复杂度总是比冒泡排序低。

    • 【判断结果】:错误(×)
    • 【考点定位】:基础排序算法时间复杂度对比。
    • 【深度解析】:
      1. 插入排序和冒泡排序在平均情况和最坏情况下的渐进时间复杂度完全相同,均为 O(n2)O(n^2)。
      2. 在最佳情况(输入序列已有序)下,两者的最优时间复杂度也均为 O(n)O(n)。
      3. 题干断言“总是比冒泡排序低”,说法过于绝对,错误。

    第 23 题

    【题目重现】
    在 C++ 中,如果没有捕获到异常(没有匹配的 catch 块),程序会继续执行而不会终止。

    • 【判断结果】:错误(×)
    • 【考点定位】:C++ 未捕获异常(Uncaught Exception)的处理行为。
    • 【深度解析】: 在 C++ 中,如果抛出的异常沿调用栈向上回溯(Stack Unwinding)后依然没有找到匹配的 catch 块,系统会自动调用 std::terminate() 函数,导致程序异常终止(Abnormal Termination),而绝不可能“继续执行”。判断错误。

    第 24 题

    【题目重现】
    以下代码用递推法求斐波那契数列的第 nn 项,时间复杂度为指数级。

    int fibonacci(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
    
        int f0 = 0; // F(0)
        int f1 = 1; // F(1)
        int current;
    
        for (int i = 2; i <= n; i++) {
    	    current = f0+ f1; // F(n) = F(n-1) + F(n-2)
    	    f0 = f1;
    	    f1 = current;
        }
        return current;
    }
    
    • 【判断结果】:错误(×)
    • 【考点定位】:递推求解斐波那契数列的时间复杂度分析。
    • 【深度解析】:
      1. 该代码采用单重循环 for (int i = 2; i <= n; i++) 进行线性递推,仅循环了 n−1n - 1 次。
      2. 每次循环内部只进行常数次加法与赋值运算,时间复杂度为严格的 O(n)O(n)(线性级)。
      3. 只有未记忆化的朴素递归实现 fib(n-1) + fib(n-2) 的时间复杂度才是指数级 O(2n)O(2^n)。题干称该递推代码为指数级,判断错误。

    第 25 题

    【题目重现】
    执行下面 C++ 代码后,输出的是 20。

    int point(int* p){
        return *p * 2;
    }
    
    int main() {
        int a = 10;
        int* p = &a;
        *p = point(p);
        cout << *p << endl;
    }
    
    • 【判断结果】:正确(√)
    • 【考点定位】:指针传递、解引用与赋值过程追踪。
    • 【深度解析】:
      1. 初始 a = 10,指针 p 指向 a(即 *p 的值为 10)。
      2. 调用 point(p),函数内部 *p * 2 计算得到 10×2=2010 \times 2 = 20 并返回。
      3. 执行 *p = point(p);,将返回值 20 赋给 *p(即变量 a 的值被修改为 20)。
      4. cout << *p << endl; 输出当前 *p 的值,屏幕打印 20。表述正确。
    • 1

    信息

    ID
    4739
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    3
    已通过
    1
    上传者