1. 内存和地址
程序运行时,数据存储在内存中。内存可以看作一个连续的字节序列,每个字节都有一个唯一的编号,这个编号就是内存地址。地址从 0 开始递增,操作系统通过地址来定位和访问内存中的数据。
在 C 语言中,变量在定义时会向内存申请一块空间,这块空间的大小由变量的类型决定。例如,int类型通常占用 4 个字节,char类型占用 1 个字节。变量所占内存空间的首字节地址,就是该变量的地址。
#include <stdio.h> int main() { int a = 10; printf("变量 a 的值:%d\n", a); printf("变量 a 的地址:%p\n", &a); return 0; }上面的代码中,&a用于取出变量a的地址,%p是打印地址的格式说明符。运行结果会显示类似0x7ffd8f3a2b4c的十六进制地址。
2. 指针变量和地址
指针变量是专门用来存放地址的变量。通过指针,我们可以间接访问和修改它所指向的变量。
定义指针变量的语法为:类型 * 变量名。例如int *p表示p是一个指向int类型数据的指针变量。
#include <stdio.h> int main() { int a = 10; int *p = &a; // p 存放变量 a 的地址 printf("a 的值:%d\n", a); printf("p 中存放的地址:%p\n", p); printf("通过 p 访问 a 的值:%d\n", *p); *p = 20; // 通过指针修改 a 的值 printf("修改后 a 的值:%d\n", a); return 0; }这里有两个关键运算符:
- 取地址运算符
&:取出变量的地址。 - 解引用运算符
*:通过地址访问该地址上存储的数据。
当执行*p = 20时,程序会找到p中存放的地址,并往这个地址写入 20,因此变量a的值被修改为 20。
3. 指针变量类型的意义
指针变量本身存放的是地址,无论指向什么类型,指针变量所占的内存大小通常是相同的(在 64 位系统上为 8 字节)。那么为什么指针还要区分类型呢?
指针的类型决定了两个重要信息:
- 解引用时访问的字节数:
int *解引用时读取 4 个字节,char *解引用时只读取 1 个字节。 - 指针加减运算时的步长:
int *加 1 跳过 4 个字节,char *加 1 只跳过 1 个字节。
#include <stdio.h> int main() { int a = 0x12345678; int *p = &a; char *c = (char *)&a; printf("int 指针解引用:%x\n", *p); printf("char 指针解引用:%x\n", *c); printf("int 指针加 1 的地址:%p\n", p + 1); printf("char 指针加 1 的地址:%p\n", c + 1); return 0; }运行结果中,p + 1比p大 4 个字节,而c + 1比c大 1 个字节。这说明指针类型决定了指针运算的步长,也决定了解引用时能访问的数据范围。
4. 指针运算
指针运算主要包括以下几种:
- 指针加整数:
p + n表示向后移动n个同类型元素。 - 指针减整数:
p - n表示向前移动n个同类型元素。 - 指针减指针:
p1 - p2表示两个指针之间相隔的元素个数。 - 指针比较:可以用关系运算符比较两个指针指向的地址大小。
指针运算在遍历数组时非常常用。下面通过一个数组遍历的例子来演示:
#include <stdio.h> int main() { int arr[] = {10, 20, 30, 40, 50}; int *p = arr; // 数组名就是首元素地址 for (int i = 0; i < 5; i++) { printf("arr[%d] = %d,地址:%p\n", i, *(p + i), p + i); } // 指针减指针:计算元素个数 int *start = &arr[0]; int *end = &arr[4]; printf("相隔元素个数:%ld\n", end - start); return 0; }在这个例子中,*(p + i)等价于arr[i],p + i指向数组第i个元素。而end - start的结果是 4,表示两个指针之间相隔 4 个元素。
需要注意的是,指针运算必须限定在同一个数组或同一块连续内存范围内,否则结果是未定义的,容易引发难以排查的错误。
5. 小结
本文介绍了指针的基础知识:内存地址是数据在内存中的唯一编号,指针变量用于存放地址,指针类型决定了解引用范围和运算步长,指针运算则提供了灵活访问连续内存的手段。理解这些概念,是后续学习数组、字符串、函数指针和动态内存管理的基础。