万字解析动态内存从编译到运行

枚举与动态内存:从「编译时常量」到「运行时空间」

1. 一句话总结

枚举把散落的魔法数字变成有名字的常量,而动态内存让你在程序运行时按需索取堆空间------两者一个管编译时,一个管运行时,都是写出灵活 C 程序的基石。


2. 知识地图

复制代码
枚举与动态内存
├── 枚举(enum)
│   ├── 基本概念:给整数常量起名字
│   ├── 默认编号:从 0 开始递增
│   ├── 手动赋值:可以打断默认编号,后续继续递增
│   └── 实战应用:配合 switch-case 避免魔法数字
│
├── 动态内存分配(malloc / calloc / realloc)
│   ├── malloc:分配未初始化内存
│   ├── calloc:分配并初始化为 0
│   ├── realloc:调整已分配内存的大小
│   └── 核心原则:谁申请谁释放,释放后指针置 NULL
│
├── 常见错误
│   ├── 忘记检查返回值(NULL 判断)
│   ├── 忘记 free(内存泄漏)
│   ├── free 之后继续使用(野指针)
│   ├── 对非动态内存调用 free(未定义行为)
│   └── realloc 返回值直接赋给原指针(丢失原始地址)
│
└── 柔性数组(Flexible Array Member)
    ├── 定义:结构体最后一个成员,不写元素个数
    ├── 特点:sizeof 不计算柔性数组大小
    ├── 分配:malloc(sizeof(struct) + N * sizeof(element))
    └── 优势:一次 malloc 同时管理结构体和数组,减少内存碎片

3. 核心概念深度解析

3.1 malloc 的内存布局:堆上的一次「圈地」

当你调用 malloc(20) 时,操作系统在堆上为你划出 20 个字节。整个过程可以用 ASCII 字符画来理解:

复制代码
调用前──堆区空闲
┌──────────────────────────────────────────────┐
│  ... 空闲空间 ...                             │
└──────────────────────────────────────────────┘

int *pf = (int *)malloc(20);
// 在堆上分配 20 字节,返回起始地址 0x1000

调用后──堆区已分配
       pf = 0x1000
       │
       ▼
┌──────┬──────┬──────┬──────┬──────┬────────────┐
│  ?   │  ?   │  ?   │  ?   │  ?   │  空闲空间   │
│ 4B   │ 4B   │ 4B   │ 4B   │ 4B   │            │
└──────┴──────┴──────┴──────┴──────┴────────────┘
 0x1000 0x1004 0x1008 0x100C 0x1010 0x1014

写入数据后:
*(pf+0) = 1;  *(pf+1) = 2;  *(pf+2) = 3;  *(pf+3) = 4;  *(pf+4) = 5;

┌──────┬──────┬──────┬──────┬──────┬────────────┐
│  1   │  2   │  3   │  4   │  5   │  空闲空间   │
│ 4B   │ 4B   │ 4B   │ 4B   │ 4B   │            │
└──────┴──────┴──────┴──────┴──────┴────────────┘

free(pf);  // 归还给系统,但这块内存的旧数据可能还在
pf = NULL; // 切断指针联系,避免野指针

关键点:为什么用 *(pf+i) 而不是 pf++

复制代码
错误做法:pf++
┌──────┬──────┬──────┬──────┬──────┐
│  1   │  2   │  3   │  4   │  5   │
└──────┴──────┴──────┴──────┴──────┘
  ↑                           ↑
  pf 原来指向这里             pf 最终指向这里(第 5 个元素之后)

  free(pf);  ← 危险!pf 不是起始地址了,释放的是错误位置!

正确做法:*(pf+i)
  pf 始终指向起始地址 0x1000
  *(pf+0) → 访问第 0 个元素
  *(pf+1) → 访问第 1 个元素
  ...
  free(pf);  ← 安全!pf 还是起始地址

我的理解: pf 是你在堆上「圈地」的房产证,只有拿着原件(起始地址)才能把地还给系统。pf++ 相当于把房产证上的地址涂改了------系统不认了。*(pf+i) 则是你拿着原件去查册,不影响原件本身。


3.2 realloc 扩容:原地扩容 vs 异地搬迁

realloc 的逻辑比很多人想的复杂------它不是简单的「把原来的空间变大」:

复制代码
场景一:原地扩容(理想情况,后面有连续空闲空间)

扩容前:
┌──────┬──────┬──────┬──────┬──────┬──────────────────┐
│  1   │  2   │  3   │  4   │  5   │   空闲空间        │
└──────┴──────┴──────┴──────┴──────┴──────────────────┘
  ↑                                   ↑
  ptr                             空闲空间足够大

realloc(ptr, 40*sizeof(int)) 后:
┌──────┬──────┬──────┬──────┬──────┬──────┬─────┬─────┐
│  1   │  2   │  3   │  4   │  5   │ 新空间...      │
└──────┴──────┴──────┴──────┴──────┴──────┴─────┴─────┘
  ↑
  ptr 不变!返回值 == 原指针


场景二:异地搬迁(后面空间不够,只能找新地方)

扩容前:
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│  1   │  2   │  3   │  4   │  5   │ 已占用 │ 已占用 │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
  ↑
  ptr(后面没空间了!)

realloc 内部做的事:
  1. 在堆上找一块新的 40*sizeof(int) 空间
  2. 把旧空间的 5 个 int 拷贝过去
  3. free 旧空间
  4. 返回新空间的地址

┌──────┬──────┬──────┬──────┬──────┬─────────────────────┐
│  1   │  2   │  3   │  4   │  5   │  新空间(35个int)   │
└──────┴──────┴──────┴──────┴──────┴─────────────────────┘
  ↑
  新指针!返回值 ≠ 原指针


⚠️ 踩坑:为什么不能直接 ptr = realloc(ptr, new_size)?

  int *ptr = malloc(20);
  ptr = realloc(ptr, 4000);  // 如果 realloc 失败返回 NULL
                              // ptr 变成 NULL,原来 20 字节的内存再也找不回来了!
                              // 这就是「内存泄漏」

正确做法:
  int *p = realloc(ptr, 4000);
  if (p != NULL) {
      ptr = p;  // 确认成功再赋值
  } else {
      free(ptr);  // 即使扩容失败,也要释放原来的内存
      ptr = NULL;
  }

3.3 柔性数组:一次 malloc 搞定结构体 + 变长数组

柔性数组是 C99 的特性,它把「结构体」和「变长数组」绑定在同一块堆内存中,结构非常优雅:

复制代码
struct S {
    char c;
    int i;
    int arr[];  // 柔性数组,不占 sizeof
};

sizeof(struct S) = 8(仅 c + 对齐填充 + i)

分配时:malloc(sizeof(struct S) + 5 * sizeof(int))

┌──────┬──────────┬──────┬──────┬──────┬──────┬──────┐
│  c   │ 对齐填充  │  i   │ arr[0]│arr[1]│arr[2]│arr[3]│arr[4] │
│ 1B   │  3B      │ 4B   │  4B   │ 4B   │ 4B   │ 4B   │ 4B    │
└──────┴──────────┴──────┴──────┴──────┴──────┴──────┴──────┘
  ◀────────── sizeof(struct S) ──────▶◀──── 5 * sizeof(int) ────▶
  ◀────────────────── 一整块连续的堆内存 ────────────────────────▶
                                       ↑
                                pf->arr 直接指向这里

对比传统做法(指针方案):

  struct S {
      char c;
      int i;
      int *arr;  // 存指针,不是直接存数组
  };

  // 需要两次 malloc!
  struct S *pf = malloc(sizeof(struct S));
  pf->arr = malloc(5 * sizeof(int));

  // 也需要两次 free!
  free(pf->arr);
  free(pf);

  // 两块内存可能不连续,增加内存碎片

柔性数组的优势:

对比维度 柔性数组方案 指针方案
malloc 次数 1 次 2 次(结构体 + 数组)
free 次数 1 次 2 次(顺序还不能错)
内存连续性 连续 可能不连续
内存碎片
缓存局部性 好(数据挨在一起) 差(可能跨页)

4. 代码实战

4.1 枚举:让 switch-case 变得可读

复制代码
#include <stdio.h>

// 枚举常量:给整数起有意义的名字
// 默认从 0 开始编号,也可以手动指定
enum Color
{
    RED,    // 0
    GREEN,  // 1
    BLUE    // 2
};

enum Sex
{
    FEMALE = 2,  // 手动指定为 2
    MALE,        // 自动编号为 3(接着上一个 +1)
    SECRET       // 4
};

// 实战:用枚举配合 switch 替代魔法数字
enum Option
{
    EXIT,  // 0
    ADD,   // 1
    SUB,   // 2
    MUL,   // 3
    DIV    // 4
};

void menu()
{
    printf("**************************\n");
    printf("****  1. add    2.sub ****\n");
    printf("****  3. mul    4.div ****\n");
    printf("****  0. exit         ****\n");
    printf("**************************\n");
}

int main()
{
    int input = 0;
    menu();
    printf("请选择>");
    scanf("%d", &input);

    // 可读性对比:
    // case 1:  → 这是什么?不翻文档根本不知道
    // case ADD: → 一看就知道是加法操作
    switch (input)
    {
        case ADD:
            // 执行加法逻辑
            break;
        case SUB:
            // 执行减法逻辑
            break;
        case MUL:
            // 执行乘法逻辑
            break;
        case DIV:
            // 执行除法逻辑
            break;
        case EXIT:
            printf("退出程序\n");
            break;
        default:
            printf("无效输入\n");
            break;
    }
    return 0;
}

4.2 malloc:动态内存的标准三步走

复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 第一步:申请内存
    // malloc(20) 在堆上分配 20 字节,返回 void*,需要强制转换
    int *pf = (int *)malloc(20);

    // 第二步:检查是否申请成功
    // malloc 失败时返回 NULL,必须检查,否则后续解引用 NULL 直接崩溃
    if (pf == NULL)
    {
        perror("malloc");  // 打印系统错误信息
        return 1;
    }

    // 第三步:使用内存
    // 用 *(pf+i) 而不是 pf++,保持 pf 指向起始地址
    for (int i = 0; i < 5; i++)
    {
        *(pf + i) = i + 1;
    }

    // 第四步:释放内存
    free(pf);

    // 第五步:指针置空------防止野指针
    // free 之后 pf 仍然存着原来的地址,但那里已经是「别人的地盘」了
    pf = NULL;

    return 0;
}

4.3 calloc:自带初始化功能的 malloc

复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // calloc 的两个参数:元素个数、每个元素大小
    // 它会在分配内存后把所有字节初始化为 0
    int *pf = (int *)calloc(5, sizeof(int));

    if (pf == NULL)
    {
        perror("calloc");
        return 1;
    }

    // 此时 pf[0] ~ pf[4] 全部是 0,不需要手动初始化
    for (int i = 0; i < 5; i++)
    {
        *(pf + i) = i + 1;  // 覆盖默认值
    }

    free(pf);
    pf = NULL;
    return 0;
}

malloc vs calloc 对比:

特性 malloc calloc
参数形式 malloc(size) calloc(n, size)
初始化 不初始化,内容是随机值 全部初始化为 0
性能 更快(不擦除数据) 稍慢(多了清零操作)
适用场景 马上覆盖全部数据时 需要默认值 0 时(如计数器、标志位)

我的理解: calloc 的「清零」不是免费的午餐------它本质上是多了一次 memset。如果你接下来马上要把所有元素赋值一遍,用 malloc 更高效。但如果你需要「某些位置天然就是 0」的语义保证(比如用作标记数组),calloc 更省心。

4.4 realloc:安全扩容的标准写法

复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 先分配 5 个 int 的空间
    int *ptr = (int *)malloc(5 * sizeof(int));
    if (ptr == NULL)
    {
        perror("malloc");
        return 1;
    }

    // 初始化
    for (int i = 0; i < 5; i++)
    {
        *(ptr + i) = i + 1;
    }

    // 扩容到 40 个 int
    // 关键:用一个临时指针接收 realloc 的返回值
    int *p = (int *)realloc(ptr, 40 * sizeof(int));

    if (p != NULL)
    {
        // 扩容成功,才把新地址赋给 ptr
        ptr = p;

        // 继续使用扩容后的空间
        for (int i = 0; i < 40; i++)
        {
            *(ptr + i) = i + 1;
        }

        for (int i = 0; i < 40; i++)
        {
            printf("%d ", *(ptr + i));
        }
    }
    else
    {
        // 扩容失败,但原来的内存还在!
        // 不能直接放弃,要先处理原来的内存
        perror("realloc");
        // 注意:即使 realloc 失败,原来的 ptr 仍然有效,必须释放
    }

    free(ptr);
    ptr = NULL;
    return 0;
}

realloc 的一个特殊用法:

复制代码
// 当第一个参数是 NULL 时,realloc 的行为等同于 malloc
int *p = (int *)realloc(NULL, 20);  // 等价于 malloc(20)

4.5 柔性数组:一次分配,全部搞定

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

struct S
{
    char c;
    int i;
    int arr[];  // 柔性数组:必须放在结构体最后一个成员,不写元素个数
};

int main()
{
    // sizeof(struct S) 不包括 arr 的大小
    // 分配时手动加上数组需要的空间
    struct S *pf = (struct S *)malloc(sizeof(struct S) + 5 * sizeof(int));
    if (pf == NULL)
    {
        perror("malloc");
        return 1;
    }

    // 像普通数组一样使用
    for (int i = 0; i < 5; i++)
    {
        pf->arr[i] = i + 1;
    }

    // 扩容柔性数组
    struct S *ps = (struct S *)realloc(pf, sizeof(struct S) + 20 * sizeof(int));
    if (ps != NULL)
    {
        pf = ps;
        for (int i = 0; i < 20; i++)
        {
            pf->arr[i] = i + 1;
        }
    }

    // 一次 free 就够了!arr 和结构体在同一块内存上
    free(pf);
    pf = NULL;
    return 0;
}

4.6 常见错误示范

复制代码
#include <stdio.h>
#include <stdlib.h>

// 错误 1:对非动态内存调用 free
void error_free_stack()
{
    int a = 10;
    int *p = &a;
    free(p);  // ❌ 未定义行为!a 在栈上,不是 malloc 出来的
    p = NULL;
}

// 错误 2:对动态内存的一部分调用 free
void error_free_middle()
{
    int *p = (int *)malloc(20);
    p++;       // p 指向了第二个元素
    free(p);   // ❌ free 必须传起始地址,否则行为未定义
}

// 错误 3:使用 assert 替代 NULL 检查(仅在 Debug 模式有效)
void error_assert_instead_of_check()
{
    int *p = (int *)malloc(sizeof(int));
    assert(p);  // Release 模式下 assert 会被编译器移除!
                // 生产环境应该用 if(p == NULL) 判断
    *p = 20;
    free(p);
}

// 正确:通过二级指针修改外部指针
void GetMemory(char **p)
{
    *p = (char *)malloc(100);  // 解引用二级指针,修改外部的 str
}

void Test(void)
{
    char *str = NULL;
    GetMemory(&str);  // 传指针的地址,让函数内部能修改 str 本身

    if (str == NULL)  // 记得检查!
    {
        perror("GetMemory");
        return;
    }

    strcpy(str, "hello world");
    printf("%s\n", str);

    free(str);
    str = NULL;
}

5. 注释提炼

知识注解类

「枚举常量通过枚举列出所有的可能情况,枚举是有编号的,从 0 开始」

枚举(enum)是 C 语言提供的一种定义「命名整数常量」的机制。编译器会为枚举中的每个标识符分配一个 int 类型的值,默认从 0 开始递增。你也可以手动指定某个值,后续的标识符会自动 +1:

复制代码
enum Weekday { MON = 1, TUE, WED, THU, FRI };  // TUE=2, WED=3, ...

枚举的本质是编译时常量,不占用运行时内存。它的主要价值在于提高代码可读性 ------用 RED 代替 0,用 EXIT 代替 0,让代码自文档化。

「动态内存,向系统申请一块内存,用来存放。在收回时,需要主动释放,将这块内存还给系统,但是这块内存依旧存在」

这段注释包含两个关键信息:

  1. 主动释放 :堆内存的生命周期由程序员手动管理。栈上的局部变量在函数返回时自动销毁,但 malloc 出来的内存在 free 之前一直存在,即使指针变量已经出了作用域。忘记 free 就是内存泄漏。

  2. 「这块内存依旧存在」free 之后,内存被标记为「可用」归还给操作系统,但物理上的数据不会立刻被擦除 。这就是为什么 free 后必须把指针置 NULL------指针还存着那个地址,如果误用,可能读到旧数据,也可能读到被其他代码覆盖的新数据,行为完全不可预测。

「calloc 会将所有内容初始化为 0」

calloc 的全称可以理解为 "clear allocation"。它的内部实现大致是:先 malloc,再 memset(ptr, 0, size)。所以初始化 0 是有性能代价的。另外,calloc 对浮点数初始化为 0.0 也是安全的(IEEE 754 中全零位模式就是 0.0),但对指针类型初始化为 NULL 在绝大多数平台上也是安全的,不过 C 标准并不严格保证这一点。

「柔性数组基于结构体,放在最后一个定义,且不写数组元素个数」

这是 C99 引入的 flexible array member。关键限制:

  • 必须是结构体的最后一个成员
  • 不能是唯一成员(结构体至少要有另一个成员)
  • 声明时不写元素个数(int arr[])或写 int arr[0](GCC 扩展)
  • sizeof 不计算柔性数组的大小
  • 必须通过动态内存分配来使用

「assert(p)」

assert 是一个宏,定义在 <assert.h> 中。当表达式为假时,它会打印错误信息并终止程序。重要: assert 只在 Debug 模式(未定义 NDEBUG)下生效。Release 版本中 assert 会被编译器完全移除,所以它不能替代生产代码中的 if(p == NULL) 检查。一般用于「在开发阶段尽早暴露逻辑错误」,而不是处理运行时可能发生的错误。

个人理解类

「用 *(pf+i) 比 pf++ 更好,不改变 pf 的值,因为等释放内存时需要释放的地址是起始申请空间的地址」

我的理解: 这就像你借了一本书,管理员在借阅登记表上写的是「第 3 排第 5 本」。如果你把书往前挪了一格,还书时管理员按登记表的位置去找,找到的是另一本书------系统就乱了。pf 就是那张登记表,你必须保持它指向原始位置。*(pf+i) 是「去第 3 排,往前数 i 格,找到那本书」,但不改登记表上的信息。

「释放后 pf 指向的内容是未知区域,变成了野指针,危险,所以要赋为空」

我的理解: free 相当于把房子的产权还给银行,但你的钥匙(指针值)还在口袋里。如果不把钥匙销毁(pf = NULL),以后你可能会下意识地掏出这把钥匙去开门------但门后面可能住着别人了。NULL 是一种「空钥匙」,系统约定「空钥匙开不了任何门」,解引用 NULL 至少会确定性地崩溃,比随机行为更容易排查。

踩坑感悟类

⚠️ 踩坑 1:realloc 返回值直接赋给原指针

复制代码
// ❌ 错误写法
ptr = realloc(ptr, new_size);

如果 realloc 失败返回 NULLptr 被覆盖为 NULL,原来的内存地址丢失,造成内存泄漏且无法恢复。正确做法是用临时变量接收,判断成功后再赋值。
⚠️ 踩坑 2:忘记 free 导致内存泄漏

动态分配的内存不会自动回收。每次 malloc 都必须有对应的 free。在一个循环里不断 malloc 而不 free,程序的内存占用会持续增长,最终被系统 OOM Killer 杀掉。
⚠️ 踩坑 3:free 后继续使用指针

复制代码
free(p);
printf("%d\n", *p);  // ❌ 未定义行为!p 已经是野指针

free 之后,那塊内存可能已经被分配给其他变量,读到的内容可能是任何值,写入更可能破坏其他数据。预防方法:free 后立即 p = NULL,后续使用前检查 if (p != NULL)
⚠️ 踩坑 4:对非动态内存调用 free

复制代码
int a = 10;
int *p = &a;
free(p);  // ❌ a 在栈上,不是堆内存!程序大概率崩溃

free 只能释放 malloc / calloc / realloc 返回的指针。栈变量、全局变量、静态变量的地址都不能传给 free
⚠️ 踩坑 5:free 不传起始地址

复制代码
int *p = malloc(20);
p++;        // 偏移了
free(p);    // ❌ 不是起始地址,行为未定义

堆管理器内部维护了每块内存的元数据(大小、状态等),这些信息通常存在起始地址之前。如果你传的不是起始地址,管理器会读取错误的内存管理信息,导致崩溃或堆损坏。

对比总结类

概念 枚举 宏定义
类型检查 有(枚举是独立类型) 无(纯文本替换)
调试信息 调试器能看到符号名 预处理后符号消失
作用域 受作用域控制 全局有效(除非 #undef
适用场景 一组相关的整数常量 单个常量或任意代码片段
函数 分配 初始化 释放
malloc malloc(size) 不初始化 free
calloc calloc(n, size) 初始化为 0 free
realloc realloc(ptr, size) 保留旧数据,新空间不初始化 free
柔性数组 指针方案
一块连续内存,一次 malloc 一次 free 两块内存,两次 malloc 两次 free
更好的缓存局部性 可能内存不连续,缓存不友好
sizeof 不包含数组大小 sizeof 包含指针大小(8 字节)
不能直接赋值(数组名是常量) 可以重新赋值指针指向新数组

6. 代码修正

修正 1:原代码中的 printf 格式错误

修改前:

复制代码
enum Color color1 = RED;
printf("%s\n", color1);  // ❌ 枚举值是 int,用 %s 会导致未定义行为

修改后:

复制代码
enum Color color1 = RED;
printf("%d\n", color1);  // ✅ 枚举本质是 int,用 %d 打印

原因: 枚举常量在 C 语言中的类型是 int%s 期望的是一个 char* 指针(字符串地址)。把 int 当作地址去访问字符串,轻则输出乱码,重则段错误。

修正 2:realloc 扩容后缺少原有数据的保护

修改前:

复制代码
int *p = (int *)realloc(ptr, 4000);
if (p != NULL)
{
    ptr = p;
    // 直接覆盖了所有元素,包括前面 5 个已经赋过值的
    for (int i = 0; i < 40; i++)
    {
        *(ptr + i) = i + 1;
    }
}

虽然这段代码逻辑上没问题(覆盖了全部 40 个元素),但如果是「部分扩容」场景(只初始化新增部分),这样写就有问题了。

修改后(展示更健壮的写法):

复制代码
int *p = (int *)realloc(ptr, 40 * sizeof(int));
if (p != NULL)
{
    ptr = p;
    // 只初始化新增的元素(第 6~40 个),前 5 个保留原值
    for (int i = 5; i < 40; i++)
    {
        *(ptr + i) = i + 1;
    }
}

说明: realloc 扩容后,旧数据会被保留。如果不需要覆盖全部数据,应该只初始化新增部分,这样既保留了原有数据,又避免了不必要的写入。

修正 3:assert 不应该替代 NULL 检查

修改前:

复制代码
int *p = (int *)malloc(INT_MAX);
assert(p);  // Release 模式不会执行
*p = 20;

修改后:

复制代码
int *p = (int *)malloc(INT_MAX);
if (p == NULL)
{
    perror("malloc");
    return 1;
}
*p = 20;

原因: assert 在定义 NDEBUG 宏后(通常是 Release 构建)会被编译器移除,变成空操作。如果 malloc 失败,程序会直接解引用 NULL 指针崩溃,而且没有任何错误提示。正确的做法是始终用 if 判断,perror 打印错误原因。


7. 小结

枚举给编译期常量命名,动态内存给运行时数据安家------一个让代码可读,一个让程序灵活,两条腿走路才能写出真正的 C 程序

相关推荐
光影少年12 分钟前
react navite封装一个RN 通用按钮组件
前端·javascript·react native·react.js·前端框架
晴天1614 分钟前
CSS 预处理器深度解析-Day35
前端·css
阳明山水16 分钟前
概念漂移分类与自适应策略解析
人工智能·深度学习·算法·机器学习·架构
恋猫de小郭16 分钟前
AI 时代,也许你的 Flutter 需要一套 Dartastic OpenTelemetry 监控
android·前端·flutter
wujiuhsu19 分钟前
前端实现二维码生成器:Canvas、SVG、纠错等级、扫码校验与批量
前端
计算机魔术师24 分钟前
从卖铲人到圈地:英伟达129亿拿下Hugging Face
前端
攻城狮-申28 分钟前
git本地分支对齐远程分支
前端·git
欧特克_Glodon30 分钟前
OpenCV计算机视觉开发入门与实践<二十九>:grabCut算法分割图像
opencv·算法·计算机视觉·图像分割·grabcut 算法
坐吃山猪34 分钟前
【多线程】什么时候必须/推荐使用Thread类
java·开发语言