指针是C语言中最强大且最具挑战性的特性之一,它直接操作内存地址,赋予程序员对数据存储和访问的精准控制能力。无论是动态内存分配、函数参数传递,还是复杂数据结构的构建,指针都扮演着不可替代的角色。然而,其灵活的语法和潜在的陷阱也让许多初学者望而生畏。理解指针的本质,不仅能提升代码效率,还能深入掌握计算机底层运作机制。本博客将分为多期,从多个角度对C语言中的指针展开详细分析,旨在帮助大家更好地学习和掌握指针。接下来就让我们正式开始指针的学习吧!
在讲解内存和地址之前,我们可以先引入一个生活中的案例:
假设有一栋宿舍楼,把你放在这栋楼里,楼上有100个房间,但是房间却没有编号。你的一个朋友来找你玩,如果想找到你,就必须得一个个房间挨个去找,这样效率是很低的。但如果我们根据楼层和楼层房价的情况来给每个房间都编上号,如:
⼀楼:101,102,103...
⼆楼:201,202,203...
...有了房间号之后,如果你的朋友知道了房间号,就可以通过房间号快速的找到你的房间。


同理,我们也可以把上面的例子对照到计算机中,又是怎么样的呢?
我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理之后的数据也会放回到内存中。那么这些内存空间是如何高效的管理的呢?
其实在计算机内部是把内存划分为一个个的内存单元,每个内存单元的大小取一个字节。
在计算机中常见的内存单位如下:
1Byte = 8bit
1KB = 1024Byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
//一个比特位可以储存一个2进制的位1或者0
其中,每一个内存单元其实就相当于一个学生宿舍,在一个字节的内存空间中能存放8个比特位,就好比学生住的一个八人间,每个人是一个比特位。
每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。
在生活中我们把门牌号也可以称作为地址,在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了一个新的名字就叫做:指针。
所以我们可以简单理解为:内存单元的编号 == 地址 == 指针
CPU访问内存中的某个字节空间时,必须知道这个字节空间在内存中的什么位置,而因为内存中字节有非常多,所以需要给内存进行编址(就如同宿舍很多,需要给宿舍编号一样)。
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件上的设计完成的。
首先我们必须理解,计算机内有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据的传递。
但是硬件与硬件之间又是相互独立的,那么它们应该如何进行通信呢?答案很简单,用“线”连起来。而CPU与内存之间也存在着大量的数据交互,所以两者也必须用线相连。不过我们今天只需要关心一组线,叫做地址总线。如下图所示:

我们可以简单理解,32位机器有32根地址总线,每根线都只有两态,表示0和1(即电脉冲无和有),那么一根线就能表示2种含义,两根线就能表示4种含义,依此类推。32根地址线就能够表示2^32种含义,每一种含义都代表一个地址。
地址信息被下达给内存,在内存上就可以找到该地址对应的数据,将数据再通过数据总线传入CPU内的寄存器中。
在理解了内存和地址的关系之后,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间,比如:
#include <stdio.h>
int main()
{
int a = 10;
return 0;
}
比如,上述的代码就是创建了整型变量a,内存中申请4个字节,用于存放整数10,其中每个字节都有地址,上图中4个字节的地址分别是:
0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73那我们如何能够得到a的地址呢?在这里我们学习一个操作符 --- & 取地址操作符:
#include <stdio.h>
int main()
{
int a = 10;
&a;//取出a的地址
printf("%p\n", &a);
return 0;
}下面我给大家画一个示意图,来表示变量在内存中的存储:

按照如上的例子,程序会打印处理:006FFD70
&a取出的是a所占4个字节中地址较小的字节的地址。
虽然整型变量占用了4个字节,我们只要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。
我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也是需要存储起来,方便后期再使用的,那么我们把这样的地址存放在哪里呢?答案是:指针变量中。
比如:
#include <stdio.h>
int main()
{
int a = 10;
int * pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
我们看到上述代码中pa的类型是 int* ,我们该如何理解指针的类型呢?先看下面的示例:
int a = 10;
int * pa = &a;这里pa左边写的是 int* ,* 是在说明pa是指针变量,而前面的 int 是在说明pa指向的是整型(int)类型的对象。

那如果有一个char类型的变量ch,ch的地址要放在什么类型的指针变量中呢?
char ch = 'w';
pc = &ch;//pc 的类型怎么写呢?我们将地址保存起来,未来是要使用的,那么怎么使用呢?
在现实生活中,我们使用地址要找到一个房间,在房间里可以拿去或者存放物品。
C语言中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫解引用操作符(*)。
#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的修改就多了一种新的途径,写代码就会更加的灵活,后期我们慢慢就能理解了。
在前面的内容中我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那么我们把32根地址线产生的二进制序列当作一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是用来存放地址的,那么指针变量的大小就得是4个字节的空间才可以。
同理,对于64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。
#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环境)


结论:
指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小的都是一样的,为什么还要有各种各样的指针类型呢?
其实指针类型是有特殊的意义的,我们接下来继续学习。
下面让我们对比下面两段代码,在调试时观察内存的变化:
//代码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个字节。
我们先来观察下面这段代码,调试并观察地址的变化:
#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.
因此我们可以得出结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
在指针类型中有一种特殊的类型是 void* 类型的,可以把它理解为无具体类型的指针(或者称其为泛型指针),这种类型的指针可以用来接受任意类型的地址。但是它也有局限性,void* 类型的指针不能直接进行指针的 +- 整数和解引用的运算。
例如:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}在上面的代码中,我们将一个int类型的变量的地址赋值给了一个char*类型的指针变量。编译器这时给出了一个警告(如下图所示),是因为类型不兼容。而如果我们使用void*类型的指针就不会存在这样的问题。

使用void*:
#include <stdio.h>
int main()
{
int a = 10;
void* pa = &a;
void* pc = &a;
*pa = 10;
*pc = 0;
return 0;
}用VS编译代码结果如下:

在这里我们可以看出,void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。
那么 void* 类型的指针到底有什么用呢?
一般来说,void* 类型的指针是使用在函数参数的部分,用于接受不同类型的数据的地址,这样的设计就可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。
指针的基本运算有三种,分别是:
因为数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能找到后面的所有元素。
int arr[10] = {1,2,3,4,5,6,7,8,9,10};下面我们来看一段打印数组中元素的代码:
#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的这个元素。
前提:两个指针指向的是同一块空间,否则不能相减。代码如下:
//指针-指针
#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;
}//指针的关系运算
#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语言指针中的相关概念,对于指针的进一步拓展我会在接下来的博客中为大家更新哦~敬请关注!