首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C语言】深入理解指针(一)

【C语言】深入理解指针(一)

作者头像
Yue丶越
发布2025-12-17 16:33:53
发布2025-12-17 16:33:53
1770
举报
文章被收录于专栏:C语言C语言

前言

指针是C语言的核心特性,也是初学者入门的难点。很多开发者在接触指针时会困惑:“为什么要使用指针?直接操作变量不行吗?”事实上,指针的本质是内存地址,理解指针就是理解计算机管理内存的底层逻辑。本文将从内存与地址的基础概念出发,逐步拆解指针的定义、类型意义、运算规则及实战用法,帮你彻底打通指针的“任督二脉”。


一、基础认知:内存和地址是什么?

在学习指针前,我们必须先搞清楚“内存”和“地址”的关系——这就像我们去找朋友需事先知道“楼栋号+房间号”,内存是数据的“居住空间”,地址则是数据的“门牌号”。

1.1 内存:数据的“楼栋”

计算机处理数据时,所有待处理和已处理的数据都存储在内存中。为了高效管理,内存被划分为一个个大小相等的“房间”——每个“房间”即1个字节(byte),可存储8个二进制位(bit,值为0或1)。我们常说的“8GB/16GB内存”,就是指内存的总容量,其单位换算规则如下:

  • 1byte = 8bit
  • 1KB = 1024byte
  • 1MB = 1024KB
  • 1GB = 1024MB
  • 1TB = 1024GB
  • 1PB = 1024TB

可以这样类比:1个字节的内存单元就像“八人间宿舍”,每个床位对应1个bit,8个床位共同组成1个“宿舍”(字节)。

1.2 地址:内存的“门牌号”

如果“楼栋”没有门牌号,寻找数据则需要逐个“敲门”,效率极低。因此,每个内存单元(字节)都有唯一的“门牌号”——这就是地址。计算机通过“地址总线”识别地址:

  • 32位机器有32根地址总线,可表示2^32个地址(对应4GB内存);
  • 64位机器有64根地址总线,可表示2^64个地址 。

在C语言中,“地址”又被称为“指针”,三者本质相同:

内存单元编号 = 地址 = 指针


二、指针变量:存储地址的“容器”

我们了解了“地址=指针”,接下来要解决的问题是:地址该存放在哪里答案是指针变量

2.1 取地址符&:获取变量的“门牌号”

在C语言中,定义变量的本质是向内存申请空间。例如int a = 10,会申请4个连续字节的内存(int类型占4字节)存储数值10。若要获取这4个字节的首地址,需使用取地址符&

示例代码如下:

代码语言:javascript
复制
#include <stdio.h>
int main() 
{
    int a = 10;
    printf("变量a的地址:%p\n", &a); // %p为地址的格式化输出符
    return 0;
}

运行后可能输出006FFD70(地址为随机值,每次运行可能不同),这个值就是变量a的指针 、。

2.2 指针变量的定义:存放地址的“盒子”

指针变量专门用于存储地址,其定义格式为:

数据类型* 指针变量名 = &目标变量

例如,要存储int变量a的地址,需定义int* pa = &a,其中:

  • *表示“该变量是指针变量”;
  • int表示“指针指向的变量是int类型”;
  • pa是指针变量名,存储a的地址 、。

示例代码:

代码语言:javascript
复制
#include <stdio.h>
int main() {
    int a = 10;
    int* pa = &a; // 指针变量pa存储a的地址
    printf("pa的值(a的地址):%p\n", pa);  // 输出a的地址
    printf("&a的值(a的地址):%p\n", &a); // 与pa的值完全相同
    return 0;
}
2.3 解引用符*:通过“门牌号”操作数据

拿到地址(指针)后,如何操作其指向的变量?这就需要解引用符*——通过指针找到对应的内存单元,相当于“用门牌号打开宿舍门”。

示例代码:

代码语言:javascript
复制
#include <stdio.h>
int main() {
    int a = 10;
    int* pa = &a;
    
    *pa = 20; // 解引用:通过pa的地址修改a的值
    printf("a的值:%d\n", a); // 输出20,a被成功修改
    return 0;
}

这里的*pa等同于变量a,修改*pa本质是直接操作内存中的a 。

2.4 指针变量的大小:只与平台相关

初学者常问:“int*char*的大小是否相同?”答案是:同一平台下,所有指针变量的大小均相同

因为指针变量存储的是地址,而地址的长度由平台决定:

  • 32位平台:地址为32位(4字节),故指针变量大小为4字节;
  • 64位平台:地址为64位(8字节),故指针变量大小为8字节 。

验证代码:

代码语言:javascript
复制
#include <stdio.h>
int main() {
    // 所有指针变量大小相同
    printf("char* 大小:%zd\n", sizeof(char*));  // 32位输出4,64位输出8
    printf("int* 大小:%zd\n", sizeof(int*));    // 同上
    printf("double* 大小:%zd\n", sizeof(double*));// 同上
    return 0;
}

三、指针类型的意义:不是“多余”,而是“权限控制”

既然所有指针变量大小相同,为何还要区分int*char*double*?因为指针类型决定了两种核心“权限”

3.1 权限1:解引用时操作的字节数

指针类型决定了解引用时能“操作多少字节的内存”:

  • char*解引用:操作1字节(char类型占1字节);
  • int*解引用:操作4字节(int类型占4字节);
  • double*解引用:操作8字节(double类型占8字节) 、。

对比代码如下:

代码语言:javascript
复制
// 代码1:int* 解引用(操作4字节)
#include <stdio.h>
int main() {
    int n = 0x11223344; // 4字节十六进制数据
    int* pi = &n;
    *pi = 0; // 4字节全部改为0
    printf("n = 0x%x\n", n); // 输出0x00000000
    return 0;
}

// 代码2:char* 解引用(操作1字节)
#include <stdio.h>
int main() {
    int n = 0x11223344;
    char* pc = (char*)&n; // 强制转换为char*
    *pc = 0; // 仅第1字节改为0
    printf("n = 0x%x\n", n); // 输出0x11223300
    return 0;
}
3.2 权限2:指针±整数时的“步长”

指针±整数时,跳过的字节数(即“步长”)由指针类型决定:

  • char*±1:跳1字节(char类型大小);
  • int*±1:跳4字节(int类型大小);
  • double*±1:跳8字节(double类型大小) 。

示例代码:

代码语言:javascript
复制
#include <stdio.h>
int main() {
    int n = 10;
    char* pc = (char*)&n;
    int* pi = &n;
    
    printf("&n = %p\n", &n);    // 假设输出00AFF974
    printf("pc = %p\n", pc);    // 输出00AFF974(与&n相同)
    printf("pc+1 = %p\n", pc+1); // 输出00AFF975(跳1字节)
    printf("pi = %p\n", pi);    // 输出00AFF974
    printf("pi+1 = %p\n", pi+1); // 输出00AFF978(跳4字节)
    return 0;
}

这一特性对数组遍历至关重要——数组在内存中连续存储,int arr[10]中,arr+1即下一个元素的地址。

3.3 特殊指针:void*泛型指针

void*是“无具体类型的指针”(泛型指针),可接收任意类型的地址,但存在两个限制:

  1. 不能直接解引用(无法确定操作字节数);
  2. 不能直接±整数(无法确定步长) 。

void*常用于函数参数,实现“泛型编程”(如接收int、char、double的地址),示例如下:

代码语言:javascript
复制
#include <stdio.h>
int main() {
    int a = 10;
    char ch = 'a';
    void* p1 = &a; // 接收int*类型地址
    void* p2 = &ch; // 接收char*类型地址
    
    // *p1 = 20; // 错误:void*不能直接解引用
    // p1 += 1;  // 错误:void*不能直接±整数
    return 0;
}

四、const修饰指针:控制“修改权限”

实际开发中,我们常需要限制指针的修改(如保护数据不被篡改),此时需用const修饰指针。核心规则是:

const在*左侧,限制“指针指向的内容”;const在*右侧,限制“指针本身”

四种const修饰场景对比

指针类型

能否修改指向的内容(*p)

能否修改指针本身(p)

适用场景

int* p

普通指针,无限制

const int* p(或int const* p)

不能

保护数据不被篡改

int* const p

不能

指针固定指向某变量

const int* const p

不能

不能

数据和指针均不可修改

验证代码:

代码语言:javascript
复制
#include <stdio.h>
void test() {
    int a = 10, b = 20;
    
    // 1. const在*左:不能改*p,能改p
    const int* p1 = &a;
    // *p1 = 20; // 错误:无法修改指向的内容
    p1 = &b;     // 正确:可修改指针指向
    
    // 2. const在*右:能改*p,不能改p
    int* const p2 = &a;
    *p2 = 20;    // 正确:可修改指向的内容
    // p2 = &b;     // 错误:无法修改指针指向
    
    // 3. 两边都有const:均不能改
    const int* const p3 = &a;
    // *p3 = 20; // 错误
    // p3 = &b;  // 错误
}
int main() { 
   test();
   return 0;
 }

五、指针运算:三种核心操作

指针支持三种运算:±整数、指针-指针、关系运算,且均需基于“指向同一块连续内存”(如同一数组) 。

5.1 指针±整数:遍历连续内存

最常用场景是数组遍历——通过p+i访问数组第i个元素(等同于arr[i]):

代码语言:javascript
复制
#include <stdio.h>
int main() {
    int arr[5] = {1,2,3,4,5};
    int* p = arr; // 数组名arr即首元素地址(&arr[0])
    int i = 0;
    for(i=0; i<5; i++) {
        printf("arr[%d] = %d\n", i, *(p+i)); // 等同于arr[i]
    }
    return 0;
}

运行后输出数组所有元素,实现高效遍历 、。

5.2 指针-指针:计算元素个数

两个指针相减,结果为“两指针之间的元素个数”(非字节数),前提是指向同一块连续内存。典型场景是模拟实现strlen函数(统计字符串长度):

代码语言:javascript
复制
#include <stdio.h>
int my_strlen(char* s) {
    char* p = s;
    while(*p != '\0') { // 遍历至字符串结束符'\0'
        p++;
    }
    return p - s; // 指针-指针,返回字符个数
}
int main() {
    printf("字符串长度:%d\n", my_strlen("abcdef")); // 输出6
    return 0;
}

该代码通过指针移动计算字符串中\0前的字符数,与库函数strlen功能一致 、。

5.3 指针的关系运算:比较地址大小

指针可通过<><=>=比较地址大小,常用于遍历数组时的边界判断:

代码语言:javascript
复制
#include <stdio.h>
int main() {
    int arr[5] = {1,2,3,4,5};
    int* p = arr;
    while(p < arr + 5) { // 指针比较:未超出数组范围
        printf("%d ", *p);
        p++;
    }
    return 0; // 输出1 2 3 4 5
}

这里arr+5是数组末元素地址的下一位,通过p < arr+5确保不越界 、。


六、野指针:C语言中的“隐形炸弹”

野指针是指“指向未知内存的指针”,访问野指针会导致程序崩溃(内存越界)。必须明确其成因并规避 、。

6.1 野指针的三大成因

指针未初始化:局部指针变量默认值为随机值,直接解引用会访问未知内存。

代码语言:javascript
复制
#include <stdio.h>
int main() {
    int* p; // 未初始化,p值随机
    *p = 10; // 危险:野指针解引用
    return 0;
}

指针越界访问:指针超出目标内存范围(如数组下标越界)。

代码语言:javascript
复制
#include <stdio.h>
int main() {
    int arr[5] = {0};
    int* p = &arr[0];
    for(int i=0; i<=5; i++) { // i=5时越界
        *(p++) = i; // 越界后p成为野指针
    }
    return 0;
}

指针指向的空间已释放:局部变量在函数结束后销毁,返回其地址会导致野指针。

代码语言:javascript
复制
#include <stdio.h>
int* test() {
    int n = 10; // 局部变量,函数结束后销毁
    return &n; // 返回销毁后的地址,成为野指针
}
int main() {
    int* p = test();
    *p = 20; // 危险:访问已释放内存
    return 0;
}
6.2 规避野指针的四大方法

指针初始化:确定指向时直接赋值地址;不确定时赋值NULL(C语言定义的空指针,值为0,不可访问)。

代码语言:javascript
复制
int num = 10;
int* p1 = &num; // 确定指向,赋值地址
int* p2 = NULL; // 不确定指向,赋值NULL

小心指针越界:仅访问程序申请的内存空间,不超出范围。

指针不用时置为NULL,使用前检查有效性:不再使用的指针置为NULL,使用前通过if(p != NULL)判断。

代码语言:javascript
复制
int arr[5] = {1,2,3,4,5};
int* p = &arr[0];
// 遍历后p越界,置为NULL
p = NULL;
// 使用前检查
if(p != NULL) {
    *p = 10; // 不会执行,避免野指针访问
}

避免返回局部变量的地址:局部变量在函数结束后销毁,其地址无意义 。


七、assert断言:调试时的“安全网”

assert是C语言的调试工具,用于运行时检查“条件是否成立”——若不成立,程序报错并终止,帮开发者快速定位问题 。

7.1 assert的用法
  • 头文件:#include <assert.h>
  • 语法:
  • assert(条件表达式)
  • 逻辑:条件为真则继续运行;为假则报错(显示文件名、行号、未通过的表达式)。

示例代码(检查指针非空):

代码语言:javascript
复制
#include <stdio.h>
#include <assert.h>
void print(int* p) {
    assert(p != NULL); // 断言p非空,否则报错
    printf("值:%d\n", *p);
}
int main() {
    int a = 10;
    int* p1 = &a;
    int* p2 = NULL;
    
    print(p1); // 条件成立,正常输出10
    print(p2); // 条件不成立,报错终止
    return 0;
}
7.2 禁用assert:Release版本优化

assert仅在"“调试”“(Debug)时有用,”“发布”"(Release)版本中会增加运行开销。可通过定义NDEBUG禁用assert:

代码语言:javascript
复制
#define NDEBUG // 定义后,assert失效
#include <assert.h>

VS、GCC等编译器在Release模式下会自动禁用assert,无需手动定义 。


八、指针实战:传址调用 vs 传值调用

指针的核心用途之一是传址调用——让函数直接修改外部变量的值。与之对应的传值调用仅能操作变量的“拷贝”,无法修改原变量 。

8.1 反例:传值调用无法修改外部变量

以“交换两个整数”为例,传值调用会失败:

代码语言:javascript
复制
#include <stdio.h>
// 传值调用:x、y是a、b的拷贝
void Swap1(int x, int y) {
    int tmp = x;
    x = y;
    y = tmp; // 仅修改x、y,未修改a、b
}
int main() {
    int a = 10, b = 20;
    Swap1(a, b);
    printf("a=%d, b=%d\n", a, b); // 输出a=10, b=20,未交换
    return 0;
}

原因:函数参数x、y是a、b的临时拷贝,修改拷贝不会影响原变量 。

8.2 正例:传址调用修改外部变量

通过指针传递变量地址,函数通过解引用修改原变量:

代码语言:javascript
复制
#include <stdio.h>
// 传址调用:px、py是a、b的地址
void Swap2(int* px, int* py) {
    int tmp = *px;
    *px = *py; // 通过地址修改a
    *py = tmp; // 通过地址修改b
}
int main() {
    int a = 10, b = 20;
    Swap2(&a, &b); // 传递a、b的地址
    printf("a=%d, b=%d\n", a, b); // 输出a=20, b=10,交换成功
    return 0;
}

传址调用让函数与主调函数建立“直接联系”,可修改主调函数的变量,是指针的核心实战场景 。


指针虽难,但只要结合内存逻辑和代码调试,逐步理解其底层原理,就能掌握这一C语言的“灵魂”特性。后续我们还会继续深入讲解指针与数组、函数的结合用法,敬请关注!

以上就是本期博客的全部内容了,感谢各位的阅读以及关注。如有内容存在疏漏或不足之处,恳请各位技术大佬不吝赐教、多多指正。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、基础认知:内存和地址是什么?
    • 1.1 内存:数据的“楼栋”
    • 1.2 地址:内存的“门牌号”
  • 二、指针变量:存储地址的“容器”
    • 2.1 取地址符&:获取变量的“门牌号”
    • 2.2 指针变量的定义:存放地址的“盒子”
    • 2.3 解引用符*:通过“门牌号”操作数据
    • 2.4 指针变量的大小:只与平台相关
  • 三、指针类型的意义:不是“多余”,而是“权限控制”
    • 3.1 权限1:解引用时操作的字节数
    • 3.2 权限2:指针±整数时的“步长”
    • 3.3 特殊指针:void*泛型指针
  • 四、const修饰指针:控制“修改权限”
    • 四种const修饰场景对比
  • 五、指针运算:三种核心操作
    • 5.1 指针±整数:遍历连续内存
    • 5.2 指针-指针:计算元素个数
    • 5.3 指针的关系运算:比较地址大小
  • 六、野指针:C语言中的“隐形炸弹”
    • 6.1 野指针的三大成因
    • 6.2 规避野指针的四大方法
  • 七、assert断言:调试时的“安全网”
    • 7.1 assert的用法
    • 7.2 禁用assert:Release版本优化
  • 八、指针实战:传址调用 vs 传值调用
    • 8.1 反例:传值调用无法修改外部变量
    • 8.2 正例:传址调用修改外部变量
    • 以上就是本期博客的全部内容了,感谢各位的阅读以及关注。如有内容存在疏漏或不足之处,恳请各位技术大佬不吝赐教、多多指正。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档