【C语言】万字速通初阶指针 zero → One

🚀write in front🚀


🔎大家好,我是謓泽,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
🏅2021年度博客之星物联网与嵌入式开发TOP5→周榜43→总榜2331🏅
🆔本文由 謓泽 原创 CSDN首发
🙉如需转载还请通知
📝个人主页:謓泽的博客_CSDN博客🎓
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​
📣系列专栏:【C】系列_打打酱油desu-CSDN博客🎓
✉️我们并非登上我们所选择的舞台,演出并非我们所选择的剧本📩


🍅目录🍅

🚀write in front🚀

💎 概念

🍁 前言 

⚔ 内存

🗡 地址与指针 

🗡 变量与指针 

⚔ 定义指针变量

💣有效声明指针

🗡使用指针

💣指针变量初始化 

💣赋值语句的方法

🗡指针变量的大小

⚔ 上述总结

💣 * 和 & 认识

💣 对解引用(*)认识

🗡 " * & 的应用" 

🗡 通过指针交换变量值 

🗡 指针变量的说明 

🗡 指针类型的意义(1)

🗡 指针类型的意义(2) 

⚔ 野指针  

🗡 如何规避野指针

💣 指针的未初始化

💣 指针越界访问

⚔ 空指针 - NULL

💣 指针使用之前检查有效性

🗡 指针运算 

💣 指针+- 整数

💣 指针 - 指针

💣 指针关系运算

💣 标准规定

⚔ 指针和数组

⚔ 二级指针 

⚔ 指针数组



💎 概念

指针是C语言的显著的优势之一其中使用起来是十分的灵活。而且是能够高效率的提高程序的使用,但是,如果使用不恰当的话程序是很容易被"挂死"的往往都是错误导致指针造成的。之所以C语言到现在还能够适应时代其中指针是不可或缺的 🎄

那再来说说指针的优缺点吧。

指针的优点→是标识一块内存。电脑内存上的每一个字节都具有一个编号,称为地址(可以简单理解为指针),任何读写内存的指令都必须携带地址信息,否则电脑不知道读写那块内存。不管程序是用什么语言写的,要运行数据和代码必须驻留内存,CPU要执行指令必须有一个"指针"程序计数器指向内存的代码块,如果某个指令要操作内存数据,该指令必须携带额外的地址信息 🌹

指针的缺点→指针可以操作任何东西,所以指针很灵活、很强大,但也引入了复杂性 🎋


🍁 前言 

指针!指针!指针!重要的事情说三遍,之所以这样说是因为指针对于我们学习C语言真的是特别特别的重要。可以说会指针和不会指针那就是天壤之别 🤐

你想要成为"C语言大佬"指针就必须玩的起来,这样就是你成为大佬的第一步,相比之前的内容,指针会难上一点,但只要肯下功夫,多多打磨、多去理解、多去上手练习,迟早你就能把指针玩开了 😋

有些初始C语言的小伙伴们,可能一遇到指针就会放弃或者对指针不够重视。千万不能有这样的想法,你想学习C语言到后面的话指针是你一定要跨越的"山峰"当你跨过这段"山峰"的时候到达顶端时候,你就会感慨值了",所以加油,干就完事了 😤

指针是C语言的显著的优势之一中使用是十分灵活的而且能提高某些程序的效率,但是如果使用不当则很容易造成系统错误。许多程序"挂死"往往都是错误地使用指针造成的 😱  


⚔ 内存

计算机当中所有的数据都是必须要放在内存当中的,不同类型的数据占用的字节数不一样。

如果当我们买回来的计算机当中有 4g 内存或者 8g 内存空间,那么我们因该如何去使用它们呢🤔

解释:内存(空间)的使用跟我们"现实"生活当中有非常相似的地方,在我们"现实"生活当中国土面积总共有960万平方公里,就像是当我们真的是去访问这些内存空间的时候,都给了它们的一个有效的地址。比如:这个时候我们想去找到一个地方,有省,其次市、县、镇、乡这样不同的规划。然后找到你人在哪里,而我们现实生活中市怎么样找到这一块空间的那就是通过地址,而这个地址又是跟我们一个个"房间"是相互关联的。我们是不是跟每一个房间都整理了编号,然后通过地址就可以找到房间⇩

那么其实对于内存也是一样的,内存是一块大的空间,如下流程图所示

当我把内存划分成这样一个长方体的格子之后, 那么其实就是和生活当中的房间是一样我们现实生活当中给每一个房间都编了号。

而我们内存空间也是一样,划分着每一个格子也相应的进行编号。这就是内存空间的管理方式。


🗡 地址与指针 

概述:在计算机中,所有的数据都是存放存储器内存当中。一般把存储器中的一个字节称为一个 内存 占用的内存单元数不等,如整型量占 4 个单元,字符量占 1 个单元等,在前面已有详细的介绍。为了正确地访问这些内存单元,必须为每个内存单元编上号。根据一个内存单元的编号即可准确的找到该内存单元,内存单元的编号也叫做地址。 既然根据内存单元的编号或地址就可以找到所需的内存单元,所以通常也把这个地址称为指针。 内存单元可以用一个通俗的例子来说明 它们之间的 到银行去存取款时, 银行工作人员将根据我们的帐号去找我们的存款单, 取款的金额。在这里,帐号就是存单的指针, 存款数 是存单的内容。对于一个内存单元来说,单元的地址即为指针,其中存放的数据才是该单元 的内容。 语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针 🤩

Ⅰ地址就是内存区中对每个字节的编号

Ⅱ指针看作是内存中的一个地址,多数情况下,这个地址是内存中另一个变量的位置

在程序中定义一个变量,在进行编译的时候就会给改变了在内存当中分配一个地址,通过访问这个地址可以找到所需的变量,这个变量的地址称之为该变量的 "指针"

🍊指针就是用来存储内存变量的当中的地址!①个内存单元 == ①字节 

🍊地址就是用来通过内存区的编号找到变量,然后再把自己内存区的编号赋值给指针。

从上述的话就可以看出来,为什么也会说其实指针无非就是地址,地址无非就是指针了。


🗡 变量与指针 

变量的地址是 变量 指针 二者之间纽带,如果一个变量包含了另一个变量的地址,则可以理解成第一个变量指向第二个变量。所谓 "指向" 就是通过地址来进行实现的。 因为指针变量是指向一个变量的地址,所以将一个变量的地址值 赋值给这个指针就 "指向" 了该变量 😜

例如:将变量 i 的地址值赋值给指针变量 p 中,p 就指向 i,其关系如下所示↓

在程序代码中是通过 变量名 内存 单元进行存取操作的,但是代码经过编译后已经将变量名转换为该变量在内存中存放的地址,对变量值的存取都是通过地址进行的。如下代码所示

a+b

其含义是:根据变量名与地址的对应关系,找到变量 a 的地址。假设 1000,找到变量 i 的地址 1000,然后从 1000 开始读取 4 个字节(整形类型)存放在 CPU 的寄存器当中,再找到变量 j 的地址 1004 ,从 1004 开始读取 4 个字节数据存放在 CPU 另一个寄存器当中,通过 CPU 的加减法从中计算出来 🙄


⚔ 定义指针变量

对指针变量的定义包括 个内容↓

  1. 指针类型说明,即为变量为一个指针变量
  2. 指针变量名
  3. 变量值(指针)所指向的变量的

定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号(*) 格式如下所示↓

datatype *name;或者 datatype *name = value;

解释:* 表示一个指针变量,datatype 表示该 指针变量所指向的数据的类型。如↓

int *p;// %p打印地址

表示 p 这是一个指针变量,变量名即为指针的变量名,类型说明符表示本指针变量所指向的变量的数据类型

是指向int 类型数据的指针变量,至于究竟指向哪一份数据,应该由赋予它的值决定。如下↓代码所示。

int *p1;   // p1 是指向整形变量的指针
float *p2; // p2 是指向浮点变量的指针
char *p3;  // p3 是指向浮点变量的指针

🔥注意:个指针变量只能是指向同类型的变量。

int a = 100;    
int *pa = &a;   

(1) a在内存中要分配空间4个字节。

(2) 取出a的地址赋值给指针变量papa说明执行对象是int类型。

🔥注意→取地址a并不会拿出4个字节的地址,只会拿出第一个字节地址。

1字节 = 8比特位,按照十六(0x)进制的方式来的。


💣有效声明指针

有效指针,顾名思义就是可以有效的在程序当中运行不会出现错误的指针类型 😐

int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;    /* 一个字符型的指针 */

🗡使用指针

使用指针时会频繁进行以下几个操作:定义一个指针变量把变量地址赋值给指针访问指针变量中可用地址的值。这些是通过使用一元运算符(*)来返回位于操作数所指定地址的变量的值。下面的实例涉及到了这些操作如下↓ 

#include<stdio.h>
int main(void)
{
    int a = 20;                //a在内存中要分配空间4个字节
	int *pa = &a;              //取出a的地址赋值给指针变量pa, pa说明执行对象是int类型!
	printf("无改变:%d\n", *pa);

	*pa = 30;//进行解引用操作符 *pa 就是通过解引用(pa)里边的地址来找到地址a的值。
	printf("改变的:%d\n", *pa);//解引用操作符是可以改变取地址原来的值的!

	return 0;
}

运行结果🖊

a = 20

b = 30

知识内容→上面的结果也是通过指针变量取得数据, 然后再通过解引用(*)操作符改变取地址原来的值。指针的解引用可以获取地址赋值给指针变量从而获取数值的大小(这个是初学者有时候不明白的地方,不懂可以多看几遍或者自己上手代码进行调试)


💣指针变量初始化 

指针变量初始化是非常重要的,很多初学指针的小伙伴们很容易就会把指针没有进行指针变量的初始化。

如下代码所示↓

#include<stdio.h>
int main(void)
{
    int a = 10; 
    int *p = &a;
    printf("*p = %d\n",p);
    return 0;
}

🔥注意→上述代码是错误的,在打印printf的时候,p并没有对其进行解引用操作(找不到p的数值) 此时p是找不到a的地址的,也可以说并没有指向&a。

如下代码所示↓

#include<stdio.h>
int main(void)
{
    int a = 10; 
    int *p = &a;
    printf("*p = %d\n",*p);
    return 0;
}

此时,对其中打印 printf 进行解引用操作找到 a 的地址。运行结果为:*p = 10 

🔥注意→在使用指针的时候,是必须要给指针变量进行初始化的,不然就会是野指针。关于野指针是什么这个在后面会说的。  


💣赋值语句的方法

如下代码所示↓

int a;
int *p;
p= &a; 

不允许把一个数赋予指针变量,故下面的赋值是错误的。

int * p;
p = 10;

被赋值的指针变量前不能再加“*”说明符,如写为 *p=&a 也是错误的。

另外,指针变量和一般变量一样,存放在它们之中的值是可以改变的,也就是说可以改变它们的指向。


🗡指针变量的大小

如下代码所示

#include<stdio.h>
int main(void)
{
    printf("%d\n", sizeof(int *));
	printf("%d\n", sizeof(long *));
	printf("%d\n", sizeof(long long*));
	printf("%d\n", sizeof(float *));
	printf("%d\n", sizeof(double *));
	printf("%d\n", sizeof(short *));
	return 0;
}

运行结果🖊

④(全部)

从上述结果可以看出指针变量都是④个字节

为什么不同类型的变量的地址所占的字节数都是一样的呢

原因是:他们数据类型都是指针类型,切记(☆-v-)

🔥注意→这个是在32位的操作系统 = 4字节,64位的操作系统上 = 8字节


⚔ 上述总结

指针就是变量,用来存放地址的变量。(存放在指针当中的值都是会被当做是地址来处理)

还有下列②个问题如下↓

  1. 一个小的单元到底是多大(①个字节)
  2. 如何进行编址

经过仔细的计算,一个字节(⑧比特位)对应其一个地址是比较合适的(①字节等于①地址)

指针就是用来存储地址的,地址是唯一表示一块地址空间。

指针大小在32位的平台上是④个字节,在64位上的平台是⑧个字节。

在32位平台上产生的地址线就是由 32个0&1组成的地址线,在64位平台上产生的地址线就是由64个二进制0&1组成的地址线。


💣 * 和 & 认识

假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*&a&*pa分别是什么意思呢?

*&a可以理解为*(&a)&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a

&*pa可以理解为&(*pa)*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa


💣 对解引用(*)认识

表示乘法,例如 int a = 3, b = 5, c;  c = a * b; 这是最容易理解的。

表示定义一个指针变量,以和普通变量区分开,例如int a = 100;  int *p = &a;

表示获取指针指向的数据,是一种间接操作,例如 int a, b, *p = &a; *p = 100; b = *p;

表示获取指针指向的数据,是一种间接操作。这里我来举出一个代码例子↓

#include<stdio.h>
int main(void)
{
	int a = 10;
	int *p = &a;
	printf(" a = %d\n", a);
	printf("*p = %d\n", *p);
	printf("a = %p *p = %p &p = %p p = %p\n", a,*p,&p,p);
	//注意:解引用改变p,同时也会改变指向a的地址(同样改变a的值同样p也会跟着改变)
	a = 20;
	printf(" a = %d\n", a);
	printf("*p = %d\n", *p);
	*p = 50;
	printf(" a = %d\n", a);
	printf("*p = %d\n", *p);
	int b = *p;
	printf(" b = %d", b);
	return 0;
}

运行结果🖊

这里大家可以好好思考下为什么,为什么运行结果是这样看看能不能说出来。这样你才能真正的掌握这些知识点。


🗡 " * & 的应用" 

如下所示👇

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main(void)
{
	int i = 0;
	int* p = &i;
	printf("请输入数字:");
	scanf("%d", &i);
	printf("numbers1 = %d\n", *&i);
	printf("numbers1 = %d\n", i);
	printf("numbers1 = %d\n", *p);

	return 0;
}

运行结果

三个 printf() 打印的值都是一样的(同样可以思考下这个是为什么)


🗡 通过指针交换变量值 

既然都看到这里了,那么我们就来尝试做一道题目吧,看看你是否掌握这些知识点了。

题目内容:用指针交换两个值。

#include <stdio.h>
void swap(int *pa, int *pb)
{
	int temp = *pa; //10  
	*pa = *pb;      //20
	*pb = temp;     //10
}
int main(void)
{
	int a = 10, b = 20;
	int *pa = &a, *pb = &b;
	printf("交换前的值:a=%d, b=%d\n", a, b);

	swap(pa,pb);
	printf("交换后的值:a=%d, b=%d\n", a, b);
	return 0;
}

运行结果🖊

交换前的值:a = 10, b = 20

交换后的值:a = 20, b = 10

对程序进行说明如下↓

swap()是用户定义的函数,它的作用是交换两个变量(a 和 b)的值。swap 函数的形参 papb 是指针变量。程序运行时,先执行 main 函数,已知 a 和 b 的值。然后将 a 和 b的地址分别赋给指针变量 pa pb使 pa 指向 a,pb 指向 b

用图形表示如下如下↓


🗡 指针变量的说明 

指针变量可以进行某些运算,但其运算的种类是有限的。它只能进行赋值运算和部分算术运算符以及关系运算

指针运算符如下↓

  1. 取地址运算符&: 取地址运算符&单目运算符,其结合性为自右至左,其功能是取变量 的地址。
  2. 取内容运算符*: 取内容运算符*单目运算符,其结合性为自右至左,用来表示指针变 量所指的变量。在*运算符之后跟的变量必须是指针变量。

需要注意的是指针运算符* 和指针变量说明中的指针说明符* 不是一回事。在指针变量说明中,“*”是类型说明符,表示其后的变量是指针类型。而表达式中出现的“*”则是一个运算符用以表示指针变量所指的变量


🗡 指针类型的意义(1)

指针类型的意义,代码如下所示↓

int a = 0x11223344;
int *pa = &a;
*pa = 0;

此时,经过调试阶段我们可以发现,首先 a 的地址赋值给指针变量并且对象类型是 int 类型。然后,再通过 解引用 *pa 把原来 &a 的地址变成0,那么通过这个现象,我们就可以知道用int * 进行解引用的话,我们是成功访问到 ④个字节 的地址。是不是可以说明指针访问字节是个字节呢。

int a = 0x11223344;
char *pa = &a;
*pa = 0;

经过调试阶段我们可以发现,首先 a 的地址赋值给 指针变量并且对象类型是 char 类型。然后,再通过 解引用 *pa 把原来 &a 的地址变成0。那么通过这个现象,我们就可以知道用char * 进行解引用的话,而我们是成功访问到 ①个字节 的地址。可以看出只是类型发生了变化,访问权限就会发生变化从这里说明了指针是只能访问个字节的,这是C语言语法标准规定死的


所以从上面两个不同指针对象,我们就可以从中发现。只是不同类型的变化,就可以在调试当中发现它们的访问权限就发生了变化这就从中说明指针类型是具有意义的

如果我是个整形指针(int*)的话,解引用的时候,我去访问一次的话。解引用一次就可以访问个字节。按照上面图中我就可以把个字节全部变成0了。

然而,如果我是一个字符指针(char*)的话。我解引用一次我就只能访问其中的个字节。

通过这个例子可以知道:不同类型指针在解引用的时候就决定它的访问权限有多大


🗡 指针类型的意义(2) 

接下来,举出第②个例子。如下代码所示↓

#include<stdio.h>
int main(void)
{
    int arr[10] = {0};
    int *p = arr;
    char *pc = arr;
    printf("%p\n",p);
    printf("%p\n",p+1);

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

编译器运行结果如下↓

在第二行编译运行结果上,如果是我整形指针+1的话。我实际上的值+4

在第四行编译运行结果上,如果是我字符指针+1的话。我实际上的值+1

字符指针+1的话,相当于跳过了一个字符。那么跳过一个字符的话,我不就相当于+1

如果我是整形指针+1,相当于跳过一个整形。如果跳过一个整形,就相当于跳过4个字节。那其实就是相当于+4

那这个为什么会产生这样的不同呢?因为 p 以及 pc 的指针类型不同

那么从这个角度可以知道:指针类型决定了指针走一步就可以走多远,也就是步长


⚔ 野指针  

概念:野指针就是指针指向的位置是不可知的。

  1. 随机的
  2. 不正确的
  3. 没有明确的限制

指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的

指针越界也会导致野指针问题,这里解释下:就是当你指针指向的范围超过数组名范围时,那么那个指针就是野指针了


🗡 如何规避野指针

指针的初始化,不光是指针也要初始化,其它的任何类型都需要初始化。

小心指针越界。

指针指向空间释放即使置NULL。

指针使用之前检查有效性。

示例代码如下 ↓

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main(void)
{
	int *p = NULL;
	*p = 20;
	return 0;
}

在上述代码当中我们把指针变量p赋值给了空指针NULL,但是这里我们又把p进行解引用操作改变成了20,就相当于我们访问了NULL的地址。实际上NULL相当于是0,如果我们要访问0的地址的话它是不属于我们用户当中的,并没有分配给我们用户,所以当我们把20数字放进去的话是不能放进去的属于是非法访问了!

运行结果如下↓

0x002A1002 处有未经处理的异常(在 ConsoleApplication1.exe 中):  0xC0000005:  写入位置 0x00000000 时发生访问冲突。 


💣 指针的未初始化

如下代码所示↓

int* p;
*p = 10;

p 是一个局部的指针变量,局部变量不初始化的话,默认是为随机值的。而随机的值放在指针变量 p 里面,就会以为这里面上放的是一个随机值。而随机的值放在指针变量 p 里面上,p 就会以为这是一个随机值的地址。

解引用 *p,那么我就会当 p 里面是一块地址。然后进行访问这块空间,因为 p 里面的这个地址是随机的值,这个时候就是通过这块地址在内存中由那个地址中指向的一块空间。所以,那个地址指向空间可能不是你的。那么这个时候就会造成非法访问内存。

以上讲解,所以才会把这里的 指针变量p 称之为野指针

int a = 10;	     
int* pa = &a;	  

以上指针变量的初始化√ 

指针常见错误

引用未初始化的指针变量

试图引用未初始化的指针变量是初学者最容易犯的错误。未初始化的指针变量就是"野"指针,它指向的是无效的地址

有些书上说:"如果指针变量不初始化,那么它可能指向内存中的任何一个存储单元,这样就会很危险。如果正好指向存储着重要数据的内存单元,而且又不小心向这个内存单元中写入了数据,把原来的重要数据给覆盖了,这样就会导致系统崩溃",这种说法是不正确的!如果真是这样的话就是编译器的一个严重的BUG(o゚v゚)ノ

  • 所以,指针虽好但你也得会用不然你的BUG就会满天飞

💣 指针越界访问

示例代码如下↓

#include<stdio.h>

int main(void)
{
	int arr[10] = { 0 };
	int i = 0;				
	int* p = arr;			 //接收arr数组首元素的地址。
	for (i = 0; i <= 12; i++)
    //当i=10的时候已经是非法访问内存了,因为,我数组名的常量表达式内容只有10个元素。
	{
		*p = i;				//i每次循环赋值给指针p
		p++;				//指针自增+1,代指arr元素+1

		//*p++ = i 也是可以,这里虽说++优先级更高,但是它是后置运算符。
	}
	return 0;
}

指针变量越界数组导致野指针问题

注意→C语言是不会本身检测数组的越界行为的。


⚔ 空指针 - NULL

NULL——空指针,用来初始化指针或者给指针赋值,可以转到定义看看。 

#define NULL    ((void *)0)  本质上 NULL 其实就是 (0)

代码如下所示↓

int* p = NULL;

当然,如果你不知道 p 应该初始化什么地址的时候,就直接初始化 NULL。 

注意→如果当你这里 *p = 10 的时候依然会出现问题。 

NULL 是在计算中具有保留的值,用于指示指针不引用有效对象。程序通常使用空指针来表示条件,例如未知长度的列表结尾或未执行某些操作; 这种空指针的使用可以与可空类型和选项类型中的 Nothing 值进行比较。 

空指针不应与未初始化的指针混淆:保证空指针与指向有效对象的任何指针进行比较。但是,根据语言和实现,未初始化的指针可能没有任何此类保证。它可能与其他有效指针相等; 或者它可能比较等于空指针。它可能在不同的时间做两件事。 

#include<stdio.h>
int main(void)
{
	int a = 10;			
	int* pa = &a;

	printf("one pa = %d\n", *pa);

	*pa = 20;				//此时当我们不想用它时候
	pa = NULL;				//就把 pa 指针置成 NULL

	printf("two pa = %d\n",pa);
    return 0;
}

编译运行结果👇

  • one pa = 10
  • two pa = 0 

💣 指针使用之前检查有效性

当你指针变量不可以用的时候就把它设置成NULL,当你指针变量可以用的时候就不是NULL 

当我们对这个指针进行初始化的话,那么它就是有效的,如果没有初始化那么就是无效的

	if (pa != NULL)
	{
		//进行使用
	}
	if (pa == NULL)
	{
		//不进行使用
	}

🗡 指针运算 

💣 指针+- 整数

示例,代码如下所示👇

#define Macro 5
	int values[Macro];
	int* p;
	for (p = &values[0]; p = &values[Macro];)
	{
		*p++ = 0;
	}
    return 0;

这里的代码会使得 values 的数组下标结果全部初始化为 0。

在这里我们就运用到了 指针 + 整数 : *p++ = 0;

注意: p = &values[Macro];这个实际上就是指针的关系运算。指针比较了大小。随着数组的下标的变化,地址是由低到高进行变化的。


💣 指针 - 指针

如下代码所示↓

#include<stdio.h>
int main(void)
{
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	printf("%d\n", *(&arr[9]) - *(&arr[0]));
	return 0;
}

运行编译结果:9

从上面的编译结果我们可以知道→指针 - 指针 得到的两个指针之间的元素个数

注意:指针和指针相减的前提是→两个指针类型必须指向同一块空间


💣 指针关系运算

for 循环遍历数组元素全部初始化为 0

第①种代码 

#define Macro 5
int value[Macro];
int* p;
for(p = &value[macro];p > &value[0];)
{
    *--p = 0;
}

第②种代码

#define Macro 5
int value[Macro];
int* p;
for(p = &value[macro - 1];p > &value[0];p--)
{
    *p = 0;
}

实际上绝大多数的代码编译器上是可以顺利完成的

然而写代码的时候我们还是应该避免第二种写法,因为标准并不保证它是可行的

💣 标准规定

允许指向数组元素的指针与指向数组最后一个元素的那个内存位置的指针进行比较,但是不允许指向第一个元素之前的那个内存位置的指针去进行比较,第二种就会导致数组越界。


⚔ 指针和数组

数组名是首元素的地址,如下代码所示↓

#include<stdio.h>
int main(void)
{
	int arr[10] = { 0 };
    //这两种情况是等价得
	printf("%p\n", &arr);
	printf("%p\n", &arr[0]);
	return 0;
}

代码运行结果所示↓

  • 004FFAD8 - 数组名
  • 004FFAD8 - 数组名首元素地址

正是因为数组名是首元素的地址,所以我们可以这样去理解。如下代码所示↓

#include<stdio.h>
int main(void)
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%p--《======》--%p\n", &arr[i], p + i);
	}
	return 0;
}

上面的代码:p 是首元素的地址,首元素的地址 p + i,产生的就是下标为 arr[i] 的这个元素的地址。它们两个是一模一样的。

运行结果🖊

0x7ffe5a4ed550--《======》--0x7ffe5a4ed550
0x7ffe5a4ed554--《======》--0x7ffe5a4ed554
0x7ffe5a4ed558--《======》--0x7ffe5a4ed558
0x7ffe5a4ed55c--《======》--0x7ffe5a4ed55c
0x7ffe5a4ed560--《======》--0x7ffe5a4ed560
0x7ffe5a4ed564--《======》--0x7ffe5a4ed564
0x7ffe5a4ed568--《======》--0x7ffe5a4ed568
0x7ffe5a4ed56c--《======》--0x7ffe5a4ed56c
0x7ffe5a4ed570--《======》--0x7ffe5a4ed570
0x7ffe5a4ed574--《======》--0x7ffe5a4ed574

如果这个时候要用指针遍历数组首元素的下标的话那也是可以的,此时就可以通过解引用来找到下标 i 的元素,从而 i 放进去,再把每个元素所打印出来。

#include<stdio.h>
int main(void)
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	return 0;
}

运行结果🖊

0 1 2 3 4 5 6 7 8 9 


⚔ 二级指针 

如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。

	int a = 10;    //(1)
	int* pa = &a;  //(2)
	int** ppa = &pa;//(3)
	printf("%d %d %d\n", **ppa, *pa, a);//10 10 10

在第②行当中:pa 是指针变量,一级指针。它就只是仅有一层指向关系。pa 指针对象是整形,因为它存储的是地址&a。

在第③行当中:由于 pa 也是个变量,&pa 取出 pa 在内存当中的起始地址。ppa 首先是指针的话我需要写上 *ppa,ppa 指向的对象是 pa,pa 整体的类型叫做是 int* pa。所以,在这个 * 的前面,我还是需要写上int*。*ppa 是一个指针,而int*是我所指向的指针变量。于是就是 int** ppa,这种的话也被称之为是:二级指针。

说的明白一点就是:ppa 有两层指向关系,1:pa,2:a那么从这里我们不难发现 三级指针、四级指针、以及 N级指针 都是这样的关系。不过二级指针之后就很少用到了,所以大家只需要了解二级指针的概念即可。

关系图如下所示↓


⚔ 指针数组 

一个数组的元素值为指针则是指针数组,指针数组是一组有序的指针的集合。 指针数组的所有元素都必须是具有相同存储类型和指向相同数据类型的指针变量。

那么指针数组是怎么样的呢,如下代码所示↓

int* arr[5];//是什么?

 arr是一个数组,有⑤个元素,每个元素是一个整形指针

注意:除了每个元素的不同,而指针数组和普通数组在其它方面其实都是一样的!

#include <stdio.h>
int main(void)
{
	int a = 12, b = 17, c = 55;

	int *arr[3] = { &a, &b, &c };

	int **parr = arr;
	printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
	printf("%d, %d, %d\n", **(parr), **(parr+1), **(parr + 2));

	return 0;
}

运行结果🖊

12 17 55 

12 17 55  

arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、b、c 的地址对它进行了初始化,这和普通数组是多么地类似。

parr 是指向数组 arr 的指针,确切地说是指向 arr0 个元素的指针(首元素的地址),它的定义形式应该理解为int *(*parr),括号中的*表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。arr0 个元素的类型为 int *,所以在定义 parr 时要加两个 *

第一个 printf() 语句中,arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 *(解引用) 才能取得它指向的数据,也即 *arr[i] 的形式。

第二个 printf() 语句中,parr+i 表示第 i 个元素的地址,*(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。

指针还可以与字符串相互结合进行使用,如下代码所示↓

#include <stdio.h>
int main(void)
{
	char *arr[3] = { "C", "CSDN", "Electricity" };
	printf("%s <==> %s <==> %s\n", arr[0], arr[1], arr[2]);
    return 0;
}

 需要注意的是,字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。

也只有当指针数组中每个元素的类型都是char *时,才能像上面那样给指针数组赋值,其他类型不行。

改成下面的形式,与上面的数组是等价的,如下代码所示↓

#include <stdio.h>
int main(void)
{
	char *str0 = "C";
	char *str1 = "CSDN";
	char *str2 = "Electricity";
	char **str[3] = { str0, str1, str2 };
	printf("%s <==> %s <==> %s", str[0], str[1], str[2]);

    return 0;
}

运行结果🖊

C <==> CSDN <==>  Electricity


后续还会有高阶指针内容!喜欢还请多多支持吧 🌹

评论 59
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

謓泽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值