【C语言程序设计】第33篇:二级指针与指针数组

1 引言

考虑这样一个问题:我们需要编写一个函数,修改传入的指针本身(而不仅仅是指针指向的内容)。比如,让指针指向一个新分配的内存块:

c

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

/* 错误:试图修改指针本身,但只是修改了副本 */
void allocate_bad(int *p, int size)
{
    p = (int*)malloc(size * sizeof(int));  /* 只修改了局部指针 */
}

/* 正确:使用二级指针,可以修改调用者的指针 */
void allocate_good(int **p, int size)
{
    *p = (int*)malloc(size * sizeof(int));  /* 修改调用者的指针 */
}

int main(void)
{
    int *arr = NULL;
    
    allocate_bad(arr, 10);
    if (arr == NULL) {
        printf("arr 仍然是 NULL\n");
    }
    
    allocate_good(&arr, 10);
    if (arr != NULL) {
        printf("arr 现在指向分配的内存\n");
        free(arr);
    }
    
    return 0;
}

这就是二级指针的典型应用场景。本章我们将深入探讨这类问题。


2 二级指针(指向指针的指针)

2.1 基本概念

二级指针(Pointer to Pointer)是指针的指针,它存储的是另一个指针变量的地址。

c

复制代码
int a = 10;     /* 普通变量 */
int *p = &a;    /* 一级指针:指向 int */
int **pp = &p;  /* 二级指针:指向 int* */

内存示意图:

text

复制代码
变量 a:  [10]      地址 0x1000
指针 p:  [0x1000]  地址 0x2000  (存储 a 的地址)
指针 pp: [0x2000]  地址 0x3000  (存储 p 的地址)

2.2 定义与使用

c

复制代码
#include <stdio.h>

int main(void)
{
    int a = 10;
    int *p = &a;      /* 一级指针 */
    int **pp = &p;    /* 二级指针 */
    
    printf("a = %d\n", a);
    printf("*p = %d\n", *p);        /* 通过一级指针访问 a */
    printf("**pp = %d\n", **pp);    /* 通过二级指针访问 a */
    
    /* 修改值 */
    **pp = 20;        /* 修改 a 的值 */
    printf("a = %d\n", a);
    
    return 0;
}

2.3 多级指针

理论上可以有多级指针,但实际很少用到三级以上:

c

复制代码
int a = 10;
int *p = &a;      /* 一级 */
int **pp = &p;    /* 二级 */
int ***ppp = &pp; /* 三级 */

printf("%d\n", ***ppp);  /* 输出 10 */

2.4 二级指针的典型应用

2.4.1 修改调用者的指针

c

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

/* 为字符串分配内存并拷贝 */
void create_string(char **str, const char *content)
{
    *str = (char*)malloc(strlen(content) + 1);
    if (*str != NULL) {
        strcpy(*str, content);
    }
}

/* 释放字符串并置空 */
void destroy_string(char **str)
{
    if (*str != NULL) {
        free(*str);
        *str = NULL;  /* 置空调用者的指针 */
    }
}

int main(void)
{
    char *name = NULL;
    
    create_string(&name, "Alice");
    if (name != NULL) {
        printf("name = %s\n", name);
        destroy_string(&name);
    }
    
    /* name 现在是 NULL,安全 */
    return 0;
}
2.4.2 二维数组的动态分配

c

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

/* 动态分配二维数组(每行长度可以不同) */
int** create_matrix(int rows, int cols)
{
    int **matrix = (int**)malloc(rows * sizeof(int*));
    if (matrix == NULL) return NULL;
    
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int*)malloc(cols * sizeof(int));
        if (matrix[i] == NULL) {
            /* 分配失败,释放已分配的行 */
            for (int j = 0; j < i; j++) {
                free(matrix[j]);
            }
            free(matrix);
            return NULL;
        }
    }
    
    return matrix;
}

/* 释放矩阵 */
void free_matrix(int **matrix, int rows)
{
    if (matrix == NULL) return;
    
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
}

int main(void)
{
    int rows = 3, cols = 4;
    int **mat = create_matrix(rows, cols);
    
    if (mat != NULL) {
        /* 使用矩阵 */
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                mat[i][j] = i * cols + j;
            }
        }
        
        /* 打印 */
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                printf("%2d ", mat[i][j]);
            }
            printf("\n");
        }
        
        free_matrix(mat, rows);
    }
    
    return 0;
}

3 指针数组与数组指针

这两个概念名称相似但含义完全不同,非常容易混淆。

3.1 指针数组

指针数组:一个数组,其元素都是指针。

c

复制代码
int *arr[5];  /* arr 是一个数组,包含5个 int* 元素 */

c

复制代码
#include <stdio.h>

int main(void)
{
    int a = 10, b = 20, c = 30;
    int *arr[3];  /* 指针数组,可以存放3个 int* */
    
    arr[0] = &a;
    arr[1] = &b;
    arr[2] = &c;
    
    for (int i = 0; i < 3; i++) {
        printf("%d ", *arr[i]);  /* 输出 10 20 30 */
    }
    printf("\n");
    
    return 0;
}

内存布局

text

复制代码
arr[0]: [地址指向 a]
arr[1]: [地址指向 b]
arr[2]: [地址指向 c]

3.2 数组指针

数组指针:一个指针,指向整个数组。

c

复制代码
int (*p)[5];  /* p 是一个指针,指向包含5个 int 的数组 */

c

复制代码
#include <stdio.h>

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int (*p)[5] = &arr;  /* p 指向整个数组 */
    
    for (int i = 0; i < 5; i++) {
        printf("%d ", (*p)[i]);  /* 通过指针访问数组元素 */
    }
    printf("\n");
    
    printf("arr 大小: %zu\n", sizeof(arr));   /* 20 */
    printf("p 大小: %zu\n", sizeof(p));       /* 8(指针大小) */
    printf("*p 大小: %zu\n", sizeof(*p));     /* 20(指向的数组大小) */
    
    return 0;
}

内存布局

text

复制代码
p: [地址指向整个数组]
    ↓
arr: [1][2][3][4][5]

3.3 辨析对比

写法 名称 含义 大小示例(64位)
int *p[5] 指针数组 数组,有5个 int* 元素 数组大小:5×8=40
int (*p)[5] 数组指针 指针,指向有5个 int 的数组 指针大小:8

记忆技巧

  • [] 优先级高于 *,所以 int *p[5] 先形成数组

  • 括号改变优先级:(*p) 表示 p 是指针,然后指向数组

c

复制代码
/* 复杂声明解析 */
int *arr[5];     /* arr 是数组,元素是 int* */
int (*ptr)[5];   /* ptr 是指针,指向 int[5] 数组 */
int *func();     /* func 是函数,返回 int* */
int (*fp)();     /* fp 是函数指针 */

4 命令行参数 argv 的实质

4.1 argv 的类型

main 函数的第二个参数 argv 就是一个经典的指针数组:

c

复制代码
int main(int argc, char *argv[])
  • argc:命令行参数的个数

  • argv:一个指针数组,每个元素指向一个命令行参数字符串

4.2 内存布局

假设程序这样运行:

bash

复制代码
./program hello world 123

内存中的结构:

text

复制代码
argv 本身是一个指针数组(在栈上):
argv[0] → "./program\0"  (程序名)
argv[1] → "hello\0"
argv[2] → "world\0"
argv[3] → "123\0"
argv[4] → NULL           (结束标志)

4.3 等价的表示方式

argv 也可以写成 char **argv

c

复制代码
int main(int argc, char **argv)  /* 完全等价 */

这清楚地表明:argv 是一个二级指针,它指向指针数组的第一个元素。

4.4 遍历命令行参数

c

复制代码
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("参数个数:%d\n", argc);
    
    /* 方式1:用下标访问 */
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    
    /* 方式2:用指针遍历 */
    char **p = argv;
    while (*p != NULL) {
        printf("%s ", *p);
        p++;
    }
    printf("\n");
    
    /* 方式3:分析选项 */
    for (int i = 1; i < argc; i++) {  /* 从1开始,跳过程序名 */
        if (argv[i][0] == '-') {
            printf("发现选项:%s\n", argv[i]);
        }
    }
    
    return 0;
}

4.5 修改命令行参数

理论上可以修改 argv 指向的字符串(但通常不推荐):

c

复制代码
int main(int argc, char *argv[])
{
    /* 可以修改 argv 指向的字符串内容(但要注意长度) */
    if (argc > 1) {
        argv[1][0] = 'H';  /* 修改第一个参数字符串 */
    }
    
    /* 也可以修改 argv 指针数组本身 */
    argv[1] = "modified";  /* 指向新的字符串常量 */
    
    return 0;
}

但这样做容易引起混乱,一般只在特定场景下使用。


5 综合示例

5.1 字符串排序(使用指针数组)

c

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

/* 对指针数组中的字符串进行排序 */
void sort_strings(char *strings[], int n)
{
    for (int i = 0; i < n - 1; i++) {
        for (int j = i + 1; j < n; j++) {
            if (strcmp(strings[i], strings[j]) > 0) {
                /* 交换指针,而不是交换字符串内容 */
                char *temp = strings[i];
                strings[i] = strings[j];
                strings[j] = temp;
            }
        }
    }
}

int main(void)
{
    /* 指针数组,指向字符串常量 */
    char *fruits[] = {
        "banana", "apple", "orange", "grape", "pear"
    };
    int n = sizeof(fruits) / sizeof(fruits[0]);
    
    printf("排序前:");
    for (int i = 0; i < n; i++) {
        printf("%s ", fruits[i]);
    }
    printf("\n");
    
    sort_strings(fruits, n);
    
    printf("排序后:");
    for (int i = 0; i < n; i++) {
        printf("%s ", fruits[i]);
    }
    printf("\n");
    
    return 0;
}

5.2 动态字符串数组

c

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

typedef struct {
    char **data;    /* 二级指针,指向字符串数组 */
    int size;
    int capacity;
} StringArray;

/* 初始化 */
void str_array_init(StringArray *arr, int capacity)
{
    arr->data = (char**)malloc(capacity * sizeof(char*));
    arr->size = 0;
    arr->capacity = arr->data ? capacity : 0;
}

/* 添加字符串 */
int str_array_add(StringArray *arr, const char *str)
{
    if (arr->size >= arr->capacity) {
        int new_cap = arr->capacity == 0 ? 4 : arr->capacity * 2;
        char **new_data = (char**)realloc(arr->data, new_cap * sizeof(char*));
        if (new_data == NULL) return -1;
        arr->data = new_data;
        arr->capacity = new_cap;
    }
    
    /* 为字符串分配内存并拷贝 */
    arr->data[arr->size] = (char*)malloc(strlen(str) + 1);
    if (arr->data[arr->size] == NULL) return -1;
    
    strcpy(arr->data[arr->size], str);
    arr->size++;
    return 0;
}

/* 释放所有内存 */
void str_array_destroy(StringArray *arr)
{
    for (int i = 0; i < arr->size; i++) {
        free(arr->data[i]);  /* 释放每个字符串 */
    }
    free(arr->data);         /* 释放指针数组 */
    arr->data = NULL;
    arr->size = arr->capacity = 0;
}

int main(void)
{
    StringArray arr;
    str_array_init(&arr, 2);
    
    str_array_add(&arr, "Hello");
    str_array_add(&arr, "World");
    str_array_add(&arr, "C Language");
    
    for (int i = 0; i < arr.size; i++) {
        printf("%s\n", arr.data[i]);
    }
    
    str_array_destroy(&arr);
    return 0;
}

5.3 简单命令行解析器

c

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

typedef struct {
    const char *name;
    const char *value;
} Option;

int parse_options(int argc, char *argv[], Option opts[], int max_opts)
{
    int opt_count = 0;
    
    for (int i = 1; i < argc; i++) {
        if (argv[i][0] == '-') {
            /* 发现选项 */
            if (opt_count < max_opts) {
                opts[opt_count].name = argv[i] + 1;  /* 跳过 '-' */
                
                /* 检查是否有参数值(下一个参数不以 '-' 开头) */
                if (i + 1 < argc && argv[i + 1][0] != '-') {
                    opts[opt_count].value = argv[i + 1];
                    i++;  /* 跳过参数值 */
                } else {
                    opts[opt_count].value = NULL;
                }
                opt_count++;
            }
        }
    }
    
    return opt_count;
}

int main(int argc, char *argv[])
{
    Option options[10];
    int n = parse_options(argc, argv, options, 10);
    
    printf("发现 %d 个选项:\n", n);
    for (int i = 0; i < n; i++) {
        if (options[i].value) {
            printf("  -%s = %s\n", options[i].name, options[i].value);
        } else {
            printf("  -%s (无参数)\n", options[i].name);
        }
    }
    
    return 0;
}

6 常见错误与注意事项

6.1 混淆指针数组和数组指针

c

复制代码
int *p[5];   /* 指针数组 */
int (*p)[5]; /* 数组指针 */

/* 错误赋值 */
int arr[5];
p = &arr;    /* 如果 p 是指针数组,错误 */

6.2 二级指针初始化错误

c

复制代码
int a = 10;
int **pp;
*pp = &a;    /* 错误!pp 未初始化,*pp 指向随机位置 */

6.3 忘记多级解引用

c

复制代码
int a = 10;
int *p = &a;
int **pp = &p;

printf("%d\n", *pp);   /* 输出 p 的值(地址),不是 a */
printf("%d\n", **pp);  /* 正确,输出 10 */

6.4 内存管理错误

c

复制代码
char **arr = malloc(5 * sizeof(char*));
for (int i = 0; i < 5; i++) {
    arr[i] = malloc(10);  /* 为每个字符串分配内存 */
}
/* 使用... */
free(arr);  /* 错误!只释放了指针数组,字符串内存泄漏 */

/* 正确释放 */
for (int i = 0; i < 5; i++) {
    free(arr[i]);  /* 先释放每个字符串 */
}
free(arr);         /* 再释放指针数组 */

6.5 对 argv 的错误假设

c

复制代码
int main(int argc, char *argv[])
{
    /* 假设 argv[1] 一定存在 */
    printf("%s\n", argv[1]);  /* 如果没有参数,越界! */
    
    /* 应该先检查 argc */
    if (argc > 1) {
        printf("%s\n", argv[1]);
    }
}

7 本章小结

本章系统介绍了二级指针和指针数组:

1. 二级指针(指向指针的指针)

  • 定义:int **pp;

  • 用于修改调用者的指针

  • 动态二维数组的实现

  • 多级指针理论上存在,但很少用

2. 指针数组 vs 数组指针

写法 名称 含义
int *p[5] 指针数组 数组,元素是 int*
int (*p)[5] 数组指针 指针,指向 int[5] 数组

3. 命令行参数 argv 的实质

  • char *argv[] 等价于 char **argv

  • 是指针数组,每个元素指向一个参数字符串

  • NULL 结尾

  • argc 表示参数个数

4. 典型应用

  • 修改函数外部的指针(分配内存)

  • 动态二维数组

  • 字符串数组处理

  • 命令行参数解析

5. 常见错误

  • 混淆指针数组和数组指针

  • 二级指针未初始化

  • 多级解引用错误

  • 内存释放不完全

  • 对 argv 越界访问

相关推荐
LSL666_1 小时前
IService——使用和新增
java·开发语言·mybatisplus
falldeep1 小时前
LLM中的强化学习方法分类
开发语言·人工智能·机器学习
雨落在了我的手上2 小时前
C语言之数据结构初见篇(6):单链表的介绍(2)
数据结构
DANGAOGAO2 小时前
数据结构复习(持续更新)
数据结构
落地加湿器2 小时前
Acwing算法课图论与搜索笔记
c++·笔记·算法·图论·dfs·bfs·图搜索算法
cui_ruicheng2 小时前
C++ 数据结构进阶:哈希表原理
数据结构·c++·算法·哈希算法
WG_172 小时前
Linux44:POSIX信号量:
java·开发语言
黎阳之光2 小时前
黎阳之光:AI硬核技术锚定十五五,赋能海空天全域智能感知
大数据·人工智能·算法·安全·数字孪生