简介:本资源为2024年3月GESP编程能力认证C++四级真题资料,面向备考该等级认证的中学生与编程学习者,帮助其熟悉考试题型、检验知识掌握程度。内容覆盖变量与函数引用传递、指针与数组索引运算、二维数组内存布局、插入排序与时间复杂度、文件输入输出、结构体与封装、递归与迭代、动态内存分配等核心考点,并配有单选题与判断题的完整题目及参考答案,便于自测与查漏补缺。资源包共1个文件,为pdf格式,大小约1.04MB,可直接阅读打印,适合作为考前模拟与知识点梳理的练习材料。目前已有281人学习下载,读者可通过真题演练定位薄弱环节,结合题目解析理解指针、数组、排序算法等易错点,提升代码阅读与调试能力,为顺利通过四级认证提供针对性训练。
1. 从一道“改不动变量”的题说起:C++4级到底在考什么
很多人第一次看到“函数里改了参数,外面却没变”这种现象时,都会愣一下。明明传进去的是同一个变量,怎么函数里加了三,外面还是原地不动?这种困惑,恰恰是C++编程能力认证四级要解决的核心问题之一。它不像一级二级那样只考你会不会写循环、会不会定义变量,而是开始追问:数据在内存里到底怎么放的?函数之间怎么传东西?指针和引用差在哪?这些问题不搞懂,写出来的代码就像隔着一层毛玻璃看东西,能跑,但不知道为什么跑、什么时候会翻车。
2024年3月的这场四级认证,把变量与函数、数组内存布局、指针运算、排序算法、文件操作、结构体、递归与迭代这些知识点串成了一张网。它不要求你背多少语法,而是看你能不能读懂一段代码背后的数据流向。换句话说,它考的是“C++的内存观”和“算法观”。这篇文章就把这套认证里最容易被搞混的概念一个个拆开,用大白话讲清楚它们到底是什么、为什么存在、彼此之间怎么区分。看完你至少能明白:为什么有些参数传进去改了没用,为什么二维数组的地址算起来那么绕,以及插入排序最好情况下到底快在哪。
2. 变量、函数与参数传递:为什么你的修改“丢”了
2.1 值传递、引用传递、指针传递,三兄弟到底差在哪
先看一个最经典的场景。你写了一个函数int f(int &x){ x+=3; return x; },然后声明int a=3;,问下面哪个调用能改变a的值。选项里有f(&a)、f(*a)、f(a)、f(a-3)。正确答案是f(a)。为什么?因为函数声明里的int &x是一个引用参数,它要求传入的是一个变量本身,而不是变量的地址或值。f(a)把a这个变量直接交给函数,函数里的x就成了a的别名,改x就是改a。而f(&a)传的是地址,类型对不上;f(*a)更是语法错误,a不是指针,不能解引用;f(a-3)传的是一个临时计算结果,引用不能绑定到临时值上。
打个比方,值传递就像你把一份文件复印一份交给同事,同事在复印件上怎么涂改,你的原件都不受影响。引用传递是你直接把原件递过去,同事改的就是你的原件。指针传递则是你把文件柜的钥匙给了同事,他顺着钥匙找到原件再改。三者的区别不在于“能不能改”,而在于“改的是不是同一个东西”。很多初学者写函数时习惯性写void swap(int a, int b),结果发现交换不了,就是因为值传递只动了复印件。
注意:引用传递在函数声明时用
&,调用时直接写变量名,不要加&。这一点和取地址符号长得一样,但位置不同,含义完全不同。
2.2 指针参数和引用参数,什么时候用哪个
指针和引用都能让函数内部修改外部变量,那为什么C++还要保留两种?因为它们的语义和适用场景不一样。引用必须绑定到一个已经存在的变量,而且一旦绑定就不能再指向别人,所以它更适合“这个参数就是某个对象的别名,我保证它有效”的场景。指针可以为空,可以重新指向,可以参与算术运算,所以它更适合“这个参数可能没有、可能是一段连续内存的起点、需要遍历”的场景。
比如题目里有一道关于函数参数为指针的判断题:“在C++语言中,函数的参数为指针时,可以在函数内部修改该参数的值。”这句话是对的,但要注意区分“修改指针指向的值”和“修改指针本身”。如果你在函数里写*p = 10;,外面那个变量会变;如果你写p = nullptr;,外面那个指针变量本身不会变,因为你只是把函数内部的副本指针指向了空。这和引用不同,引用一旦绑定,你改引用就是改原变量,不存在“改引用本身”这回事。
再看一道关于全局变量的判断题:“两个函数之间可以使用全局变量来传递数据。”这也是对的。全局变量在程序整个生命周期都存在,任何函数都能读写它。但正因为谁都能改,它容易让代码变得难以追踪,所以实际工程里能不用就不用。认证考试里考它,是为了让你知道数据传递不止参数一条路,但知道不等于推荐。
2.3 函数声明里的&和调用时的&,别搞混
这是四级考试里最容易翻车的一个点。函数声明int f(int &x)里的&是引用声明符,意思是“x 是一个引用”。而调用时写f(&a)里的&是取地址运算符,意思是“把 a 的地址传进去”。两者长得一样,但一个在类型旁边,一个在变量旁边,编译器看到的位置不同,解释就不同。
更绕的是,如果你声明的是int f(int *x),那么调用时必须写f(&a),因为函数要的是指针,你得把地址给它。而如果你声明的是int f(int &x),调用时必须写f(a),因为函数要的是变量本身。很多人在考场上看到f(&a)就以为是在传引用,其实那是在传指针。区分方法很简单:看函数声明里参数类型是int&还是int*。int&调用时直接写变量名,int*调用时写&变量名。
提示:引用参数在函数内部使用时,不需要解引用,直接当普通变量用;指针参数在函数内部使用时,需要用
*解引用才能拿到值。这是两者在写法上最直观的区别。
3. 数组与内存布局:为什么二维数组的地址算起来那么绕
3.1 一维数组和二维数组,在内存里其实是“一条线”
很多人以为二维数组int arr[3][16]在内存里是像表格一样一行一行分开存的,其实不是。内存是线性的,二维数组只是“按行优先”把数据排成一条长线。arr[0]占 16 个 int,紧接着是arr[1]的 16 个 int,再接着是arr[2]的 16 个 int。所以arr[1]占用的内存大小是16 * sizeof(int),在常见平台上sizeof(int)是 4,所以是 64 字节。题目里问arr[1]占多少字节,答案就是 64。
那如果arr的地址是0x28cbc0,arr[1]的值是多少?因为arr[0]占了 64 字节,arr[1]的起始地址就是0x28cbc0 + 64。64 在十六进制里是0x40,所以0x28cbc0 + 0x40 = 0x28cc00。这道题考的就是“二维数组的每一行在内存里是连续排列的,行与行之间没有空隙”。你只要知道每行多少字节,就能算出下一行的地址。
3.2 数组名、指针、取地址,三者什么时候等价
在C++里,数组名在大多数情况下会“退化”成指向首元素的指针。比如int a[10];,a在表达式里通常等价于&a[0]。但有两个例外:一是sizeof(a)得到的是整个数组的字节数,不是指针大小;二是&a得到的是整个数组的地址,类型是int(*)[10],而不是int*。虽然a和&a的值可能一样,但类型不同,加一之后跳过的距离也不同。
题目里有一道关于指针运算的题:char *s[] = {(char*)"2024", (char*)"3.16", (char*)"GESP"};然后循环输出*s+i。这里s是一个指针数组,*s拿到的是第一个字符串的地址,也就是"2024"的首地址。*s+i就是在这个地址上偏移i个字符。当i=0时输出2024,当i=1时输出024。这道题考的是“指针加法是按元素类型大小偏移的”,char*加一就是往后挪一个字节。
3.3 数组作为函数参数,传的到底是什么
当你把数组传给函数时,传的不是整个数组的拷贝,而是数组首元素的地址。所以函数里对数组元素的修改,会直接反映到外面的数组上。这也是为什么void f(int arr[])和void f(int *arr)在函数签名里是等价的。但要注意,函数内部无法通过sizeof(arr)得到数组长度,因为arr已经退化成了指针,sizeof得到的是指针大小。
题目里有一道关于int a[2024][3][16] = {2,0,2,4,3,1,6};的判断题,问cout << a[2023][2][15]的结果是否不确定。答案是“不确定”,因为初始化只给了前几个元素,后面的元素会被默认初始化为 0,但具体是 0 还是其他值,取决于数组是全局还是局部。全局数组会零初始化,局部数组不会。这道题考的是“数组初始化规则”和“内存布局”的结合。
注意:二维数组的
arr[1]是一个一维数组,它的类型是int[16],在表达式里会退化成int*,指向arr[1][0]。所以arr[1]和&arr[1][0]的值相同,但含义不同。
4. 算法与数据结构:递归、迭代、排序,别被名字唬住
4.1 递归和迭代,一个绕圈一个直走
题目里有一道关于fib函数的题,问它采用的是什么算法。代码里用了一个循环for(int i=2; i<=n; i++) f[i]=f[i-1]+f[i-2];,这是典型的迭代,不是递归。递归是函数自己调用自己,迭代是用循环反复执行同一段逻辑。两者都能解决同一类问题,但递归写起来更接近数学定义,迭代跑起来更省内存。
打个比方,递归就像你问一个人“到山顶还有多远”,他说“你往前走一步,再问下一个人”,一直问到山顶的人。迭代是你自己一步一步往前走,直到走到山顶。递归的优点是代码简洁,缺点是每次调用都要压栈,层数多了会栈溢出。迭代的优点是效率高,缺点是有时代码不如递归直观。四级考试里考这个,是让你能看懂代码结构,而不是让你背定义。
4.2 插入排序最好情况为什么是 O(n)
插入排序的基本思想是:把每个元素插入到前面已经排好序的序列里。最好情况是数组已经有序,这时候每个元素只需要和前面一个元素比较一次,发现不用挪,直接放在原位。所以总共比较 n-1 次,时间复杂度是 O(n)。最坏情况是数组完全逆序,每个元素都要和前面所有元素比较并挪动,时间复杂度是 O(n²)。
题目里还有一道关于选择排序的判断题:“对int a[]={2,0,2,4,3,1,6},执行第一趟选择排序处理后 a 中数据变为{0,2,2,4,3,1,6}。”这是对的。选择排序第一趟是找到最小的元素,和第一个位置交换。原数组里最小的是 0,在第二个位置,和第一个位置的 2 交换,就得到{0,2,2,4,3,1,6}。选择排序和插入排序的区别在于:选择排序每次从未排序部分选最小的放到已排序部分末尾,插入排序是把当前元素插到已排序部分的合适位置。
4.3 外排序:数据装不进内存怎么办
判断题里有一句:“如果待排序数据不能都装进内存,需要使用外排序算法。”这是对的。外排序就是先把数据分成若干块,每块单独排序后写到磁盘,然后再把这些有序块合并成一个大有序文件。它和内存排序的区别在于,外排序要频繁读写磁盘,所以算法设计时要尽量减少磁盘 I/O 次数。四级考试里考这个,是让你知道排序不只是内存里的事,数据量大了要考虑存储层次。
提示:插入排序在数据量小或基本有序时效率很高,很多标准库的排序算法在子数组长度小于某个阈值时,会切换成插入排序。
5. 文件操作、结构体与异常:那些容易被忽略的细节
5.1 文件读取时,fin >> x到底读到了什么
题目里有一道文件读取题:文件1.txt内容如下:
50 2024 3.16 IloveGESP!代码先循环两次fin >> line,每次读一个以空格分隔的字符串。第一次读到50,第二次读到2024。然后fin >> x,这里x是int类型,它会尝试从文件里读一个整数。此时文件指针停在2024后面的空格处,下一个 token 是3.16。fin >> x读到3就停了,因为.不是整数的合法字符。所以x的值是3。这道题考的是“输入流在读取不同类型时的解析规则”,整数读取遇到非数字字符就停止。
5.2 结构体的内存对齐,为什么地址不一定是连续的
题目里有一道关于结构体地址的题:定义struct pass { int no; char name[20]; int level; };,然后输出&XiaoYang和&(XiaoYang.no)。这两个地址的值是相同的,因为结构体的首地址就是第一个成员的地址。但&(XiaoYang.level)就不一定等于&XiaoYang + sizeof(int) + 20,因为编译器可能会在name和level之间插入填充字节,让level对齐到 4 字节边界。这就是内存对齐。
内存对齐的目的是让 CPU 访问内存更快。很多处理器要求数据的地址必须是某个值的倍数,比如 int 的地址必须是 4 的倍数。如果不对齐,访问时可能需要两次内存读取,甚至在某些平台上直接报错。所以编译器会自动在成员之间插入空白字节。四级考试里考这个,是让你知道“结构体的大小不等于成员大小之和”。
5.3 异常捕获和cout << 9^2的坑
判断题里有一句:“在C++语言中 try 子句里抛出的结构体等类型的异常无法被 catch 捕获。”这是错的。C++ 的异常捕获不限制类型,你可以抛任何类型,也可以捕获任何类型。只要 catch 的参数类型匹配,就能捕获。结构体、类、基本类型都可以。
另一道判断题:“C++语言中cout << 9^2 << endl;会输出81。”这是错的。^在C++里是异或运算符,不是乘方。9^2的二进制是1001 ^ 0010 = 1011,也就是 11。所以输出是 11,不是 81。这道题考的是“运算符优先级和含义”,很多人会把^当成数学里的幂,但在C++里幂运算要用pow(9,2)。
注意:
&和|是位运算符,&&和||是逻辑运算符,^是异或。这些符号在C++里都有特定含义,不能按数学直觉用。
6. 什么时候该用这些知识,什么时候别硬套
学完这些概念,你可能会问:我平时写代码,什么时候该用引用,什么时候该用指针?什么时候该用递归,什么时候该用迭代?什么时候该关心内存布局,什么时候不用管?
先说引用和指针。如果你写的函数只是要修改一个已经存在的变量,而且这个变量一定存在,用引用。比如void increment(int &x) { x++; }。如果你要处理一段可能为空的内存、要遍历数组、要做指针运算,用指针。比如void printArray(int *arr, int n)。如果你只是读取数据不想拷贝,用常量引用const int&。如果你要传递一个可选参数,用指针,因为指针可以是nullptr,引用不行。
再说递归和迭代。如果问题的定义本身就是递归的,比如树的遍历、斐波那契数列的数学定义,用递归写起来更清晰。但如果递归深度可能很大,或者性能要求高,用迭代。比如计算斐波那契数列的第 1000 项,递归会栈溢出,迭代不会。实际工程里,很多递归都可以改写成迭代加栈的方式,既保留逻辑清晰又避免栈溢出。
至于内存布局,如果你只是写上层业务逻辑,用std::vector、std::string这些标准库容器,不用太关心底层怎么排。但如果你要做性能优化、要写底层数据结构、要处理二进制数据、要和硬件打交道,那就必须搞清楚数组怎么排、结构体怎么对齐、指针怎么算。四级考试考这些,不是让你每次都手动算地址,而是让你在遇到“为什么这个结构体这么大”“为什么这个数组访问这么慢”的时候,能有个排查方向。
我自己的习惯是:每次写函数参数时,先问自己“这个参数要不要改”“能不能为空”“要不要拷贝”,三个问题答完,用引用还是指针就清楚了。每次写循环时,先想“数据量多大”“递归深度多深”,再决定用递归还是迭代。这些判断不需要背,写多了就成条件反射。希望这些拆解能帮你在下次看到类似题目时,不再靠猜,而是靠理解。
本文还有配套的精品资源,点击获取