08-C语言指针

plana_lab 发布于 21 天前 62 次阅读



通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。


1. 什么是指针

指针就是一个用来保存地址的变量,*p 可以通过这个地址访问对应的数据。

使用例:

int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;    /* 一个字符型的指针 */

内存地址

程序运行时,变量会被存放在计算机的内存中。

每一块内存都有一个自己的编号,这个编号就是内存地址。

例如:

#include <stdio.h>

int main() {

    int a = 10;

    printf("a的值:%d\n", a);
    printf("a的地址:%p\n", &a);

    return 0;
}

运行结果可能是:

a的值:10
a的地址:000000000061FE1C //地址不是每次运行都一样,使用不是硬编写

这里:

a

表示变量 a 中保存的数据:

10

而:

&a

表示获取变量 a 的内存地址。

所以可以简单理解为:

变量 a
┌──────────────┐
│      10      │
└──────────────┘
       ↑
       │
      &a
       │
       ▼
   内存地址

使用 %p 查看地址

使用 printf() 输出内存地址时,需要使用:

%p

例如:

printf("%p\n", &a);

通常还会将地址转换成 void *:

printf("%p\n", (void *)&a);

这里的:

&

叫做取地址运算符。

例如:

int a = 10;

&a

就是:

获取变量 a 的内存地址。


地址和数据

要区分两个概念:

int a = 10;

其中:

a   → 变量
10  → 变量保存的数据
&a  → 变量的地址

可以理解成:

          地址
           │
           ▼
      ┌─────────┐
      │   10    │
      └─────────┘
           ▲
           │
           a

因此:

printf("%d\n", a);

是在查看 a 保存的数据。

而:

printf("%p\n", &a);

是在查看 a 所在的内存地址。

记住:

a   → 值
&a  → 地址

这也是学习指针最重要的基础。

通过指针保存地址

前面我们可以通过:

&a

获取变量 a 的地址。

如果想把这个地址保存下来,就可以使用指针变量:

int a = 10;

int *p = &a;

这里可以拆开理解:

int *p

表示创建一个 int 类型的指针变量 p。

&a

表示获取变量 a 的地址。

所以:

int *p = &a;

就是:

创建一个指向 int 类型数据的指针 p,并让它保存变量 a 的地址。

例如:

#include <stdio.h>

int main() {

    int a = 10;

    int *p = &a;

    printf("a的值:%d\n", a);
    printf("a的地址:%p\n", (void *)&a);
    printf("p保存的地址:%p\n", (void *)p);

    return 0;
}

可以看到:

a的值:10
a的地址:000000000061FE1C
p保存的地址:000000000061FE1C

也就是说:

        a
        │
        ▼
┌─────────────────┐
│       10        │
└─────────────────┘
        ▲
        │
        │ 地址
        │
        p

此时:

a

表示 a 的值。

&a

表示 a 的地址。

p

表示 p 中保存的地址。

*p

则可以通过这个地址访问 a 中的数据。

因此:

printf("%d\n", *p);

输出:

10

可以记成:

a      → 10
&a     → a的地址
p      → a的地址
*p     → 10

核心关系:

int *p = &a;

就是:

       &a
        ↓
      地址
        ↓
        p
        ↓
       *p
        ↓
       a
        ↓
       10

指针变量

前面我们知道:

int a = 10;

可以通过:

&a

获取变量 a 的地址。

如果想保存这个地址,就需要使用指针变量。

例如:

int a = 10;

int *p = &a;

其中:

int *p;

表示创建一个 int 类型的指针变量 p。

而:

&a

表示获取变量 a 的地址。

所以:

int *p = &a;

表示:

创建一个 int 类型的指针变量 p,并将 a 的地址保存到 p 中。

可以理解为:

变量 a

┌──────────────┐
│      10      │
└──────────────┘
       ▲
       │
       │ 地址
       │
       p

此时:

a

表示 a 中保存的数据。

&a

表示 a 的地址。

p

表示 p 中保存的地址。

*p

表示通过 p 保存的地址访问对应的数据。

因此:

int a = 10;
int *p = &a;

printf("%d\n", a);
printf("%d\n", *p);

输出:

10
10

虽然访问方式不同,但访问的是同一个数据。

可以理解为:

直接访问:

a
↓
10


通过指针访问:

p
↓
a 的地址
↓
*p
↓
10

* 的两种含义

需要注意,* 在不同位置有不同含义。

声明指针时:

int *p;

表示 p 是一个指针变量。

使用指针时:

*p

表示对 p 进行解引用,通过 p 中保存的地址访问数据。

例如:

int a = 10;
int *p = &a;

*p = 20;

printf("%d\n", a);

输出:

20

因为 p 保存的是 a 的地址,所以:

*p = 20;

实际上修改的是 a 所在内存中的数据。

因此:

a      → 10
&a     → a的地址
p      → a的地址
*p     → 10

修改:

*p = 20;

之后:

a      → 20
*p     → 20

这就是指针最基本的作用:

通过地址间接访问和修改数据。

指针的类型

指针也有自己的类型。

常见的指针类型有:

int *p;       // int 类型指针
char *p;      // char 类型指针
float *p;     // float 类型指针
double *p;    // double 类型指针

例如:

int a = 10;
int *p = &a;

这里:

a    → int 类型
p    → int* 类型

表示 p 是一个指向 int 类型数据的指针。


为什么指针需要类型?

指针保存的主要是一个内存地址。

例如:

int a = 10;
int *p = &a;

可以理解为:

p
│
│ 保存地址
▼
┌─────────────┐
│     10      │
└─────────────┘
     int

但是,仅仅知道一个地址还不够。

C 语言还需要知道:

这个地址中的数据应该按照什么类型来读取。

例如:

int *p;

表示:

p 指向的数据按照 int 类型处理。

而:

char *p;

表示:

p 指向的数据按照 char 类型处理。

所以:

int *     → 指向 int 数据
char *    → 指向 char 数据
float *   → 指向 float 数据
double *  → 指向 double 数据

指针类型和解引用

例如:

int a = 10;
int *p = &a;

因为 p 是 int * 类型,所以:

*p

会按照 int 类型访问 a。

因此:

printf("%d\n", *p);

可以正确输出:

10

再看:

char c = 'A';
char *p = &c;

这里 p 是 char * 类型。

所以:

*p

会按照 char 类型访问 c。

例如:

printf("%c\n", *p);

输出:

A

指针类型决定指针移动的大小

指针类型还有一个非常重要的作用:

指针进行加减运算时,会根据指针指向的数据类型移动相应的大小。

例如:

int a[3] = {10, 20, 30};

int *p = a;

假设:

sizeof(int) = 4

那么数组可能是:

地址
1000        1004        1008
 ↓           ↓           ↓
┌──────┐   ┌──────┐   ┌──────┐
│  10  │   │  20  │   │  30  │
└──────┘   └──────┘   └──────┘
   a[0]       a[1]       a[2]

此时:

p

指向 a[0]。

执行:

p++;

并不是简单地让地址 +1。

而是:

1000 + sizeof(int)

也就是:

1000 + 4 = 1004

所以:

printf("%d\n", *p);

就会得到:

20

再执行:

p++;

就会移动到 a[2]:

1008

于是:

printf("%d\n", *p);

得到:

30

不同类型的指针

例如:

char *cp;
int *ip;
double *dp;

假设:

sizeof(char)   = 1
sizeof(int)    = 4
sizeof(double) = 8

那么:

cp++  → 地址增加 1
ip++  → 地址增加 4
dp++  → 地址增加 8

所以指针的类型不仅仅是为了区分数据类型,也会影响指针运算。


指针类型的本质

可以简单记成:

int *p
   │
   ├─ p 保存一个地址
   │
   └─ int 表示这个地址对应的数据按照 int 类型处理

例如:

int *p;

可以理解为:

p 是一个指向 int 类型数据的指针。

char *p;

可以理解为:

p 是一个指向 char 类型数据的指针。

记住:

指针保存地址
指针类型决定如何解释地址中的数据
指针类型还决定指针运算时移动多少字节

NULL 空指针

有时候我们创建了一个指针,但是暂时不让它指向任何数据。

这时候可以让指针保存一个空指针值。

通常使用:

NULL

例如:

int *p = NULL;

表示:

创建一个 int 类型的指针 p,但现在 p 不指向任何有效的对象。

可以理解为:

p
│
▼
NULL
│
└── 不指向任何有效数据

为什么需要 NULL?

例如:

int *p;

这里的 p 没有初始化。

它里面可能保存一个未知的地址,这种指针不能直接使用。

而:

int *p = NULL;

明确表示:

p 当前没有指向任何对象。

这样我们就可以在使用指针之前进行判断:

if (p != NULL) {
    printf("%d\n", *p);
}

只有当 p 不为空时,才通过 *p 访问数据。


NULL 指针不能解引用

例如:

int *p = NULL;

printf("%d\n", *p);

这是错误的。

因为:

*p

表示:

访问 p 所指向的数据。

但是 p 根本没有指向有效的数据。

所以:

p = NULL
   ↓
没有指向有效对象
   ↓
*p
   ↓
错误

可能导致程序崩溃或产生未定义行为。


判断指针是否为空

可以使用:

if (p == NULL) {
    printf("p是空指针\n");
}

也可以写成:

if (p) {
    printf("p不是空指针\n");
}

或者:

if (!p) {
    printf("p是空指针\n");
}

初学阶段推荐使用第一种:

if (p == NULL)

可读性更好。


NULL 和普通地址

例如:

int a = 10;

int *p = &a;
int *q = NULL;

可以理解成:

p
│
▼
┌─────────┐
│   10    │
└─────────┘
    a


q
│
▼
NULL

不指向任何有效对象

因此:

*p

可以访问 a:

*p → 10

而:

*q

不能访问数据,因为 q 是空指针。


NULL 和未初始化指针的区别

这是非常重要的区别。

int *p;

这是未初始化指针。

它里面的值是不确定的。

而:

int *p = NULL;

这是空指针。

它明确表示:

当前不指向任何有效对象。

可以记成:

int *p;
    ↓
未初始化
    ↓
不要使用


int *p = NULL;
        ↓
明确表示没有指向对象
        ↓
可以进行 NULL 判断

记住:

NULL
 ↓
空指针
 ↓
不指向任何有效对象
 ↓
不能解引用
此作者没有提供个人介绍。
最后更新于 2026-09-17