小熊奶糖(BearCandy)
小熊奶糖(BearCandy)
发布于 2025-11-05 / 5 阅读
0
0

C语言指针运算完全指南

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++;  // 正确

📝 核心总结

关键概念

  1. 指针本质:所有指针物理上都是内存地址(整数)
  2. 类型意义:指针类型告诉编译器如何解释内存内容和进行运算
  3. 运算基础:指针运算基于 sizeof(类型) 作为步长
  4. 安全限制:禁止乘除法是为了防止内存访问错误

实用公式

// 指针加减运算
新地址 = 原地址 ± (n × sizeof(数据类型))

// 指针相减  
元素个数 = (地址差) / sizeof(数据类型)

// 数组边界
有效范围: arr <= ptr < arr + 数组长度

最佳实践

  1. 始终使用正确的指针类型
  2. 进行指针运算前检查边界
  3. 使用 sizeof 计算步长和大小
  4. 避免void指针的直接运算
  5. 优先使用数组语法,必要时使用指针运算

评论