C语言第十一讲: 深入理解指针(1)

目录

[1. 内存和地址](#1. 内存和地址)

[1.1 内存](#1.1 内存)

[1.2 究竟该如何理解编址](#1.2 究竟该如何理解编址)

[2. 指针变量和地址](#2. 指针变量和地址)

[2.1 取地址操作符(&)](#2.1 取地址操作符(&))

[2.2 指针变量和解引用操作符(*)](#2.2 指针变量和解引用操作符(*))

[2.2.1 指针变量](#2.2.1 指针变量)

[2.2.2 解引用操作符](#2.2.2 解引用操作符)

[2.3 指针变量的大小](#2.3 指针变量的大小)

[3. 指针变量类型的意义](#3. 指针变量类型的意义)

[3.1 指针的解引用](#3.1 指针的解引用)

[3.2 指针+-整数](#3.2 指针+-整数)

[3.3 void*指针](#3.3 void*指针)

[4. 指针运算](#4. 指针运算)

[4.1 指针+-整数](#4.1 指针+-整数)

[4.2 指针-指针](#4.2 指针-指针)

[4.3 指针的关系运算](#4.3 指针的关系运算)


1. 内存和地址

1.1 内存

在讲内存和地址之前,我们想有个生活中的案例:

假设有一栋宿舍楼,把你放在楼里,楼上有 100 个房间,但是房间没有编号,你的一个朋友来找你玩,如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:

1 一楼: 101, 102, 103...

2 二楼: 201, 202, 203...

3 ...

有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你

生活中,每个房间有了房间号,就能提高效率,能快速的找到房间

如果把上面的例子对照到计算机中,又是怎么样呢?

我们知道计算机上 CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是 8GB/16GB/32GB 等,那这些内存空间如何高效的管理呢?

其实也是把内存划分为一个个的内存单元,每个内存单元的大小取 1 个字节。

计算机中常见的单位(补充):

一个比特位可以存储一个 2 进制的位 1 或者 0

1.2 究竟该如何理解编址

2. 指针变量和地址

2.1 取地址操作符(&)

理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量就是想内存申请空间,比如:

2.2 指针变量和解引用操作符(*)

2.2.1 指针变量

那我们通过取地址操作符 (&) 拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中。

比如:

cs 复制代码
// 引入标准输入输出头文件(后续若要打印内容会用到此头文件)
#include <stdio.h>

// 主函数:程序的执行入口
int main()
{
    // 定义整型变量a,并给a初始赋值为10
    // 变量a会占用一块内存空间,这块空间有唯一对应的内存地址
    int a = 10;

    // 定义指针变量pa:
    // 1. "int *" 表示pa是一个"指向整型变量的指针变量"(用于存储其他整型变量的内存地址)
    // 2. "&a" 是取地址操作符,作用是获取变量a的内存地址
    // 整行含义:把变量a的内存地址,存储到指针变量pa中
    int * pa = &a;//取出a的地址并存储到指针变量pa中

    // 主函数返回0,表示程序正常执行结束
    return 0;
}

指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。

2.2.2 解引用操作符

我们将地址保存起来,未来是要使用的,那怎么使用呢?

在现实生活中,我们使用地址要找到一个房间,在房间里可以拿去或者存放物品。

C 语言中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫解引用操作符 (*)。

cs 复制代码
// 引入标准输入输出头文件,用于后续可能的打印等输入输出操作
#include <stdio.h>

// 主函数:程序的执行入口
int main()
{
    // 定义整型变量a,并给a初始赋值为100
    // 变量a会占用一块内存空间,对应唯一的内存地址
    int a = 100;

    // 定义指向整型的指针变量pa:
    // 1. "int*" 表示pa是"指向整型变量的指针"
    // 2. "&a" 是取地址操作符,获取变量a的内存地址
    // 整行含义:将变量a的内存地址存储到指针变量pa中
    int* pa = &a;

    // "*pa" 是解引用操作符:通过指针pa中存储的地址,找到它指向的变量(即a)
    // 整行含义:通过指针pa,将它指向的变量a的值修改为0
    *pa = 0;

    // 主函数返回0,表示程序正常执行结束
    return 0;
}

上面代码中第 7 行就使用了解引用操作符,*pa 的意思就是通过 pa 中存放的地址,找到指向的空间,*pa 其实就是 a 变量了;所以*pa = 0,这个操作符是把 a 改成了 0.

有同学肯定在想,这里如果目的就是把 a 改成 0 的话,写成 a = 0; 不就完了,为啥非要使用指针呢?

其实这里是把 a 的修改交给了 pa 来操作,这样对 a 的修改,就多了一种的途径,写代码就会更加灵活,后期慢慢就能理解了。

2.3 指针变量的大小

前面的内容我们了解到,32 位机器假设有 32 根地址总线,每根地址线出来的电信号转换成数字信号后是 1 或者 0,那我们把 32 根地址线产生的 2 进制序列当做一个地址,那么一个地址就是 32 个 bit 位,需要 4 个字节才能存储。

如果指针变量是用来存放地址的,那么指针变量的大小就得是 4 个字节的空间才可以。

同理 64 位机器,假设有 64 根地址总线,一个地址就是 64 个二进制位组成的二进制序列,存储起来就需要 8 个字节的空间,指针变量的大小就是 8 个字节。

cs 复制代码
#include <stdio.h>  // 引入标准输入输出头文件,用于printf函数输出
// 代码功能注释:验证不同类型指针变量的大小
// 指针变量的大小由当前平台的地址总线宽度决定
// 32位平台:地址占32个bit(即4字节),所有指针变量大小为4字节
// 64位平台:地址占64个bit(即8字节),所有指针变量大小为8字节

int main()  // 主函数,程序执行入口
{
    // 打印char类型指针的大小
    // char*是指向char类型变量的指针,其大小由平台决定
    printf("%zd\n", sizeof(char *));
    // 打印short类型指针的大小
    printf("%zd\n", sizeof(short *));
    // 打印int类型指针的大小
    printf("%zd\n", sizeof(int *));
    // 打印double类型指针的大小
    printf("%zd\n", sizeof(double *));

    return 0;  // 主函数返回0,程序正常结束
}

结论:

  • 32 位平台下地址是 32 个 bit 位,指针变量大小是 4 个字节

  • 64 位平台下地址是 64 个 bit 位,指针变量大小是 8 个字节

  • 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

3. 指针变量类型的意义

指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,为什么还要有各种各样的指针类型呢?

其实指针类型是有特殊意义的,我们接下来继续学习。

3.1 指针的解引用

对比,下面两段代码,主要在调试时观察内存的变化。

cs 复制代码
// 代码1:演示int类型指针的解引用操作
#include <stdio.h>

int main()
{
    // 定义int类型变量n,赋值为十六进制数0x11223344(占4个字节)
    int n = 0x11223344;
    // 定义指向int类型的指针pi,存储n的地址
    int *pi = &n;
    // 解引用pi:通过指针访问其指向的int类型变量(占4个字节),将n的4个字节全部赋值为0
    *pi = 0;
    return 0;
}
cs 复制代码
// 代码2:演示char类型指针的解引用操作
#include <stdio.h>

int main()
{
    // 定义int类型变量n,赋值为十六进制数0x11223344(占4个字节)
    int n = 0x11223344;
    // 定义指向char类型的指针pc,将n的地址强制转换为char*类型
    char *pc = (char *)&n;
    // 解引用pc:通过指针访问其指向的char类型变量(仅占1个字节),仅将n的1个字节赋值为0
    *pc = 0;
    return 0;
}

调试结果:

  • 代码 1 会将n的 4 个字节全部改为 0;

  • 代码 2 仅将n的第一个字节改为 0。

结论:

指针的类型决定了解引用时的操作权限(一次能操作的字节数)。

示例:

  • char*类型指针解引用,仅能访问 1 个字节;

  • int*类型指针解引用,能访问 4 个字节。

3.2 指针+-整数

先看一段代码,调试观察地址的变化

cs 复制代码
#include <stdio.h>  // 引入标准输入输出头文件,用于printf函数输出

int main()
{
    // 定义int类型变量n,赋值为10(n占4个字节内存)
    int n = 10;
    // 定义char*类型指针pc,将n的地址强制转换为char*类型后赋值
    char *pc = (char*)&n;
    // 定义int*类型指针pi,存储n的地址
    int *pi = &n;

    // 打印变量n的内存地址
    printf("%p\n", &n);
    // 打印char*类型指针pc存储的地址(与&n相同)
    printf("%p\n", pc);
    // 打印pc自增1后的地址:char*指针+1,步长为1字节
    printf("%p\n", pc+1);
    // 打印int*类型指针pi存储的地址(与&n相同)
    printf("%p\n", pi);
    // 打印pi自增1后的地址:int*指针+1,步长为4字节(对应int类型的大小)
    printf("%p\n", pi+1);

    return 0;  // 主函数返回0,程序正常结束
}

代码运行的结构如下:

  1. 指针运算的现象:

    • char*类型指针变量 + 1,会跳过 1 个字节;

    • int*类型指针变量 + 1,会跳过 4 个字节。

  2. 原理说明:

    这一差异由指针变量的类型决定;指针 + 1(或 - 1)的本质,是跳过 "1 个指针指向的元素" 对应的内存空间。

  3. 结论:

    指针的类型,决定了指针向前 / 向后移动一步时的偏移距离。

内容解读

这段内容揭示了指针类型的另一核心作用:控制指针运算的偏移步长 。指针的 "+1""-1" 并非简单的数值加 / 减 1,而是以 "指针指向的元素大小" 为单位偏移 ------ 例如char*对应 1 字节元素,步长为 1;int*对应 4 字节元素,步长为 4。这一规则是 C 语言中遍历连续数据(如数组)的基础:通过指针运算,可精准定位到下一个 / 上一个同类型元素,避免内存访问错位。

3.3 void*指针

在指针类型中有一种特殊的类型是void *类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进行指针的 +- 整数和解引用的运算。

举例:

cs 复制代码
#include <stdio.h>  // 引入标准输入输出头文件(此处未实际使用)

int main()  // 主函数,程序执行入口
{
    int a = 10;  // 定义int类型变量a,占用4字节内存,赋值为10
    int* pa = &a;  // 定义int*类型指针pa,正确接收int类型变量a的地址(类型匹配)
    // 【类型不匹配警告】:char*类型指针直接接收int类型变量的地址,C语言规范写法需强制类型转换
    char* pc = &a;  // 问题行:应改为 char* pc = (char*)&a; 消除类型不匹配警告
    return 0;  // 主函数返回0,程序正常结束
}

在之前的代码中,将 int 类型变量的地址赋值给 char类型指针变量时,编译机会弹出警告,原因是两种指针类型不兼容;而若使用 void类型指针接收该地址,则不会出现此类问题。

使用void*类型的指针接收地址:

cs 复制代码
#include <stdio.h>  // 引入标准输入输出头文件

int main()
{
    int a = 10;  // 定义int类型变量a,赋值为10
    void* pa = &a;  // 定义void*类型指针pa,接收int变量a的地址(void*可接收任意类型地址)
    void* pc = &a;  // 定义void*类型指针pc,同样接收int变量a的地址

    // 【错误】void*类型指针无具体类型,无法直接解引用(无法确定访问的字节数)
    *pa = 10;
    // 【错误】同理,void*类型指针不能直接进行解引用操作
    *pc = 0;

    return 0;  // 主函数返回0
}

VS编译代码的结果:

这里我们可以看到,void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。

那么 void* 类型的指针到底有什么用呢?

一般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据,在《深入理解指针(5)》中我们会讲解。

4. 指针运算

指针的基本运算有三种,分别是:

· 指针+- 整数

· 指针-指针

· 指针的关系运算

4.1 指针+-整数

因为数组在内存中是连续存放的,只要直到第一个元素的地址,顺藤摸瓜就能找到后面的所有元素

cs 复制代码
#include <stdio.h>  // 引入标准输入输出头文件,支持printf函数的使用
// 代码功能:通过"指针+整数"的运算方式实现数组元素的遍历
int main()
{
    // 定义int类型数组arr,初始化包含10个整型元素
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    // 定义int*类型指针p,指向数组的第一个元素arr[0]的地址
    int *p = &arr[0];
    // 定义循环控制变量i,初始值为0
    int i = 0;
    // 计算数组的元素个数:数组总字节数 ÷ 单个元素的字节数
    int sz = sizeof(arr)/sizeof(arr[0]);
    // 循环遍历数组,i的取值范围是0到数组元素个数-1
    for(i = 0; i < sz; i++)
    {
        // p+i:指针p进行"+i"运算,步长为int类型的大小(通常4字节),指向数组的第i个元素
        // *(p+i):解引用运算,获取p+i指向位置的元素值,并通过printf打印
        printf("%d ", *(p + i));//p+i 这里就是指针+整数的运算形式
    }
    return 0;  // 主函数返回0,标志程序正常结束
}

4.2 指针-指针

cs 复制代码
// 代码功能:通过"指针-指针"的运算实现字符串长度的计算
#include <stdio.h>

// 自定义函数my_strlen:计算字符串的长度,参数为char*类型的字符串指针
int my_strlen(char *s)
{
    char *p = s;  // 定义指针p,初始指向字符串s的起始位置
    // 循环:当p指向的内容不是字符串结束符'\0'时,继续执行
    while(*p != '\0' )
        p++;  // 指针p自增,依次指向字符串的下一个字符
    // 返回p与s的差值:即两个指针之间的字符个数(字符串长度)
    return p-s;
}

int main()
{
    // 调用my_strlen函数,计算字符串"abc"的长度并打印
    printf("%d\n", my_strlen("abc"));
    return 0;  // 主函数返回0,程序正常结束
}

4.3 指针的关系运算

cs 复制代码
// 代码功能:通过"指针的关系运算"实现数组遍历
#include <stdio.h>

int main()
{
    // 定义int类型数组arr,初始化10个元素 
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    // 定义int*类型指针p,指向数组的第一个元素arr[0]的地址
    int *p = &arr[0];
    // 计算数组的元素个数:数组总字节数 ÷ 单个元素的字节数
    int sz = sizeof(arr)/sizeof(arr[0]);
    // 指针关系运算:p(当前指向的地址) < arr+sz(数组末尾的下一个地址)
    while(p < arr + sz) //指针的大小比较
    {
        // 解引用指针p,打印当前指向的数组元素
        printf("%d ", *p);
        // 指针p自增,指向数组的下一个元素
        p++;
    }
    return 0;  // 主函数返回0,
相关推荐
Ccjf酷儿15 小时前
C++语言程序设计 (郑莉)第三章 函数
开发语言·c++
ytttr87315 小时前
基于人工蜂群算法(ABC)的MATLAB数值计算求解框架
开发语言·算法·matlab
Dxy123931021616 小时前
Python如何使用DrissionPage做自动化:简单入门指南
开发语言·python·自动化
珂朵莉MM16 小时前
2025年睿抗机器人开发者大赛CAIP-编程技能赛-高职组(国赛)解题报告 | 珂学家
java·开发语言·人工智能·算法·机器人
do better myself16 小时前
php 使用IP2Location限制指定的国家访问实现
开发语言·php
a努力。16 小时前
虾皮Java面试被问:JVM Native Memory Tracking追踪堆外内存泄漏
java·开发语言·jvm·后端·python·面试
Kratzdisteln16 小时前
【Python】Flask
开发语言·python·flask
古城小栈16 小时前
Rust 并发、异步,碾碎它们
开发语言·后端·rust
Evand J16 小时前
【MATLAB代码介绍】【空地协同】UAV辅助的UGV协同定位,无人机辅助地面无人车定位,带滤波,MATLAB
开发语言·matlab·无人机·协同·路径·多机器人