1. 引言
指针是 C/C++ 语言中最重要也最难以掌握的概念之一。很多初学者在理解指针时,往往只关注指针的声明、赋值和解引用,却忽略了指针的算术运算。实际上,指针的算术运算(Pointer Arithmetic)是理解数组、字符串和动态内存管理的核心基础。本文将系统性地讲解指针算术运算的规则、原理和典型应用,帮助你彻底掌握这一关键技能。
2. 指针算术运算的基础规则
2.1 指针的本质
指针本质上是一个变量,它存储的是另一个变量的内存地址。在 32 位系统上,指针占 4 个字节;在 64 位系统上,指针占 8 个字节。但指针算术运算的关键在于:指针加减运算的结果取决于指针所指向的数据类型的大小,而不是简单地加减 1 个字节。
2.2 指针加减整数的规则
指针 p 加上整数 n,结果是指向地址 p + n * sizeof(类型) 的新指针。也就是说,指针每加 1,实际地址增加的是 sizeof(所指向类型) 个字节。
c
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向 arr[0]
int *q = p + 2; // q 指向 arr[2],地址偏移 2 * sizeof(int) = 8 字节
2.3 指针减法
指针减去整数 n,结果是指向地址 p - n * sizeof(类型) 的新指针。
c
int *r = q - 1; // r 指向 arr[1]
2.4 指针相减
两个同类型的指针相减,结果是它们之间相差的元素个数(不是字节数)。
c
int *p = arr; // 指向 arr[0]
int *q = arr + 4; // 指向 arr[4]
long diff = q - p; // diff = 4,表示相差 4 个元素
3. 指针算术运算的详细原理
3.1 为什么指针加 1 不是加 1 个字节?
这是指针算术运算最容易让人困惑的地方。假设 int 类型占 4 个字节,int *p 指向地址 0x1000,那么 p + 1 指向的是 0x1004,而不是 0x1001。这是因为编译器在编译时已经知道 p 指向的是 int 类型,所以会自动乘以 sizeof(int)。
c
#include <stdio.h>
int main() {
int arr[3] = {1, 2, 3};
int *p = arr;
printf("p 的地址: %p\n", (void *)p);
printf("p+1 的地址: %p\n", (void *)(p + 1));
printf("p+2 的地址: %p\n", (void *)(p + 2));
return 0;
}
运行结果示例(地址可能因系统而异):
p 的地址: 0x7ffc12345670
p+1 的地址: 0x7ffc12345674
p+2 的地址: 0x7ffc12345678
可以看到,每次加 1,地址增加 4 个字节(sizeof(int))。
3.2 不同类型指针的算术运算差异
指针算术运算的结果与指针指向的类型密切相关。下面通过对比来加深理解:
c
#include <stdio.h>
int main() {
char c_arr[4] = {'a', 'b', 'c', 'd'};
int i_arr[4] = {1, 2, 3, 4};
double d_arr[4] = {1.0, 2.0, 3.0, 4.0};
char *cp = c_arr;
int *ip = i_arr;
double *dp = d_arr;
printf("char 指针 +1 偏移: %ld 字节\n", (long)((char *)(cp + 1) - (char *)cp));
printf("int 指针 +1 偏移: %ld 字节\n", (long)((char *)(ip + 1) - (char *)ip));
printf("double 指针 +1 偏移: %ld 字节\n", (long)((char *)(dp + 1) - (char *)dp));
return 0;
}
输出结果:
char 指针 +1 偏移: 1 字节
int 指针 +1 偏移: 4 字节
double 指针 +1 偏移: 8 字节
3.3 指针算术运算的合法性
指针算术运算并不是任意指针都可以进行的,它有以下限制:
- 指针加减整数:只能对指向数组元素的指针进行(或指向单个对象时,加 1 指向下一个对象)。
- 指针相减:两个指针必须指向同一个数组(或同一个对象之后的一个位置)。
- 指针不能相加:两个指针相加没有意义,C 语言也不允许这样做。
- 指针不能乘除:指针不支持乘法和除法运算。
c
int a = 10, b = 20;
int *pa = &a, *pb = &b;
// int *pc = pa + pb; // 错误!指针不能相加
// int *pd = pa * 2; // 错误!指针不能乘
4. 指针算术运算的典型应用
4.1 遍历数组
指针算术运算最常见的应用就是遍历数组,比使用下标更灵活、更高效。
c
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p;
// 使用指针遍历数组
for (p = arr; p < arr + 5; p++) {
printf("%d ", *p);
}
printf("\n");
return 0;
}
4.2 指针与数组下标的关系
在 C 语言中,arr[i] 本质上等价于 *(arr + i)。理解这一点,就能明白为什么数组下标可以从 0 开始------因为 arr[0] 就是 *(arr + 0),即数组首元素。
c
#include <stdio.h>
int main() {
int arr[3] = {100, 200, 300};
printf("arr[1] = %d\n", arr[1]); // 下标访问
printf("*(arr+1) = %d\n", *(arr + 1)); // 指针访问,等价
return 0;
}
4.3 计算数组元素个数
利用指针相减,可以方便地计算数组中元素的个数。
c
#include <stdio.h>
int main() {
int arr[8] = {0};
int *start = arr;
int *end = arr + 8;
long count = end - start;
printf("数组元素个数: %ld\n", count); // 输出 8
return 0;
}
4.4 字符串处理
指针算术运算在字符串处理中应用非常广泛,例如实现字符串长度计算:
c
#include <stdio.h>
size_t my_strlen(const char *s) {
const char *p = s;
while (*p != '\0') {
p++;
}
return p - s; // 指针相减得到字符个数
}
int main() {
char str[] = "Hello, Pointer!";
printf("字符串长度: %zu\n", my_strlen(str));
return 0;
}
4.5 动态内存管理
指针算术运算在动态内存分配中同样重要,常用于在堆上分配的内存中定位元素。
c
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = (int *)malloc(5 * sizeof(int));
if (arr == NULL) {
return 1;
}
// 使用指针算术运算给数组赋值
for (int i = 0; i < 5; i++) {
*(arr + i) = i * i;
}
// 打印结果
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, *(arr + i));
}
free(arr);
return 0;
}
5. 指针算术运算的常见误区
5.1 误区一:指针加 1 就是地址加 1
这是最常见的误区。如前所述,指针加 1 实际地址增加的是 sizeof(类型) 字节,而不是 1 字节。
5.2 误区二:对 void 指针进行算术运算
void * 指针没有类型信息,编译器不知道它指向的对象大小,因此不能 对 void * 指针直接进行算术运算。
c
void *p = malloc(100);
// void *q = p + 1; // 错误!void 指针不能进行算术运算
如果需要操作,必须先转换为具体类型的指针:
c
char *cp = (char *)p;
char *q = cp + 1; // 正确
5.3 误区三:指针相减得到的是字节数
指针相减得到的是元素个数 ,不是字节数。如果需要字节数,需要乘以 sizeof(类型)。
c
int arr[5] = {0};
int *p = arr;
int *q = arr + 3;
long elements = q - p; // 3(元素个数)
long bytes = (char *)q - (char *)p; // 12(字节数)
5.4 误区四:越界访问
指针算术运算很容易导致越界访问,这是 C 语言中常见的 bug 来源。指针可以指向数组最后一个元素之后的一个位置(称为"尾后指针"),但不能解引用它。
c
int arr[3] = {1, 2, 3};
int *end = arr + 3; // 合法:指向尾后位置
// int x = *end; // 非法:解引用尾后指针是未定义行为
6. 指针算术运算与多维数组
6.1 二维数组的指针算术
对于二维数组,指针算术运算的规则同样适用,但需要理解"数组的数组"这一概念。
c
#include <stdio.h>
int main() {
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// matrix 是 int[2][3] 类型,matrix + 1 指向第二行
int (*row)[3] = matrix + 1; // 指向第 2 行
printf("第二行第一个元素: %d\n", (*row)[0]); // 输出 4
// 访问 matrix[1][2]
printf("matrix[1][2] = %d\n", *(*(matrix + 1) + 2)); // 输出 6
return 0;
}
6.2 指针数组与数组指针
理解指针数组和数组指针的区别,有助于正确使用指针算术运算:
c
// 指针数组:数组的每个元素是指针
int *ptr_arr[3];
// 数组指针:指向数组的指针
int (*arr_ptr)[3];
7. 总结
指针的算术运算虽然看似简单,但背后蕴含着 C 语言内存模型的深刻设计。掌握以下几点,就能灵活运用指针算术运算:
- 指针加/减整数
n,实际地址偏移n * sizeof(类型)字节。 - 两个同类型指针相减,结果是元素个数差。
- 指针算术运算必须基于同一数组(或同一对象)进行,避免越界。
void *指针不能直接进行算术运算,需先转换为具体类型。- 指针算术运算在数组遍历、字符串处理、动态内存管理等场景中应用广泛。
理解指针算术运算,是通往 C/C++ 高级编程的必经之路。建议读者在理解规则后,多动手编写和调试代码,在实践中巩固这些概念。