C语言指针的那些事:第一篇(有指针书写的技巧)

前言

指针相信大家都或多或少了解过一点,大多数朋友对指针都是望而却步,原因是因为指针总是很绕人,什么地址,什么解引用,什么指向的概念,又或者传参时候传指针可以改变外面的实参,多级指针啥啥的…。确实,指针是一个庞大的系统知识,晦涩难懂,不过,今天我要挑战这个回溯难懂的东西,把我对指针的理解分享出来。

我打算分四篇文章讲解指针:

  • 第一篇:讲讲指针基本的内容,即这篇
  • 第二篇:讲讲指针和数组那些故事,数组指针,指针数组,还有字符指针等
  • 第三篇:讲讲指针和函数的那些是,函数指针,指针函数,指向函数指针的数组等
  • 第四篇:是一些指针笔试面试题,等;

大概是这样啦
提示:本指针都是在32位的环境测试下来的。

简易的内存模样

要了解指针,不得不先了解内存这个东西,形象的感官理解内存;

从实体来看就是一个计算机的硬件设备,

而虚拟的来看就是一个由多个格子组成的长方形。
在这里插入图片描述

每个格子都有自己的名字,我们把每个格子的名字叫内存单元;而格子的标识就是内存编号

❓问:那内存编号怎么来的呢?

这是由计算机硬件设计所的来的,在我们32位计算机中,计算机的设计者设计了32根地址线,连着内存,而32根线中,每一根线都有两种状态,有点和没电,在计算机里面用,1表示有电,0表示没电;那么在有32根线,,每一个线有两种状态,那么就可以这么想:
1根线, 可以表示 1 或者 0,就有2个状态;就是 21
2根线,可以表示00 01 10 11 就有4个状态;就是22
3根线,可以表示 000 001 010 011 100 101 110 111 就有8个状态;就是23
。。。。。。。。。。。。。。。顺着思路下来。。。。。。。。。。。。。。。
32根线,就是 232个状态呀;

看图:这就是每个格子的内存编号;
在这里插入图片描述
那我们直到每个格子都用二进制去表示,显得太长了;所以一般我们内存编号都用16进制来表示,就是把2进制转化位16进制;如图
这就简单了好多

在这里插入图片描述
❓问:那内存的表示范围是多少?

在32位就是 0x00 00 00 00 到 0xff ff ff ff;也就是寻址范围:0 — 232 ,用平时得理解就是4G内存大小

❓问:内存中每个格子是什么?格子的大小有是多少?

内存中每个格子我们叫做内存单元;每个格子是1个字节

那这就引出来我们把内存单元对应的内存编号叫做内存单元的编号,那内存单元的编号叫做地址;
❓问:内存单元里面是什么?

内存内存里面就是你要存放的数据;

❓问:内存单元编号有啥作用?

通过内存单元编号,我们可以找到每个一内存单元的地址,那就可以知道地址对应的内存单元里面的数据;

❓问:如何形象的理解这种关系呢?

可以把内存类比酒店, 每个格子类比于每个房间,每个内存编号类比于每个房间的门牌号;
这就说明一个问题,你要找到房间里的人,可以通过门牌号找到,类比于,你要找到内存的数据,可以通过内存编号找到;


在这里插入图片描述

说了那么多,其实最主要的就是 :

  • 格子 = 内存单元,其大小为1byte
  • 内存编号 = 地址,在32位的字节大小位 4byte,并且我们能通过地址找到内存单元里面的数据

在《C和指针》92页中说到,对于内存,我们暂时感兴趣的:

  • 内存中的每一个位置由独一无二的地址标识
  • 内存中的每个位置都包含一个值

指针

我们大家都知道一句话指针就是地址,地址就是指针;那这句话怎么由来的呢?其实就是很形象的理解,因为,我们可以通过内存编号,也就是地址,也就是内存每一我位置独一无二的地址标识,找到内存单元,可以说,地址指向了该内存单元,所以我们形象的说地址就是指针,指针就是地址。

int a =10;
int *p = &a;
	*p = 20;

解释代码:
这里 a 就是一个变量,由于int类型,所以占用4个byte;

int *p = &a理解:*标识p是一个指针,int表示指针p指向的类型;
指针p 也是一个变量,里面存放的是变量a的地址;变量都有自己的地址所以说,p也有自己的地址;要区分指针的p的地址和指针p执行a的地址不是同一个东西。

*p = 20;表示间接访问变量a,把a 修改为 20;所以说 *p就表示 a;我们通常说解引用p得到变量a
由于p 存放的是地址,而地址又是32位的,所以指针p的大小是4个字节;
在这里插入图片描述
这里我想说明的是:

指针存放的永远是地址;

并且我还相说明一个问题:

在定义指针时候,无论你定义多少级的指针,只有一个星号*表示这个变量是一个指针,剩下的都是指针指向的类型; 请你记住这句话,下面可以帮你你理解更加复杂的指针声明,和看懂指针。


指针的大小和指针类型

我们说指针的大小是4个字节(在64位是8个字节)如何验证呢?

# include<stdio.h>

int main()

{
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(double*));
	printf("%d\n", sizeof(float*));
	return 0;
}
//这里的指针类型都是不一样的,假如我得到的结果都是一样的,就说明指针的大小都是固定的。

结果都是4;说明指针大小都是4个字节。
在这里插入图片描述
❓问:既然来说指针大小是4个byte,那么指针类型到底有什么意义呢?

指针类型的意义:
决定了指针可以访问的字节数,也说明就说明指针在解引用时所能访问的空间大小
;如:
char*表示指针每次访问是以1byte 去访问内存字节数;
int* 表示指针每次访问是以4byte 去访问字节数;

# include<stdio.h>
//指针类型的意义:说明就说明指针在解引用时所能访问的空间大小
int main()
{
	int a = 10;
	char* p = &a;
	*p = 20;
	return 0;
}

这用一个char* 指针指向了int 类型的变量,通过*p试图改变 变量a ,请问能够改变呢?也就是说a的值,是否是20呢?

答案:是可以的,但是访问的却是一个字节的内容,假如数字更大超过一个字节的大小,这样改变就不会得到预期的结果了,这种方式是不可取的。

如下图:没改变前 a的值
在这里插入图片描述
改变后 a 变红的地方只有一个字节,说明char*指针类型访问改变了一个字节的内容;
在这里插入图片描述
但是这种行为我们是认为不可取的,你定义了什么类型的指针,就应该执行什么类型的地址,比如,你定义了 int a = 20; 你想定义一个指针 p,你的指针p的类型就应该设置为 int * ,不因该设置为其他类型的;

但是,这种行为也恰恰说明了指针的强大,可以精确的访问到内存的字节单元,内存的 每一个单元的内容,指针都可以窥探


指针加减整数

既然指针的类型意义是决定了指针能访问的字节数,指针解引用所能访问空间的大小;那指针加减整数时候就可以表示指针向后向前移动了多少步长

比如:

  • int* p ; p+1 表示指针向前移动了4 个字节;
  • char* p ; p+1 表示指针向前移动了 1 个字节;
  • double* p ; p+1 表示指针向前移动了 8 个字节;
  • float* p ; p+1 表示指针向前移动了 4 个字节;
# include<stdio.h>
//测试指针加减整数的意义
int main()

{
	int a = 10;

	int *p = &a;
	//%p的形式是打印指针的地址,以十六进制未数显示
	printf("%p\n", p);
	printf("%p\n", p + 1);

	char ch = 'a';
	char *p1 = &ch;

	printf("%p\n", p1);
	printf("%p\n", p1 + 1);

	return 0;
}
//注意:指针不单单是只能加1,也是可以加2,加3,减1,减2,等整数滴。主要是看你的程序需求而定。

如下图:
在这里插入图片描述
上面的例子也可以侧面验证:指针类型决定了访问字节数是多少,指针+1,由于是 int* 类型的指针,访问的字节就为4个,由于是char*类型的指针,访问的字节就是1个。

既然指针+整数会使得地址向后移动,那么指针-整数就是向前移动,朋友们可以类比以下就好。


指针和指针相减

前提:指针和指针的类型相同指针和指针相减是得到指针之间有多少个元素个数,因为指针相减会得到地址的差值,这个差值就可以表示元素的个数啦。指针是不可以相加的,因为指针和指针相加时没有啥意义呀。得到的数就是一个更大的地址,是没有什么用的,而且语法也不允许;

int my_strlen(char *s)
{
   char *p = s;
   while(*p != '\0' )
       p++;
       //退出循环后,指针p指向‘\0’;而s指针还是指向字符串的首元素
   return p-s;
}
int main()
{
	char* s = "abcdef";//指针s指向字符串的首地址,
	int ret = my_strlen(s);

	return 0;
	
}

在监视中,我们可以看到,指针 - 指针的值,就是 abcdef 元素的个数 6 ;
在这里插入图片描述
如图:
在这里插入图片描述
具体,指针减指针有什么用,到你以后深入时候,有需求时候,自然会用上。


一些指针++和指针- -的运算操作解释

比如有时候我们看别人的代码有这个语句:
*p++; 
*p--;
*++p;
*--p;

这怎么理解呢?
首先 
*p++:是先使用解引用p,然后指针p加1;
*p--:是先使用解引用p,然后指针p加1;
*++p:先指针p加1,然后解引用指针p;
*--p:先指针p减1,然后解引用指针p;
++*p:

指针和数组的关系

我们知道数组名就是数组首元素的地址;如何理解呢?
看看一段代码:
代码就是打印一维数组数组

int arr[10] ={0};//初始化十个元素都为0
for (int i = 0;i<10;i++)
{
	printf("%d ",arr[i]);
}

我们都会这种打印数组的方式,这是通过下标操作符 [ ],去访问数组中的每一个元素
但是,接下来这种方式也可以打印数组:

//第一种
int arr[10] ={0};//初始化十个元素都为0
for (int i = 0;i<10;i++)
{
	printf("%d",*(arr + i));
}
//第二种
int arr[10] ={0};//初始化十个元素都为0
int * p = arr;
for (int i = 0;i<10;i++)
{
	printf("%d",*(p + i));
}

这种通过数组名的方式也可以访问数组的每一个元素,原理是什么呢?

本质就是数组名就是首元素的地址;地址是什么?地址就是指针,指针可以干嘛?指针可以加整数,加整数有什么用?加整数可以跳过一定的步长到达下一个位置,到达下一个位置了然后呢?就可以解引用该位置的指针去访问指针指向的变量了,这里的变量就是数组中的元素。

所以说,对于数组来说,数组名表示首元素的地址,那么就有下面的等价关系;
arr == &arr[0];
arr+1 == &arr[1];
arr+i == &arr[i];

那么,当我解引用时候得到数组下标对于的值:
*arr = arr[0];
*(arr+1) == arr[1]; 注意:这里*解引用的(arr+1)
*(arr + i) == arr[i];

也就是说
*arr + 1 == arr[0] +1; //注意:这里*解引用的arr

如下图:


看看另一代码:

int main()

{
	int arr[10] = { 0 };

	printf("arr的地址    %p\n", arr);
	printf("arr+1的地址  %p\n", arr+1);

	printf("arr[0]的地址  %p\n", &arr[0]);
	printf("arr[0]+1的地址  %p\n", &arr[0] + 1);

	printf("&arr的地址    %p\n", &arr);//注意是取&arr
	printf("&arr+1的地址  %p\n", &arr + 1);

	return 0;
}

这说明
arr = &arr[0]; arr != &arr ; &arr+1表示加整个数组大小的步长;而arr+1表示加 一个数组元素的步长;
在这里插入图片描述
注意:当我们取地址数组名,即**&arr时候,得到的是整个数组的地址,不是首元素的地址**;这个在后面深入文章会谈到这个问题。现在有个理解即可


野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。这是不好的行为,我们要避免它;

看一段代码

int main()
{
	int *p;//没有初始化的指针变量p,里面存放的是随机值
	*p = 20;//通过随机值地址找到这份空间,在这段空间存放20,
			//这就是一种非法访问,因为你找到随机值地址的空间,并不是这个程序所开辟的。
	return 0;
}
//这里的p指向的位置是不可知道的,所以p是个野指针。

这段代码有很大的问题,首先指针p是一个变量,变量就会开辟内存空间,指针p里面存放的是什么呢?是指针p所指向的内存空间,但是指针p并没有指向任何空间,并且指针p又是有内存空间,这就说明有值的,只不过这个值是随机值,随机值那就说明这个指针p指向的空间就是这个随机值所代表的地址,那么你对一个随机值地址进行解引用就会发生错误,因为你并不知道这个指针p存放的地址到底是哪里,假如这个指针p存放的是你重要的信息的变量的地址呢?
那你还解引用得到这个变量的地址去修改它的值,那就会出大问题;


看看另一段代码:

int arr[10] = {0};
int *p = arr;
for (int i = 0;i<=10;i++)
{
	printf("%d",*(p + i));
}

乍一看上面的代码好像没什么问题;可是你发现没有,这个循环变量条件i<=10,当i = 10时候,在进去循环体,这个时候*(p + i)就等于*(p + 10);很明显,这是越界数组访问的情况,越界访问数组,那段空间又不是你程序所开辟的空间,这也是会导致野指针的出现;所以这个 p+10 就是野指针,并且解引用*(p + 10)得到的值也是随机值,会发生错误;如下图:
在这里插入图片描述


看看另一段代码:

#include <stdio.h>

int* test()
{
	int a = 10;
	return &a;
}
int main()
{
  int *p = test();
  printf("%d",*p);
  return 0;
}

解释
这段代码本意是用指针p接收test函数返回的地址,则指针p就指向test函数内部的局部变量a的地址,然后解引用*p,间接打印变量a的值,但这是错误的用法,会导致指针p变为野指针;

分析指针p成为野指针的原因

我们知道局部变量的声明周期是函数结束的时候,也会自己挂掉,尽管test函数中局部变量a在挂掉之前把变量a的地址交给了指针p,一旦test函数结束后,变量a就没了,那就说明变量a的空间内容被系统回收了,就说明变量a的内容不在是以前的10了,也就是说,虽然你指针pa的地址,但是没有a的内容了,那么你通过指针解引用访问a的内容,就是得到垃圾值,并且这个地址已经不是你的了,被系统回收了,就会导致指针p成为野指针。

所以结论:
就是我们尽量不要返回局部变量的的地址,就像上面的例子一样,会得到垃圾值。

二级指针

二级指针也是指针,只不过存放的是一级指针的地址;一个*表示 一级指针,两个** 表示二级指针;
比如:

int a = 10;
int *p = &a;
int **pp = &p;

在后面文章深入时候,我们会探讨,指针作为参数到底怎么思考传参的问题,如,什么时候接设计为一级指针二级指针,他们可以接收什么参数,从设计函数的角度思考,和从调用者的角度思考这个问题。
一般二级指针用在二维数组,还有一些需要修改一级指针指向时候会用到,比如链表的指向,二叉树的结点等。。。这些以后你会慢慢接触到的。

这里透露一点书写指针的技巧,到时候,设计函数指针参数时候,也会用到这种思想方式。


指针的书写,其实是有技巧的

这都是本人总结出来的技巧,越到后面越复杂的指针类型声明,书写,阅读时候,你就会发现这些技巧越有用。
你需要有的认知

  • 等号=两边的类型是必须相同的,(除非有隐式转换,或者你可以显示转换)。
  • 要得到一个变量的类型,去掉变量名字,等号左边剩下的就是变量类型
  • 要得到一个指针的类型,也是去掉指针名字,等号左边剩下的就是指针的类型
  • 要得到一个指针指向变量的类型,去掉指针名字和一个*星号,等号左边剩下的就是指针指向变量类型
  • 要得到一个数组的类型,去掉数组的名字,等号左边剩下的就是数组的类型
  • 要得到数组里元素的类型,去掉数组名字和[ ] 中括号,等号左边剩下的就是数组里元素的类型
  • 定义指针变量时候,一个*星号就可以代表该变量为指针变量了

有了上面的认知,你就可以看下去,虽然很多不理解什么意思,但,等到后面复杂的指针时候,我会解释的。

比如 int a = 10; 我想写一个指针指向a,如何书写呢?
大多数人都可以不用思考的写出来:
int *p = &a;
可以这么解读:p是一个指针变量,指向一个类型为int的变量a;
这是一个比较简单的指针,确实可以不用思考写出来,但是假如我们碰到一个比较复杂的例子:
如: int* arr[10] = {0};我想你书写一个指针,能够指向该数组的指针,那你是如何书写呢?

我们就讲解一下书写技巧吧,就上面最简单的 int a = 101.首先我们要有人认知:要得到一个变量的类型,去掉变量名字,等号左边剩下的就是变量类型
  所以对于 int a = 10; 去掉 a后,等号左边剩下的就是 int ,这就是变量 a 的类型;
2.其次:在书写指针,时候,必须有 * 星号,这是指针变量的标识,
  所以我们先会 毫不犹豫在等号左边先书写  *pa = 
3.这时候,来到了重点,我们知道a的变量类型是 int ,所以 我们在 *pa = 这个表达式中,*星号的左边填上 int;那么表达式为 int *pa = ;
4.最后,这时候等号=右边直填上 &a就可以啦!
  所以最终的结果为 int *pa = &a;


也许看着步骤有点啰嗦,可是到后面复杂的指针书写时候,你就发现,真是很简单的事情。
到时候还会有阅读指针的方法,带你阅读复杂类型声明。

更多级别的指针也可以这么书写;很实用。

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

呋喃吖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值