C语言指针运算完全指南
📌 问题起源
最初的问题
void printPerson(const Person *p) {
printf("%s\t%d\t%.2f\t%.2f\n", p->name, p->gender, p->height, p->weight);
}
疑问:指针不都是内存地址(整数)吗?为什么这里是Person类型的指针?
🔍 指针的本质
物理层面 vs 逻辑层面
int num = 100;
Person person = {"Alice", 1, 170.0, 60.0};
// 物理上都是地址(整数)
void *genericPtr1 = # // 比如: 0x7ffd42a
void *genericPtr2 = &person; // 比如: 0x7ffd430
// 逻辑上类型不同
int *intPtr = # // 告诉编译器这是int的地址
Person *personPtr = &person; // 告诉编译器这是Person的地址
类型指针的作用
Person *p = &somePerson;
p->name // 编译器知道从偏移量0开始,读取20字节
p->gender // 编译器知道从偏移量20开始,读取4字节
p->height // 编译器知道从偏移量24开始,读取8字节
🧮 指针运算详解
基本规则表
| 运算类型 | 示例 | 是否允许 | 实际效果 |
|---|---|---|---|
| 指针 + 整数 | ptr + n |
✅ | 移动 n × sizeof(类型) 字节 |
| 指针 - 整数 | ptr - n |
✅ | 移动 n × sizeof(类型) 字节 |
| 指针 - 指针 | ptr1 - ptr2 |
✅ | 计算元素个数差 |
| 指针 + 指针 | ptr1 + ptr2 |
❌ | 无意义,编译错误 |
| 指针 × 整数 | ptr * n |
❌ | 可能指向无效内存 |
| 指针 ÷ 整数 | ptr / n |
❌ | 可能产生未对齐地址 |
使用sizeof计算步长
#include <stdio.h>
int main() {
printf("=== 各种数据类型步长 ===\n");
printf("char: %zu 字节\n", sizeof(char));
printf("int: %zu 字节\n", sizeof(int));
printf("double: %zu 字节\n", sizeof(double));
// 实际验证
int arr[] = {10, 20, 30, 40};
int *ptr = arr;
printf("\n指针运算验证:\n");
printf("ptr地址: %p, 值: %d\n", ptr, *ptr);
printf("ptr+1地址: %p, 值: %d\n", ptr+1, *(ptr+1));
printf("实际移动: %td 字节\n", (char*)(ptr+1) - (char*)ptr);
return 0;
}
🔄 类型转换与取值
方法1:直接类型转换(推荐)
int num = 12345;
void *genericPtr = #
// 转换回原类型取值
int value = *(int*)genericPtr;
printf("值: %d\n", value);
方法2:通过char指针逐字节访问
int num = 0x12345678;
char *bytePtr = (char*)#
printf("整数值: 0x%x\n", num);
printf("字节表示: ");
for(int i = 0; i < sizeof(int); i++) {
printf("%02x ", bytePtr[i]);
}
// 输出: 78 56 34 12 (小端序)
方法3:联合体(union)安全访问
typedef union {
int integer;
char bytes[4];
float floating;
} Converter;
Converter conv;
conv.integer = 0x12345678;
// 多种方式访问同一内存
printf("整数: %d\n", conv.integer);
printf("字节: ");
for(int i = 0; i < 4; i++) {
printf("%02x ", conv.bytes[i]);
}
🚫 为什么禁止乘除法?
乘法的问题
int arr[] = {10, 20, 30, 40, 50};
int *ptr = &arr[0]; // 地址: 0x1000
// 如果允许乘法
// ptr * 2 = 0x1000 * 2 = 0x2000
// 0x2000 可能指向无效内存或另一个程序的数据!
除法的问题
int *ptr = 0x1003; // 未对齐的地址
// 如果允许除法
// ptr / 2 = 0x1003 / 2 = 0x801 (不是4的倍数)
// 访问未对齐的int会导致性能下降或崩溃
💡 实际应用示例
数组遍历
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
// 正向遍历
for(int *ptr = arr; ptr < arr + size; ptr++) {
printf("%d ", *ptr);
}
// 反向遍历
for(int *ptr = arr + size - 1; ptr >= arr; ptr--) {
printf("%d ", *ptr);
}
字符串操作
// 字符串长度 - 指针版本
int strLength(const char *str) {
const char *ptr = str;
while(*ptr != '\0') {
ptr++;
}
return ptr - str; // 指针相减得到元素个数
}
// 字符串复制 - 指针版本
void strCopy(char *dest, const char *src) {
while((*dest = *src) != '\0') {
dest++;
src++;
}
}
结构体操作
typedef struct {
int id;
char name[20];
double salary;
} Employee;
Employee employees[5] = {
{1, "Alice", 5000},
{2, "Bob", 6000},
{3, "Charlie", 7000}
};
// 使用指针遍历结构体数组
Employee *ptr = employees;
for(int i = 0; i < 3; i++) {
printf("ID: %d, Name: %s, Salary: %.2f\n",
ptr->id, ptr->name, ptr->salary);
ptr++; // 自动移动 sizeof(Employee) 字节
}
🛡️ 安全注意事项
边界检查
int arr[] = {10, 20, 30};
int *ptr = arr;
// 危险的访问
// printf("%d\n", *(ptr + 10)); // 未定义行为!
// 安全的做法
int index = 2;
if(ptr + index < arr + sizeof(arr)/sizeof(arr[0])) {
printf("安全访问: %d\n", *(ptr + index));
} else {
printf("索引超出边界!\n");
}
void指针的限制
int num = 100;
void *voidPtr = #
// void指针不能直接运算
// voidPtr + 1; // 错误!
// 需要先转换类型
int *intPtr = (int*)voidPtr;
intPtr++; // 正确
📝 核心总结
关键概念
- 指针本质:所有指针物理上都是内存地址(整数)
- 类型意义:指针类型告诉编译器如何解释内存内容和进行运算
- 运算基础:指针运算基于
sizeof(类型)作为步长 - 安全限制:禁止乘除法是为了防止内存访问错误
实用公式
// 指针加减运算
新地址 = 原地址 ± (n × sizeof(数据类型))
// 指针相减
元素个数 = (地址差) / sizeof(数据类型)
// 数组边界
有效范围: arr <= ptr < arr + 数组长度
最佳实践
- 始终使用正确的指针类型
- 进行指针运算前检查边界
- 使用
sizeof计算步长和大小 - 避免void指针的直接运算
- 优先使用数组语法,必要时使用指针运算