C学习教程,从入门到精通,C语言指针 —— 详细知识点总结(10)

C语言指针 ------ 详细知识点总结


一、指针的相关概念

1.1 什么是地址

在计算机内存中,每一个字节(Byte)都有一个唯一的编号,这个编号就叫做内存地址。操作系统通过地址来管理和访问内存中的数据。

当我们在C语言中定义一个变量时,系统会自动在内存中为该变量分配一定大小的存储空间,该空间的起始地址就是这个变量的地址。

1.2 什么是指针

指针就是内存地址。通过指针,我们可以直接访问和操作内存中某个地址所存储的数据。

指针的核心思想:存储的不是数据本身,而是数据的地址。

1.3 什么是指针变量

指针变量 是一种特殊的变量,它用来存储另一个变量的内存地址。换句话说,指针变量的值就是另一个变量的地址。

1.4 指针的作用

  • 使函数能够修改调用者中的变量(通过传地址)
  • 高效地操作数组和字符串
  • 实现动态内存分配
  • 构建复杂的数据结构(链表、树、图等)
  • 函数返回多个值

二、地址与指针

2.1 取地址运算符 &

& 运算符用于获取变量的内存地址。它的操作数必须是一个变量,不能是常量或表达式。

语法:

c 复制代码
&变量名

示例:

c 复制代码
int a = 10;
printf("%p", &a);  // 输出变量a的内存地址

2.2 地址的大小

在不同的系统中,地址的大小不同:

  • 32位系统:地址占4个字节(32位),指针变量大小为4字节
  • 64位系统:地址占8个字节(64位),指针变量大小为8字节

可以使用 sizeof 运算符来查看指针变量的大小。

2.3 入门案例:查看变量的地址

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int a = 100;          // 定义整型变量a,并赋值为100
    float b = 3.14f;      // 定义单精度浮点型变量b,并赋值为3.14
    char c = 'A';         // 定义字符型变量c,并赋值为字符'A'

    // 使用%p格式符输出各变量的内存地址
    // &a、&b、&c 分别表示变量a、b、c的地址
    printf("变量a的地址: %p\n", &a);
    printf("变量b的地址: %p\n", &b);
    printf("变量c的地址: %p\n", &c);

    // 使用sizeof查看指针的大小
    printf("int*类型指针的大小: %lu 字节\n", sizeof(int*));
    printf("float*类型指针的大小: %lu 字节\n", sizeof(float*));
    printf("char*类型指针的大小: %lu 字节\n", sizeof(char*));

    return 0;  // 程序正常结束
}

运行结果示例(64位系统):

复制代码
变量a的地址: 0x7ffd5e8a3b4c
变量b的地址: 0x7ffd5e8a3b48
变量c的地址: 0x7ffd5e8a3b47
int*类型指针的大小: 8 字节
float*类型指针的大小: 8 字节
char*类型指针的大小: 8 字节

说明:

  • 每次运行程序,变量的地址可能不同(由操作系统分配)
  • 不管指向什么类型,指针变量本身的大小在同一个系统中是固定的

三、变量与指针

3.1 变量的直接访问与间接访问

访问方式 说明
直接访问 通过变量名直接访问变量的值,例如 a = 20;
间接访问 通过指针变量找到变量的地址,再通过地址访问变量的值

打个比方:

  • 直接访问 = 你直接去找某个人(知道他在哪个房间)
  • 间接访问 = 你先找到一张纸条(指针),纸条上写着那个人的房间号(地址),你再去找

3.2 变量的两个重要属性

每个变量都有两个属性:

  1. 地址(左值):变量在内存中的位置
  2. 值(右值):变量中存储的数据
c 复制代码
int num = 42;
// num 的地址(左值):系统分配的内存位置,例如 0x7ffd...
// num 的值(右值):42

四、指针变量

4.1 指针变量的定义

语法:

c 复制代码
基类型 *指针变量名;

说明:

  • * 表示这是一个指针变量
  • 基类型决定了指针变量可以指向的变量类型,也决定了指针移动时的步长
  • * 是指针声明符,可以靠近类型名,也可以靠近变量名

示例:

c 复制代码
int *p;        // p是指向int类型的指针变量
float *fp;     // fp是指向float类型的指针变量
char *cp;      // cp是指向char类型的指针变量
double *dp;    // dp是指向double类型的指针变量

注意:

c 复制代码
int *p, *q;    // 定义了两个指针变量p和q(都指向int)
int *p, q;     // p是指针变量,q是普通int变量(容易混淆,不推荐)

4.2 指针变量的初始化与赋值

方式一:定义时初始化
c 复制代码
int a = 10;
int *p = &a;    // 定义指针p,初始化为变量a的地址
方式二:先定义后赋值
c 复制代码
int a = 10;
int *p;         // 先定义指针变量
p = &a;         // 再将a的地址赋值给p
方式三:赋值为NULL(空指针)
c 复制代码
int *p = NULL;  // 定义一个空指针,表示当前不指向任何有效地址

重要规则:

  • 指针变量只能存储地址,不能直接存储整数等数据
  • 指针变量的类型必须与所指向变量的类型匹配
  • 未初始化的指针(野指针)非常危险,一定要先赋值再使用

4.3 取值运算符 *(解引用运算符)

* 运算符用于通过指针访问(读取或修改)其所指向的变量的值,也叫做解引用。

语法:

c 复制代码
*指针变量名   // 获取指针所指向的变量的值

4.4 入门案例:指针的基本使用

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int a = 10;          // 定义整型变量a,初始值为10
    int *p = &a;         // 定义整型指针p,指向变量a(存储a的地址)

    // 直接访问变量a的值
    printf("a的值(直接访问): %d\n", a);

    // 通过指针p间接访问变量a的值
    // *p 表示"取出p所指向地址中存储的值"
    printf("a的值(间接访问): %d\n", *p);

    // 输出变量a的地址和指针p中存储的地址(两者相同)
    printf("a的地址: %p\n", &a);
    printf("p存储的地址: %p\n", p);

    // 通过指针修改变量a的值
    *p = 100;            // 将p所指向的地址中的值修改为100
    printf("修改后a的值: %d\n", a);  // a的值变成了100

    // 指针p本身的地址(p自己也是一个变量,也有地址)
    printf("p自身的地址: %p\n", &p);

    return 0;  // 程序正常结束
}

运行结果示例:

复制代码
a的值(直接访问): 10
a的值(间接访问): 10
a的地址: 0x7ffd12345678
p存储的地址: 0x7ffd12345678
修改后a的值: 100
p自身的地址: 0x7ffd12345680

4.5 入门案例:两个指针指向同一个变量

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int a = 50;           // 定义整型变量a,初始值为50
    int *p1 = &a;         // 定义指针p1,指向变量a
    int *p2 = &a;         // 定义指针p2,也指向变量a

    // 两个指针指向同一个变量,通过任一指针都可以访问a
    printf("a = %d\n", a);          // 直接访问: 50
    printf("*p1 = %d\n", *p1);     // 通过p1访问: 50
    printf("*p2 = %d\n", *p2);     // 通过p2访问: 50

    // 通过p1修改a的值
    *p1 = 200;
    printf("\n通过p1修改后:\n");
    printf("a = %d\n", a);          // 200
    printf("*p1 = %d\n", *p1);     // 200
    printf("*p2 = %d\n", *p2);     // 200(p2看到的值也变了)

    return 0;  // 程序正常结束
}

五、指针的自增、自减运算

5.1 基本概念

指针变量可以进行自增(++)和自减(--)运算,其含义是使指针向前或向后移动一个"单元"。

关键点: 指针移动的字节数取决于指针的基类型。

基类型 大小(字节) p++ 后移动的字节数
char 1 1 字节
int 4 4 字节
float 4 4 字节
double 8 8 字节

5.2 指针自增/自减的形式

假设 p 是一个指针变量:

表达式 含义 说明
p++ 或 ++p p 指向下一个元素 先用原值(或先加),然后地址 +sizeof(基类型)
p-- 或 --p p 指向上一个元素 先用原值(或先减),然后地址 -sizeof(基类型)
*p++ 先取 *p 的值,然后 p 指向下一个元素 等价于 *(p++),后缀++优先级高于*
*++p 先让 p 指向下一个元素,再取 *p 的值 等价于 *(++p)
(*p)++ 先取 *p 的值,然后将该值加1 先解引用,再对值做自增
++(*p) 先将 *p 的值加1,再取值 先对值做自增,再使用

5.3 入门案例:指针自增运算

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int arr[] = {10, 20, 30, 40, 50};  // 定义一个整型数组,包含5个元素
    int *p = arr;    // 定义指针p,指向数组的第一个元素(arr等价于&arr[0])

    // 输出初始状态
    printf("初始状态:\n");
    printf("p指向的值: %d\n", *p);       // 输出10(arr[0]的值)
    printf("p存储的地址: %p\n\n", p);    // 输出arr[0]的地址

    // p++:先使用p的当前值,然后p向后移动一个int的位置(4字节)
    printf("执行*p++后:\n");
    printf("*p++的返回值: %d\n", *p++);  // 先取*p=10,然后p后移
    printf("现在p指向的值: %d\n", *p);   // 输出20(arr[1]的值)
    printf("p存储的地址: %p\n\n", p);    // 地址增加了4

    // ++p:先让p向后移动一个int的位置,再取值
    printf("执行*++p后:\n");
    printf("*++p的返回值: %d\n", *++p);  // p先后移到arr[2],再取*p=30
    printf("现在p指向的值: %d\n\n", *p); // 输出30

    // (*p)++:先取*p的值,然后该值加1(但不改变p的指向)
    printf("执行(*p)++后:\n");
    printf("(*p)++的返回值: %d\n", (*p)++);  // 先取*p=30,然后* p变为31
    printf("现在*p的值: %d\n\n", *p);         // 输出31

    // 指针自减
    printf("执行*p--后:\n");
    printf("*p--的返回值: %d\n", *p--);  // 先取*p=31,然后p前移
    printf("现在p指向的值: %d\n", *p);   // 输出20(arr[1])

    return 0;  // 程序正常结束
}

5.4 入门案例:用指针遍历数组

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};  // 定义并初始化数组
    int n = 10;                // 数组元素个数
    int *p = arr;              // 指针p指向数组首元素

    printf("数组元素: ");
    // 使用指针的自增运算来遍历数组
    for (int i = 0; i < n; i++)
    {
        printf("%d ", *p);    // 输出当前p指向的元素值
        p++;                  // p向后移动一个int的位置,指向下一个元素
    }
    printf("\n");

    return 0;  // 程序正常结束
}

运行结果:

复制代码
数组元素: 1 2 3 4 5 6 7 8 9 10

5.5 入门案例:指针运算的地址变化

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int arr[] = {100, 200, 300};  // 定义整型数组
    int *p = arr;                  // p指向arr[0]

    // 观察指针自增过程中地址的变化
    printf("元素值\t地址\n");
    printf("%d\t%p\n", *p, p);    // arr[0]: 100

    p++;                            // p指向arr[1]
    printf("%d\t%p\n", *p, p);    // arr[1]: 200

    p++;                            // p指向arr[2]
    printf("%d\t%p\n", *p, p);    // arr[2]: 300

    // 注意:相邻int元素的地址差为4(32位int占4字节)
    // 即每次p++地址值增加了4

    return 0;  // 程序正常结束
}

六、数组与指针

6.1 一维数组与指针

6.1.1 数组名与指针的关系

在C语言中,数组名在大多数情况下会自动转换为指向数组第一个元素的指针(即数组首元素的地址)。

c 复制代码
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;   // arr 等价于 &arr[0]

重要等价关系:

表达式 等价形式 含义
arr &arr[0] 数组首元素的地址
arr + i &arr[i] 第i个元素的地址
*(arr + i) arr[i] 第i个元素的值
p + i &p[i] 指针p后移i个位置的地址
*(p + i) p[i] p后移i个位置后取值
6.1.2 用指针访问一维数组元素的三种方式
c 复制代码
// 方式1:下标法
arr[i]

// 方式2:指针偏移法(不改变指针)
*(arr + i)

// 方式3:指针变量自增法(改变指针)
*p++
6.1.3 入门案例:一维数组与指针
c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int arr[] = {10, 20, 30, 40, 50};  // 定义并初始化一维数组
    int *p = arr;       // 定义指针p,指向数组首元素
    int n = 5;          // 数组元素个数

    // 方法1:下标法访问数组元素
    printf("方法1(下标法):\n");
    for (int i = 0; i < n; i++)
    {
        printf("arr[%d] = %d\n", i, arr[i]);  // 用下标直接访问
    }

    // 方法2:数组名+偏移量法
    printf("\n方法2(数组名偏移法):\n");
    for (int i = 0; i < n; i++)
    {
        // *(arr + i) 等价于 arr[i]
        printf("*(arr + %d) = %d\n", i, *(arr + i));
    }

    // 方法3:指针变量法
    printf("\n方法3(指针变量法):\n");
    for (int i = 0; i < n; i++)
    {
        // *(p + i) 等价于 p[i]
        printf("*(p + %d) = %d\n", i, *(p + i));
    }

    return 0;  // 程序正常结束
}
6.1.4 入门案例:指针与数组名的区别
c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int arr[] = {1, 2, 3, 4, 5};  // 定义数组
    int *p = arr;                   // 指针p指向arr[0]

    // 数组名是"常量指针",不能被赋值(不能改变指向)
    // arr++;  // 错误!数组名不能自增

    // 指针变量可以自增,改变指向
    p++;           // 合法,p现在指向arr[1]
    printf("*p = %d\n", *p);  // 输出2

    // sizeof的区别
    printf("sizeof(arr) = %lu\n", sizeof(arr));  // 整个数组的大小: 5*4=20字节
    printf("sizeof(p) = %lu\n", sizeof(p));      // 指针变量的大小: 8字节(64位)
    printf("数组元素个数 = %lu\n", sizeof(arr) / sizeof(arr[0]));  // 计算元素个数:5

    return 0;  // 程序正常结束
}

6.2 二维数组与指针

6.2.1 二维数组的地址结构

二维数组可以看作"数组的数组"。以 int a[3][4] 为例:

复制代码
a[0] → a[0][0] a[0][1] a[0][2] a[0][3]    // 第0行(一个含4个int的数组)
a[1] → a[1][0] a[1][1] a[1][2] a[1][3]    // 第1行
a[2] → a[2][0] a[2][1] a[2][2] a[2][3]    // 第2行
6.2.2 二维数组与指针的等价关系

设 int a[3][4];,则:

表达式 含义 类型
a 第0行的地址(行指针) int (*)[4]
a + i 第i行的地址 int (*)[4]
*(a + i) 第i行第0列的地址(等价于 a[i]) int *
*(a + i) + j 第i行第j列的地址(等价于 &a[i][j]) int *
*(*(a + i) + j) 第i行第j列的值(等价于 a[i][j]) int
a[i] 第i行第0列的地址 int *
a[i] + j 第i行第j列的地址 int *
a[i][j] 第i行第j列的值 int
6.2.3 行指针(数组指针)

指向一维数组的指针叫做数组指针(也叫行指针):

c 复制代码
int a[3][4];
int (*p)[4] = a;   // p是指向含4个int元素的数组的指针
  • p + 1 跳过一整行(4个int,16字节)
  • *(p + i) + j 等价于 &a[i][j]
  • *(*(p + i) + j) 等价于 a[i][j]
6.2.4 入门案例:二维数组与指针
c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int a[3][4] = {          // 定义3行4列的二维数组
        {1, 2, 3, 4},        // 第0行
        {5, 6, 7, 8},        // 第1行
        {9, 10, 11, 12}      // 第2行
    };

    int i, j;  // 循环变量

    // 方法1:下标法
    printf("方法1(下标法):\n");
    for (i = 0; i < 3; i++)       // 遍历每一行
    {
        for (j = 0; j < 4; j++)   // 遍历每一列
        {
            printf("%4d", a[i][j]);  // 用下标法访问元素
        }
        printf("\n");               // 每行输出后换行
    }

    // 方法2:指针法(使用*(a+i)+j形式)
    printf("\n方法2(指针法):\n");
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 4; j++)
        {
            // *(*(a + i) + j) 等价于 a[i][j]
            printf("%4d", *(*(a + i) + j));
        }
        printf("\n");
    }

    // 方法3:使用行指针(数组指针)
    printf("\n方法3(行指针法):\n");
    int (*p)[4] = a;   // p是指向含4个int的数组的指针(行指针)
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 4; j++)
        {
            // *(*(p + i) + j) 等价于 a[i][j]
            printf("%4d", *(*(p + i) + j));
        }
        printf("\n");
    }

    return 0;  // 程序正常结束
}

运行结果:

复制代码
方法1(下标法):
   1   2   3   4
   5   6   7   8
   9  10  11  12

方法2(指针法):
   1   2   3   4
   5   6   7   8
   9  10  11  12

方法3(行指针法):
   1   2   3   4
   5   6   7   8
   9  10  11  12

七、字符串与指针

7.1 用字符指针指向字符串

在C语言中,字符串可以用字符数组存储,也可以用字符指针指向一个字符串常量。

c 复制代码
// 方式1:字符数组
char str[] = "Hello";

// 方式2:字符指针
char *p = "Hello";   // p指向字符串常量"Hello"的首字符

两种方式的区别:

特性 char str[] = "Hello"; char *p = "Hello";
存储位置 数组在栈区,内容可以修改 指针指向常量区,内容不可修改
能否修改 str[0] = 'h'; 合法 p[0] = 'h'; 未定义行为
sizeof 6(包含'\0') 指针的大小(4或8)
str/p本身 是常量,不能被赋值 是变量,可以改变指向

7.2 字符指针的常见操作

通过字符指针可以实现:

  • 遍历字符串
  • 字符串比较
  • 字符串复制
  • 字符串连接

7.3 入门案例:字符指针的基本使用

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    char *p = "Hello, World!";  // 定义字符指针,指向字符串常量

    // 逐个字符输出
    printf("逐个字符输出: ");
    while (*p != '\0')        // 当p未指向字符串结束符时,继续循环
    {
        printf("%c", *p);     // 输出当前字符
        p++;                  // 指针后移,指向下一个字符
    }
    printf("\n");

    // 注意:此时p已经指向了'\0'之后,不再指向字符串开头
    // 如果需要再次使用,需要重新赋值
    p = "Hello, World!";      // 重新让p指向字符串开头

    // 用下标法访问
    printf("用下标法输出: ");
    for (int i = 0; p[i] != '\0'; i++)  // 用p[i]遍历直到遇到结束符
    {
        printf("%c", p[i]);   // 输出p[i]对应的字符
    }
    printf("\n");

    return 0;  // 程序正常结束
}

7.4 入门案例:字符指针与字符数组的区别

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    // 字符数组方式:可以修改内容
    char str[] = "Hello";     // str是字符数组,存储在栈区,内容可修改
    str[0] = 'h';             // 合法:将第一个字符改为小写'h'
    printf("字符数组str: %s\n", str);  // 输出: hello

    // 字符指针方式:指向字符串常量,不可修改内容
    char *p = "Hello";        // p指向常量区的字符串
    // p[0] = 'h';           // 错误!运行时会报段错误(segmentation fault)
    p = "World";              // 合法:p可以重新指向另一个字符串
    printf("字符指针p: %s\n", p);      // 输出: World

    // sizeof的区别
    printf("sizeof(str) = %lu\n", sizeof(str));  // 6(H,e,l,l,o,\0共6字节)
    printf("sizeof(p) = %lu\n", sizeof(p));      // 8(64位系统指针大小)

    return 0;  // 程序正常结束
}

7.5 入门案例:用字符指针实现字符串复制

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    char source[] = "I love C programming!";   // 源字符串(字符数组)
    char dest[50];                              // 目标字符数组,足够大
    char *ps = source;                          // ps指向源字符串
    char *pd = dest;                            // pd指向目标字符串

    // 逐字符复制,直到遇到源字符串的结束符'\0'
    while (*ps != '\0')     // 源字符串未结束
    {
        *pd = *ps;          // 将源字符复制到目标位置
        ps++;               // 源指针后移
        pd++;               // 目标指针后移
    }
    *pd = '\0';             // 在目标字符串末尾添加结束符

    printf("源字符串: %s\n", source);   // 输出源字符串
    printf("目标字符串: %s\n", dest);   // 输出复制后的字符串

    return 0;  // 程序正常结束
}

运行结果:

复制代码
源字符串: I love C programming!
目标字符串: I love C programming!

八、指针数组

8.1 指针数组的定义

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

语法:

c 复制代码
基类型 *数组名[数组大小];

说明:

  • [] 的优先级高于 *,所以 *数组名[大小] 先表示数组,再表示每个元素是指针
  • 每个数组元素都是一个指向"基类型"的指针

8.2 指针数组的典型用途

用途 说明
管理多个字符串 每个元素指向一个字符串的首地址
模拟二维数组 元素可以指向不同长度的数组
main函数参数 char *argv[] 就是一个指针数组

8.3 入门案例:指针数组管理多个字符串

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    // 定义一个指针数组,每个元素指向一个字符串常量
    char *fruits[] = {
        "Apple",       // fruits[0] 指向 "Apple"
        "Banana",      // fruits[1] 指向 "Banana"
        "Cherry",      // fruits[2] 指向 "Cherry"
        "Durian",      // fruits[3] 指向 "Durian"
        "Elderberry"   // fruits[4] 指向 "Elderberry"
    };

    // 计算数组元素个数
    int n = sizeof(fruits) / sizeof(fruits[0]);  // 5个指针元素

    // 遍历指针数组,输出每个字符串
    printf("水果列表:\n");
    for (int i = 0; i < n; i++)
    {
        // fruits[i] 是一个char*类型的指针,可以直接用%s输出字符串
        printf("  fruits[%d] = %s\n", i, fruits[i]);
    }

    return 0;  // 程序正常结束
}

运行结果:

复制代码
水果列表:
  fruits[0] = Apple
  fruits[1] = Banana
  fruits[2] = Cherry
  fruits[3] = Durian
  fruits[4] = Elderberry

8.4 入门案例:指针数组与二维字符数组的区别

c 复制代码
#include <stdio.h>    // 包含标准输入输出头文件
#include <string.h>   // 包含字符串处理函数头文件

int main()
{
    // 方式1:二维字符数组(每个字符串长度固定,浪费空间)
    char names1[3][20] = {
        "Zhang San",    // 占用20字节,即使只有9个字符+1个\0
        "Li Si",        // 占用20字节
        "Wang Wu"       // 占用20字节
    };

    // 方式2:指针数组(每个字符串长度灵活,节省空间)
    char *names2[] = {
        "Zhang San",    // 只占用实际字符+1的存储空间
        "Li Si",
        "Wang Wu"
    };

    // 两种方式都可以用下标法访问
    printf("二维字符数组:\n");
    for (int i = 0; i < 3; i++)
    {
        printf("  %s\n", names1[i]);  // names1[i] 是第i行的首地址
    }

    printf("\n指针数组:\n");
    for (int i = 0; i < 3; i++)
    {
        printf("  %s\n", names2[i]);  // names2[i] 是第i个指针
    }

    // 二维数组可以修改单个字符
    names1[0][0] = 'z';               // 合法
    printf("\n修改后names1[0]: %s\n", names1[0]);  // zhang San

    // 指针数组不能修改字符串内容,但可以修改指针指向
    // names2[0][0] = 'z';  // 错误!字符串常量不可修改
    names2[0] = "New Name";           // 合法:改变指针指向
    printf("修改后names2[0]: %s\n", names2[0]);  // New Name

    return 0;  // 程序正常结束
}

九、指向指针的指针(二级指针)

9.1 基本概念

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

复制代码
变量a    → 值: 100      地址: 0x1000
指针p    → 值: 0x1000   地址: 0x2000  (存储a的地址)
二级指针pp → 值: 0x2000   地址: 0x3000  (存储p的地址)

9.2 二级指针的定义与使用

语法:

c 复制代码
基类型 **指针变量名;

说明:

  • **pp 表示pp是一个指向指针的指针
  • *pp 得到的是pp所指向的那个指针的值(即一级指针的值,也就是普通变量的地址)
  • **pp 得到的是最终的变量值

9.3 入门案例:二级指针的基本使用

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    int a = 100;          // 定义普通整型变量a,值为100
    int *p = &a;          // 定义一级指针p,指向变量a
    int **pp = &p;        // 定义二级指针pp,指向指针p

    // 通过不同方式访问a的值
    printf("a的值(直接访问): %d\n", a);       // 直接访问: 100
    printf("a的值(一级指针): %d\n", *p);      // 通过一级指针: 100
    printf("a的值(二级指针): %d\n", **pp);    // 通过二级指针: 100

    // 通过不同方式访问a的地址
    printf("\na的地址: %p\n", &a);              // a的地址
    printf("p的值(a的地址): %p\n", p);        // p中存储的值就是a的地址
    printf("*pp(p的值): %p\n", *pp);          // *pp就是p的值,即a的地址

    // 通过二级指针修改a的值
    **pp = 999;           // 等价于 *p = 999; 等价于 a = 999;
    printf("\n通过二级指针修改后:\n");
    printf("a = %d\n", a);       // 999
    printf("*p = %d\n", *p);    // 999
    printf("**pp = %d\n", **pp); // 999

    // 各变量自身的地址
    printf("\na的地址: %p\n", &a);
    printf("p的地址: %p\n", &p);     // p自身的地址
    printf("pp的地址: %p\n", &pp);   // pp自身的地址

    return 0;  // 程序正常结束
}

9.4 入门案例:二级指针与指针数组

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

int main()
{
    // 定义指针数组,每个元素指向一个字符串
    char *names[] = {"Alice", "Bob", "Charlie"};

    // 定义二级指针pp,指向指针数组names的首元素
    // names 是数组首元素的地址,即 &names[0],类型为 char**
    char **pp = names;

    // 通过二级指针遍历指针数组中的每个字符串
    printf("使用二级指针遍历:\n");
    for (int i = 0; i < 3; i++)
    {
        // *(pp + i) 等价于 names[i],得到第i个字符串的首地址
        // 用%s输出该地址处的字符串
        printf("  names[%d] = %s\n", i, *(pp + i));
    }

    // 也可以逐级解引用
    printf("\n逐字符访问:\n");
    printf("names[0]的第1个字符: %c\n", **pp);        // 'A'(names[0]的首字符)
    printf("names[0]的第2个字符: %c\n", *(*pp + 1));  // 'l'(names[0]的第2个字符)

    return 0;  // 程序正常结束
}

运行结果:

复制代码
使用二级指针遍历:
  names[0] = Alice
  names[1] = Bob
  names[2] = Charlie

逐字符访问:
names[0]的第1个字符: A
names[0]的第2个字符: l

十、指针变量作函数参数

10.1 为什么需要指针作函数参数

在C语言中,函数参数传递默认是值传递 (传值的副本),函数内部对形参的修改不会影响实参。

如果需要在函数内部修改调用者中的变量 ,就需要传递变量的地址 (指针),这就是地址传递(传址调用)。

10.2 值传递与地址传递的对比

特性 值传递 地址传递
传递内容 变量的值(副本) 变量的地址
函数内能否修改实参 不能 能
语法 void func(int a) void func(int *a)
调用方式 func(x) func(&x)

10.3 入门案例:值传递(不能修改实参)

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

// 值传递:形参a是实参x的副本
void change(int a)      // 接收int类型的值
{
    a = 999;            // 只修改了副本a,不影响实参x
    printf("函数内a = %d\n", a);  // 999
}

int main()
{
    int x = 100;        // 定义变量x
    printf("调用前x = %d\n", x);  // 100

    change(x);          // 值传递:将x的值复制给形参a

    printf("调用后x = %d\n", x);  // 100(x没有被修改!)

    return 0;  // 程序正常结束
}

运行结果:

复制代码
调用前x = 100
函数内a = 999
调用后x = 100

10.4 入门案例:地址传递(可以修改实参)

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

// 地址传递:形参p是指针,接收变量的地址
void change(int *p)     // p接收的是变量x的地址
{
    *p = 999;           // 通过指针修改p所指向地址的值(即修改了x)
    printf("函数内*p = %d\n", *p);  // 999
}

int main()
{
    int x = 100;        // 定义变量x
    printf("调用前x = %d\n", x);  // 100

    change(&x);         // 地址传递:将x的地址传给函数

    printf("调用后x = %d\n", x);  // 999(x被成功修改!)

    return 0;  // 程序正常结束
}

运行结果:

复制代码
调用前x = 100
函数内*p = 999
调用后x = 999

10.5 入门案例:用指针参数实现两个变量的交换

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

// 交换函数:接收两个指针参数
void swap(int *pa, int *pb)  // pa和pb分别指向要交换的两个变量
{
    int temp;        // 定义临时变量
    temp = *pa;      // 将pa所指向的值存入temp
    *pa = *pb;       // 将pb所指向的值赋给pa所指向的位置
    *pb = temp;      // 将temp的值赋给pb所指向的位置
}

int main()
{
    int a = 10;      // 定义变量a
    int b = 20;      // 定义变量b

    printf("交换前: a = %d, b = %d\n", a, b);  // a=10, b=20

    swap(&a, &b);    // 传递a和b的地址

    printf("交换后: a = %d, b = %d\n", a, b);  // a=20, b=10

    return 0;  // 程序正常结束
}

运行结果:

复制代码
交换前: a = 10, b = 20
交换后: a = 20, b = 10

10.6 入门案例:数组名作函数参数(本质是指针)

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

// 计算数组元素之和
// 参数arr实际上是一个指针,不是整个数组的副本
int arraySum(int arr[], int n)   // arr[]等价于 *arr
{
    int sum = 0;        // 初始化累加和为0
    for (int i = 0; i < n; i++)
    {
        sum += arr[i];  // 累加每个元素的值
    }
    return sum;         // 返回总和
}

// 用指针方式实现同样的功能
int arraySum2(int *arr, int n)   // 明确用指针接收
{
    int sum = 0;        // 初始化累加和为0
    for (int i = 0; i < n; i++)
    {
        sum += *(arr + i);  // 用指针偏移方式访问元素
    }
    return sum;         // 返回总和
}

int main()
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};  // 定义数组
    int n = sizeof(arr) / sizeof(arr[0]);  // 计算元素个数

    printf("数组元素之和: %d\n", arraySum(arr, n));    // 55
    printf("数组元素之和: %d\n", arraySum2(arr, n));   // 55

    return 0;  // 程序正常结束
}

十一、返回指针值的函数

11.1 基本概念

函数可以返回一个指针(地址),这样的函数叫做返回指针值的函数。

语法:

c 复制代码
基类型 *函数名(参数列表)
{
    // 函数体
    return &某变量;  // 或 return 某指针;
}

注意: * 紧跟在函数名前面,表示返回值是一个指针。

11.2 重要注意事项

绝对不能返回局部变量的地址! 局部变量在函数结束后被销毁,其地址不再有效。

c 复制代码
// 错误示例------返回局部变量的地址(悬垂指针)
int *func()
{
    int a = 10;   // a是局部变量,函数结束后会被销毁
    return &a;    // 危险!返回了一个已销毁变量的地址
}

安全的做法:

  • 返回全局变量或静态局部变量的地址
  • 返回通过 malloc 动态分配的内存地址
  • 返回传入的指针参数

11.3 入门案例:返回静态局部变量的地址

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

// 返回两个数中较大数的地址
int *maxPtr(int *pa, int *pb)  // 接收两个指针,返回较大者的指针
{
    // 比较两个指针所指向的值
    if (*pa >= *pb)             // 如果a >= b
    {
        return pa;              // 返回a的地址
    }
    else                        // 否则
    {
        return pb;              // 返回b的地址
    }
}

int main()
{
    int a = 30;                 // 定义变量a
    int b = 50;                 // 定义变量b

    // 调用函数,获取较大值的地址
    int *pMax = maxPtr(&a, &b);

    printf("a = %d, b = %d\n", a, b);
    printf("较大值 = %d\n", *pMax);     // 50

    return 0;  // 程序正常结束
}

11.4 入门案例:在数组中查找元素并返回指针

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件
#include <stddef.h>  // 包含NULL的定义

// 在数组arr中查找值为target的元素,找到则返回其地址,否则返回NULL
int *findElement(int arr[], int n, int target)
{
    for (int i = 0; i < n; i++)     // 遍历数组
    {
        if (arr[i] == target)       // 如果找到目标值
        {
            return &arr[i];         // 返回该元素的地址
        }
    }
    return NULL;                    // 未找到,返回NULL
}

int main()
{
    int arr[] = {10, 25, 33, 47, 58, 62, 79};  // 定义数组
    int n = sizeof(arr) / sizeof(arr[0]);       // 计算元素个数

    int target = 47;                // 要查找的目标值

    // 调用查找函数
    int *result = findElement(arr, n, target);

    if (result != NULL)             // 如果找到了
    {
        printf("找到了!值为%d,位于地址%p\n", *result, result);
        printf("它是数组的第%ld个元素\n", result - arr + 1);
        // 指针相减得到两个元素之间的距离(元素个数)
    }
    else                            // 没找到
    {
        printf("未找到值为%d的元素\n", target);
    }

    return 0;  // 程序正常结束
}

运行结果:

复制代码
找到了!值为47,位于地址0x...
它是数组的第4个元素

十二、指针数组作main函数的参数

12.1 带参数的main函数

标准的 main 函数可以接收命令行参数:

c 复制代码
int main(int argc, char *argv[])
{
    // 函数体
    return 0;
}

参数说明:

参数 含义
argc argument count,命令行参数的个数(包含程序名本身)
argv argument vector,是一个指针数组,每个元素指向一个字符串(参数内容)

注意:

  • argv[0] 始终是程序的名称(含路径)
  • argv[1] 到 argv[argc-1] 是用户输入的命令行参数
  • argv[argc] 的值是 NULL

12.2 命令行参数的执行方式

假设编译后生成可执行文件 program:

复制代码
./program hello world 123

则:

  • argc = 4
  • argv[0] = "./program"
  • argv[1] = "hello"
  • argv[2] = "world"
  • argv[3] = "123"
  • argv[4] = NULL

12.3 入门案例:输出所有命令行参数

c 复制代码
#include <stdio.h>  // 包含标准输入输出头文件

// 带命令行参数的main函数
int main(int argc, char *argv[])
{
    // argc: 命令行参数的总个数
    // argv: 指针数组,每个元素指向一个参数字符串

    printf("参数个数: %d\n\n", argc);  // 输出参数个数

    // 遍历所有参数
    for (int i = 0; i < argc; i++)
    {
        // argv[i] 是第i个参数的字符串地址
        printf("argv[%d] = \"%s\"\n", i, argv[i]);
    }

    // 输出程序名称
    if (argc > 0)
    {
        printf("\n程序名称: %s\n", argv[0]);
    }

    // 输出用户参数(跳过程序名)
    if (argc > 1)
    {
        printf("\n用户参数:\n");
        for (int i = 1; i < argc; i++)    // 从argv[1]开始
        {
            printf("  第%d个参数: %s\n", i, argv[i]);
        }
    }
    else
    {
        printf("\n未提供额外参数\n");
    }

    return 0;  // 程序正常结束
}

编译和运行:

bash 复制代码
gcc program.c -o program
./program hello world 2024

运行结果:

复制代码
参数个数: 4

argv[0] = "./program"
argv[1] = "hello"
argv[2] = "world"
argv[3] = "2024"

程序名称: ./program

用户参数:
  第1个参数: hello
  第2个参数: world
  第3个参数: 2024

12.4 入门案例:简易命令行计算器

c 复制代码
#include <stdio.h>   // 包含标准输入输出头文件
#include <stdlib.h>  // 包含atoi函数(字符串转整数)

// 通过命令行参数实现简单的两个数的运算
// 使用方式: ./calc 10 + 20
int main(int argc, char *argv[])
{
    // 检查参数个数是否正确(程序名 + 数字1 + 运算符 + 数字2 = 4个参数)
    if (argc != 4)
    {
        printf("用法: %s 数字1 运算符 数字2\n", argv[0]);
        printf("示例: %s 10 + 20\n", argv[0]);
        return 1;  // 参数错误,返回1
    }

    // 从命令行参数中读取两个操作数
    // atoi()函数将字符串转换为整数
    int num1 = atoi(argv[1]);   // 第一个数字
    char op = argv[2][0];       // 运算符(取字符串的第一个字符)
    int num2 = atoi(argv[3]);   // 第二个数字
    int result;                  // 存储运算结果

    // 根据运算符执行相应的运算
    switch (op)
    {
        case '+':                       // 加法
            result = num1 + num2;
            printf("%d + %d = %d\n", num1, num2, result);
            break;                      // 跳出switch

        case '-':                       // 减法
            result = num1 - num2;
            printf("%d - %d = %d\n", num1, num2, result);
            break;

        case '*':                       // 乘法
            result = num1 * num2;
            printf("%d * %d = %d\n", num1, num2, result);
            break;

        case '/':                       // 除法
            if (num2 != 0)              // 检查除数是否为0
            {
                result = num1 / num2;
                printf("%d / %d = %d\n", num1, num2, result);
            }
            else
            {
                printf("错误:除数不能为0!\n");
            }
            break;

        default:                        // 不支持的运算符
            printf("不支持的运算符: %c\n", op);
            break;
    }

    return 0;  // 程序正常结束
}

十三、指针相关的重要补充知识

13.1 NULL指针

c 复制代码
int *p = NULL;   // 定义一个空指针,不指向任何有效地址
  • NULL 在大多数系统中定义为 ((void*)0) 或 0
  • 使用指针前应检查是否为 NULL
  • 对 NULL 指针进行解引用会导致程序崩溃(段错误)
c 复制代码
if (p != NULL)    // 使用前先检查
{
    printf("%d\n", *p);  // 安全地使用
}

13.2 指针的常见错误

错误类型 示例 后果
野指针(未初始化) int *p; *p = 10; 未定义行为,可能崩溃
空指针解引用 int *p = NULL; *p = 10; 段错误
类型不匹配 float f; int *p = &f; 数据解释错误
返回局部变量地址 int *f() { int a; return &a; } 悬垂指针

13.3 void 指针(通用指针)

c 复制代码
void *p;   // 可以指向任何类型的数据
  • void * 不能直接解引用,需要先强制类型转换
  • void * 可以与任何类型的指针互相赋值(无需强制转换)
  • 常见于 malloc 等内存分配函数的返回值

13.4 指针的大小

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

int main()
{
    printf("char*的大小: %lu\n", sizeof(char*));      // 8(64位系统)
    printf("int*的大小: %lu\n", sizeof(int*));        // 8
    printf("float*的大小: %lu\n", sizeof(float*));    // 8
    printf("double*的大小: %lu\n", sizeof(double*));  // 8

    // 无论指向什么类型,指针本身的大小在同一个系统中是固定的
    // 32位系统: 4字节
    // 64位系统: 8字节

    return 0;
}

13.5 指针与数组常用运算总结

假设 int arr[5] = {10, 20, 30, 40, 50}; 且 int *p = arr;

表达式 值 说明
*p 10 p当前指向的元素值
*(p + 2) 30 p偏移2个位置后的元素值
*p + 2 12 p指向的值加2(注意与上面的区别)
p[3] 40 等价于 *(p + 3)
p++ --- p指向下一个元素
p - arr 0 两个指针相减,得到元素个数之差

总结图

复制代码
┌─────────────────────────────────────────────────┐
│                  指针知识体系                      │
├─────────────────────────────────────────────────┤
│                                                   │
│  基础概念                                          │
│  ├── 地址(变量在内存中的位置编号)                   │
│  ├── 指针(就是地址)                               │
│  ├── 指针变量(存储地址的变量)                       │
│  ├── &取地址运算符                                 │
│  └── *解引用运算符(通过地址访问值)                  │
│                                                   │
│  指针运算                                          │
│  ├── 赋值:p = &a                                  │
│  ├── 解引用:*p                                    │
│  ├── 自增自减:p++, p--                             │
│  ├── 指针+整数:p + i                               │
│  ├── 指针-整数:p - i                               │
│  ├── 指针-指针:p1 - p2(得到元素间距)               │
│  └── 指针比较:p1 == p2, p1 < p2                    │
│                                                   │
│  指针与数组                                         │
│  ├── 一维数组:arr 等价于 &arr[0]                    │
│  ├── *(arr + i) 等价于 arr[i]                       │
│  ├── 二维数组:*(*(a + i) + j) 等价于 a[i][j]        │
│  └── 行指针:int (*p)[列数]                          │
│                                                   │
│  指针与字符串                                       │
│  ├── char *p = "Hello";                            │
│  └── 字符数组 vs 字符指针的区别                       │
│                                                   │
│  指针数组                                           │
│  ├── int *arr[n]; (n个指针的数组)                   │
│  └── 常用于管理多个字符串                             │
│                                                   │
│  二级指针                                           │
│  ├── int **pp = &p;                                │
│  └── 通过**pp逐级解引用得到最终值                     │
│                                                   │
│  函数与指针                                         │
│  ├── 指针作函数参数(实现地址传递)                    │
│  ├── 返回指针的函数                                  │
│  └── main(int argc, char *argv[])                  │
│                                                   │
└─────────────────────────────────────────────────┘

学习建议: 指针是C语言中最核心也是最难掌握的概念。建议先理解内存地址的概念,再从最基本的"取地址+解引用"开始练习,逐步过渡到数组指针、函数指针等高级用法。多画内存图,多动手调试,是学好指针的关键。

相关推荐
熊猫钓鱼>_>1 小时前
开源鸿蒙平台 KMP 三方库 kotlin-csv 适配全流程:从编码假设到沙箱真机互通验证
开发语言·人工智能·华为·kotlin·harmonyos·鸿蒙
金融小师妹1 小时前
多因子时间序列分析:美元与美债收益率变化对黄金价格+1.6%的影响
c语言·人工智能·git·决策树·pyqt
Meteors.1 小时前
Android Compose学习02:基础组件与 Modifier
android·学习
张张123y1 小时前
# 后端第七课:FastAPI 实战入门(用刚学的 Python 写第一个 API 接口)
开发语言·python·mysql·api·fastapi
小陈不好吃1 小时前
Java 泛型与类型擦除:从 PECS 原则到 TypeToken 绕过
java·开发语言·windows·pecs 原则·typetoken绕过
Shaoshing2 小时前
存储引擎与架构
java·开发语言·架构
额额额对了2 小时前
Linux 驱动开发:基于 platform 总线 + misc 设备的 DS18B20 单总线温度驱动
linux·c语言·开发语言·驱动开发·嵌入式硬件
m4Rk_2 小时前
【论文阅读】Agent 记忆机制(102):UMA——用多个未来问题端到端训练一份共享记忆
论文阅读·人工智能·学习·开源·github
devilnumber2 小时前
Java 代理模式(简单理解回顾)
java·开发语言·代理模式