当前位置:首页 > 随笔 > 正文内容

C语言深入解析:指针的核心机制

访客 随笔 2026年7月14日 1

一、指针变量基础

1. 指针变量的大小

指针变量的尺寸取决于系统架构:在32位平台中,指针占用4字节(即32个比特位用于表示内存地址),而在64位平台中,指针占用8字节

2. 指针的本质

在C语言中,指针就是地址——每个内存单元(1字节)都有一个唯一的编号,这个编号就是地址,也就是指针。要理解指针,需要先了解内存的构造:每个计算机都有内存,用于临时存放CPU的运算数据或与外部存储设备交换数据。内存被划分为许多内存单元,每个单元大小为1字节,并对每个单元分配唯一的编号。

二、内存与地址编号

1. 内存是什么

内存是计算机的核心组件,常用于存储正在运行的程序的指令和数据。一个系统(如4GB、8GB或16GB)拥有大量内存字节,每个字节都被赋予唯一的编号,这个编号在C语言中被称为指针

总结公式:编号 == 地址 == 指针

2. 地址编号原理

计算机通过地址线(32位系统有32根,64位系统有64根)进行内存编号。以32位系统为例:通电状态下,每根电线可以是高电平或低电平,由此产生2^32种二进制组合,对应2^32个内存单元的地址。

但要注意:内存的地址并非一成不变。内存被划分为栈区、堆区和静态区三个区域。当程序创建变量时,系统在对应区域分配空间,并为每个字节分配地址。例如,定义一个 int a = 0;(占用4字节),系统会在栈区分配4个连续的内存单元,变量a的地址是这四个单元的首地址。

三、指针变量详解

1. 定义与用法

通过取地址操作符 & 可以获取变量的起始地址,并将其存储在一个指针变量中:

#include <stdio.h>
int main() {
    int a = 10;                // 在内存中分配空间
    int *p = &a;              // p存储变量a的首地址(即4个字节中的第一个字节的地址)
    return 0;
}

注意:在32位机器上,地址由32个二进制位组成,因此指针变量需要4字节来存储;在64位机器上,指针变量需要8字节。

2. 指针类型

指针变量有特定类型,定义格式为 type *,例如:

  • int *p; —— 存放整型变量的地址
  • char *p; —— 存放字符型变量的地址
  • float *p; —— 存放浮点型变量的地址
  • double *p; —— 存放双精度变量的地址

尽管所有指针变量本身的大小在相同平台上一致(32位下4字节,64位下8字节),但不同指针类型的存在非常重要。

3. 多类型指针的意义

(1) 解引用访问字节数不同

指针类型决定了解引用时能读取的字节数。例如:

  • int * 类型:解引用时访问4个字节
  • char * 类型:解引用时访问1个字节

这类似于权限控制:你需要匹配变量类型来声明对应的指针,否则可能导致数据取不完整或越界。

(2) 指针运算步长不同

指针类型决定了指针进行加减运算时的步长。例如:

#include <stdio.h>
int main() {
    int n = 10;
    char *pc = (char*)&n;
    int *pi = &n;
    printf("n的地址: %p\n", &n);
    printf("pc: %p, pc+1: %p\n", pc, pc + 1);
    printf("pi: %p, pi+1: %p\n", pi, pi + 1);
    return 0;
}

输出显示:pc+1pc 增加1字节,而 pi+1pi 增加4字节。规则:指针类型为 T * 时,指针 + n 代表 地址 + n * sizeof(T)

四、野指针问题

1. 成因

野指针是指向未知或不合法位置的指针,其产生原因包括:

  • 未初始化:局部指针变量未初始化时,其值是随机的。
  • 越界访问:指针指向数组范围之外。
  • 释放指向空间的指针:函数返回局部变量的地址,但该变量在函数结束后已被释放。
// 示例:返回局部变量地址造成野指针
int* test() {
    int a = 10;
    return &a;   // 错误:函数结束后 a 空间被释放
}
int main() {
    int *p = test();
    *p = 100;    // 危险操作:访问已释放的内存
    return 0;
}

2. 如何规避

  • 指针声明后立即初始化,若暂时无需指向任何变量,设为 NULL
  • 避免指针越界:确保指针始终在有效范围内。
  • 当指针指向的空间被释放时,立即将指针置为 NULL
  • 不要返回局部变量的地址。
  • 使用前检查指针有效性:if (p != NULL) { ... }

五、指针运算

1. 指针与整数的加减

指针加减整数时,步长由指针类型决定。例如,字符指针加1移动1字节,整型指针加1移动4字节。以下通过模拟 strlen 演示:

#include <stdio.h>
int my_strlen(char *str) {
    int count = 0;
    while (*str != '\0') {
        count++;
        str++;   // char*指针移动1字节
    }
    return count;
}
int main() {
    int len = my_strlen("hello");
    printf("%d\n", len);
    return 0;
}

2. 指针相减

指针相减的前提是:两个指针指向同一块连续内存区域,且类型相同。结果表示两个指针之间的元素个数(以指针类型为单位)。

// 模拟strlen的另一种方式
#include <stdio.h>
int my_strlen(char *str) {
    char *start = str;
    while (*str != '\0') {
        str++;
    }
    return str - start;   // 返回字符个数
}

3. 关系运算

C标准允许将数组元素指针与指向数组最后一个元素之后位置的指针进行比较,但不允许与指向第一个元素之前位置的指针进行比较。例如:

// 推荐写法
for (int *vp = &values[N_VALUES]; vp > &values[0]; ) {
    *--vp = 0;
}
// 不推荐写法:标准不保证 vp >= &values[0] 的合法性
for (int *vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--) {
    *vp = 0;
}

六、指针与数组

数组名代表数组首元素的地址,但要注意两个例外:

  • sizeof(数组名):返回整个数组的大小(单位字节)。
  • &数组名:返回整个数组的地址(而非首元素地址)。

除此之外,数组名可视为指针。下面示例演示指针访问数组:

#include <stdio.h>
int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    int *p = arr;   // 指向数组首元素
    for (int i = 0; i < 10; i++) {
        printf("&arr[%d] = %p, p+%d = %p\n", i, &arr[i], i, p + i);
    }
    return 0;
}

七、二级指针

二级指针指向一级指针的地址。一级指针存储变量的地址,二级指针存储一级指针的地址。

#include <stdio.h>
int main() {
    int a = 10;
    int *pa = &a;       // 一级指针
    int **ppa = &pa;    // 二级指针:ppa存储pa的地址
    return 0;
}

八、指针数组

指针数组是一个数组,其元素均为指针类型。

#include <stdio.h>
int main() {
    int a = 1, b = 2, c = 3;
    int *arr[] = {&a, &b, &c};   // 存储三个整型变量的地址
    for (int i = 0; i < 3; i++) {
        printf("%d ", *arr[i]);
    }
    return 0;
}

可以利用指针数组模拟二维数组:

#include <stdio.h>
int main() {
    int a[] = {1, 2, 3};
    int b[] = {4, 5, 6};
    int c[] = {7, 8, 9};
    int *arr[] = {a, b, c};
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr[i][j]);   // 使用二维下标访问
        }
        printf("\n");
    }
    return 0;
}

九、指针与const限定符

  • 指针本身为constint *const q = &a;;指针q一旦初始化后,不能修改其指向(即不能更改q的值),但可以通过q修改a的值。
  • 所指对象为constconst int *p = &a;;不能通过 *p 修改a的值,但p可以指向其他变量,a的值也可以通过其他方式修改。

两种写法的区分可参照 const* 的位置关系:const int *p 等同于 int const *p,都表示指针所指为常量;int *const p 表示指针本身为常量。

标签: C语言

相关文章

可以按小时收费的VPS

很多 VPS 提供商都支持 按小时计费(hourly billing),想短期试用 / 临时搭建节点、测试网络、短期项目等场景非常合适。下面是当前最主流且靠谱的按小时 VPS 选项,分别按不同需求场景整理: 1. Vultr(全球节点,包括日本) 按小时计费 可选机房:东京 / 大阪 / 洛杉矶 / 法兰克福 / 伦敦 … 支持 PayPal(部分情况),但更常用信用卡/PayPal+卡价格参考$...

在 iPhone 上下载国外App

地区/国家限制App Store 会根据 Apple ID 的国家或地区限制应用下载。如果你的 Apple ID 绑定的是中国大陆,就可能无法下载 OpenAI 官方的 ChatGPT 应用,因为它在大陆 App Store 不上架。解决办法:换成美国、加拿大、香港等地区的 Apple ID。或者在现有 Apple ID 上更改地区。注册一个国外 Apple ID(推荐)比如注册 美国区 Appl...

Node.js 中的异步编程:回调与 Promise

Node.js 是一个基于 JavaScript 构建的单线程、非阻塞运行环境,它通过异步编程机制来高效处理多个操作。在执行如文件读取、API 请求或数据库查询等任务时,Node.js 不会等待这些操作完成,而是使用回调函数和 Promise 来避免阻塞主线程。 回调方式实现异步 那么当异步操作完成后,Node.js 如何知道接下来要做什么呢?这就要用到 回调函数(callback)。 回调本质上...

Selenium自动化测试入门指南

Selenium自动化测试入门指南

什么是自动化测试? 自动化测试是指利用软件工具自动执行测试用例,模拟用户操作,如打开网页、点击链接、输入文本等,并验证结果是否符合预期。 其主要优点包括: 大幅减少人工成本 测试速度快 可以在非工作时间运行 支持持续集成和交付 然而,它也存在一些局限性,例如开发成本较高、不适合快速变化的项目、依赖稳定的UI界面等。 自动化测试的应用条件 适合引入自动化测试的情况包括: 手动测试耗时且需要大量...

MariaDB Galera集群故障快速恢复指南

OpenStack控制节点采用三节点MariaDB Galera集群架构。当数据库集群因故障重启时,有时会出现Galera集群无法正常启动的问题。虽然有多种方法可以恢复数据库服务,但如何实现快速启动同时确保数据完整性呢? 通过分析日志发现,MariaDB Galera集群节点宕机时会在日志中输出以下信息: [Note] WSREP: 新集群视图:全局状态: 874d8e7e-5980-11e8-8...

Android 中 EventBus 的通信机制与实现原理深度解析

EventBus 核心设计思想 EventBus 是一个基于观察者模式的事件总线框架,广泛应用于 Android 平台以实现组件解耦。它通过中心化的消息分发机制,使不同层级、不同线程的对象能够以"发布-订阅"方式通信,避免了传统接口回调或广播带来的强依赖问题。 核心角色说明 事件(Event):任意 Java 对象,作为数据载体,如网络状态变更通知、用户登录信息等。 发布者(Publi...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。