LV005-指针简介
一、指针简介
1. 什么是指针?
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节, char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的 编号 是 唯一 的,根据编号可以准确地找到某个字节。

Tips:我们可以将内存理解为一个一个小盒子,每个盒子里面可以放 1 个字节的数据。我们为每个盒子按顺序编号,每个盒子的编号就称为地址。例如上图的 00,01,02...。
内存中字节的编号称为 地址(Address ) 。在 C 语言中,内存单元的地址可以称为 指针,专门用来 存放地址的变量,称为 指针变量,在不影响理解的情况中,有时对地址、指针和指针变量不区分,通称 指针。
对于 32 位 环境,程序能够使用的内存为 4GB ,最小的地址为 0x00000000 ,最大的地址为 0xFFFFFFFF ,注意这里的地址都是用 8 个十六进制数表示的,一共是 32 位。
对于 64 位系统来说,我们打印地址的时候会发现,它的地址都是由 12 个十六进制数表示的,这样算下来,才 48 位,按理来说不应该是 64 位吗?通过查阅资料,发现 48 位其实只是表象,显示了 48 位是因为目前为止 64 位系统的地址线只有 48 条。地址中第 48 位到第 63 位由第 47 位扩展而来(全 0 全 1)。因此有两段合法的地址空间,分别是
0x0000 0000 0000 0000 - 0x0000 7FFF FFFF FFFF
0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF两段加起来总共 2^48Byte = 256TB ,一段是 128TB ,然而现在的 PC 基本达不到 128 的内存,因此第二段地址一般是见不到的,全都存在了第一段当中,所以看到的 48 位地址应该在前边再加上 0000 这才是完整的 64 位地址。
2. 一切皆地址?
C 语言 用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。 CPU 只能通过 地址 来取得内存中的 代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。
CPU 访问内存时需要的是 地址,而不是变量名和函数名。变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
例如:变量 a、b、c 在内存中的地址分别是 0X0000、0X0004、0X0008 ,那么加法运算 c = a + b; 将会被转换成类似下面的形式:
0X0008 = (0X0000) + (0X0004);这里的 ( ) 表示取值操作,整个表达式的意思是,取出地址 0X0000 和 0X0004 上的值,将它们相加,把相加的结果赋值给地址为 0X0008 的内存。
所以,从根本上来说,数据的运算其实都是通过地址来运算的。
3. 为什么要用指针?
C 程序设计中使用指针可以:
使程序简洁、紧凑、高效。
有效地表示复杂的数据结构。
动态分配内存。
得到多于一个的函数返回值。
4. 怎么使用指针?
4.1 定义指针变量
storage_type data_type *p_name;| storage_type | 存储类型(指针变量本身的存储类型, 可以说明也可以不说明) |
| data_type | 任意有效的 C 数据类型(指针目标的数据类型,必须说明) |
| p_name | 指针变量名 |
例如:
static int *p1; /* p1 是一个指向静态整型变量的指针变量 */
float *p2; /* p2 是一个指向浮点型变量的指针变量 */
char *p3; /* p3 是一个指向字符型变量的指针变量 */
int *a, *b, *c; /* a、b、c 的都是 int* 类型的指针变量 */
int *a, b, c; /* a 是 int* 类型的指针变量,b、c 都是类型为 int 的普通变量 */指针说明时指定的数据类型不是指针变量本身的数据类型,而是 指针目标的数据类型,简称为指针的数据类型。
【说明】引入指针要注意程序中的 p 、 *p 和 &p 三种表示方法的不同意义。设 p 为一个指针( int *p; )则:
| p | 指针变量, 它的内容是地址量 |
| *p | 指针所指向的对象, 它的内容是数据 |
| &p | 指针变量占用的存储区域的地址,是个常量 |
指针变量在使用前,不仅要 定义说明,而且要 赋予具体的值,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值(赋为 NULL 值的指针被称为 空指针),未经赋值的指针不能随便使用,否则将导致程序的错误,并且指针变量的值只能是 变量的地址,不能是其他数据,否则将会导致错误产生。
没有合法指向的指针称为 “野”指针。“野”指针随机指向一块空间,该空间中存储的可能是其他程序的数据甚至是系统数据,故不能对“野”指针所指向的空间进行存取操作,否则轻者会引起程序崩溃,严重的可能导致整个系统崩溃。
/* 1. 定义时直接赋值 */
<存储类型> <数据类型> *<指针变量名> = <地址量>;
/* 2. 定义完成后再赋值 */
<存储类型> <数据类型> *<指针变量名>;
<指针变量名> = <地址量>;例如:
int a = 10; /* 定义一个整型变量 */
int *p = NULL; /* 定义一个空指针 */
int *p1 = &a; /* 定义一个指针变量 p1,同时赋初值,使其指向整型变量 a */
int *p2; /* 定义一个指针变量 p2,不进行初始化赋值 */
p2 = &a; /* 将变量 a 的地址赋给指针变量 p2 ,使其指向整型变量 a */【注意】
(1)定义 指针变量时必须 带 * ,给指针变量赋值时不能带 *。
(2)和普通变量一样,指针变量也可以被多次修改,可以改变指针变量的值,使其指向不同的地址。
(3)地址量必须是一个 地址,若是普通变量,要加上 取地址 & 符号。
4.3 指针变量的引用
4.3.1 获取数据
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:
*<p_name>;这里的 * 并不是乘号,而是被称为 指针运算符 ,用来获取指针变量所指向地址中所存储的数据。例如:
#include <stdio.h>
int main(int argc, char *argv[])
{
int a =10;
int *p1 = &a;
int *p2;
p2 = &a;
printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);
return 0;
}4.3.2 修改数据
通过指针可以直接修改指针所指向变量的数据,使用格式如下
*<p_name> = value;例如:
#include <stdio.h>
int main(int argc, char *argv[])
{
int a =10;
int *p1 = &a;
int *p2;
p2 = &a;
printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);
*p1 = 20;
printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);
*p2 = 30;
printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);
a = 40;
printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);
return 0;
}4.3.3 指针的数据类型
那如果指针的数据类型和实际的数据类型不匹配的话,会发生什么?例如,我们现在内存中是一个 int 类型的数据,占用了 4 个字节,现在定义了一个指针 p,这个指针是指向 char 类型的,这个时候会怎么样?
Tips:大小端序(Endianness)是指 多字节数据在内存中的存储顺序。
名称 英文 含义 低地址→ 高地址 小端序 Little Endian 低位字节存储在低地址 0x78 0x56 0x34 0x12 ← 低位在前(0x12345678) 大端序 Big Endian 高位字节存储在低地址 0x12 0x34 0x56 0x78 ← 高位在前(0x12345678) 有以下方法判断当前主机是大端还是小端:
shelllscpu # 打印信息会含有字节序信息 echo -n "ABCD" | od -A n -t x1 # 小端序输出:41 42 43 44(倒序存储),大端序输出:44 43 42 41(顺序存储)
这里我们在一个小端序的主机中进行测试,我们定义一个 int 类型数据 0x12345678, 然后定义一个 char 类型的指针指向它:
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a = 0x12345678;
char *p = &a;
printf("&a=%p, a=0x%x\n", &a, a);
printf("p=%p, *p=0x%x\n", p, *p);
return 0;
}编译并运行,可以得到以下输出:

可以看到这里其实是有一个警告的,因为我们定义的指针 p 指向的数据类型和目标数据类型不匹配,这里我们先忽略警告。发现指针 p 取出的值为 0x78,我们来分析一下。
我们定义了 0x12345678 这个变量,它在内存中是这样的:
Tips:图中地址省略了一部分相同的内容。
int 类型在内存中是占 4 个字节,在低地址中存放的是 0x78,我们定义的指针 p 指向变量 a,从打印可以看出这个时候 p 指向的地址就是 0x7ffc67b6058c,这个地址中存放的 1 个字节数据是 0x78,我们使用指针运算符获取数据的时候只能获取到和指针数据类型相同字节数的数据,也就是 1 个字节,这里我们就得到了 0x78。
指针运算符可以获取到的字节数由指针指向的数据类型决定。
4.4 直接访问地址?
其实做过单片机开发的话,就会知道,我们经常需要操作单片机的寄存器,操作寄存器的时候我们可以查到这个寄存器的地址,甚至我们都不需要定义指针变量,直接使用 *运算符就可以取出这个地址的数据,就像:
#define CCM_CCGR0 *((volatile unsigned int *)0X020C4068)通过这个宏,我们可以直接访问0X020C4068这个地址开始的4个字节数据。从这里也可以看到,指针本质上就是地址,定义变量无非就是为这个地址取一个好记的名字。
5. 内存相关
5.1 指针占几个字节?
从上边对指针的概念的阐述中可以得出,指针也可以称之为 地址,而地址的大小就与计算机多少位相关联起来。常见的就是 64 位系统和 32 位系统,接下来就来验证一下不同位数的系统中的指针大小。
5.1.1 64 位系统
#include <stdio.h>
int main(int argc, char *argv[])
{
int a =10;
int *p = &a;
printf("sizeof(p)=%ld\n",sizeof(p));
return 0;
}可以看出在 64 位系统下,指针变量 p 占据了 8Byte 的空间,一共就是 64bit 。
5.1.2 32 位系统
#include <stdio.h>
int main(int argc, char *argv[])
{
int a =10;
int *p = &a;
printf("sizeof(p)=%ld\n",sizeof(p));/* %ld 要改为 %d,否则就会有一个警告 */
return 0;
}
可以看出在 32 位系统下,指针变量 p 占据了 4Byte 的空间,一共就是 32bit 。
5.2 指针和变量的关系
要想更加清楚地理解指针变量,我们可以编写程序来进行测试,好清楚它在内存中的情况。
#include <stdio.h>
int main(int argc, char *argv[])
{
int a =10;
int *p1 = &a;
int *p2;
p2 = &a;
printf("a=%d, &a=%p, sizeof(a)=%ld\n", a, &a, sizeof(a));
printf("*p1=%d, p1=%p, &p1=%p, &(*p1)=%p, sizeof(p1)=%ld\n", *p1, p1, &p1, &(*p1), sizeof(p1));
printf("*p2=%d, p2=%p, &p2=%p, &(*p2)=%p, sizeof(p2)=%ld\n", *p2, p2, &p2, &(*p2), sizeof(p2));
return 0;
}
通过打印指针变量的地址以及普通变量的地址可以得到指针变量和普通变量在内存中的存放如下所示:

这里需要说明的一点是,其实变量在内存中并不一定是按图中这样依次放的,只是这个程序刚好放在一起了。从打印信息来看:
(1)指针变量 p1 和 p2 分别存放在内存中的 0x7ffdd3b94e18 和 0x7ffdd3b94e20,p1 是 0x7ffdd3b94e18 的标识符,就 表示 0x7ffdd3b94e18 这个数据,p2 是 0x7ffdd3b94e20 的标识符,就 表示 0x7ffdd3b94e20 这个数据,指针变量自己在 64 位系统中占 8 字节。
(2)指针变量 p1 和 p2 非值都是 0x7ffdd3b94e14,这是一个内存地址,且正是数值 10 在内存中存放的地址。
(3)a 是这个 数据 10 的一个标识符,a 就表示 10 这个数据,&a 可以拿到数据 10 在内存中的地址。
&a → 0x7ffdd3b94e14 (变量 a 的地址,这里的数据是10)
a → 10 (变量 a 的值)
&p1 → 0x7ffdd3b94e18 (指针 p1 自己在内存中的地址)
p1 → 0x7ffdd3b94e14 (指针 p1 存储的值,即 &a)
*p1 → 10 (解引用:取指针 p1 指向位置的值,p1指向的是内存0x7ffdd3b94e14,这里面存放的是 10)
&p2 → 0x7ffdd3b94e20 (指针 p2 自己在内存中的地址)
p2 → 0x7ffdd3b94e14 (指针 p2 存储的值,即 &a)
*p2 → 10 (解引用:取指针 p2 指向位置的值,p2指向的是内存0x7ffdd3b94e14,这里面存放的是 10)6. 怎么知道取多少字节数据?
有一个疑问,指针为什么知道要取多少字节?
首先我们要知道指针的本质,指针的本质是一个内存地址,它表示某个数据在内存中的位置。在底层,指针只是一个数值,例如 0x1000,表示内存中的某个地址。
指针只存储地址,不存储"要取多少字节"的信息。那我们引用指针的时候怎么知道取多少字节?这个时候指针的类型就派上用场了。编译器根据指针的类型在编译时决定访问这个地址的时候一次性操作多少字节的数据:
int *p1; // 编译器知道:int 占 4 字节
char *p2; // 编译器知道:char 占 1 字节
double *p3; // 编译器知道:double 占 8 字节当我们写 *p1 时:
编译器生成的机器指令:
┌─────────────────────────────────────┐
│ 从地址 p1 读取 4 个字节,按 int 解释 │
└─────────────────────────────────────┘内存中的数据(同一块内存):
地址: 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
数据: 48 65 6E 67 00 ?? ?? ??不同类型的指针看到不同的"解释":
char *p → 读 1 字节 → 'H' (ASCII 48)
int *p → 读 4 字节 → 0x006E6548 (小端序)
double *p → 读 8 字节 → 5.3156e-31 (乱解释)7. 小结
7.1 三者关系
| 概念 | 本质 | 关键点 |
|---|---|---|
| 地址 | 内存单元的编号(整数) | 一个数值,用于定位内存位置 |
| 变量 | 数据的 命名存储 | 有名字、有类型,存储具体数据值 |
| 指针 | 存储地址的 变量 | 特殊变量,值是另一个内存位置的编号 |
7.2 内存模型
int a = 100; // 变量 a
int *p = &a; // 指针 p,存储 a 的地址最后 a、p 在内存中的分布如下:
内存模型:
┌─────────┬────────────────────────────────────────────┐
│ 地址 │ 说明 │
├─────────┼────────────────────────────────────────────┤
│ 0x1000 │ 变量 a 的存储位置(占用 4 字节) │
│ │ ┌──────────────────────────────────────┐ │
│ │ │ 存储的值: 100 │ │
│ │ └──────────────────────────────────────┘ │
├─────────┼────────────────────────────────────────────┤
│ 0x1004 │ 指针 p 的存储位置(占用 8 字节,64位系统) │
│ │ ┌──────────────────────────────────────┐ │
│ │ │ 存储的值: 0x1000 (即变量 a 的地址) │ │
│ │ └──────────────────────────────────────┘ │
└─────────┴────────────────────────────────────────────┘
&a → 0x1000 (变量 a 的地址)
a → 100 (变量 a 的值)
&p → 0x1004 (指针 p 自己的地址)
p → 0x1000 (指针 p 存储的值,即 &a)
*p → 100 (解引用:取指针 p 指向位置的值)六、指针的运算
指针运算是以指针变量所存放的地址量作为运算量而进行的运算,指针运算的 实质就是地址的计算,指针运算的种类是有限的,它只能进行 赋值运算、算术运算 和 关系运算。
1. 指针的赋值运算
见 4.2 指针变量的赋值 一节。
2. 指针的算术运算
2.1 算术运算
先定义指针变量 px 和 py 。
| 运算符 | 计算形式 | 含义 |
| + | px + n | 指针向地址大的方向移动 n 个数据 实际位置的地址量: (px) + sizeof(px 的指向的数据类型) * n |
| - | px - n | 指针向地址小的方向移动 n 个数据 实际位置的地址量是: (px) - sizeof(px 的指向的数据类型) * n |
| ++ | px++ | 指针向地址大的方向移动 n 个数据 |
| ++px | ||
| -- | px-- | 指针向地址小的方向移动 n 个数据 |
| --px | ||
| - | px - py | 指针 px 与 py 之间间隔元素的个数 px 和 py 必须是同意数据类型的指针变量,否则两者相减毫无意义。 |
【注意】
(1)不同数据类型的两个指针实行加减整数运算是无意义的。
(2) px - py 运算的结果 不是地址量,而 是一个整数值,意思就它们相减的结果是 两指针 指向的地址位置 之间相隔数据的个数,而 不是 两指针持有的 地址值相减 的结果。(两指针数据类型要一致)
2.2 使用实例
这里需要提前用一下下边要说的指针与数组的知识,这样通过数组来进行验证,对于指针的算术运算更容易理解些。这里主要验证两指针相减的情况,其他的都比较简单,下一节介绍数组与指针的时候也会经常用到。
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[6] = {0, 1, 2, 3, 4, 5};
char b[6] = {0, 1, 2, 3, 4, 5};
int i;
int *px1 = &a[0];
int *py1 = &a[5];
char *px2 = &b[0];
char *py2 = &b[3];
for(i = 0; i < 6; i++)
{
printf("a[%d]=%d , &a[%d]=%p | b[%d]=%d , &b[%d]=%p\n", i, a[i], i, &a[i], i, b[i], i, &b[i]);
}
printf("%p - %p = %ld\n", py1, px1, py1 - px1);
printf("%p - %p = %ld\n", py2, px2, py2 - px2);
return 0;
}
3. 指针的关系运算
3.1 关系运算
两指针之间的关系运算表示它们指向的 地址位置之间的关系。指向地址大的指针大于指向地址小的指针。
| 运算符 | 说明 | 举例 |
| > | 大于 | px > py |
| < | 小于 | px < py |
| >= | 大于等于 | px >= py |
| <= | 小于等于 | px <= py |
| == | 等于 | px == py |
| != | 不等于 | px != py |
(1)不同数据类型的指针之间的关系运算没有任何意义,指向不同数据区域的数据的两个指针之间的关系运算也没有意义。
(2)指针与一般整数变量之间的关系运算没有意义。但可以和 0(NULL) 进行等于或不等于的关系运算,判断指针是否为空。
七、void 指针
1. void 指针概念
void 指针是一种 不确定数据类型的指针变量,它可以通过 强制类型转换 让该变量 指向任何数据类型 的变量。由于 void 指针没有特定的类型,因此它可以 指向任何类型的数据。
(1)任何类型的指针都可以直接赋值给 void 指针,而无需进行其他相关的强制类型转换。
(2)要将 void 指针 赋给其他类型的指针,则需要强制类型转换。
2. 一般形式
void *<指针变量名称>;在 ANSI C 标准中,对于 void 指针,虽然任何类型的指针都可以直接赋值给 void 指针,但是在 没有强制类型转换之前,不能进行任何指针的算术运算,这是因为在引用指针目标值时, void * 相当于类型不确定,只知道指针指的起始地址,但是不知道占用的字节数,所以就没有颁发决定以什么单位来进行偏移,就会出现编译错误。
在 GNU 中则允许其进行算术运算,因为在默认情况下, GNU 认为 void * 和 char * 一样,既然是确定的,当然可以进行一些算术操作。
3. 使用规则
进行强制类型转换格式:
*(<目标数据类型> *)<指针变量名>3.1 对 void 指针赋值
void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针赋值。
例如:
int *a; /* 定义一个整型指针变量 a */
void *p; /* 定义一个 void 型指针变量 p */
p = a; /* 将指针变量 a 指向的地址赋值给 p */3.2 将 void 指针赋给其他类型指针
“空类型”可以包容“有类型”,而“有类型”则不能包容“空类型”,要将 void 指针赋值给其他类型的指针,必须进行强制类型转换。
void *p1; /* 定义一个 void 型指针变量 p1 */
int *p2; /* 定义一个整型指针变量 p2 */
p2 = (int*)p1;/* 将 void 指针变量 p1 赋值给 int 指针变量 p2 */3.3 使用 void 指针
必须进行强制类型转换才可以使用。
int *a; /* 定义一个整型指针变量 a */
void *p; /* 定义一个 void 型指针变量 p */
p = a; /* 将指针变量 a 指向的地址赋值给 p */
ptrintf("*p=%d\n", *(int *)p);八、const 指针
1. const 变量
C 语言 中,关键字 const 修饰变量,可以使 变量常量化,这样就使得变量的值不能修改,从而达到保护变量的目的。一般的说明形式如下:
const <数据类型> 变量名 = [<表达式>] ;那如果说当变量有 const 修饰时,想用指针间接访问变量,指针也要有 const 修饰。那么 const 放在指针声明的什么位置呢?请接着往下看。
【说明】 修饰指针的时候可以简记为左数右指,意思就是 const 在*号左边,那么指针指向的数据不可改变,但是指针变量可以改变。const 在*号右边,表示指针变量指向的数据可以改变,但是指针变量不可改变,需要注意的是 const 不可以放在指针变量后边。
Tips:const 离谁近,谁不能变!!!
2. 常量化指针目标表达式
常量化指针目标是 限制通过指针改变其目标的数值 ,但 <指针变量> 存储的地址值可以修改。
const <数据类型> *<指针变量名称>[= <指针运算表达式>];例如:
int a = 10;
int b = 20;
const int *p = &a;上边定义了两个整型变量 a, b 和一个带有 const 修饰的指针变量,此时指针 p 指向的是 a ,我们 可以 改变 p 中的地址值,即通过 p = &b; 使其指向 b ;我们 也可以 通过 *p 来访问相应的目标值,但是,我们 无法通过 * p = 30; 这样的赋值操作来改变目标值,若强行修改则会报以下错误:
error: assignment of read-only location ‘*p’3. 常量化指针变量
常量化指针变量,使得 <指针变量> 存储的地址值不可被修改,但是*可以通过 <指针变量名称> 修改指针所指向变量的值。
<数据类型> * const <指针变量名> = <指针运算表达式> ;例如:
int a = 10;
int b = 20;
int * const p = &a;上边定义了两个整型变量 a,b 和一个带有 const 修饰的指针变量,此时指针 p 指向的是 a ,我们 无法 改变 p 中的地址值,即无法通过 p = &b; 使其指向 b ,若强行修改则会报以下错误:
error: assignment of read-only variable ‘p’我们 可以 通过 *p 来访问相应的目标值,我们 也可以通过 *p = 30; 这样的赋值操作来改变目标值。
4. 常量化指针变量及其目标表达式
常量化指针变量及其目标表达式,使得 既不可以修改 <指针变量> 的地址,也*不可以通过 <指针变量名称> 修改指针所指向变量的值。
const <数据类型> *const <指针变量名> = <指针运算表达式> ;例如:
int a = 10;
int b = 20;
const int * const p = &a;上边定义了两个整型变量 a, b 和一个带有 const 修饰的指针变量,此时指针 p 指向的是 a ,我们 无法 改变 p 中的地址值,即通过 p = &b; 使其指向 b ;我们 可以 通过 *p 来访问相应的目标值,但是,我们 无法 通过 *p = 30; 这样的赋值操作来改变目标值。若强行修改地址值或者目标值,则会报以下错误:
error: assignment of read-only variable ‘p’ /* 通过 p =&b; 修改存储的地址值 */
error: assignment of read-only location ‘*p’ /* 通过*p = 30; 修改目标值 */5. 小结
总之一句话,const 在指针中,离谁近,谁不能变。