如果你正卡在“C++入门练习题里的指针冒泡排序”上,这篇笔记应该能帮到你。作为C++入门阶段最常被拿来练手的组合题,指针和冒泡排序绑在一起,难度其实没有想象中那么高,但它确实是检验你三样基本功够不够扎实的好题目:指针变量的本质理解、数组和指针的内在关系、函数参数传递的地址语义。这篇文章会从题目拆解开始,逐步讲清楚背后原理、完整代码、常见报错,再到优化方向和进阶练习,尽量让刚学完语法但还没“开窍”的同学也能照着写、照着调试、照着把这个知识点吃透。
在开始之前,先把结论放在前面:这道题真正想让你掌握的,不是“背一个排序算法”,而是让你明白数组名在函数传参时到底发生了什么,以及为什么用指针能直接改到外部数据。搞懂这两点,指针冒泡排序就不存在任何魔法了。
1. 题目拆解:这道经典练习题真正在考你什么
1.1 为什么偏偏是指针加冒泡排序
学习C/C++的路上,几乎每个人都写过冒泡排序,也几乎每个人都被指针卡过一段时间。但把这两者放在同一道练习题里,并不是为了单纯增加题目难度,而是因为它们在底层逻辑上有天然联系。
冒泡排序操作的对象是一段连续存储的数据,这段数据在C/C++里最自然的载体就是数组。而数组这一块连续内存的起始地址,恰恰就是指针变量最擅长保存和处理的东西。所以说白了,用指针实现冒泡排序,本质上就是在模拟“数组下标背后发生的事情”:你用arr[i]能访问数据,是因为编译器帮你做了“基地址加偏移量”的换算;而指针则是把这个换算过程摊开给你看,让你明确知道arr + i这个地址里存的是什么。
很多同学不理解为什么非要绕一圈用指针,直接用数组下标不香吗?我的看法是:练习题的意义从来就不是“解决这一个问题”,而是“通过这个问题掌握一类技能”。以后你要写动态内存管理、写链表、写树、做嵌入式开发或游戏引擎里的内存操作,靠的全是指针这套地址思维。在下标写法里,数组退化成指针的过程被隐藏了,一旦切换到指针写法,你必须直面地址、取值、偏移这些最底层的东西,这个思维转换本身就是这道练习题的含金量所在。
1.2 题目考察的三个核心能力点
具体拆开来看,这道题至少踩到了下面三个考察点:
- 指针的基本操作:声明指针变量、取地址符号
&、解引用符号*、指针的加减运算。这些是零散的知识点,单独考每个你都认识,但放到一个完整的排序函数里,就要求你能灵活组合它们。 - 数组名与指针的关系:数组名在很多场景下会隐式转换成指向首元素的指针。这导致了“用数组下标访问”和“用指针偏移访问”本质上是一回事。题目希望你亲自验证这个等价性,而不是停留在“听别人说”。
- 函数参数的地址传递:如果你把数组传进函数,并且在函数里交换元素,外部数组真的会被改变。这背后的原因就是传进去的是地址、是指针,函数通过指针修改的是同一块内存。这个特性是C/C++里“输入输出参数”的雏形,理解它,后面学引用、学结构体指针、学链表节点插入都会顺利很多。
把这三点连起来看,你就会发现题目设计其实很巧妙:排序算法负责制造“修改数据”的需求,指针负责提供“修改原数据”的手段,而数组刚好是把两者连接起来的桥梁。三个知识点在这样一个场景里互相印证,比分开做十道填空题记得牢固得多。
1.3 初学者最容易陷入的两个误区
第一个误区是把指针“神秘化”。有些同学一听指针就觉得是特别底层、特别难的东西,还没开始写代码就在心里打退堂鼓。其实指针说白了就是“存放地址的变量”,地址就是内存的门牌号。你要修改别人家的东西,光知道对方名字(变量名)不够,必须拿到对方的门牌号(地址),然后通过门牌号找上门(解引用),才能动里面的东西。这个类比虽然粗糙,但用来理解入门阶段90%的指针场景是够用的。
第二个误区正好反过来,觉得“反正就会用就行”,完全不关心背后发生了什么。如果你只是照着网上的代码敲一遍,能跑通就觉得自己会了,那这道题的价值就浪费了大半。考试和面试里最常问的变体就是:为什么形参写int* arr和int arr[]没区别?为什么在函数里用sizeof(arr)求不出数组长度?这些问题只要动手写过指针版本、并且稍微琢磨过一遍,基本都能答上来。只凭记忆背结论,过两天就忘了。
2. 动手前的底层原理:指针、数组、函数参数是怎么配合的
2.1 从内存视角看待指针变量
要理解指针冒泡排序,先要把指针到底在“指”什么这件事彻底搞明白。我们声明一个普通变量比如int num = 42,编译器会为它分配一小块内存,这块内存有地址(你可以理解成门牌号),也有内容(就是整数42这个值)。当你写int* p = &num时,实际上干了这样一件事:声明了一个新的指针变量p,并把num这块内存的门牌号存进了p里。
此时如果你打印p,会得到一个类似0x7ffdb8a2c4ac的十六进制数字,这就是num在内存中的地址。而如果你写*p,编译器就按p里存的地址找到对应内存,然后取出里面的值,也就是42。这个取值的动作专业术语叫“解引用”,它在表达式里就相当于“把p指向的那个变量当作普通变量来用”。
#include <iostream> using namespace std; int main() { int num = 42; int* p = # // p保存num的地址 cout << "num的地址: " << p << endl; cout << "通过指针取值: " << *p << endl; *p = 100; // 通过指针修改num的值 cout << "修改后num: " << num << endl; // 输出100 return 0; }注意一个关键点:对*p赋值*p = 100,等价于对num本身赋值。这就是指针最核心的用途——间接修改。冒泡排序里的交换操作,本质上就是对数组元素做间接修改,用指针写完全符合这个语义。
2.2 数组名和指针的“爱恨纠葛”
数组和指针的关系是C/C++里比较微妙的部分,很多人在这里被绕晕。先说硬性结论:在绝大多数表达式中,“数组名”会隐式转换成“指向首元素的指针”。比如你声明int scores[5],那么scores这一个名字,在当参数传、做赋值、参与运算时,都会变成int*类型,指向scores[0]的地址。
举个例子:
int scores[5] = {88, 95, 70, 60, 99}; int* p = scores; // 不需要写 &scores,scores自动变成指向首元素的指针 cout << *(p + 0) << endl; // 输出88 cout << *(p + 3) << endl; // 输出60 cout << p[2] << endl; // 输出70,p[2]等价于*(p+2)这里*(p + 3)的含义是:从数组首地址向后移动3个int的距离(也就是移动3个int大小的字节),然后取出那个位置的元素。这个“移动”不是简单的数字加3,而是按指针类型的大小做偏移。如果p是int*,p+3就相当于地址值增加了3乘以sizeof(int)字节;如果p是double*,同样是p+3,地址值增加的就是3乘以sizeof(double)字节。这种“按类型定步长”的设计,保证了你无论操作什么类型的数组,指针加1总是能准确移动到下一个元素的位置。
这里必须单独提醒一个反直觉的点:sizeof(scores)并不会退化成指针大小。在声明scores的同一作用域里,sizeof(scores) / sizeof(scores[0])能算出数组长度,因为此时编译器知道scores是一个完整的数组对象;但一旦scores被传进函数形参,它就已经退化成指针了,函数里写sizeof(scores)得到的只是指针本身占用的字节数(64位系统下通常是8字节),而不是整个数组的字节数。这是C++初学者最容易踩的坑之一,后面实战部分我会再强调。
2.3 函数传参:为什么形参写指针就能改外部数据
C++的函数参数传递默认是“按值传递”,意思是把实参的内容拷一份给形参。对普通变量来说,在函数里给形参赋值,不会影响外部的实参。但这里有一个容易混淆的点:当你把数组名传进函数时,你拷贝的并不是整个数组,而是数组首元素的地址(因为数组名已经退化成了指针)。于是函数里拿着这个地址,完全可以顺着地址找到原始数组的内存,并修改里面的内容。
看一下这两种函数声明的关系:
void bubbleSort(int arr[], int n); void bubbleSort(int* arr, int n);这两种写法在编译器眼中是完全一样的,都表示“第一个参数是一个int指针”。int arr[]这个写法只是为了在阅读上提示“这里传进来的应该是一个数组”,它本质上还是int*。因此在函数内部,arr就是一个普通的指针变量,支持所有指针运算,比如arr + 1、*(arr + j)、arr[j]等等。
理解了这一点,你就明白为什么在做数值交换时,不需要返回值也能改到外部数组:因为函数内通过*(arr + j)访问的,就是外部那段数组内存本身。而如果你试图在函数内给arr这个指针变量本身赋值,比如arr = new int[n],那外部不会受到影响,因为这改的是形参指针的指向,不是指针指向的内存内容。这个区分很关键,很多同学就是在这里把“通过指针修改数据”和“修改指针本身”混为一谈,后面遇到链表插入、指针重指向时就乱了。
3. 核心实现:指针冒泡排序的完整思路与代码
3.1 算法本身先过关:冒泡排序的每一轮在干什么
在写指针版本之前,先把冒泡排序的逻辑用大白话捋一遍。冒泡排序的核心思想是重复地遍历待排序序列,一次比较相邻的两个元素,如果顺序不对就把它们交换过来。每一轮遍历结束,序列中最大的那个元素就会像气泡一样“浮”到当前范围的末尾,所以这一轮叫做“冒泡”。
假设数组有n个元素,那么:
- 第一轮:对下标0到n-1的相邻元素做比较交换,结束后第n-1个位置一定是最大值。
- 第二轮:对下标0到n-2的相邻元素做比较交换,结束后第n-2个位置一定是第二大的值。
- 第i轮:对下标0到n-1-i个元素做比较交换,结束时当前范围最后一个位置被确定。
因此总共需要n-1轮(最后一轮只剩一个元素,无需再排),内层每轮比较的次数依次是n-1、n-2、...、1。这个“次数递减”用两个嵌套for循环表达就是:
for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { // 比较 arr[j] 和 arr[j+1],如果 arr[j] > arr[j+1] 就交换 } }用生活场景来类比:这就像一列排队的人,每次只允许相邻两个人比较身高,身高更高的往右挪。第一轮比完,最高的人一定站到了最右边;第二轮就不用再管最右边那个人了,继续处理剩下的队列。如此重复,整个队列就从矮到高排好了。
3.2 第一版:从数组下标过渡到指针写法
很多教材会先展示数组下标版本,然后说“改成指针就行了”。但怎么改、为什么能改,往往是含糊带过。我们先从最接近下标风格的写法入手,让过渡自然一些。
#include <iostream> using namespace std; void bubbleSortByPointer(int* arr, int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { // arr[j] 等价于 *(arr + j) if (*(arr + j) > *(arr + j + 1)) { int temp = *(arr + j); *(arr + j) = *(arr + j + 1); *(arr + j + 1) = temp; } } } } int main() { int scores[] = {88, 95, 70, 60, 99, 82, 77}; int n = sizeof(scores) / sizeof(scores[0]); cout << "排序前: "; for (int i = 0; i < n; i++) { cout << *(scores + i) << " "; } cout << endl; bubbleSortByPointer(scores, n); cout << "排序后: "; // 用指针遍历输出 for (int* p = scores; p < scores + n; p++) { cout << *p << " "; } cout << endl; return 0; }这段代码里需要用到一个练习中反复练习的等价关系:*(arr + j)就是arr[j]。arr是指向首元素的指针,加j就移动到第j个元素的位置,再解引用就取到该位置的元素值。同样,*(arr + j + 1)就是arr[j + 1]。这样写出来的代码,逻辑和下标版本几乎一一对应,只是把“数组运算符”换成了“指针运算加解引用”,非常适合作为过渡。
main函数里的输出段也刻意用了指针遍历:int* p = scores; p < scores + n; p++。这里p从首元素地址开始,每次自增就跳到下一个元素,当p到达末尾地址scores + n时循环结束。这里需要留意:scores + n指向的是数组最后一个元素之后的位置,C++标准里允许这样“越界一个位置”的地址存在,只用来做比较,不能解引用。所以循环条件是p < scores + n,输出的是*p,刚好覆盖下标0到n-1的所有元素。
3.3 第二版:让指针变量直接参与内层循环
第一版虽然用了*(arr + j),但整体上还是“把数组指针当作数组名来用”,有些人觉得不够“指针味”。如果想更彻底一点,可以让指针变量本身承担遍历职责。先看代码:
void bubbleSortByPointer(int* arr, int n) { // 外循环:每轮确定一个最大值放到末尾 for (int i = 0; i < n - 1; i++) { // 内层用指针遍历未排序区域 int* current = arr; int* end = arr + (n - 1 - i); for (; current < end; current++) { if (*current > *(current + 1)) { int temp = *current; *current = *(current + 1); *(current + 1) = temp; } } } }这里end指向当前这轮最后一个需要参与比较的元素,而current遍历从首元素到end的前一个位置。因为比较的是*current和*(current + 1),所以当current等于end时就不能再比了,否则current + 1就越界了。这个写法的好处是,你把“循环变量是下标”替换成了“循环变量是地址”,更贴近指针在底层做偏移的真实过程。
说一个实际讲课时的感受:很多同学第一次看到指针做循环变量会有点不习惯,总觉得current++好像很神秘。其实和整数循环变量没本质区别:i++是把整数加1,current++是把指针变量的地址值增加一个int类型的大小,让它指向下一个元素。理解了这一点,指针遍历数组就和其他语言的迭代器、Python里的for x in列表没有本质差别了。
3.4 第三版:终极写法,用函数指针和泛型实现升级版
如果你已经掌握了前两版,可以再挑战一种更“工程化”的写法:用函数指针控制排序方向,用模板适配任意类型。这里作为入门阶段的拓展内容,可以先体会思路,不必强求立刻掌握所有细节。
#include <iostream> using namespace std; // 比较函数:升序 bool ascending(int a, int b) { return a > b; // 当前面大于后面时,需要交换 } // 比较函数:降序 bool descending(int a, int b) { return a < b; // 当前面小于后面时,需要交换 } // 用函数指针接收比较规则 void bubbleSort(int* arr, int n, bool (*compare)(int, int)) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (compare(arr[j], arr[j + 1])) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } int main() { int nums[] = {5, 2, 9, 1, 7, 3}; int n = sizeof(nums) / sizeof(nums[0]); bubbleSort(nums, n, ascending); cout << "升序: "; for (int i = 0; i < n; i++) cout << nums[i] << " "; cout << endl; bubbleSort(nums, n, descending); cout << "降序: "; for (int i = 0; i < n; i++) cout << nums[i] << " "; cout << endl; return 0; }函数指针在这里扮演的角色是“把变化的规则参数化”,这样同一个排序函数既能升序也能降序,不需要复制粘贴两套代码。这其实也是C++里“策略模式”的雏形:算法骨架固定,细节规则通过回调函数注入。如果把compare换成std::function,或者直接使用更现代的lambda表达式,逻辑会更简洁,但入门阶段用普通函数指针看一遍这种写法,对理解类型系统和函数地址的帮助挺大。
3.5 复杂度分析:为什么冒泡排序是O(n^2)
排序算法学完了,复杂度分析也得会算。冒泡排序有两层循环,外层要执行n-1轮,内层每轮平均比较约n/2次,所以总的比较次数约为:(n-1) + (n-2) + ... + 1 = n*(n-1)/2。在大O记法下,这相当于O(n^2)。最好情况下(数组已经有序),如果没做优化,仍然要进行相同次数的比较,所以也是O(n^2);如果加了提前退出的标志位,最好情况可以降到O(n)。交换次数在最坏情况下和比较次数同量级,最好情况下是0。
从实践角度看,冒泡排序因为相邻交换的特性,是稳定排序(相等的元素不会改变相对顺序),而且实现简单、便于理解,做学习和面试入门题非常合适。但如果数据规模过万,性能就比较尴尬了,这时候快速排序、归并排序、堆排序这些O(n log n)的算法才是正选。学习冒泡排序的意义更多在于“理解排序是怎么一回事”,而不是真的拿它处理大数据集。
4. 常见问题、调试技巧与避坑实录
4.1 编译报错和运行崩溃的原因对照表
根据我这些年帮人看代码的经验,指针冒泡排序的错误类型其实非常集中。我把最常见的几张“诊断卡”列在下面:
| 错误现象 | 根本原因 | 解决办法 |
|---|---|---|
| 编译报错:cannot convert 'int ()[5]' to 'int' | 调用时传了&scores而不是scores | 直接传数组名scores,不要画蛇添足加取地址符 |
| 编译报错:invalid conversion from 'int' to 'int*' | 声明指针时写成了int* p = scores[0],误把元素值当地址 | 改成int* p = scores,需要的是首元素的地址 |
| 运行结果完全没排序 | 函数形参传的是值,或者内部复制了一份数组,没有修改原数组 | 确认形参是int*或int arr[]而不是int arr[10];检查调用时传的是数组名 |
| 程序崩溃(Segmentation fault) | 内层循环越界访问,比如j < n而不是j < n-1-i,导致arr[j+1]访问到数组末尾之后 | 对照逻辑逐轮检查内层循环条件,牢记每轮比较次数递减 |
| 输出乱码或地址值 | 输出时忘记解引用,直接打印了p而不是*p | 检查输出语句,指针要加*才能取到元素值 |
| 数组长度不对,排序只排了部分 | 函数内用sizeof(arr)/sizeof(arr[0])计算长度 | 数组传入函数后大小信息丢失,必须在调用处算好长度再传进来 |
这里面最容易踩到的是最后一条。很多同学在main函数里用sizeof算数组长度算得很顺,就顺手在排序函数里也这么写。但函数形参里的arr已经退化成指针了,sizeof(arr)只会返回指针大小(比如8字节),再除以sizeof(int)(4字节),得到2,于是排序函数以为数组只有两个元素。这种错误不报错、不崩溃,但结果诡异,特别容易让人排查半天。
4.2 调试冒泡排序的实用手段
初学阶段写排序,最重要的是把“程序运行时的过程”可视化出来。我的习惯是在关键位置临时加打印语句,把每一轮比较后的数组状态打出来。比如在内层循环结束时加一行:
#include <iostream> using namespace std; void bubbleSortDebug(int* arr, int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (*(arr + j) > *(arr + j + 1)) { int temp = *(arr + j); *(arr + j) = *(arr + j + 1); *(arr + j + 1) = temp; } } cout << "第" << i + 1 << "轮结束: "; for (int k = 0; k < n; k++) { cout << *(arr + k) << " "; } cout << endl; } } int main() { int arr[] = {5, 1, 4, 2, 8}; int n = sizeof(arr) / sizeof(arr[0]); bubbleSortDebug(arr, n); return 0; }运行上面的代码,你会清楚看到每一轮之后数组的变化:
第1轮结束: 1 4 2 5 8 第2轮结束: 1 2 4 5 8 第3轮结束: 1 2 4 5 8 第4轮结束: 1 2 4 5 8第三轮和第四轮的输出完全一样,说明第三轮已经没有发生任何交换,整个数组已经有序了。如果你在代码里没有提前退出的优化,那么第四轮就是在白跑。这种“肉眼可见的浪费”就是优化冒泡排序最直观的动力。
除此之外,对指针本身的调试,我推荐两个办法。第一是打印地址值,直接观察指针的移动轨迹:
int* p = arr; for (int i = 0; i < n; i++) { cout << "p = " << p << ", *p = " << *p << endl; p++; }第二是用调试器的监视窗口,比如Visual Studio或VS Code里打断点,把p、*p、p+1、arr都加到监视列表里,看它们的变化。这样做一次调试,比空想十遍都管用。
4.3 如何给排序加上“提前退出”优化
前面提到冒泡排序最经典的优化是“如果某一轮没有任何交换,说明数组已经有序,提前结束”。实现起来很简单,加一个布尔标志即可:
void bubbleSortOptimized(int* arr, int n) { for (int i = 0; i < n - 1; i++) { bool swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (*(arr + j) > *(arr + j + 1)) { int temp = *(arr + j); *(arr + j) = *(arr + j + 1); *(arr + j + 1) = temp; swapped = true; } } if (!swapped) { break; } } }这个优化在日常数据接近有序的场景下收益非常明显。想象一个几乎排好的长度为10000的数组,普通冒泡要执行9999轮比较,每次大约一半的概率还在做无用功;加了提前退出之后,可能第2轮就发现没有任何交换,直接结束,比较次数从约5000万次骤降到约2万次,性能提升肉眼可见。这个思路在后面学习快速排序、归并排序时也通用:任何一轮操作如果没有改变数据状态,说明后续操作大概率也不会改变,及时止损。
4.4 针对指针的进阶练习:const、指针数组、动态内存、字符串排序
如果你把上面的代码都跑通了,说明这道练习题的核心目标已经达成。但指针的重量远不止于此,我建议趁热打铁做几个变体练习,把知识面再拓一拓。
练习一:了解const在指针里的两种含义
int value = 42; const int* p1 = &value; // 指针指向的内容不能通过p1修改,但p1本身可以指向其他地方 int* const p2 = &value; // 指针本身不能改指向,但可以通过p2修改指向的内容 const int* const p3 = &value; // 既不能改指向,也不能改内容这里其实就涉及到搜热词里经常提到的“顶层指针”和“底层指针”的区别。粗略理解:const修饰的是“指针本身”就是顶层指针,修饰的是“指向的内容”就是底层指针。底层指针可以赋值给底层指针,顶层指针也可以赋值给顶层指针,但要小心底层指针赋值给顶层指针是合法的,因为权限没有扩大;反过来顶层指针赋值给底层指针就不行,因为这个操作让原本只读的内容变得可修改了,编译器会拦下来。排序函数里如果想承诺“不修改数组”,可以写成const int* arr,但那样交换元素也会被禁,所以排序场景下不能加这个限制,这个矛盾本身也是值得思考的好题目。
练习二:用动态内存分配创建数组再排序
int n; cin >> n; int* dynamicArr = new int[n]; for (int i = 0; i < n; i++) { cin >> dynamicArr[i]; } bubbleSort(dynamicArr, n); delete[] dynamicArr;这道题的意义在于打破“数组长度必须编译期确定”的限制。new出来的数组同样可以用下标或指针访问,排序函数完全不需要改动,因为函数接收的就是一个int*。注意排序结束后要delete[]释放内存,避免内存泄漏。理解这个流程之后,你就初步掌握了堆内存管理的基本套路。
练习三:对字符串数组进行排序
如果你已经掌握了指针数组的概念,可以试着一个更进阶的题目:输入若干字符串,用指针数组管理,然后按字典序排序。这里的核心是把“交换两个字符串的指向”和“交换两个字符串本身的内容”区分开。如果每个字符串是char*,交换指针指向往往比直接复制字符内容高效得多,这也是指针在字符串处理中的经典应用。类似题目在面试里非常常见,本质上是考察“修改指针本身”和“通过指针修改数据”的区别。
练习四:归并排序、快速排序的指针实现
冒泡排序掌握后,可以看看如何在快速排序里用双指针法做分区(partition):
int partition(int* arr, int low, int high) { int pivot = arr[high]; int i = low - 1; for (int j = low; j < high; j++) { if (arr[j] < pivot) { i++; swap(arr[i], arr[j]); } } swap(arr[i + 1], arr[high]); return i + 1; }这里的双指针i和j一次遍历就完成“小于pivot的都放左边,大于的放右边”,比冒泡排序每轮只移动一个最大值要高效得多。你会发现,理解指针偏移和数组内存布局后,这些算法读起来就没有那么晦涩了。
5. 写在最后的细节与个人建议
这里想分享一个我自己当年踩过的坑,也算给后来者提个醒。刚开始写指针版本时,我理解错了数组名和首元素地址的关系,以为数组名本身就是一个指针变量,于是试图在函数里修改数组名的指向。直到后来我才明白,数组名更像是一个“常量指针”,它固定指向数组首元素的地址,不能被重新赋值。所以你可以用另一个指针变量去保存它、去移动它,但不能直接改数组名本身。搞清这一点后,很多关于数组和指针的诡异报错就都说得通了。
做这道练习题时,我的建议是不要只满足于“把代码跑通”。你可以在纸上手动模拟一遍循环,写出每一轮结束后数组的样子,和程序实际输出做对照;也可以故意把内层循环的边界写错,观察报错结果、记住越界访问带来的后果;还可以把数组换成double、char、结构体数组,看看排序逻辑怎么变化。把一个题目吃透到可以应对任意变体,比草草写十道类似的题更有价值。
最后再给一个小技巧:无论用哪种写法,在main函数开头用sizeof算好长度再传给排序函数,永远不要在排序函数内部尝试重新计算数组长度。这个问题我在上面反复提了好几次,因为它确实是初学者最高频的失误之一。把这一条牢牢记住,你在写这类题目时会少踩很多坑。
指针和冒泡排序的结合练习,算是C++入门阶段一个非常经典的“关卡”。跨过去之后,你对内存、地址、间接访问的理解会上一个台阶,后面学链表、学二叉树、学容器源码都会顺畅很多。希望这篇笔记能帮你把这个知识点彻底拿下。