C语言超详讲解☀️指针是个什么针?(一次性搞定指针问题)

Posted Go-ly

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言超详讲解☀️指针是个什么针?(一次性搞定指针问题)相关的知识,希望对你有一定的参考价值。

目录

前言

一、 什么是指针?

引例

计算机是怎么对内存单元编号的呢?

内存空间的地址如何得到

想存地址怎么办?

本质目的不是为了存地址

 二、指针和指针类型

为什么有不同类型的指针

1.指针的解引用

2.指针+-整数

三、野指针

造成野指针的原因

1.未主动初始化指针

 2.指针越界访问

3.指针指向的空间释放

规避野指针

四、指针运算

1.指针+-整数

 2.指针-指针

 3.指针的关系运算

五、指针与数组

六、二级指针 

 七、指针数组 


前言

指针这一部分可能很多人在学习的时候都觉得很难,但在这里我想说的是:不要自己吓自己,想一想,你当初刚上大学的时候可能觉得高数非常难,最后学完整本书的时候回过头再看还觉得很难吗? 肯定已经觉得没有刚开始学那么难了,那么其实指针也是这样的,只要把里面的东西都搞清楚,你就不觉得难了。

 

 

一、 什么是指针?

 在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的内存单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址
的内存单元。

 其实计算机在设计的时候很多东西都是参考的生活中的东西

引例

eg.我国的国土面积:960万平方公里,这么大的国土面积是怎么合理使用的呢?聪明的国家领导人就想到,将这一大块区域进行划分,先划分为各个省,每个省再划分为各个市区,市区又划分为县城,县划分为镇,镇又划分为村,每个村的每户人家都有门牌号,可以很方便的找到,这样就对这样一大块区域进行了很好的管理。

那么其实电脑的设计也是这样的,在学C语言的这个阶段,我们常说的计算机的内存划分为3部分,栈区,堆区和静态区。

 计算机在最终也是将内存划分为了很小的空间--内存单元

我们平时要在学校找一个同学,也是先确定他的宿舍编号然后才去找的,不可能盲目的去找,而这里宿舍编号就是地址。

那在计算机内存中,我们想要找到某个内存单元,也是要知道它的编号呀,所以这里就对每个内存单元都进行了编号处理,将编号就称为内存单元的地址。

那么问题又来了

计算机是怎么对内存单元编号的呢?

1.计算机中的32位或者62位

32位的机器,它是有32根地址线(32根物理的电线),通电之后会将电信号转换成数字信号(正电--1,负电--0),32根地址线通电之后会产生以下的二进制序列数字信号:

00000000000000000000000000000000(32位)

00000000000000000000000000000001

00000000000000000000000000000010

………………………………………………

11111111111111111111111111111111

这个时候呢将二进制序列与内存单元一一对应,那么与内存单元相对应的二进制序列就是它的编号--地址。

 2.每一个内存单元到底多大?

这个我们也是可以来计算一下

比如一个内存单元是1bit,那么32位机器的所产生的序列共计2^32个,即2^32bit,转化为MB就是512MB,要想想我们买的计算机都是起步2G/4G,所以内存单元以bit为单位肯定是不行的。在C语言中,我们创建一个变量所申请的空间最小都是char--1byte,所以其实内存单元单位其实是字节

请看如下代码:

int main()
{
	int a = 10;     //创建变量a,向内存申请4个字节的空间,将10存储进去
	return 0;
}

内存空间的地址如何得到

我们创建了变量a,如何得到它的地址呢?  我们只需要在a的前面加上 & (取地址操作符

)符号,便可以得到它的空间地址

int main()
{
	int a = 10;     //创建变量a,向内存申请4个字节的空间,将10存储进去
	printf("%p\\n", &a);  //%p--打印地址(16进制)
	return 0;
}

我们可以把打印出来的地址00EFF6E8(16进制)转化成二进制来看看1110 1111 1111 0110 1110 1000

想存地址怎么办?

我们平时可以创建字符变量来存字符,创建整型变量来存整型,那如果想将地址存起来呢?那我们就得创建一个指针变量----用来存放地址

int main()
{
	int a = 10;     //创建变量a,向内存申请4个字节的空间,将10存储进去
	int* p = &a;      //&a取出a的地址,创建指针变量p,接收a的地址
	return 0;
}

* 说明p是指针变量,前面的int说明p指向的是整型变量

 指针变量p里面存放的就是a的地址,我们可以通过p里面所存储的值(地址)来找到变量a的内存空间,所以我们就说p指向了a,所以将p形象的称为指针(注意变量名字是p,不是*p)

那么我们以后想要存储地址的时候,就可以用指针

int main()
{
	float b = 2.5;
	float* p = &b;
	return 0;
}

本质目的不是为了存地址

我们创建指针存放变量地址的本质目的并不是为了存放变量的地址,而是为了将来能够通过地址来找到这个变量的内存空间,使用它。

我们上面已经存放了a的地址在p里面,那么我们怎么找到a呢

int main()
{
	int a = 10;     
	int* p = &a;    
	*p = 20;          //这里*---解引用操作符
	return 0;
}

 这里的 * --解引用操作符(间接访问操作符),p里面存的是a的地址,那么*p就是通过地址来找到a

 二、指针和指针类型

通过上面的代码我们会发现,一个char类型的变量的地址放在char*的指针里面,整型变量的地址存放在整型指针里面,为什么要这样呢?

为什么有不同类型的指针

1.指针的解引用

如下代码:

int main()
{
	int a = 10;
	char ch = 'f';     
	int* pa = &a;     
	char* pc = &ch;     //两个指针都是4个字节
}

这里要存放a的地址和ch的地址,都只需要4个字节(32位平台)的指针就可以,那为什么还要用不同类型的指针呢?

虽然它们都是4个字节,但是也是有区别的,我们通过以下代码的对比来看看

代码1:

int main()
{
	int a = 0x11223344;
	int* pa = &a;
	*pa = 0;
}

在内存里面看

a的内存里面的内容(执行*pa = 0;这个语句前后对比)(先不要管为什么内存里面是倒着存放的)

 代码2:

int main()
{
	int a = 0x11223344;
	char* ca = &a;
	*ca = 0;
}

 a的内存里面的内容(执行*ca = 0;这个语句前后对比)

 

 我们通过代码1和代码2以及他们的内存情况可以看出在代码1int*类型的指针在解引用的时候将a里面4个字节的内容全部改成了0(可访问4个字节),但是代码2中char*类型的指针在解引用的时候只改了1个字节的内容(只能访问一个字节),这就是指针不同类型的差异

2.指针+-整数

通过以下代码来体验一下:

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	int* pa = &a;
	char* ca = &a;
	printf("%p\\n", pa);
	printf("%p\\n", pa + 1);
	printf("\\n");
	printf("%p\\n", ca);
	printf("%p\\n", ca + 1);
	return 0;
}

 通过这段代码我们可以发现int*类型的指针进行+1操作地址加了4,char*类型的指针进行+1操作地址加了1

我们也可以通过下面的例子再来看看(通过指针来打印整型数组的内容)

代码1(通过整型指针来访问整型数组)

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

 

分析:

整型指针进行+1操作,意味着一次跳过一个整型(如下图)

 代码2(通过字符指针来访问整型数组)

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	char* parr = arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(parr + i));
	}
	return 0;
}

 字符类型的指针+1每次跳过一个char(1个字节)

 总结:

指针类型的意义

  1. 指针的解引用:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节),能一次性访问几个字节
  2. 指针+-整数:指针的类型决定了指针向前或向后走一步有多大(它是什么类型的指针,+1就跳过该类型大小的字节,eg.char*+1跳过一个字节,short*+1跳过2个字节,int*+1跳过4个字节,float*+1跳过4个字节,double*+1跳过8个字节)

三、野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

造成野指针的原因

1.未主动初始化指针

请看如下代码:

int main()
{
	int* p;
	*p = 8;
}

分析:

这段代码里面的p就是野指针,因为p是一个局部变量,指针变量,未主动初始化,那么它里面就是随机值(随机地址),就是说这个地址是不清楚的,然后后面又对它进行解引用,将8存放到这块不明确的地址里面,这就是又问题的

运行结果:

 2.指针越界访问

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* pa = arr;
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		*pa = 1;
		pa++;
	}
	return 0;
}

分析:

这里的数组长度为10,但明显可以看到循环执行了11次,pa最后所指向的已经不是数组里面的内存单元,越界访问了

可以来看看内存

这是在数组初始化之后数组中的内容

 

 在执行完11次循环之后

 此时会发现指针pa最后一次循环时越界,并且将数组arr后面的一个内存单元里面的内容改成了1,但是要知道这块内存并不属于该程序(这就是非法的)

3.指针指向的空间释放

如下代码

int* app()
{
	int b = 3;
	return &b;
}
int main()
{
	int* ps = app();
	return 0;
}

分析:

这段代码中,app这个函数返回了临时变量b的地址,ps这个指针也接受到了b的地址,但是要知道在这个函数调用完之后所创建的b这块内存空间就已经销毁了(或者说还给操作系统了),那么ps再记住这块空间的地址就没有意义了(保存了一个非法的地址)

规避野指针

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放即使置NULL
  4. 指针使用之前检查有效性

四、指针运算

1.指针+-整数

示例:(通过指针打印数组元素)

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

运行结果:

 2.指针-指针

示例:

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

请问这段代码的运行结果是什么呢?  是36?

需要知道的是 指针-指针:得到的是两个指针之间元素的个数(前提:两个指针指向同一块空间)

运行结果:

 3.指针的关系运算

代码1:

#include <stdio.h>
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* ps = &arr;
	for (ps = &arr[5]; ps > &arr[0];)
	{
		*--ps = 0;
	}
	return 0;
}

这段代码将数组中的元素改成了0

代码2:

#include <stdio.h>
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* ps = &arr;
	for (ps = &arr[4]; ps >= &arr[0];ps--)
	{
		*ps = 0;
	}
	return 0;
}

代码2与代码1相比更容易理解,但是要知道这样写是很大有问题的,C标准并不保证它可行

标准规定: 

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许
与指向第一个元素之前的那个内存位置的指针进行比较

五、指针与数组

再来啰嗦一下

1.数组名表示首元素地址

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

可以看到这种方式打印出来的结果是一样的

 数组名表示数组首元素的地址在绝大多数情况下都成立

两个例外

  1. sizeof(数组名)
  2. &数组名

可以通过指针来访问数组

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

六、二级指针 

 我们都知道指针是用来存地址的,指针也是在内存中开辟了一块空间然后将地址存储在里面,所以指针本身也是有自己的地址的

那我们就可以取出指针的地址来进行存放

int main()
{
	int a = 8;
	int* ps = &a;    //ps称为一级指针
	int** pps = &ps;    //pps称为二级指针  用来 存储一级指针的地址
}

存储地址就是为了后面能用得到的

现在对它进行解引用操作

#include <stdio.h>
int main()
{
	int a = 8;
	int b = 55;
	int* ps = &a;    //ps称为一级指针
	int** pps = &ps;    //pps称为二级指针  用来 存储一级指针的地址
	*pps = &b;
	**pps = 666;
	printf("%d\\n", b);
	printf("%d\\n", **pps);
}

*pps 通过对ppa中的地址进行解引用,这样找到的是ps,*pps 其实访问的就是ps

**pps 先通过*pps 找到ps ,然后对ps 进行解引用操作

运行结果:

 七、指针数组 

我们前面用过的整型数组拿来存放整型,字符数组里面存放字符,那么现在里面存放指针的数组就叫做指针数组

现在先简单说下,后面在指针进阶阶段还会再详细说明

通过以下代码来体验一下

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 1;
	int c = 2;
	int* arr[3] = { &a,&b,&c };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", *arr[i]);
	}
}

这里是创建了一个整型指针数组,并对它进行了初始化,里面存放有a,b,c的地址,再通过解引用去打印出a,b,c

运行结果:

 如下图:

 

 这个数组里面每个元素都是一个整型指针

-----------------------------------------------------------------

-----------------C语言操作符部分完结-------------------

关于C语言,每个知识点后面都会单独写博客更加详细的介绍

欢迎大家关注!!!

一起学习交流 !!!

让我们将编程进行到底!!!

--------------整理不易,请三连支持------------------
 

以上是关于C语言超详讲解☀️指针是个什么针?(一次性搞定指针问题)的主要内容,如果未能解决你的问题,请参考以下文章

搞定C语言指针,指针超详细讲解,及指针面试题

一篇文章搞定C语言指针,指针超详细讲解,及指针面试题

C语言学习阶段性总结!(内附超详知识导图+重点知识详解)

JavaSE数组超详讲解一次性 弄懂 数组问题

☀️☀️☀️不会还有人学不会C语言中的指针和数组吧?Chris带你走进并玩转指针与数组的世界☀️☀️☀️

彻底搞定C语言指针详解