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

C语言基础:(十一)深入理解指针(1)

作者头像
_OP_CHEN
发布2026-01-14 09:20:51
发布2026-01-14 09:20:51
650
举报
文章被收录于专栏:C++C++

        指针是C语言中最强大且最具挑战性的特性之一,它直接操作内存地址,赋予程序员对数据存储和访问的精准控制能力。无论是动态内存分配、函数参数传递,还是复杂数据结构的构建,指针都扮演着不可替代的角色。然而,其灵活的语法和潜在的陷阱也让许多初学者望而生畏。理解指针的本质,不仅能提升代码效率,还能深入掌握计算机底层运作机制。本博客将分为多期,从多个角度对C语言中的指针展开详细分析,旨在帮助大家更好地学习和掌握指针。接下来就让我们正式开始指针的学习吧!

一、内存和地址

1.1  内存

        在讲解内存和地址之前,我们可以先引入一个生活中的案例:

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

代码语言:javascript
复制
⼀楼:101,102,103...
⼆楼:201,202,203...
...

        有了房间号之后,如果你的朋友知道了房间号,就可以通过房间号快速的找到你的房间。

        同理,我们也可以把上面的例子对照到计算机中,又是怎么样的呢?

        我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理之后的数据也会放回到内存中。那么这些内存空间是如何高效的管理的呢?

        其实在计算机内部是把内存划分为一个个的内存单元,每个内存单元的大小取一个字节

        在计算机中常见的内存单位如下:

代码语言:javascript
复制
1Byte = 8bit
1KB = 1024Byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB

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

        其中,每一个内存单元其实就相当于一个学生宿舍,在一个字节的内存空间中能存放8个比特位,就好比学生住的一个八人间,每个人是一个比特位。

        每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。

        在生活中我们把门牌号也可以称作为地址,在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了一个新的名字就叫做:指针

        所以我们可以简单理解为:内存单元的编号 == 地址 == 指针

1.2  如何理解编址

CPU访问内存中的某个字节空间时,必须知道这个字节空间在内存中的什么位置,而因为内存中字节有非常多,所以需要给内存进行编址(就如同宿舍很多,需要给宿舍编号一样)。

        计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件上的设计完成的。

        首先我们必须理解,计算机内有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据的传递。

        但是硬件与硬件之间又是相互独立的,那么它们应该如何进行通信呢?答案很简单,用“线”连起来。而CPU与内存之间也存在着大量的数据交互,所以两者也必须用线相连。不过我们今天只需要关心一组线,叫做地址总线。如下图所示:

        我们可以简单理解,32位机器有32根地址总线,每根线都只有两态,表示0和1(即电脉冲无和有),那么一根线就能表示2种含义,两根线就能表示4种含义,依此类推。32根地址线就能够表示2^32种含义,每一种含义都代表一个地址。

        地址信息被下达给内存,在内存上就可以找到该地址对应的数据,将数据再通过数据总线传入CPU内的寄存器中。

二、指针变量和地址

2.1  取地址操作符

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

代码语言:javascript
复制
#include <stdio.h>
int main()
{
    int a = 10;
    return 0;
}

        比如,上述的代码就是创建了整型变量a,内存中申请4个字节,用于存放整数10,其中每个字节都有地址,上图中4个字节的地址分别是:

代码语言:javascript
复制
0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73

        那我们如何能够得到a的地址呢?在这里我们学习一个操作符 --- & 取地址操作符:

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

        下面我给大家画一个示意图,来表示变量在内存中的存储:

        按照如上的例子,程序会打印处理:006FFD70

        &a取出的是a所占4个字节中地址较小的字节的地址。

        虽然整型变量占用了4个字节,我们只要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。

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

2.2.1  指针变量

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

        比如:

代码语言:javascript
复制
#include <stdio.h>
int main()
{
    int a = 10;
    int * pa = &a;//取出a的地址并存储到指针变量pa中
    return 0;
}

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

2.2.2  如何拆解指针类型

        我们看到上述代码中pa的类型是 int* ,我们该如何理解指针的类型呢?先看下面的示例:

代码语言:javascript
复制
int a = 10;
int * pa = &a;

        这里pa左边写的是 int* ,* 是在说明pa是指针变量,而前面的 int 是在说明pa指向的是整型(int)类型的对象。

        那如果有一个char类型的变量ch,ch的地址要放在什么类型的指针变量中呢?

代码语言:javascript
复制
char ch = 'w';
pc = &ch;//pc 的类型怎么写呢?
2.2.3  解引用操作符

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

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

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

代码语言:javascript
复制
#include <stdio.h>
int main()
{
    int a = 100;
    int* pa = &a;
    *pa = 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根地址线产生的二进制序列当作一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。

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

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

代码语言:javascript
复制
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)、

int main()
{
    printf("%zd\n", sizeof(char *));
    printf("%zd\n", sizeof(short *));
    printf("%zd\n", sizeof(int *));
    printf("%zd\n", sizeof(double *));
    return 0;
}

        输出结果如下:(左图为x86环境,右图为x64环境)

        结论:

  • 32位平台下地址是32个bit位,指针变量大小是4个字节
  • 64位平台下地址是64个bit位,指针变量大小是8个字节
  • 需要注意,指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的

三、指针变量类型的意义

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

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

3.1  指针的解引用

        下面让我们对比下面两段代码,在调试时观察内存的变化:

代码语言:javascript
复制
//代码1
#include <stdio.h>

int main()
{
    int n = 0x11223344;
    int *pi = &n;
    *pa = 0;
    return 0;
}

//代码2
int main()
{
    int n = 0x11223344;
    char *pc = (char *)&n;
    *pa = 0;
    return 0;
}

        用VS2022观察内存的变化如下图所示:

        我们可以观察到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第一个字节改为0。

        因此我们可以得出结论:指针的类型决定了对指针解引用的时候有多大的权限(一次能操作几个字节)。

        比如:char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问4个字节。

3.2  指针 + - 整数

        我们先来观察下面这段代码,调试并观察地址的变化:

代码语言:javascript
复制
#include <stdio.h>
int main()
{
    int n = 10;
    char *pc = (char*)&n;
    int *pi = &n;

    printf("%p\n", &n);
    printf("%p\n", pc);
    printf("%p\n", pc+1);
    printf("%p\n", pi);
    printf("%p\n", pi+1);
    return 0;
}

        代码的运行结果如下:

        我们可以看出,char* 类型的指针变量 +1 就是跳过一个字节,而 int* 类型的指针变量 +1 则跳过了4个字节。这就是指针变量的类型差异所带来的变化。指针 +1 ,其实就是跳过1个指针指向的元素。指针可以+1,也可以-1.

        因此我们可以得出结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。

3.3  void* 指针

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

        例如:

代码语言:javascript
复制
#include <stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    char* pc = &a;
    return 0;
}

        在上面的代码中,我们将一个int类型的变量的地址赋值给了一个char*类型的指针变量。编译器这时给出了一个警告(如下图所示),是因为类型不兼容。而如果我们使用void*类型的指针就不会存在这样的问题。

        使用void*:

代码语言:javascript
复制
#include <stdio.h>
int main()
{
    int a = 10;
    void* pa = &a;
    void* pc = &a;

    *pa = 10;
    *pc = 0;
    return 0;
}

        用VS编译代码结果如下:

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

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

        一般来说,void* 类型的指针是使用在函数参数的部分,用于接受不同类型的数据的地址,这样的设计就可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。

四、指针运算

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

  • 指针 +- 整数
  • 指针 - 指针
  • 指针的关系运算

4.1  指针 +- 整数

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

代码语言:javascript
复制
int arr[10] = {1,2,3,4,5,6,7,8,9,10};

        下面我们来看一段打印数组中元素的代码:

代码语言:javascript
复制
#include <stdio.h>
//指针+- 整数
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = &arr[0];
    int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);
    for(i=0; i<sz; i++)
    {
        printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
    }
    return 0;
}

        首先我们需要明确一个定论:数组名其实就是数组首元素的地址

        对于数组p而言,p就表示数组中首元素的地址,也就是一个指向数组中首元素的指针,在这为 int* 类型。因此 p+1 就表示p指针向后移1位之后所指向的元素地址,同理也就可以知道 p+i 就表示p指针向后移i位之后所指向元素的地址,即数组中下标为i的元素的地址,*(p+i)就是下标为i的这个元素。

4.2  指针 - 指针

        前提:两个指针指向的是同一块空间,否则不能相减。代码如下:

代码语言:javascript
复制
//指针-指针
#include <stdio.h>
int my_strlen(char *s)
{
    char *p = s;
    while(*p != '\0' )
    p++;
    return p-s;
} 

int main()
{
    printf("%d\n", my_strlen("abc"));
    return 0;
}

4.3  指针的关系运算

代码语言:javascript
复制
//指针的关系运算
#include <stdio.h>

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    //打印数组的内容
    int *p = &arr[0];
    int sz = sizeof(arr)/sizeof(arr[0]);
    while(p < arr + sz) //指针的⼤⼩⽐较
    {
        printf("%d ", *p);
        p++;
    } 
    return 0;
}

        本期初步为大家介绍了C语言指针中的相关概念,对于指针的进一步拓展我会在接下来的博客中为大家更新哦~敬请关注!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-14,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、内存和地址
    • 1.1  内存
    • 1.2  如何理解编址
  • 二、指针变量和地址
    • 2.1  取地址操作符
    • 2.2  指针变量和解引用操作符(*)
      • 2.2.1  指针变量
      • 2.2.2  如何拆解指针类型
      • 2.2.3  解引用操作符
    • 2.3  指针变量的大小
  • 三、指针变量类型的意义
    • 3.1  指针的解引用
    • 3.2  指针 + - 整数
    • 3.3  void* 指针
  • 四、指针运算
    • 4.1  指针 +- 整数
    • 4.2  指针 - 指针
    • 4.3  指针的关系运算
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档