编程随笔思考:指针int*数据-凭什么要转换一次?深入理解指针与数组内存异同!

引子:

昨天做这个题目的时候,突然发现一个问题:

int*给我转换的过程中,

代码:

// /**
//  * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
//  *
//  * 计算成功举办活动需要多少名主持人
//  * @param n int整型 有n个活动
//  * @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
//  * @param startEndRowLen int startEnd数组行数
//  * @param startEndColLen int* startEnd数组列数
//  * @return int整型
//  */

// #include <stdio.h>
// #include <stdlib.h>
// int cmp(const void *a, const void *b)
// {
//     // return *(int *)a - *(int *)b;
//     int x = *(int *)a;
//     int y = *(int *)b;
//     if (x < y)
//     {
//         return -1;
//     }
//     else if (x > y)
//     {
//         return 1
//     }
//     else
//     {
//         return 0;
//     }
// }

// int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen)
// {
//     // write code here
//     int *start = (int *)malloc(n * sizeof(int));
//     int *end = (int *)malloc(n * sizeof(int));
//     for (int i = 0; i < n; i++)
//     {
//         start[i] = startEnd[i][0];
//         end[i] = startEnd[i][1];
//     }
//     qsort(start, n, sizeof(int), cmp);
//     qsort(end, n, sizeof(int), cmp);

//     int i = 0, j = 0, count = 0, maxLen = 0;
//     while (i < n)
//     {
//         if (start[i] < end[j])
//         {
//             i++;
//             count++;
//             if (count > maxLen)
//             {
//                 maxLen = count;
//             }
//         }
//         else
//         {
//             count--;
//             j++;
//         }
//     }
//     return maxLen;
// }

// 2刷 25.10.19
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 * 计算成功举办活动需要多少名主持人
 * @param n int整型 有n个活动
 * @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
 * @param startEndRowLen int startEnd数组行数
 * @param startEndColLen int* startEnd数组列数
 * @return int整型
 */

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
int funCmp(const void *a, const void *b)
{
    //!!!vip #self 为什么?
    // return *(int *)a - *(int *)b;
    // double x = *((*double)a);
    // double y = *((*double)b);
    // return (int)(*x - *y);
    int x = *(int *)a;
    int y = *(int *)b;
    if (x > y)
    {
        return 1;
    }
    else if (x < y)
    {
        return -1;
    }
    else
    {
        return 0;
    }
}

int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen)
{
    // write code here
    // qsort(startEnd, n, sizeof(startEnd[0]), cmp);
    int *start = (int *)malloc(n * sizeof(int));
    int *end = (int *)malloc(n * sizeof(int));
    for (int i = 0; i < n; i++)
    {
        start[i] = startEnd[i][0];
        end[i] = startEnd[i][1];
    }
    qsort(start, n, sizeof(start[0]), funCmp);
    qsort(end, n, sizeof(end[0]), funCmp);

    int i = 0, j = 0, count = 0, maxNum = 0;
    while (i < n)
    {
        if (start[i] < end[j])
        {
            i++;
            count++;
            if (count > maxNum)
            {
                maxNum = count;
            }
        }
        else
        {
            j++;
            count--;
        }
    }
    return maxNum;
}

这里的代码,为什么会这样?

--------------------------------------------------------------------------------------------------------编程思考:

qsort排序坑+双指针精髓,嵌入式工程师必看的C语言实战解析

昨天二刷「计算最少主持人」这道题时,盯着排序函数里的int* 转换发呆了半天——第一次刷题时只想着“能跑就行”,二刷才发现当初踩的坑全是C语言的核心考点。比如为什么qsort的比较函数参数必须是void*?直接用*(int*)a - *(int*)b排序有什么隐患?双指针遍历的时候为什么要先动i再更改进度?

这道题看似是算法题,实则把C语言动态内存、库函数使用、指针操作全揉进去了,尤其适合刷过百题但对底层原理模糊的同学,以及正在学嵌入式的工程师——毕竟嵌入式开发里,内存管理和指针操作可是保命技能。今天咱们就从这道题出发,扒透所有考点,不仅教你做对题,更让你搞懂“为什么要这么做”。

本文适合人群:中等以下C语言水平、刷过牛客/力扣100热题、正在学习嵌入式的开发者。全文用纯C语言实战,无花里胡哨的框架,所有代码可直接复制到Keil或GCC编译运行。

一、先把问题聊透:这道题到底在考什么?

很多人刷题时只看“输入输出”,却忽略了题目背后的工程意义——这道题的本质是“区间重叠问题”,在嵌入式开发里太常见了:比如传感器数据采集任务调度、串口通信帧的时间分配,本质都是“多个时间区间如何安排资源”,而“最少主持人”就是“最少资源数”的具象化。

1.1 题目核心需求拆解

先明确题目要求(避免像我一刷时漏看细节):

参数说明

具体含义

工程场景对应

n

活动总数

待执行的任务总数

startEnd[][]

二维数组,startEnd[i][0]是活动i开始时间,startEnd[i][1]是结束时间

任务的开始和截止时间列表

返回值

成功举办所有活动的最少主持人数量

完成所有任务的最少硬件资源(如CPU核心、传感器通道)

举个实际例子:输入n=3,startEnd=[[1,3],[2,4],[3,5]],需要多少主持人?

画个时间轴就清楚了:1-3点第一个活动,2-4点第二个活动(和第一个重叠,需加1个主持人),3-5点第三个活动(此时第一个结束,主持人减1,第二个还在进行,所以总主持人还是2)。答案就是2。

1.2 解题核心思路:把“重叠问题”转化为“排序+遍历”

一刷时我想过“暴力法”:对每个活动,检查它和之前所有活动的重叠情况,记录最大重叠数。但这种方法时间复杂度是O(n²),如果n=1000(嵌入式里任务数可能真这么多),程序就卡了。

二刷时才悟到最优解:将开始时间和结束时间分开排序,用双指针遍历计算最大重叠数,时间复杂度O(nlogn)(排序的时间),空间复杂度O(n)(存储排序后的时间),完全符合嵌入式的性能要求。

核心逻辑用思维导图表示更清晰:

暂时无法在豆包文档外展示此内容

这个思路的本质是:用排序让时间“有序化”,用双指针线性遍历替代嵌套循环,把时间复杂度降下来。这是嵌入式开发里常用的“降维”思想——把复杂问题转化为库函数能处理的简单问题。

二、逐行拆解你的代码:那些看似对的地方,其实藏着坑

先贴你二刷的代码(我标了关键行),咱们逐行扒:


// 比较函数 int funCmp(const void *a, const void *b) { //!!!vip #self 为什么? // return *(int *)a - *(int *)b; // double x = *((*double)a); // double y = *((*double)b); // return (int)(*x - *y); int x = *(int *)a; int y = *(int *)b; if (x > y) { return 1; } else if (x < y) { return -1; } else { return 0; } } int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen) { // 分配内存存储开始和结束时间 int *start = (int *)malloc(n * sizeof(int)); int *end = (int *)malloc(n * sizeof(int)); // 拆分时间 for (int i = 0; i < n; i++) { start[i] = startEnd[i][0]; end[i] = startEnd[i][1]; } // 排序 qsort(start, n, sizeof(start[0]), funCmp); qsort(end, n, sizeof(end[0]), funCmp); int i = 0, j = 0, count = 0, maxNum = 0; // 双指针遍历 while (i < n) { if (start[i] < end[j]) { i++; count++; if (count > maxNum) { maxNum = count; } } else { j++; count--; } } return maxNum; }

2.1 内存分配:为什么必须用malloc,而不是栈数组?

你用了int *start = (int *)malloc(n * sizeof(int)),这个选择是对的,但一刷时我犯过错:直接定义int start[n](变长数组VLA),结果在嵌入式开发板上跑崩了。

这里必须讲透:栈内存和堆内存的区别,这是嵌入式开发的高频坑!

对比项

栈内存(如int start[n])

堆内存(如malloc分配)

嵌入式场景建议

大小限制

通常很小(比如Keil里默认栈大小1KB),n稍大就栈溢出

受限于RAM总大小,可分配较大空间

任务数n可能超过栈容量,必须用堆

分配/释放

自动分配释放,无需手动操作

手动malloc分配,必须手动free释放

记得free,否则内存泄漏(后面优化会加)

兼容性

C99标准才支持变长数组,老编译器不兼容

所有C编译器都支持

嵌入式编译器可能是老版本,优先用malloc

你的代码里没写free!虽然在OJ上刷题时不用管(程序结束后系统回收),但在嵌入式里,这个函数如果被反复调用(比如循环处理任务),会导致内存泄漏,最终程序崩溃。后面优化部分会补全。

2.2 排序函数:你注释里的3个问题,终于有答案了

你在funCmp里标了“!!!vip #self 为什么?”,还留了3种写法的注释。这正是这道题的核心考点——qsort的比较函数到底怎么写才对?

先明确qsort的函数原型(C标准库定义):


void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

四个参数的含义必须记死,嵌入式开发里调库函数全靠这个:

参数

含义

你的代码对应值

base

要排序的数组首地址(必须转void*)

start、end数组

nmemb

数组元素个数(size_t类型,本质是无符号整数)

n(活动总数)

size

每个元素的字节数(必须准确,否则排序错乱)

sizeof(start[0])(int类型,4字节)

compar

比较函数指针,定义两个元素的排序规则

funCmp函数

问题1:为什么比较函数的参数必须是const void*?

你可能疑惑:我要排序的是int数组,为什么不能直接用int*当参数?

答案是:qsort是通用排序函数,要支持int、double、结构体等所有类型的排序。void*是“无类型指针”,可以接收任何类型的数组首地址;加上const是为了防止在比较函数里修改原数组的值(只读操作更安全)。

所以在比较函数里,必须把void*强制转换成实际的类型指针(比如你的代码里转成int*),才能取值比较。这就是“类型转换”的必要性。

问题2:直接return *(int*)a - *(int*)b 有什么隐患?

你注释了return *(int*)a - *(int*)b,改用了if-else判断。这个改动非常正确!因为直接减法会导致整数溢出,这是嵌入式里的致命bug。

举个例子:如果a指向的是INT_MIN(-2147483648),b指向的是1,那么*(int*)a - *(int*)b = -2147483649,超过了int的最小值(-2147483648),发生溢出,结果变成一个正数(具体值取决于编译器),导致排序错乱。

而if-else判断通过比较大小返回1、-1、0,完全避免了溢出问题。用表格总结两种写法的区别:

写法

优点

缺点

适用场景

return *(int*)a - *(int*)b

代码简洁

可能整数溢出,导致排序错误

确定数值范围不会溢出(如0-100的小整数)

if-else判断大小

无溢出风险,稳定可靠

代码稍长

所有场景,尤其是嵌入式开发(数值范围不确定)

问题3:注释里的double转换为什么错了?

你写了double x = *((*double)a);,这个写法有两个致命错误:

  1. 类型转换错误:a是void*,要转成double*应该是(double*)a,而不是(*double)a(语法错误,编译器会报错)。正确的取值应该是*((double*)a)

  2. 类型不匹配:你的start和end数组是int类型,却用double*去解析,会导致内存解析错误。比如int是4字节,double是8字节,取出来的值会是乱码,排序自然错误。

这个错误提醒我们:qsort的比较函数里,转换的类型必须和要排序的数组元素类型一致。如果要排序double数组,才需要转成double*;排序int数组,就必须转成int*。

2.3 双指针遍历:最容易忽略的“边界细节”

双指针部分是代码的核心,你的写法整体正确,但有个细节值得深挖:为什么while循环的条件是i < n,而不是i < n || j < n

我们用实际例子拆解遍历过程,以输入n=3,start=[1,2,3],end=[3,4,5]为例:

步骤

i值

j值

start[i]与end[j]比较

count变化

maxNum变化

说明

初始

0

0

-

0

0

指针都在起始位置

1

0

0

1 < 3

0→1

0→1

第一个活动开始,主持人加1

2

1

0

2 < 3

1→2

1→2

第二个活动开始,和第一个重叠,主持人加1

3

2

0

3 不小于 3

2→1

不变

第一个活动结束,主持人减1

4

2

1

3 < 4

1→2

不变

第三个活动开始,和第二个重叠,主持人加1

5

3

1

i=3 ≥n=3

不变

不变

循环结束,maxNum=2

从步骤可以看出:当i遍历完所有开始时间(i=n),剩下的end[j]即使没遍历完,也不会影响最大重叠数了——因为没有新的活动开始,只会有活动结束,count只会减少。所以循环条件用i < n就足够,更简洁高效。

另一个细节:else分支里是先j++再count--,而不是先count--再j++。这是因为end[j]是当前要结束的活动,必须先移动指针再减主持人,否则会多减一次。比如步骤3中,j从0移到1后,才表示第一个活动结束,count减1。

三、代码优化:从“能跑”到“嵌入式生产级”

刷题时的代码只要能过用例就行,但嵌入式开发里,代码要考虑稳定性、兼容性和可维护性。你的代码有3个可以优化的点,改完之后就是生产级代码了。

3.1 优化1:补全内存释放,避免内存泄漏

前面提到,你的代码里没有free(start)和free(end)。在嵌入式里,如果这个函数被多次调用(比如每次接收新的任务列表时调用),堆内存会被不断占用,最终导致内存耗尽。

优化方案:在return之前释放内存。但要注意:必须先把maxNum存起来,再释放,否则释放后就没法返回了。


int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen) { // 处理n=0的边界情况(避免malloc(0)) if (n == 0) { *returnSize = 0; return 0; } int *start = (int *)malloc(n * sizeof(int)); int *end = (int *)malloc(n * sizeof(int)); // 检查内存分配是否成功(嵌入式里内存可能不足) if (start == NULL || end == NULL) { // 分配失败时释放已分配的内存,避免内存泄漏 if (start != NULL) free(start); if (end != NULL) free(end); return -1; // 返回错误码,提示内存不足 } // 拆分时间、排序、双指针遍历...(和之前一样) // 先存下结果 int result = maxNum; // 释放内存 free(start); free(end); // 避免野指针(虽然函数结束后会销毁,但好习惯) start = NULL; end = NULL; return result; }

这里加了两个嵌入式开发的必备操作:

  1. 检查内存分配结果:malloc可能返回NULL(比如RAM不足),必须检查,否则会导致空指针访问崩溃。

  2. 分配失败时的清理:如果start分配成功但end失败,要释放start,避免内存泄漏。

3.2 优化2:处理边界情况,覆盖所有输入

你的代码没处理n=0的情况(没有活动)。虽然OJ上可能不会给这种用例,但嵌入式里可能会遇到(比如任务列表为空)。另外,startEnd为NULL的情况也要处理。

完整的边界处理代码:


int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen) { // 边界情况1:没有活动 if (n == 0) { return 0; } // 边界情况2:二维数组为空,或列数不是2(每个活动必须有开始和结束时间) if (startEnd == NULL || *startEndColLen != 2) { return -2; // 返回不同的错误码,区分错误类型 } // 边界情况3:行数和活动数不匹配(防止传参错误) if (startEndRowLen != n) { return -3; } // 后续代码... }

用错误码区分不同的错误原因,方便调试——嵌入式开发里,调试不像PC端那么方便,明确的错误码能快速定位问题。

3.3 优化3:封装工具函数,提高可维护性

如果项目里有多个地方需要排序int数组,每次都写一遍funCmp太麻烦,也容易出错。可以把比较函数封装成通用工具函数,放在公共头文件里。


// 通用int数组升序比较函数(放在common.h里) #ifndef COMMON_H #define COMMON_H #include <stdlib.h> // 功能:int数组升序排序的比较函数 // 参数:a-第一个元素地址,b-第二个元素地址 // 返回值:a>b返回1,a<b返回-1,相等返回0 int intCmpAsc(const void *a, const void *b) { int val1 = *(const int *)a; int val2 = *(const int *)b; if (val1 > val2) { return 1; } else if (val1 < val2) { return -1; } else { return 0; } } // 通用int数组降序比较函数(备用) int intCmpDesc(const void *a, const void *b) { return -intCmpAsc(a, b); } #endif // 在主函数里直接调用 #include "common.h" int minmumNumberOfHost(...) { // ... qsort(start, n, sizeof(int), intCmpAsc); qsort(end, n, sizeof(int), intCmpAsc); // ... }

这样做的好处,嵌入式开发者最有体会:

  • 可维护性拉满:如果后续需要修改排序规则(比如改成降序),只需要改common.h里的函数,不用在所有调用排序的地方逐个修改——我之前维护一个传感器任务调度模块时,就是因为没封装函数,改排序规则改了3个小时,还漏改了1处导致程序崩溃。

  • 避免重复造轮子:串口日志排序、传感器数据排序都能直接用这个比较函数,不用每次都写if-else,减少出错概率。

  • 新人友好:函数名intCmpAsc清晰易懂,新人一看就知道是“int数组升序比较”,不用读一堆注释。

3.4 优化4:适配嵌入式特殊场景——时间类型与编译器兼容

刷题时时间是int类型,但嵌入式里时间往往是unsigned int(比如从单片机定时器读取的时间戳,不会是负数),而且不同编译器对qsort的支持有差异(比如Keil C51对void*的转换限制)。这部分优化能让代码适配90%的嵌入式场景。

3.4.1 适配unsigned int时间类型

如果start和end是unsigned int,比较函数要改吗?答案是要!因为unsigned类型的比较不能用int的逻辑(比如0 < 0xFFFFFFFF在unsigned里是真,但转成int会判错)。优化后的比较函数:


// 通用unsigned int数组升序比较函数(嵌入式时间常用) int uintCmpAsc(const void *a, const void *b) { unsigned int val1 = *(const unsigned int *)a; unsigned int val2 = *(const unsigned int *)b; // 用减法判断会溢出(比如val1=0,val2=0xFFFFFFFF),必须用比较运算符 if (val1 > val2) { return 1; } else if (val1 < val2) { return -1; } else { return 0; } } // 对应的主函数修改 int minmumNumberOfHost(int n, unsigned int **startEnd, int startEndRowLen, int *startEndColLen) { unsigned int *start = (unsigned int *)malloc(n * sizeof(unsigned int)); unsigned int *end = (unsigned int *)malloc(n * sizeof(unsigned int)); // 后续逻辑和int版本一致,排序时用uintCmpAsc qsort(start, n, sizeof(unsigned int), uintCmpAsc); // ... }

我之前在STM32开发板上踩过的坑:把unsigned int的时间用int的比较函数排序,导致0点附近的时间(比如0xFFFFFFFE)被当成负数,排序完全错乱。嵌入式里时间、地址这些无符号类型,一定要用对应的比较逻辑!

3.4.2 编译器兼容性处理(以Keil为例)

有些老的嵌入式编译器(比如Keil C51)对ANSI C的支持不完整,qsort的比较函数参数如果不加const会报错,或者malloc需要显式指定内存区域。兼容代码如下:


#ifdef __C51__ // Keil C51环境:指定malloc从外部RAM分配(根据硬件配置调整) #define MALLOC(size) malloc_xdata(size) #else // 其他环境(如GCC、Keil MDK) #define MALLOC(size) malloc(size) #endif // 兼容老编译器的比较函数(去掉const,仅在老编译器使用) #ifdef __C51__ int intCmpAsc(void *a, void *b) #else int intCmpAsc(const void *a, const void *b) #endif { int val1 = *(int *)a; int val2 = *(int *)b; // 比较逻辑不变... }

这样修改后,代码既能在PC端的GCC编译,也能在Keil开发环境下跑通,嵌入式开发的“跨编译器”痛点就解决了。

四、实战测试:从编译到运行,踩遍嵌入式开发的坑

代码优化完不是结束,嵌入式开发讲究“能跑通,更要跑稳”。这部分咱们从测试用例设计、编译命令、运行结果分析全流程实战,用GCC和Keil两种环境演示,新手也能跟着做。

4.1 测试用例设计:覆盖所有嵌入式可能遇到的场景

嵌入式开发里,边界情况最容易出问题。我设计了5个测试用例,覆盖“正常、极端、异常”场景,每个用例都附代码和预期结果:

用例类型

编号

输入参数

预期输出

嵌入式场景对应

正常场景

1

n=3,startEnd=[[1,3],[2,4],[3,5]]

2

3个传感器采集任务,部分重叠

边界场景

2

n=0(无活动)

0

任务列表为空

边界场景

3

n=1(单个活动)

1

单个串口通信任务

极端场景

4

n=4,所有活动重叠:[[1,10],[2,9],[3,8],[4,7]]

4

4个任务同时执行,需4个硬件通道

异常场景

5

n=2,startEnd=NULL(数组为空)

-2(错误码)

任务列表指针异常(硬件传输错误)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值