1 引言
考虑这样一个问题:我们有一个变量 int a = 10;,它存储在内存中的某个位置。我们可以通过变量名 a 来访问它,但计算机实际上是通过地址来找到它的。指针就是用来存储这些地址的变量。
c
#include <stdio.h>
int main(void)
{
int a = 10; /* 普通变量 */
int *p = &a; /* 指针变量 p 存储 a 的地址 */
printf("a 的值:%d\n", a); /* 直接访问 */
printf("a 的地址:%p\n", &a); /* 取地址 */
printf("p 的值:%p\n", p); /* p 存储的就是地址 */
printf("通过 p 访问 a:%d\n", *p); /* 间接访问 */
return 0;
}
这段代码展示了指针的核心操作:取地址(&)和间接访问(*)。本章我们将深入理解这些概念。
2 内存地址
2.1 内存的基本模型
计算机的内存可以看作一个巨大的字节数组 ,每个字节都有一个唯一的编号,这个编号就是内存地址。
text
内存示意图:
地址: 0x1000 0x1001 0x1002 0x1003
+---------+---------+---------+---------+
| 字节0 | 字节1 | 字节2 | 字节3 | ...
+---------+---------+---------+---------+
-
内存以字节为单位编址
-
每个字节对应一个地址
-
地址通常用十六进制表示(如
0x7ffd5a3e4a00)
2.2 变量与地址
当我们定义一个变量时,系统会为它分配一块内存空间,这块空间有一个起始地址。
c
int a = 10; /* 假设分配到地址 0x1000 */
float b = 3.14; /* 假设分配到地址 0x1004 */
不同的数据类型占用不同大小的内存:
-
char:1字节 -
int:通常4字节 -
double:通常8字节
2.3 取地址运算符 &
使用 & 可以获取变量的地址:
c
int a = 10;
printf("a 的地址:%p\n", &a); /* 输出类似 0x7ffd5a3e4a00 */
%p 是专门用于打印地址的格式说明符。
注意:
-
&只能用于变量和数组元素,不能用于常量或表达式 -
&10是错误的,常量没有地址 -
&(a+b)也是错误的,表达式结果没有地址
3 指针变量
3.1 什么是指针变量
指针变量(Pointer Variable)是专门用来存储地址的变量。它和普通变量一样,也占用内存,但存储的内容是地址。
text
内存示意图:
变量 a (地址 0x1000): [ 10 ] ← int 类型,存的是数值
变量 p (地址 0x2000): [ 0x1000 ] ← 指针类型,存的是地址
3.2 指针变量的定义
c
类型 *变量名;
-
类型:指针指向的数据的类型 -
*:表明这是一个指针变量 -
变量名:指针的名字
c
int *p; /* p 是一个指向 int 类型的指针 */
float *fp; /* fp 是一个指向 float 类型的指针 */
char *cp; /* cp 是一个指向 char 类型的指针 */
void *vp; /* vp 是一个无类型指针(可以指向任何类型) */
理解 :int *p 读作"p 是一个指针,它指向一个 int"。
3.3 指针变量的初始化
c
int a = 10;
int *p = &a; /* 定义时初始化,p 指向 a */
int *q; /* 只定义,未初始化(危险!) */
q = &a; /* 之后赋值 */
int *r = NULL; /* 初始化为空指针(安全) */
重要 :未初始化的指针是野指针,必须避免。
3.4 指针的大小
指针变量本身也有大小,它取决于系统架构:
c
#include <stdio.h>
int main(void)
{
char *cp;
int *ip;
double *dp;
printf("char* 大小:%zu\n", sizeof(cp)); /* 32位系统:4;64位系统:8 */
printf("int* 大小:%zu\n", sizeof(ip)); /* 都是4或8,和指向类型无关 */
printf("double* 大小:%zu\n", sizeof(dp)); /* 都是4或8 */
return 0;
}
关键:所有类型的指针大小在同一个系统中是相同的,因为它们存储的都是地址。
4 间接访问运算符 *
4.1 基本用法
* 是间接访问运算符(dereference operator),用于通过指针访问它指向的变量。
c
int a = 10;
int *p = &a;
printf("%d\n", *p); /* 输出 10,*p 等价于 a */
*p = 20; /* 通过指针修改 a 的值 */
printf("%d\n", a); /* 输出 20 */
理解 :*p 就是"p 指向的那个变量"。
4.2 & 和 * 的关系
& 和 * 互为逆运算:
-
&取变量的地址 -
*通过地址访问变量
c
int a = 10;
int *p = &a;
/* & 和 * 的关系 */
*p == a; /* 真 */
&(*p) == p; /* 真(前提 p 有效) */
*(&a) == a; /* 真 */
4.3 多级指针
指针也可以指向另一个指针,形成多级指针:
c
int a = 10;
int *p = &a; /* 一级指针:指向 int */
int **pp = &p; /* 二级指针:指向 int* */
int ***ppp = &pp; /* 三级指针:指向 int** */
printf("%d\n", ***ppp); /* 输出 10 */
理解:
-
*ppp得到pp(类型int**) -
**ppp得到p(类型int*) -
***ppp得到a(类型int)
5 空指针与野指针
5.1 空指针 NULL
空指针(NULL pointer)是指不指向任何有效对象的指针。
c
#include <stdio.h>
#include <stddef.h> /* 包含 NULL 的定义 */
int main(void)
{
int *p = NULL; /* p 不指向任何有效内存 */
if (p == NULL) {
printf("p 是空指针\n");
}
/* *p = 10; */ /* 危险!解引用空指针会导致程序崩溃 */
return 0;
}
-
NULL在 C 语言中通常定义为((void*)0)或简单的0 -
解引用空指针是未定义行为,通常导致程序崩溃(段错误)
-
总是检查指针是否为 NULL 再使用
5.2 野指针
野指针(Wild pointer)是指指向不确定位置的指针,是 C 语言中最危险的错误来源之一。
c
int *p; /* 未初始化,p 的值是随机的(野指针) */
*p = 10; /* 危险!写入未知位置,可能崩溃或破坏数据 */
int *q;
q = NULL; /* 好习惯:初始化指针 */
产生野指针的常见情况:
-
指针未初始化
c
int *p; /* 野指针 */ -
指向已释放的内存
c
int *p = malloc(sizeof(int)); free(p); /* p 现在是野指针(悬空指针) */ -
指向局部变量的地址
c
int *get_int(void) { int x = 100; return &x; /* x 是局部变量,函数返回后销毁 */ } /* 返回的指针成为野指针 */
5.3 如何避免野指针
-
初始化所有指针
c
int *p = NULL; /* 好习惯 */ -
释放后置为 NULL
c
free(p); p = NULL; /* 防止悬空指针 */ -
使用前检查
c
if (p != NULL) { *p = 10; /* 安全 */ } -
不要返回局部变量的地址
6 const 与指针
6.1 指向常量的指针
c
const int *p; /* p 指向一个 const int */
int const *p; /* 同上,两种写法等价 */
特点:
-
不能通过
*p修改指向的值 -
指针本身可以修改(可以指向别处)
c
int a = 10, b = 20;
const int *p = &a;
*p = 30; /* 错误!不能修改 */
p = &b; /* 可以,p 可以指向别处 */
6.2 指针常量
c
int * const p = &a; /* p 是一个常量指针 */
特点:
-
可以通过
*p修改指向的值 -
指针本身不能修改(不能指向别处)
c
int a = 10, b = 20;
int * const p = &a;
*p = 30; /* 可以,a 变成 30 */
p = &b; /* 错误!p 是常量,不能修改 */
6.3 指向常量的指针常量
c
const int * const p = &a; /* 都不能修改 */
特点:
-
不能通过
*p修改值 -
指针本身也不能修改
7 指针的运算
7.1 指针的算术运算
指针支持有限的算术运算:加法、减法、自增、自减。
c
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; /* 指向 arr[0] */
p++; /* 现在指向 arr[1] */
printf("%d\n", *p); /* 输出 20 */
p += 2; /* 现在指向 arr[3] */
printf("%d\n", *p); /* 输出 40 */
int *q = &arr[4];
int diff = q - p; /* 两个指针的差(元素个数) */
printf("相差 %d 个元素\n", diff); /* 输出 1(从 3 到 4)*/
重要 :指针的加减运算是以指向的类型大小为单位,而不是字节。
c
int *p;
p + 1; /* 实际地址增加 sizeof(int) 字节 */
7.2 指针的比较
可以用关系运算符比较两个指针:
c
if (p < q) {
printf("p 在低地址\n");
}
if (p == NULL) {
printf("p 是空指针\n");
}
注意:只有指向同一数组的指针比较才有意义。
8 指针与数组的关系(预览)
指针和数组有着密切的关系,将在下一章详细讨论,这里先提几点:
c
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; /* p 指向 arr[0] */
/* 下面三组等价 */
arr[2] = 10;
*(arr + 2) = 10;
p[2] = 10; /* 指针也可以使用下标 */
*(p + 2) = 10;
-
数组名在大多数表达式中被当作指向首元素的指针
-
指针可以通过下标访问(
p[i]等价于*(p + i))
9 常见错误与注意事项
9.1 混淆指针定义
c
int* p, q; /* p 是 int*,q 是 int,不是 int*! */
int *p, *q; /* 正确:p 和 q 都是 int* */
9.2 解引用未初始化的指针
c
int *p;
*p = 10; /* 危险!p 指向哪里? */
9.3 类型不匹配
c
int a = 10;
float *p = &a; /* 警告:类型不匹配 */
/* 虽然能编译,但解引用时会把 int 当 float 解释,结果错误 */
9.4 忘记取地址
c
int *p;
p = a; /* 错误:a 是 int,不能赋给 int* */
p = &a; /* 正确 */
9.5 对 NULL 解引用
c
int *p = NULL;
*p = 10; /* 运行时错误:段错误 */
9.6 指针运算越界
c
int arr[5];
int *p = arr + 5; /* 指向数组末尾的下一个位置(允许) */
*p = 10; /* 错误!不能访问这个位置 */
10 本章小结
本章系统介绍了指针的基本概念:
1. 内存地址
-
内存以字节编址,每个字节有一个唯一地址
-
&运算符获取变量的地址
2. 指针变量
-
存储地址的变量,定义:
类型 *变量名 -
必须初始化,避免野指针
-
所有指针大小相同(取决于系统位数)
3. 间接访问
-
*运算符通过指针访问指向的变量 -
&和*互为逆运算
4. 空指针与野指针
-
空指针(NULL):不指向任何有效对象,解引用会崩溃
-
野指针:指向不确定位置,是主要错误来源
-
初始化指针、释放后置 NULL、使用前检查
5. const 与指针
-
const int *p:不能通过 p 修改数据 -
int * const p:不能修改 p 本身 -
const int * const p:都不能修改
6. 指针运算
-
加减运算以类型大小为单位
-
可以比较指针大小(同一数组内)
7. 与数组的初步关系
-
数组名是首元素地址
-
指针可以用下标访问(
p[i])