第十八天 C语言 Day18:二级指针(与指针数组等效)· main函数原型(argc/argv)· 常量指针与指针常量 · 野指针/空指针/空悬指针 · void与void*万能指针


系列:粤嵌嵌入式培训学习笔记 (C语言篇-第18天)
上一篇第十七天 C语言 Day17:字符指针与字符串 · 字符串作函数参数 · 指针函数 · 函数指针
环境:Ubuntu 22.04 · GCC · Vim | 2026年8月19日


① 概览

指针最后的收尾与避坑。先把指针家族八个易混概念一次性理清(指针变量/变量指针/指针函数/函数指针/数组指针/指针数组/数组元素指针/回调函数);再学二级指针——指向指针的指针,核心结论"二级指针与指针数组等效,与二维数组不等效";然后搞懂 main 函数原型 argc/argv(命令行传参);接着是三个"const 指针"的组合(常量指针/指针常量/常量指针常量)和三个"危险指针"(野指针/空指针/空悬指针);最后认识 void 与 void*(万能指针,malloc 返回的就是它)。

模块 一句话
指针家族八概念 ⭐ []()* 的优先级定本质:谁先结合就是谁
二级指针 ⭐ int **p 存一级指针地址;解引用 1 次得串、2 次得字符
与指针数组等效 ⭐ char **p = 指针数组;但 int **k = 二维数组 编译报错
main 原型 ⭐ int main(int argc, char *argv[])argc 参数个数、argv 参数数组
常量指针 ⭐ const int *p 指向可改、值不可改(保护数据)
指针常量 ⭐ int *const p 指向不可改、值可改(必须初始化)
野指针 ⭐ 未初始化/已释放/越界,访问即 UB
空悬指针 ⭐ free 后没置 NULL,野指针特例
void* ⭐ 万能指针,解引用前必须强制转具体类型

② 指针家族八个易混概念

名称 本质 一句话说明
指针变量 变量 专门存放地址的变量
变量指针 地址 变量在内存中的地址,& 取得
指针函数 函数 返回值是指针的函数
函数指针 指针 指向函数的指针变量
数组指针 指针 指向整个数组的指针变量
数组元素指针 指针 指向数组某个元素的指针变量
指针数组 数组 元素为指针的数组
回调函数 函数 通过函数指针被间接调用的函数

记忆方法:看结合优先级

[]() 优先级高于 *,谁先和名字结合,本质就是谁:

写法 结合过程 本质
int *p[5] p 先与 [5] 结合 数组 → 指针数组
int (*p)[5] 括号让 p 先与 * 结合 指针 → 数组指针
int *f(void) f 先与 (void) 结合 函数 → 指针函数
int (*f)(void) 括号让 f 先与 * 结合 指针 → 函数指针

③ 二级指针(多重指针)⭐

定义:存一级指针地址的指针,解引用两次才能访问原始数据。

int a = 10;
int  *p = &a;     // 一级指针,指向 a
int **w = &p;     // 二级指针,指向 p

printf("*p  = %d\n", *p);    // 10(解引用1次得 a 值)
printf("*w  = %p\n", *w);    // a 的地址(解引用1次得 p 的值)
printf("**w = %d\n", **w);   // 10(解引用2次得 a 值)

int ***k = &w;               // 三级指针,指向 w
printf("***k = %d\n", ***k); // 10(解引用3次)
*p=10
*w=0x7ffc98125b1c
**w=10

💡 开发中最常用就是一级、二级指针,三级以上很少见。

3.1 二级指针与指针数组等效

char *str_arr[] = {"jiaojiao","panpan","leilei"};  // 指针数组(字符串数组)
char **p = str_arr;     // 二级指针接收指针数组,等效

int len = sizeof(str_arr) / sizeof(str_arr[0]);
for (; p < str_arr + len; p++)
    printf("%s\n", *p);   // 解引用1次拿到字符串首地址,配合%s输出
jiaojiao
panpan
leilei

⚠️ char 型二级指针只需解引用 1 次就能拿到字符串地址;解引用 2 次才拿到单个字符。

3.2 二级指针遍历字符串/字符

char *str[3] = {"abc", "aaa034", "12a12a"};
char **p = str;

// 方式1:遍历所有字符串
for (p = str; p < str + 3; p++)
    printf("%s\n", *p);

// 方式2:遍历每个字符串的所有字符
for (p = str; p < str + 3; p++)
{
    while (**p != '\0')     // 第二层循环,遍历到字符
    {
        printf("%c", **p);
        (*p)++;             // 字符偏移
    }
    printf("\t");
}
abc
aaa034
12a12a
abc     aaa034  12a12a

💡 对一个字符串数组:解引用 1 次得字符串,解引用 2 次得字符。

3.3 二级指针与二维数组不等效

二维数组名是数组指针类型(int (*)[3]),不是二级指针:

int arr[][3] = {1, 3, 5, 11, 13, 15};

int (*p)[3] = arr;    // 正确:数组指针

// int **k = arr;     // 编译报错:int ** 与 int (*)[3] 类型不兼容
// printf("%d\n", **k); // 段错误

总结对照:

类型 本质 等效性
二级指针 int** 指向指针的指针 与指针数组等效
指针数组 int *[] 元素为指针的数组 退化为二级指针
二维数组 int[][3] 数组的数组 与数组指针 int (*)[3] 等效

④ main 函数原型 ⭐

main 是程序入口函数,标准写法:

int main(int argc, char *argv[])   // 指针数组形式
int main(int argc, char **argv)    // 二级指针形式(等价)
  • argc(argument count):参数个数,默认 1(只含程序名)
  • argv(argument vector):参数数组,存所有参数的字符串
int main(int argc, char *argv[])
{
    printf("外部参数个数:%d\n", argc);
    for (int i = 0; i < argc; i++)
    {
        printf("%s\n", argv[i]);       // 下标法
        printf("%s\n\n", *(argv+i));   // 指针法
    }
    return 0;
}
$ ./a.out yueqian 192.168.30.131
外部参数个数:3
./a.out
./a.out

yueqian
yueqian

192.168.30.131
192.168.30.131

💡 两个等效记忆:二维数组 ⟷ 数组指针二级指针 ⟷ 指针数组


⑤ 常量指针与指针常量 ⭐

三个"const 指针"组合,本质区别在 const 相对 * 的位置。

5.1 常量指针(指向常量的指针)

const int *p——指向可改,指向的值不可改(保护数据)。

int a = 10;
const int *p = &a;
// *p = 100;     // 错:指向的数据不可改
printf("*p=%d\n", *p);   // 10

int b = 20;
p = &b;                  // 对:指向可以改
printf("*p=%d\n", *p);   // 20

5.2 指针常量(指针本身是常量)

int *const p——指向不可改,指向的值可改(必须初始化)。

int a = 10;
int *const p = &a;   // 定义时必须初始化
*p = 100;            // 对:指向的值可变
printf("*p=%d\n", *p);   // 100

int b = 20;
// p = &b;           // 错:指向不可变

5.3 常量指针常量

const int *const p——指向、值都不可改(最严格,必须初始化)。

int a = 10;
const int *const p = &a;
// *p = 100;  错:值不可改
// p = &b;    错:指向不可改
printf("*p=%d\n", *p);   // 10

总结对比 + 记忆

类型 语法 指向可变 值可变 核心用途
常量指针 const int *p 保护数据(函数参数)
指针常量 int *const p 固定指向(全局指针)
常量指针常量 const int *const p 完全只读

💡 const* 左侧 → 修饰数据(值不可改);const* 右侧 → 修饰指针(指向不可改)。 函数参数优先用常量指针 void func(const int *p),防意外改数据。


⑥ 野指针、空指针、空悬指针 ⭐

6.1 野指针

定义:未初始化、指向非法内存的指针(未初始化/已释放/越界),访问触发未定义行为(UB)

三大产生场景 + 规避:

// 场景1:未初始化 → 置 NULL
int *p = NULL;
if (p == NULL) { printf("指针未指向有效内存!\n"); return -1; }

// 场景2:指向已释放的内存 → 释放后置空
int *p = malloc(sizeof(int));
free(p);
p = NULL;            // 立即置空

// 场景3:返回局部变量地址 → 避免返回栈地址
int* fun(int a, int b)
{
    int sum = a + b;   // 局部变量在栈上
    return &sum;       // 错:函数返回后 sum 销毁
}

通用规避四招:① 定义立即初始化(NULL 或有效地址)② 释放后立即置 NULL ③ 不返回局部变量地址 ④ 使用前非空校验。

6.2 空指针

定义:值为 NULL 的指针(NULL 本质是 0,地址 0x00000000,系统保留不可访问)。

int *p = NULL;
printf("空指针地址:%p\n", p);   // 0x0
free(p);   // 安全:free(NULL) 不报错(系统兼容处理)
p = NULL;

6.3 空悬指针

定义:指针指向的内存已释放,但指针本身没置空(野指针特例)。

char *p = (char*)malloc(100);
free(p);                      // 释放内存
printf("指针地址:%p\n", p);   // 仍是非 NULL 地址(已失效)
// printf("%c\n", *p);        // 危险:访问空悬指针 = UB
p = NULL;                     // 置空后变空指针,不再是空悬指针

⑦ void 与 void* ⭐

类型 本质 核心用途
void 无类型/空类型 函数返回值/参数(表示"无")
void* 通用指针(万能指针) 指向任意类型,跨类型兼容

7.1 void 的用法

void print_hello()   // void 作返回值:无返回值
{
    printf("hello world!\n");
    // 正常结束省略 return;提前结束要写 return;
}
void func(void)      // void 作参数:无参数
{
}

7.2 void* 的用法

  • 可指向任意类型数据,但必须强制转换后才能解引用
  • 仅支持指针赋值、地址偏移,不能直接解引用
  • 常用于 malloc/memcpy、通用接口
#include <stdlib.h>

// 通用处理函数:接收/返回任意类型指针
void* process_data(void* p)
{
    return p;
}

int main()
{
    // 处理 int
    int m = 10;
    int *r1 = (int*)process_data(&m);      // 强制转 int*
    printf("Integer value:%d\n", *r1);    // 10

    // 处理 double
    double pi = 3.14;
    double *r2 = (double*)process_data(&pi);
    printf("Double value:%lf\n", *r2);    // 3.140000

    // void* 用于 malloc(返回 void*)
    void* mem = malloc(sizeof(int));
    if (mem == NULL) { perror("malloc failed!"); return -1; }
    int *p_mem = (int*)mem;   // 强制转 int* 再解引用
    *p_mem = 100;
    printf("Malloc value:%d\n", *p_mem);  // 100
    free(mem);
    return 0;
}
Integer value:10
Double value:3.140000
Malloc value:100

⚠️ 三条注意:① void 不能定义变量(void a; 编译报错)② void* 可与任意指针直接转换,但解引用前必须显式转具体类型malloc 返回的就是 void*


⑧ 踩坑记录 ⭐

坑1:二级指针接二维数组

  • 现象int arr[][3]; int **k = arr; 编译报错、运行段错误
  • 根因:二维数组名是数组指针 int (*)[3],不是二级指针
  • 解决:二维数组用 int (*p)[3] = arr;;指针数组才用 int **p = 指针数组;

坑2:改字符指针指向的字符串常量

  • 现象char *s = "abc"; s[1] = 'X'; 段错误
  • 根因:字符串常量在只读区
  • 解决:要改内容用字符数组 char s[] = "abc";

坑3:常量指针/指针常量搞反

  • 现象const int *pint *const p 分不清谁可改
  • 根因const 位置决定——左侧修饰数据、右侧修饰指针
  • 解决:记口诀"* 左值不可改,* 右针不可改"

坑4:free 后没置 NULL

  • 现象:free 后还解引用,程序随机崩溃
  • 根因:free 只归还内存,指针还留着旧地址(空悬指针)
  • 解决free(p); p = NULL;

坑5:void* 直接解引用

  • 现象void *p; *p = 20; 编译报错
  • 根因:void* 不知道指向对象大小,无法解引用
  • 解决:先强制转具体类型 *(int*)p = 20;

⑨ 小结

今天完成指针家族八概念(本质 + 结合优先级记忆法)→ 二级指针(解引用1次得串/2次得字符、与指针数组等效、与二维数组不等效、遍历字符串/字符)→ main 函数原型(argc/argv 命令行传参)→ 常量指针/指针常量/常量指针常量(const 位置记忆法 + 对比表)→ 野指针/空指针/空悬指针(三大场景 + 规避四招)→ void 与 void*(万能指针,malloc 返回值)。

下一篇:动态内存管理——malloc/calloc/realloc/free、内存泄漏与越界访问、结构体入门。


本文首发于 CSDN,转载请注明出处。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐