深入理解C语言之精髓——指针两万字全解!

前言

指针是C语言重要的特色,也是C语言的重难点部分,对于很多人来说也是个痛点。刚接触指针的人,如果没有一个正确的学习方向,学起来是比较痛苦的。而对于接触过的人,有些知识点掌握的可能也是懵懵懂懂的状态。本篇文章共有13节,每一节我都尽可能的去解释好对应的知识点,以及对于一些知识点,我会用图解的方式进行辅助理解。
如果你是初学者,我希望你可以耐心的看完每个知识点,并且一定要理解前面的知识后才去学习后面的知识,千万不可着急。
如果你是接触过指针,可以自行选择想要观看的内容,或者温故而知新即可。

若在阅读过程中,有不理解的地方、或者错误的地方都可以评论或私信我!话不多说,接下来就开始指针之旅!

1、初识指针

我们知道,学校的宿舍楼、酒店的房间通常都有房间号。有了这些房间号,就可以很方便地进行住宿管理。计算机的内存空间就像一栋房,里面有连续的内存空间来存放数据。每个内存空间都有自己的编号,也称为地址。有了这些内存单元的地址,计算机系统就可以快速方便地存储和管理数据。
1.1数据的存储:

了解指针之前,我们须弄清楚数据在内存中是怎样存放的。
计算机中的内存是由很多的单元组成的,而这些单元存储的都是数据,这些单元都是以字节为单位,连续的单元(字节)就是一块内存空间。为了正确的访问这些内存单元(存储的数据),必须给每个内存单元编号,然后通过这些编号即可准确地找到该对应的内存单元。而这些编号也叫作地址,通常也称为指针。如下图:
在这里插入图片描述
上图中,数据20存放在a变量中的,当把a的地址赋给一个p变量,此时p变量就称为指针变量!我们可以通过操作p指针变量来间接操作a变量。

1.2指针变量的定义:

指针变量是专门用来存放地址的,所以我们可以通过&(取地址操作符)取出变量在内存的首地址,然后把该地址放到另一个变量中,那么这个变量就是指针变量,即存放地址的变量。

注意:存放在指针中的值都被当成地址处理

#include<stdio.h>
int main()
{
   
   
	int a = 10; //为变量a在内存中开辟一块空间用来存数据10

	int *p = &a;//a为4个字节,有4个地址。
				//这里是把变量a的首地址放到变量p中的,此时p就是一个指针变量。
				//int表示p指向的a类型是int,*告诉我们p是个指针变量。

	//存放在指针中的值都被当成地址处理:
	int *pc = 10;//把一个非地址的值存放进指针变量,
				 //此时就会把10强行改成一个地址,而这个地址
				 //并不属于你本身的,这是极其危险的行为!			 
	return 0;
}

1.3指针变量的使用:

#include<stdio.h>
int main()
{
   
   
	int  a = 10; 
	int *p = &a;
	*p = 20;// 指针p存着a的地址,即p == &a,
			// 当*p时,相当于*&a,*&抵消,剩下a,
			// 即*p == a,所以改变*p就是改变a。
			
	printf("%d",a);//结果为20;
						 
	return 0;
}

其中 * 表示间接寻址运算符,如*p就表示找到p所指向的地址,然后对该地址 * 解引用,就找到其内容。
&表示取地址运算符,即取出其地址。可以把 和& 想象成一个是来一个是去,当它们在一起时就会相互抵消。
我们需要注意的是,如果是在声明处 ,
* 只是表示这是一个指针,而不是解引用。如下:

int a = 10;
//声明处的 *
int *p = &a;//这里的*先和p结合,告诉我们p是个指针,而不是对其&a进行*解引用。

1.4指针变量的编址:

一个小的单元是1个字节,且经过计算和权衡我们发现一个字节给一个对应的地址是比较合适的,如下图:
在这里插入图片描述

每个地址标识一个单元(1个字节),那我们就可以给
(2^32Byte == 2^32/1024KB ==2^32/1024/1024MB ==2^32/1024/1024/1024GB == 4GB) 4G的空间进行编址。

所以在32位机器上,一个地址是由32位0或1二进制序列组成的,而1Byte=8bit,那一个地址就需要4个字节的空间来存储。而一个指针变量只能存储一个地址,所以一个指针变量的大小就应该为4个字节。
( 64位机器可自行计算,原理都是一样的,这里就不展示了)。

1.5总结:

指针变量是用来存放地址的,通过地址可以找到对应的内存单元。
指针的大小在32位平台是4个字节,在64位平台是8个字节。

2、指针类型

我们都知道,变量有不同的类型,如整形,浮点型等,而指针同样也有类型。

普通类型

int a;//类型为int
float b;//类型为float
char c;//类型为char

通过观察就会发现,只要把变量名去掉,剩下的就是类型!
所以同样的,指针的类型我们只需要去掉变量名即可:

#include<stdio.h>
int main()
{
   
   
	int a = 10;//类型为int
	int b = 20;//类型为int
	int*pa= &a;//类型为int*
	int*pb= &b;//类型为int*
}

2.1指针类型的意义:
2.1.1指针类型决定了在对p解引用操作时可以访问多大字节(即可以修改多大字节的空间)。在这里插入图片描述

2.1.2指针类型决定了地址向前( - ) / 向后( + )一次走多少个字节的空间。

在这里插入图片描述
无论是指针类型还是什么类型,去掉名字剩下的就是类型。当以后遇到指针或者其他变量、数组、函数等时,我们应该问一问自己类型是什么?。只有明白了类型之后,我们才能更好的使用指针去对其操作。并且类型的理解,对后面的内容体现的是很明显的。

3、野指针

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

3.1未初始化指针

#include<stdio.h>
int main()
{
   
   
	//局部变量指针未初始化,默认为随机值
	int* p;//这里p就是个野指针,因为指向的位置是随机值
	*p =10;//*p对其解引用操作会对把该随机值当成一个地址,
		   //然后在把10存放到该地址,但这个地址不属于我们当前程序的。

	return 0;
}

3.2指针越界访问
在这里插入图片描述

3.3指针指向已释放的空间
在这里插入图片描述

3.4如何规避野指针:

  1. 指针初始化:
#include<stdio.h>
int main()
{
   
   
	//(1)
	int  a = 10;
	int *p = &a;//明确初始化(明确指向a)

	//(2)
	int *p = NULL;//把指针p初始化成空指针
				  //当不知道指针指向哪里的时候可以初始化成NULL
				  //NULL就是用来初始化指针的
				  //而NULL的本质就是0

	return 0;
}

  1. 注意指针越界
  2. 指针指向空间释放,及时置NULL(后面会详细讲更多相关内容)
  3. 避免返回局部变量(栈空间)的地址(因为栈空间上的一些地址,出了函数空间就被释放了)
  4. 指针使用之前检查有效性(重):
#include<stdio.h>
int main()
{
   
   
	//(1)
	int *p=NULL;
	printf("%d",*p);//强行解引用访问会把NULL(0)强行改成地址,对于NULL(0)指向的空间是不能访问的,此时程序就会崩掉

	//所以我们使用指针前应该检查
	//(2)
	int *p=NULL;
	
	if(p != NULL)//判断指针是个有效指针而不是空指针时才进行操作,前提是要在判断之前要确保指针初始化
	{
   
   
		printf("%d",*p);
	}
	
	//(3)
	//也可以这样
	int  a = 10;
	int* p = &a; 
	
	if(p != NULL)//判断指针是个有效指针而不是空指针时才进行操作,前提是要在判断之前要确保指针初始化
	{
   
   
		printf("%d",*p);
	}

	//(4)
	//对于空指针,我们是不能直接进行解引用操作的

	//错误写法
	int* p = NULL;
	*p = 10;//error,空指针是不能访问的,解引用修改值程序就会崩掉
	
	//正确写法
	int* p = NULL;
	int  a = 10;
	p = &a;
	*p = 20;
	
	return 0;
}

以上几点在一定程度上可以避免野指针的出现,但关于如何避免野指针的问题不止这几点,只有我们在写代码的过程中不断的积累经验和学习,功底越来越厚之后,才能更好的避免野指针的出现。

4、指针运算

4.1指针± 整数
在这里插入图片描述

4.2指针的关系运算
若指针要进行关系运算,前提是同时指向一块空间。若不是同时指向同一块空间,比较将无意义!

#include<stdio.h>
int main()
{
   
   
	//(1)
	//无意义的比较
	int Arr1[5];
	int Arr2[10];
	int* p1 = &Arr1[0];
	int* p2 = &Arr2[9];
	
	if(p1<p2)//条件不成立,因为两个指针没有同时指向一块空间,此时比较的两个指针(地址)也将毫无意义
	{
   
   
		printf("0");//不会输出0
	}

	//(2)
	//通常的比较
	//例1
	int a[5];
	int* p1 = &a[0];
	int* p2 = &a[4];

	if(p1 < p2)//&a[0]的地址 < &a[4]的地址,所以条件成立!
	{
   
   
		printf("1");//成功输出1
	}

	//例2
	int arr[5]={
   
   1,2,3,4,5};
	int *p = &arr[4];
	for(p; p >= &arr[0];p--)
	{
   
   
		printf("%d ",*p);
	}
	return 0;
}

上述的例2,实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

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

图解:
在这里插入图片描述

4.3指针 - 指针
1、需同时指向一块连续的空间,否则结果是无法预测的(结果取决于编译器)。
2、指针 - 指针的绝对值是指针和指针之间的元素个数。
3、指针 + 指针无意义,且程序会出现错误。
在这里插入图片描述

5、二级指针

指针变量也是变量,它也有自己的地址,而当指针变量的地址存放在另一个变量里时,此时该变量就是二级指针 。

在这里插入图片描述

6、指针数组

指针数组是数组,是用来存放指针的数组(存放地址的数组)。

6.1指针数组的声明:

#include<stdio.h>
int main()
{
   
   
	int a = 10;
	int b = 20;
	int* parr[2] = {
   
   &a,&b};//[2]先与parr结合成为一个数组,数组有2个元素,
						   //之后的* int表示数组的每个元素是指针,
						   //所以这是个指针数组,去掉数组名,剩下的就是类型
						   //所以该指针数组的类型为int*  [2]
	return 0;
}

6.2指针数组的使用:
在这里插入图片描述

以上是指针初级主题的内容,当我们了解了以上内容之后,接下来才能更好的了解下面的高级主题内容。

7、字符指针

字符指针是个指针,是用来存放字符的指针。

7.1字符指针的声明:
7.1.1一般声明:

#include<stdio.h>
int main()
{
   
   
	char  ch = 'h';
	char* pc = &ch;
}

7.1.2另外一种声明:

#include<stdio.h>
int main()
{
   
   
	char* pc = "abcdef";
}

7.2字符指针的使用:

7.2.1一般使用:

#include<stdio.h>
int main()
{
   
   
	char c = 's';
	char* pc = &c;
	*pc = 't';
	printf("%c",c);//结果为 t
	return 0;
}

7.2.2另外一种使用:

#include<stdio.h>
int main()
{
   
   
	const char* pc = "hello";//pc是字符指针,且是字符串常量(不可修改的字符串),
							 //所以我们加上const来修饰,使其不可修改。
							 
	printf("%c\n", *pc);//结果为h
	printf("%s",  pc);//结果为 hello
	return 0;
}

图解:
在这里插入图片描述

不是说指针存放的是地址吗? 为什么这里存的是字符串?如上述的 pc = “hello”,其实C语言中编译器会给字符串常量分配地址,所以pc = "hello"就是个地址,即本质就是pc = “hello” = 0xffffff40所以字符指针指向的是首字符的地址,而不是把整个字符串存放进指针。

7.3字符数组和字符指针的区别:

接下来我们来比较一下下面两个看起来很相似的代码。

	char  str1[] = "hello";
	char  *Str2  = "hello";

前者是声明str1是一个字符数组,后者是声明Str2是一个字符指针。
上面的这两个声明都可以用作字符串,但需要注意的是,不能错误地认为上面的str1和Str2可以互换或等价,字符数组和字符指针之间是有很大的差别的。

7.3.1区别1:

在声明数组时,就像任意数组元素一样,可以修改存储在数组中的字符。而在声明为字符指针时,指针指向的是字符串常量,而字符串常量是不可修改的。

	#include<stdio.h>
	int main()
	{
   
   	
		//数组可以任意修改数组元素的内容
		char  data[7] = "hello";//data是数组名
		data[0] = 'E';//把数组中的第一个元素改成H
		
		//字符指针
		//因为字符串常量是不可修改的,所以我们为严谨起见最好加上const来修饰。
		const char *Data  = "hello";//Data是指针变量,指向的是字符串常量
		*Data = "ABC";//error,因字符串常量不可修改
		return 0;
	}

7.3.2区别2:

在声明为数组时,data是数组名(数组名是首元素地址)。在声明为指针时,Data是指针变量,而这个变量可以在程序执行期间指向其他字符串。

	#include<stdio.h>
	int main()
	{
   
   	
		char  data[] = "hello";//data是数组名
		
		char  *Data  = "hello";//Data是指针
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值