引言
指针是C语言的灵魂,也是最让初学者头疼的概念。很多人在学习指针时,往往只记住了"指针就是地址"这句话,却没有真正理解指针与类型、内存布局之间的深层关系。
今天,我将从内存底层视角,通过大量代码示例,深入讲解指针与数组的关系、指针的类型转换、数组指针与指针数组的区别,以及函数指针的高级用法。
第一部分:指针的基本运算规则
一、指针加减法的本质
核心公式:
p + n = p + n * sizeof(指向的类型)
p - n = p - n * sizeof(指向的类型)
cpp
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
printf("p = %p\n", p);
printf("p + 1 = %p\n", p + 1); // 偏移4字节(sizeof(int))
printf("p + 2 = %p\n", p + 2); // 偏移8字节
char* cp = (char*)arr;
printf("cp = %p\n", cp);
printf("cp + 1 = %p\n", cp + 1); // 偏移1字节(sizeof(char))
return 0;
}
二、解引用操作
cpp
// *指针:从当前指针的地址访问Type个字节的值,然后把这个值看作是Type类型的变量
int main() {
int a = 0x12345678;
char* p = (char*)&a;
printf("*p = %x\n", *p); // 78(小端模式下低地址存低位)
printf("*(p+1) = %x\n", *(p+1)); // 56
printf("*(p+2) = %x\n", *(p+2)); // 34
printf("*(p+3) = %x\n", *(p+3)); // 12
return 0;
}
第二部分:数组指针与指针数组
一、数组指针 int(*p)[n]
数组指针是指向整个数组的指针,而不是指向数组首元素。
cpp
int main() {
int arr[5] = {1, 2, 3, 4, 5};
// 数组指针:指向有5个int元素的数组
int(*p)[5] = &arr;
printf("arr = %p\n", arr); // 首元素地址
printf("&arr = %p\n", &arr); // 整个数组的地址(数值相同,类型不同)
printf("p = %p\n", p);
printf("p + 1 = %p\n", p + 1); // 偏移20字节(5 * sizeof(int))
// 通过数组指针访问元素
for (int i = 0; i < 5; i++) {
printf("%d ", (*p)[i]); // 注意:需要先解引用
}
return 0;
}
二、指针数组 int* p[n]
指针数组是数组元素为指针的数组。
cpp
int main() {
int a = 1, b = 2, c = 3;
// 指针数组:数组的每个元素都是int指针
int* arr[3] = {&a, &b, &c};
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 24(64位,3*8)
for (int i = 0; i < 3; i++) {
printf("%d ", *arr[i]);
}
return 0;
}
三、二维数组的指针理解
cpp
int main() {
int a[3][4] = {0};
// a 的类型:int(*)[4](指向有4个int的数组的指针)
// a[0] 的类型:int*(指向int的指针)
// a[0][0] 的类型:int
printf("a = %p\n", a);
printf("a + 1 = %p\n", a + 1); // 偏移16字节(4 * sizeof(int))
printf("a[0] = %p\n", a[0]);
printf("a[0] + 1 = %p\n", a[0] + 1); // 偏移4字节(sizeof(int))
// sizeof计算
printf("sizeof(a) = %zu\n", sizeof(a)); // 48(3*4*4)
printf("sizeof(a[0]) = %zu\n", sizeof(a[0])); // 16(4*4)
printf("sizeof(a[0][0]) = %zu\n", sizeof(a[0][0])); // 4
printf("sizeof(a + 1) = %zu\n", sizeof(a + 1)); // 8(指针大小)
printf("sizeof(*(a + 1)) = %zu\n", sizeof(*(a + 1))); // 16(a[1]的类型是int[4])
return 0;
}
四、经典指针运算题目
cpp
// 题目1:&a + 1 与 (int)a + 1 的区别
int main() {
int a[4] = {1, 2, 3, 4};
// &a 类型:int(*)[4]
// &a + 1:跳过整个数组(16字节),指向数组末尾
int* ptr1 = (int*)(&a + 1);
// (int)a:将数组首地址强制转换为整数
// (int)a + 1:地址值+1,指向第一个字节后的位置
int* ptr2 = (int*)((int)a + 1);
// ptr1[-1]:从ptr1向前4字节,取出最后一个元素
// *ptr2:从地址a+1开始读取4字节,解释为int(小端模式)
printf("%x, %x\n", *(ptr1 - 1), *ptr2);
return 0;
}
// 题目2:&a + 1 与数组末尾元素访问
int main() {
int a[5] = {1, 2, 3, 4, 5};
// a:首元素地址,类型int*
// &a:整个数组的地址,类型int(*)[5]
// &a + 1:跳过整个数组(20字节)
int* ptr = (int*)(&a + 1);
// *(a + 1):a[1] = 2
// *(ptr - 1):ptr向前4字节,取出a[4] = 5
printf("%d, %d\n", *(a + 1), *(ptr - 1)); // 输出:2, 5
return 0;
}
第三部分:指针的强制类型转换
一、不同类型指针访问内存
cpp
int main() {
int a = 0x12345678;
char* p1 = (char*)&a;
short* p2 = (short*)&a;
int* p3 = &a;
printf("char* 读取:%x\n", *p1); // 78(1字节)
printf("short* 读取:%x\n", *p2); // 5678(2字节)
printf("int* 读取:%x\n", *p3); // 12345678(4字节)
return 0;
}
二、指针类型转换的应用
cpp
// 使用short指针修改int变量的部分字节
int main() {
int a = 0;
short* p = (short*)&a;
p[0] = 0x1234;
p[1] = 0x5678;
printf("a = %x\n", a); // 56781234(小端模式)
return 0;
}
第四部分:函数指针
一、函数指针的基本概念
函数指针是指向函数的指针,存储的是函数的入口地址。
cpp
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int main() {
// 定义函数指针
int (*p)(int, int) = add;
// 三种调用方式等价
printf("%d\n", add(10, 20)); // 直接调用
printf("%d\n", p(10, 20)); // 通过指针调用
printf("%d\n", (*p)(10, 20)); // 通过指针解引用调用
// 函数名和&函数名只有类型不同,本质相同
printf("add = %p\n", add);
printf("&add = %p\n", &add);
return 0;
}
二、函数指针数组
cpp
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }
int main() {
// 函数指针数组
int (*arr[4])(int, int) = {add, sub, mul, div};
int a = 10, b = 5;
for (int i = 0; i < 4; i++) {
printf("%d\n", arr[i](a, b));
}
return 0;
}
三、回调函数
cpp
// 回调函数:通过函数指针调用的函数
void comp(int a, int b, int (*tmp)(int, int)) {
printf("%d\n", tmp(a, b));
}
int main() {
comp(10, 20, add); // 30
comp(10, 20, sub); // -10
comp(10, 20, mul); // 200
return 0;
}
四、简易计算器(函数指针数组实现)
cpp
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }
int (*arr[4])(int, int) = {add, sub, mul, div};
void menu() {
int choice = 0;
int a = 10, b = 20;
while (1) {
printf("****简易计算器*****\n");
printf("***** 1.+ *******\n");
printf("***** 2.- *******\n");
printf("***** 3.* *******\n");
printf("***** 4./ *******\n");
printf("***** 0.退出 *****\n");
printf("请选择: ");
scanf("%d", &choice);
if (choice == 0) break;
if (choice >= 1 && choice <= 4) {
printf("%d\n", arr[choice - 1](a, b));
}
}
}
int main() {
menu();
return 0;
}
第五部分:指针与数组传参
一、一维数组传参
cpp
// 以下四种写法等价,都是接收指针
void print1(int arr[]) { } // 形参写法1
void print2(int arr[10]) { } // 形参写法2(长度被忽略)
void print3(int* arr) { } // 形参写法3
void print4(int* arr, int len) { } // 推荐:同时传递长度
int main() {
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 数组名作为参数时,退化为指针
print1(arr);
print2(arr);
print3(arr);
return 0;
}
二、二维数组传参
cpp
// 二维数组传参,必须指定第二维的大小
void print(int arr[][4], int row) {
for (int i = 0; i < row; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
// 等价写法:使用数组指针
void print2(int (*arr)[4], int row) {
// 同上
}
int main() {
int arr[3][4] = {0};
print(arr, 3);
return 0;
}
第六部分:字符指针与字符串常量
一、字符串常量的存储
cpp
int main() {
// 字符数组:存储在栈上,可修改
char str1[] = "abcdef";
char str2[] = "abcdef";
// 字符指针:指向常量区,不可修改
const char* str3 = "abcdef";
const char* str4 = "abcdef";
// 数组:不同数组不同地址
printf("str1 = %p\n", str1);
printf("str2 = %p\n", str2);
// 指针:指向同一常量字符串,地址相同
printf("str3 = %p\n", str3);
printf("str4 = %p\n", str4);
return 0;
}
重要结论:
-
C/C++会把常量字符串存储到单独的内存区域(只读数据段)
-
多个指针指向同一字符串常量时,指向同一块内存
-
用字符串常量初始化不同数组时,会开辟不同的内存块
二、字符指针的使用
cpp
int main() {
const char* p = "abcdef"; // p保存'a'的地址
// 访问字符串
for (int i = 0; i < 6; i++) {
printf("%c ", p[i]);
}
// 二级指针
const char** pp = &p;
printf("%c\n", **pp); // 'a'
return 0;
}
第七部分:sizeof 与指针运算总结
一、sizeof 计算规则
cpp
int main() {
int a[10];
int* p = a;
printf("sizeof(a) = %zu\n", sizeof(a)); // 40(整个数组)
printf("sizeof(p) = %zu\n", sizeof(p)); // 8(指针大小)
printf("sizeof(&a) = %zu\n", sizeof(&a)); // 8(指针大小)
printf("sizeof(*&a) = %zu\n", sizeof(*&a));// 40(*&a = a)
return 0;
}
重要规则:
-
sizeof(数组名):计算整个数组的大小 -
&数组名:取整个数组的地址,类型为数组指针 -
其他情况(数组名作为右值):退化为首元素指针
二、指针与数组名总结
cpp
int main() {
int arr[10];
// arr 的类型:int*(首元素地址)
// &arr 的类型:int(*)[10](整个数组的地址)
// *arr 的类型:int(首元素的值)
// arr[0] 的类型:int
// 关键区别:
// arr + 1:偏移4字节
// &arr + 1:偏移40字节
return 0;
}
总结
一、指针核心规则
| 规则 | 说明 |
|---|---|
| 指针大小 | 32位4字节,64位8字节 |
| p + n | 偏移 n * sizeof(指向类型) 字节 |
| 数组名 | 除sizeof和&外,退化为首元素指针 |
| 数组指针 | int(*p)[n],指向整个数组 |
| 指针数组 | int* p[n],数组元素是指针 |
| 函数指针 | int (*p)(int, int),指向函数 |
二、常见陷阱
| 陷阱 | 说明 |
|---|---|
| &a + 1 vs (int)a + 1 | 前者跳过整个数组,后者地址+1 |
| 二维数组传参 | 必须指定第二维大小 |
| 字符串常量 | 不可修改,修改会导致未定义行为 |
| 数组名退化 | 只有sizeof和&时不退化 |
指针是C语言最核心、最强大的特性。理解指针与数组的关系、指针的类型转换、以及函数指针的用法,是掌握C语言的关键。
学习建议:
-
理解指针加减法的本质(偏移字节数 = n × sizeof(类型))
-
区分数组指针和指针数组
-
掌握二维数组的指针访问方式
-
理解函数指针的声明和使用