通过指针,可以简化一些 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
↓
空指针
↓
不指向任何有效对象
↓
不能解引用
Comments NOTHING