C语言指针深度解析:从数组指针到函数指针

引言

指针是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语言的关键。

学习建议:

  1. 理解指针加减法的本质(偏移字节数 = n × sizeof(类型))

  2. 区分数组指针和指针数组

  3. 掌握二维数组的指针访问方式

  4. 理解函数指针的声明和使用

相关推荐
Jasmine_llq2 小时前
《B4356 [GESP202506 二级] 数三角形》
开发语言·c++·双重循环枚举算法·顺序输入输出算法·去重枚举算法·整除判断算法·计数统计算法
止语Lab2 小时前
Go vs Java GC:同一场延迟战争的两条路
java·开发语言·golang
Rust研习社2 小时前
Rust 多线程从入门到实战
开发语言·后端·rust
Ulyanov2 小时前
《玩转QT Designer Studio:从设计到实战》 QT Designer Studio数据绑定与表达式系统深度解析
开发语言·python·qt
棋子入局3 小时前
C语言制作消消乐游戏(4)
c语言·开发语言·游戏
froginwe113 小时前
Python3 实例
开发语言
xiaoshuaishuai83 小时前
C# ZLibrary数字资源分发
开发语言·windows·c#
小碗羊肉3 小时前
【从零开始学Java | 第四十二篇】生产者消费者问题(等待唤醒机制)
java·开发语言
流年如夢3 小时前
自定义类型进阶:联合与枚举
java·c语言·开发语言·数据结构·数据库·c++·算法