指针

「指针」:从内存地址、取址、解引用到指针与数组,附大小端&指针尺寸实测,对指针的理解更加深刻!


一、内存地址与基地址

内存地址

  • 字节:字节是内存的容量单位,英文称为 byte,一个字节有8位,即 1byte = 8bits
  • 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址。
内存地址

基地址

  • 单字节数据:对于单字节数据而言,其地址就是其字节编号。
  • 多字节数据:对于多字节数据而言,期地址是其所有字节中编号最小的那个,称为基地址、入口地址。
基地址
术语 含义 图示
字节 8 bit,最小寻址单位
地址 字节的编号 0x7FFD EFC8
基地址 多字节数据的最低字节地址 int a 占4 B,地址=&a
#include <stdio.h>

int main(void)
{
    int a = 0x12345678;
    printf("&a   = %p\n", &a);      // 基地址
    printf("&a+1 = %p\n", &a + 1);  // 跳过整个 int
    
    // 验证字节序(大端/小端)
    unsigned char *p = (unsigned char*)&a;
    printf("内存布局: ");
    for (int i = 0; i < sizeof(a); i++) {
        printf("%02X ", p[i]);
    }
    printf("\n");
    
    return 0;
}

内存布局详解(字节序)

大小端序

小端序系统(x86/x64)输出示例:

&a   = 0x7FFD42A1B23C
&a+1 = 0x7FFD42A1B240    ← 相差4字节
内存布局: 78 56 34 12     ← 低位在前

二、取址符 & · 解引用符 *

操作 读法 示例值
&变量 取地址 0x7FFD EFC8
*指针 解引用 得到变量本体
int a = 100;
int *p = &a;        // p 保存 a 的地址
*p = 200;           // 等价 a = 200;
printf("%d %d\n", a, *p); // 200 200

详细解释(& 取地址,* 取内容)

1. 取址符 &

  • 作用:获取变量在内存中的地址

  • 返回值:指针类型,指向该变量

  • 限制:不能对常量、表达式使用 &

int x = 10;
float y = 3.14;
char c = 'A';

int *px = &x;       // 获取整型变量地址
float *py = &y;     // 获取浮点变量地址  
char *pc = &c;      // 获取字符变量地址

// ❌ 错误示例
// &100;           // 不能对常量取地址
// &(x + y);       // 不能对表达式取地址

2. 解引用符 *

  • 作用:通过指针访问或修改指向的内存内容

  • 要求:指针必须已初始化并指向有效内存

  • 风险:野指针解引用会导致未定义行为

int value = 50;
int *ptr = &value;

printf("值: %d\n", *ptr);    // 读取:输出 50
*ptr = 100;                  // 写入:value 变为 100
printf("新值: %d\n", value); // 输出 100

类型匹配重要性

// ✅ 正确:类型匹配
int a = 10;
int *p1 = &a;       // int* 指向 int

// ⚠️ 警告:类型不匹配
float *p2 = &a;     // float* 指向 int,编译器警告

// ❌ 危险:void* 需要显式转换
void *p3 = &a;
// *p3 = 20;        // 错误:void* 不能直接解引用
int *p4 = (int*)p3; // 需要显式类型转换
*p4 = 20;           // 正确

实际应用场景

① 函数参数传递(按引用)

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 5, y = 10;
    swap(&x, &y);           // 传递地址
    printf("x=%d, y=%d\n", x, y); // x=10, y=5
    return 0;
}

② 数组遍历

int arr[5] = {1,2,3,4,5};
int *p = arr;               // 数组名即首地址

for (int i = 0; i < 5; i++) {
    printf("%d ", *(p + i)); // 指针算术和解引用
}
// 输出:1 2 3 4 5

③ 动态内存管理

int *dynamic = malloc(10 * sizeof(int)); // 动态分配
if (dynamic != NULL) {
    *dynamic = 100;         // 访问动态内存
    free(dynamic);          // 释放内存
}

重要提醒:

  • 始终确保指针指向有效内存后再解引用

  • 使用const修饰符保护不该被修改的数据

  • 动态分配的内存要及时释放,避免内存泄漏


三、指针基础

指针的概念:

  • 地址。比如 &a 是一个地址,也是一个指针,&a 指向变量 a的地址。
  • 专门用于存储地址的变量,又称指针变量。
  • 指针的本质就是一个变量,只不过该变量用于存放地址

结论:指针大小只与系统位数有关,与类型无关

  • 指针的定义:
#include <stdio.h>

int main(void)
{
    // 用于存储 char 型数据的地址,p2 被称为 char 型指针,或称字符指针
    printf("char *  = %zu B\n", sizeof(char *));   // 8 B(64位)
    // 用于存储 int  型数据的地址,p1 被称为 int  型指针,或称整型指针
    printf("int *   = %zu B\n", sizeof(int *));    // 8 B 
    // 用于存储double型数据的地址,p3 被称为 double 型指针
    printf("double * = %zu B\n", sizeof(double *)); // 8 B
    printf("void *  = %zu B\n", sizeof(void *));   // 8 B
    
    // 函数指针也是相同大小
    printf("函数指针 = %zu B\n", sizeof(int(*)(void)));
    
    //指针的赋值:给指针变量存入一个具体的数据地址,类型需跟指针的类型相匹配
    int a = 100;
    int * p1 = &a; // 将一个整型地址,赋值给整型指针p1

      
    char c = 'x';
    char * p2 = &c; // 将一个字符地址,赋值给字符指针p2


    double f = 3.14;
    double * p3 = &f; // 将一个浮点地址,赋值给浮点指针p3
  
    return 0;
}
图示

注意: 指针的类型实际上是告诉操作系统该如何解析该指针变量指向的内存中的二进制编码。


四、指针运算:± 整数 · 关系 · 差值

运算 含义 结果类型
p + n 跳过 n 个元素 同类型指针
p - n 回退 n 个元素 同类型指针
p1 - p2 两指针间元素个数 ptrdiff_t
p1 > p2 是否在前/后 逻辑 0/1
int arr[5] = {10,20,30,40,50};
int *p = &arr[2];      // 指向 30
printf("%d\n", *(p+1)); // 40
printf("%d\n", *(p-1)); // 20
/*如果arr是int数组,每个int占4字节,那么p和arr实际相差8字节,但指针减法结果仍然是2(个元素)
指针减法的结果类型是ptrdiff_t,通常用%td格式符打印*/
printf("%td\n", p - arr); // 2 (元素个数)
图示

单位是元素,不是字节! 步长 = sizeof(指向类型)

详细解释

1. 指针加减运算

#include <stdio.h>

int main(void)
{
    int arr[5] = {10,20,30,40,50};
    int *p = arr;  // 指向第一个元素
    
    printf("初始: p指向arr[0], 值=%d\n", *p);
    
    p = p + 2;     // 前进2个int元素
    printf("p+2: 指向arr[2], 值=%d\n", *p);
    
    p = p - 1;     // 回退1个int元素  
    printf("p-1: 指向arr[1], 值=%d\n", *p);
    
    return 0;
}

2. 指针关系运算

int arr[5] = {10,20,30,40,50};
int *p1 = &arr[1];  // 指向20
int *p2 = &arr[3];  // 指向40

printf("p1 < p2: %d\n", p1 < p2);   // 1 (true)
printf("p1 > p2: %d\n", p1 > p2);   // 0 (false)
printf("p1 == &arr[1]: %d\n", p1 == &arr[1]); // 1 (true)

3. 指针差值运算

#include <stddef.h>

int arr[5] = {10,20,30,40,50};
int *start = &arr[0];
int *end = &arr[4];

ptrdiff_t diff = end - start;
printf("元素个数: %td\n", diff);     // 4
printf("字节距离: %td\n", (char*)end - (char*)start); // 16
  • 步长验证实验
#include <stdio.h>

int main(void)
{
    char char_arr[5] = {'a','b','c','d','e'};
    int int_arr[5] = {10,20,30,40,50};
    double double_arr[5] = {1.1,2.2,3.3,4.4,5.5};
    
    char *cp = char_arr;
    int *ip = int_arr;
    double *dp = double_arr;
    
    printf("=== 步长验证 ===\n");
    printf("char*  步长: %zu字节 (p+1=%p, p=%p)\n", 
           sizeof(char), cp+1, cp);
    printf("int*   步长: %zu字节 (p+1=%p, p=%p)\n", 
           sizeof(int), ip+1, ip);
    printf("double*步长: %zu字节 (p+1=%p, p=%p)\n", 
           sizeof(double), dp+1, dp);
    
    return 0;
}
运行结果

五、数组名 ≠ 指针变量

对比项 数组名 arr 指针变量 p
类型 int[5] int *
可修改性 ❌ 常量地址 ✅ 可重新指向
sizeof 整个数组字节数 指针字节数
取地址 &arr 整个数组地址 指针变量地址
#include <stdio.h>

int main(void)
{
    int arr[5] = {1,2,3,4,5};
    int *p = arr;  // p指向数组首元素
    
    printf("sizeof(arr) = %zu\n", sizeof(arr)); // 20 = 5*4
    printf("sizeof(p)   = %zu\n", sizeof(p));   // 8  指针大小
    
    // 地址验证
    printf("arr    = %p\n", arr);      // 数组首元素地址
    printf("&arr   = %p\n", &arr);     // 整个数组地址(值与arr相同)
    printf("&arr[0]= %p\n", &arr[0]);  // 首元素地址(值与arr相同)
    printf("p      = %p\n", p);        // 指针存储的地址
    printf("&p     = %p\n", &p);       // 指针变量本身的地址
    
    return 0;
}

关键区别详解

1. 类型差异

int arr[5];    // 类型: int[5] (5元素整型数组)
int *p;        // 类型: int*   (整型指针)

// 编译器视角:
sizeof(arr);   // 计算整个数组大小:5 * sizeof(int)
sizeof(p);     // 计算指针大小:sizeof(void*)

2. 可修改性差异

int arr[5] = {1,2,3,4,5};
int *p = arr;
int other[3] = {10,20,30};

// ❌ 错误:数组名是常量,不能重新赋值
// arr = other;    
// arr++;          

// ✅ 正确:指针变量可以重新指向
p = other;      // p现在指向other数组
p++;            // p指向other[1]

3. 取地址操作差异

int arr[5];
int *p = arr;

printf("arr + 1 = %p\n", arr + 1);    // 前进一个int元素
printf("&arr + 1 = %p\n", &arr + 1);  // 前进整个数组(20字节)

printf("p + 1 = %p\n", p + 1);        // 前进一个int元素  
printf("&p + 1 = %p\n", &p + 1);      // 前进一个指针大小(8字节)

结论

1.数组名是标签,指针是变量

  • 数组名:编译器符号表中的固定标签
  • 指针:内存中的变量,存储地址值

2.函数参数中的退化

void func(int arr[]);   // 实际被编译器视为:void func(int *arr)
void func(int *arr);    // 等价声明

3.何时数组名不退化为指针

  • sizeof(arr) - 获取整个数组大小

  • &arr - 获取整个数组的地址

  • 字符数组初始化:char str[] = "hello"


六、野指针和空指针

野指针

概念

指向一块未知区域的指针,被称为野指针。野指针是危险的。

运行结果

危害

a. 引用野指针,大概率访问了非法的内存,常常会导致段错误(segmentation fault 程序崩溃)
b. 引用野指针,可能会破坏系统的关键数据,导致系统崩溃等严重后果

产生原因

a. 指针定义之后,未初始化
b. 指针所指向的内存,被系统回收
c. 指针越界(超出了原本申请的可用范围)

如何防止

a. 指针定义时,及时初始化
b. 绝不引用已被系统回收的内存
c. 确认所申请的内存边界,谨防越界


很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存。

空指针

概念

空指针即保存了零地址的指针,亦即指向零地址的指针。

运行结果

示例

// 1,刚定义的指针,让其指向零地址以确保安全:
char *p1 = NULL;
int  *p2 = NULL;

// 2,被释放了内存的指针,让其指向零地址以确保安全:
char *p3 = malloc(100); // a. 让 p3 指向一块大小为100个字节的内存
free(p3);               // b. 释放这块内存,此时 p3 相当于指向了一块非法内存
p3 = NULL;              // c. 让 p3 指向零地址

实践原则

//1.定义即初始化
// ❌ 危险
int *ptr;

// ✅ 安全
int *ptr = NULL;

//2.释放后置空
free(ptr);
ptr = NULL;  // 重要:防止悬空指针

//3.使用前检查
if (ptr != NULL) {
    *ptr = value;  // 安全操作
}

空指针的特殊性

  • NULL 在大多数系统中定义为 (void*)0

  • 解引用空指针会导致段错误(安全崩溃)

  • 相比野指针,空指针的崩溃是可预测和可调试的

  • C标准保证不对空指针进行内存访问


七、多级指针

概念层次

  • 如果一个指针变量 p1 存储的地址,是另一个普通变量 a 的地址,那么称 p1一级指针
  • 如果一个指针变量 p2 存储的地址,是指针变量 p1 的地址,那么称 p2二级指针
  • 如果一个指针变量 p3 存储的地址,是指针变量 p2 的地址,那么称 p3三级指针
  • 以此类推,p2p3等指针被称为多级指针

示例

#include <stdio.h>

int main(void)
{
    int a = 100;
    int   *p1 = &a;  // 一级指针,指向普通变量
    int  **p2 = &p1; // 二级指针,指向一级指针
    int ***p3 = &p2; // 三级指针,指向二级指针

    printf("变量 a 的值: %d\n", a);
    printf("通过 p1 访问: %d\n", *p1);
    printf("通过 p2 访问: %d\n", **p2);
    printf("通过 p3 访问: %d\n", ***p3);
    
    return 0;
}
运行结果

多级指针的实际应用

1. 动态二维数组

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int rows = 3, cols = 4;
    
    // 创建二维数组
    int **matrix = malloc(rows * sizeof(int*));
    for (int i = 0; i < rows; i++) {
        matrix[i] = malloc(cols * sizeof(int));
    }
    
    // 初始化
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;
        }
    }
    
    // 访问元素
    printf("matrix[1][2] = %d\n", matrix[1][2]); // 6
    
    // 释放内存
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
    
    return 0;
}

2.修改指针变量(函数参数)

#include <stdio.h>
#include <stdlib.h>

void allocate_memory(int **ptr)
{
    *ptr = malloc(100);  // 修改外部指针的指向
}

int main(void)
{
    int *data = NULL;
    allocate_memory(&data);  // 传递指针的地址
    
    if (data != NULL) {
        data[0] = 42;
        printf("分配成功: %d\n", data[0]);
        free(data);
    }
    
    return 0;
}

3.字符串数组(命令行参数)

#include <stdio.h>

int main(int argc, char **argv)
{
    printf("命令行参数:\n");
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

多级指针的声明规则

级别 声明 读法 解引用
一级 int *p 指向int的指针 *p
二级 int **p 指向int指针的指针 **p
三级 int ***p 指向int二级指针的指针 ***p

八、指针万能拆解法

核心原理

  • 任意的指针,不管有多复杂,其定义都由两部分组成
  • 第1部分:指针所指向的数据类型,可以是任意的类型
  • 第2部分:指针的名字

拆解规则

从指针名开始,按照优先级规则逐步分析:

  1. 先看括号内的内容
  2. 然后向右看([] 数组、() 函数)
  3. 最后向左看(类型修饰符)

示例拆解

原始声明 拆解步骤 第1部分(指向类型) 第2部分(指针名)
char (*p1); char (*p1) char p1
char *(*p2); char * (*p2) char * p2
char **(*p3); char ** (*p3) char ** p3
char (*p4)[3]; char [3] (*p4) char [3] p4
char (*p5)(int, float); char (int, float) (*p5) char (int, float) p5

详细解释

  • 示例:
char (*p5)(int, float); // 第2部分:*p5; 第1部分:char (int, float);

p5 是一个指针
p5 指向 char (int, float) 类型(接受intfloat参数返回char的函数)
结论:p5 是函数指针

指针图解
  • 复杂声明
//案例1:指针数组 vs 数组指针
int *p1[5];    // 指针数组:5个int指针的数组
int (*p2)[5];  // 数组指针:指向5个int数组的指针

// 拆解:
// p1[5] - 5个元素的数组,元素类型是 int*
// (*p2) - 指针,指向 int[5] 类型

//案例2:函数指针数组
int (*p3[5])(void); // 5个函数指针的数组

// 拆解:
// p3[5] - 5个元素的数组
// (*p3[5]) - 数组元素是指针
// int (void) - 指针指向返回int无参数的函数

//案例3:复杂嵌套
char *(*(*p4)(int))[5];

// 拆解步骤:
// 1. (*p4) - p4是指针
// 2. (int) - 指向接受int参数的函数
// 3. *(*p4)(int) - 函数返回指针
// 4. [5] - 返回的指针指向5元素数组
// 5. char * - 数组元素是char指针

// 结论:p4是函数指针,函数接受int参数,返回指向5个char指针数组的指针

九、void型指针

概念

无法明确指针所指向的数据类型时,可以将指针定义为 void 型指针

要点

a. void 型指针无法直接索引目标,必须将其转换为一种具体类型的指针方可索引目标
b. void 型指针无法进行加减法运算(实际上可以加减按1字节算)

void关键字的三个作用

a. 修饰指针,表示指针指向一个类型未知的数据
b. 修饰函数参数列表,表示函数不接收任何参数(如:int main(void)
c. 修饰函数返回类型,表示函数不返回任何数据(如:void func(int)

示例

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    // 指针 p 指向一块 4 字节的内存,且这4字节数据类型未确定
    void *p = malloc(4);

    // 1,将这 4 字节内存用来存储 int 型数据
    *(int *)p = 100;
    printf("%d\n", *(int *)p);

    // 2,将这 4 字节内存用来存储 float 型数据
    *(float *)p = 3.14;
    printf("%f\n", *(float *)p);

    free(p);
    return 0;
}

十、const 型指针

const型指针的两种形式

1. 常指针:const修饰指针本身

指针图解
int * const p; 
// p = &a; 【错误】无法改变指针的指向

2. 常目标指针:const修饰指针的目标

指针图解
int const * p;
// *p = 666; // 【错误】无法通过p修改目标内存中的数据

详细解释与示例

1. 常指针(指针本身是常量)

#include <stdio.h>

int main(void)
{
    int a = 10, b = 20;
    
    // 常指针 - 指针本身是常量
    int * const p1 = &a;
    
    *p1 = 100;      // ✅ 正确:可以修改指向的内容
    printf("a = %d\n", a);  // 输出: a = 100
    
    // p1 = &b;    // ❌ 错误:不能改变指针指向
    // 编译错误:assignment of read-only variable 'p1'
    
    return 0;
}

2. 常目标指针(指向的内容是常量)

#include <stdio.h>

int main(void)
{
    int a = 10, b = 20;
    
    // 常目标指针 - 指向的内容是常量
    int const * p2 = &a;
    const int * p3 = &a;  // 等价写法
    
    // *p2 = 200;  // ❌ 错误:不能通过p2修改内容
    // 编译错误:assignment of read-only location '*p2'
    
    p2 = &b;        // ✅ 正确:可以改变指针指向
    printf("p2现在指向b,值=%d\n", *p2);  // 输出: 20
    
    return 0;
}
  • 常指针在实际应用中不常见。
  • 常目标指针在实际应用中广泛可见,用来限制指针的读写权限(只读起保护作用)
posted @ 2025-10-14 19:15  林明杰  阅读(9)  评论(0)    收藏  举报