C语言精华 - 指针初识篇

Posted 跳动的bit

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言精华 - 指针初识篇相关的知识,希望对你有一定的参考价值。

前言

在之前的文章中有简单介绍指针,但在C语言初识这个专栏里也不会深入,在未来C语言进阶这个专栏会详细了解

一、指针是什么

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

#include<stdio.h>
int main()
{
	int a = 10;//a占4个字节,1个字节对应1个编号,a有4个地址,如果用4个地址去访问a比较麻烦
	int* pa = &a;//&a拿到的是a的4个字节中每一个字节的地址。通过int*类型的pa存储a的地址
	*pa = 20;//再通过*解引用操作去访问a
	return 0;
}

一个小的单元是多大?以及如何编址?

经过仔细计算和权衡我们发现一个字节对应一个地址是比较合适的
对于32位的机器,假设有32根地址线,那么假设每根地址线寻址产生一个电信号正电/负电(1或0)

假设一个内存单元是1bit - 2^32bit:在这里插入图片描述
如果给每个bit都有一个地址 - 太浪费了。经过平衡后,以一个字节为内存单元,然后分配地址
这里就有2的32次方个地址。 每个地址标识一个字节,那么:
在这里插入图片描述
同理64位也是一样
在32位机器上,地址是32个0或者1组成的二进制序列,那地址就得用4个字节的空间来存储所以一个指针变量的大小就是4个字节
在64位机器上,如果有64根地址线,只有一个指针变量的大小是8个字节,才能存放一个地址
小结:1.每个地址标识一个字节 &emsp;&emsp;&emsp;2.指针的大小在32位平台是4个字节,在64位平台是8个字节

二、指针和指针类型

1、指针类型

不同类型的数据交给指针时也要使用不同的指针类型去存储
由以下代码发现:不同的指针类型都是4个字节或8个字节,那么指针类型是否有存在的必要?

#include<stdio.h>
int main()
{
	int* pa;
	char* pc;
	float* pf;

	printf("%d\\n", sizeof(pa));//4
	printf("%d\\n", sizeof(pc));//4
	printf("%d\\n", sizeof(pf));//4
	return 0;
}

2、指针类型的意义

先了解一下:1个十六进制位是4个二进制位
0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f  -> 十六进制位
1 1 1 1 1 1 1 1 -> 二进制位
     8 4 2 1 = 15 = f
所以说4个二进制位是1个十六进制位,1个字节是2个十六进制位

#include<stdio.h>
int main01()
{
	
	int a = 0x11223344;
	int* pa = &a;
	*pa = 0;
	return 0;
}
//--------------------------------------------------
int main02()
{
	int a = 0x11223344;
	char* pc = &a;
	*pc = 0;
	return 0;
}

这里使用int和char类型的指针存储a的值。调试发现:同一个值,被不同类型指针存储后,在解引用操作时,它们的访问权限不一样
在这里插入图片描述


为了能够更直观的认识指针类型的意义,再看以下代码:

#include<stdio.h>
int main()
{
	int arr[10] = {0};
	int* p1 = arr;
	char* p2 = arr;
	printf("%p\\n", p1);
	printf("%p\\n", p1 + 1);
	printf("----------分割线---------\\n");
	printf("%p\\n", p2);
	printf("%p\\n", p2 + 1);
	return 0;
}

这里使用int和char类型的指针存储arr数组首元素的地址。运行发现:被不同类型指针存储后,统一加1后的结果不同这里是引用

小结:1、指针类型决定了指针解引用的权限有多大 &emsp;&emsp;&emsp;2、指针类型决定了指针走一步,能走多远(步长)** &emsp;&emsp;&emsp;**3、int类型指针+1跳过4个字节;char类型指针+1跳过1个字节;数组类型指针+1跳过过1个数组(暂时不了解)

3、简单应用

/***********************************************************************
目的:借助指针输出将数组里的元素1 - 10输出
分析:使用int*类型的指针存储数组首地址,并利用解引用操作符
平台:Visual studio 2017 && windows
***********************************************************************/

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

输出结果:在这里插入图片描述


相反,如果使用char*类型的指针去存储:每次加1后所走的步长不同

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

输出结果:
在这里插入图片描述

三、野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。以下代码列举几个野指针

1、野指针的产生

#include<stdio.h>
//1、
int main01()
{  
	int* p;//p是一个局部的指针变量,局部变量不初始化的话,默认是随机值 
	*p = 20;//非法访问内存
	return 0;
}

//2、
int main02()
{
	int arr[10] = {0};
	int* p = arr;
	int i = 0;
	for(i = 0; i <= 10; i++)//当i = 10时,此时的p就是野指针,再去解引用的话就是非法访问内存
	{
		*p = i;
		p++;
	}
}

//3、
int* test()
{
	int a = 10;
	return &a;
}
int main03()
{
	int* p = test(); //a是局部变量,一旦test函数执行完成,test函数被销毁,变量a被释放。p虽然拿到了a的地址,但是指向的空间就是未知的 
	*p = 20;//非法访问内存
	return 0;
}


小结: 1、指针变量未初始化的情况是野指针
   2、指针指向的数组越界后是野指针
   3、指针去接收一个函数的局部变量的返回地址时,这个程序结束,函数销毁,指针指向的空间被释放,也会导致野指针

2、如何规避野指针

#include<stdio.h>
//1、
int main01()
{
	//`1.明确知道要初始化的值时:
	int a1 = 10;
	int* p1 = &a1;
	
	//2.不明确知道要初始化的值时
	//养成好的习惯,定义变量的时候对变量初始化为0
	int a = 0;
	//而指针可以初始化为NULL(空)
	int* p = NULL;
	return 0;
}
//2、
int main02()
{
	int arr[10] = {0};
	int* p = arr;
	int i = 0;
	for(i = 0; i < 10; i++)//C语言本身是不会检查数组越界的,要保证数组不越界
	{
		*p = i;
		p++;
	}
}
//3、
int* test()
{
	int a = 10;
	return &a;
}
int main03()
{
	int* p = test(); 
	p = NULL;//将p置为空指针
	*p = 20;//err
	return 0;
}
//4、
int main04()
{
	int* p = NULL;
	if(p != NULL)
		*P = 20;
	return 0;
}

总结: 1、初始化变量
   2、注意不要数组越界
   3、指针指向的空间释放后及时置为NULL
   4、指针使用之前检查有效性

四、指针运算

1、利用指针进行简单运算

#define N_VALUES 5
#include<stdio.h>
//1、使用指针运算将数组从前往后被初始化为0
int main01()
{
	float values[N_VALUES];
	float* vp;
	for(vp = &values[0]; vp < &values[N_VALUES];)//指针的关系运算
	{
		*vp++ = 0;//指针加减运算
	}
	return 0;
}
//2、使用指针运算将数组从后往前被初始化为0
int main02()
{
	float values[N_VALUES];
	float* vp;
	for(vp = &values[N_VALUES]; vp > &values[0];)
	{
		*--vp = 0;
	}
	return 0;
}
//3、将main02代码简化一点
int main03()
{
	float values[N_VALUES];
	float* vp;
	for(vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
	{
		*vp = 0;
	}
	return 0;
}

观察main02和main03
main02和main03的功能是一样的,都能初始化数组为0,且main03相对来说更容易理解
在这里插入图片描述

实际上main03在绝大部分的编译器上是可以完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行
标准规定:允许指向数组元素的指针与指向数组最后1个元素后面的那个内存位置的指针比较,但是不允许与指向第1个元素之前的那个内存位置的指针进行比较


#include<stdio.h>
//1、利用指针运算打印数组元素
int main01()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int* p = arr;//将数组的第1个元素的地址交给p
	int* pend = arr + 9;//将数组的最后1个元素的地址交给pend
	while(p <= pend)//利用数组的地址 -> 从低到高
	{
		printf("%d ", *p);//1 2 3 4 5 6 7 8 9 10
		p++;
	}
	return 0;
}
//2、指针相减
int main02()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	//指针相减运算 -> 指针-指针 = 2个指针之间的元素个数 -> 但前提是2个指针指向同一块空间
	printf("%d\\n", &arr[9] - &arr[0]);//9
	//细想一下,其实指针+指针其实是没有意义的
	return 0;
}

2、简单应用

/***********************************************************************

目的:使用指针与指针的运算模拟strlen
分析:找到目标字符串\\0的位置和首元素地址相减即可
平台:Visual studio 2017 && windows
*************************************************************************/

#include<stdio.h>
int my_strlen(char* str)
{
	//备份1份首地址
	char* start = str;
	while(*str)
	{
		str++;
	}
	return str - start;
}
int main()
{
	int len = my_strlen("abc");//传过去的仅仅是a的地址
	printf("%d\\n", len);
	return 0;
}

五、指针和数组

1、指针和数组的关联

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

2、更深层次的了解指针和数组

#include<stdio.h>
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10}; 
	int* p = arr;
	//打印第3个元素:
	printf("%d\\n", arr[2]);
	printf("%d\\n", 2[arr]);
	printf("%d\\n", p[2]);
	//arr[2] --> *(arr + 2) --> *(2 + arr) --> 2[arr]
	//p[2] --> *(p + 2)
	
	
	//可推出:
	//arr[2] <==> *(arr + 2) <==> *(p + 2) <==> *(2 + p) <==> *(2 + arr) <==> 2[arr]
	return 0;
}

六、二级指针

#include<stdio.h>
int main()
{
	int a = 10;
	
	int* pa = &a;//此时pa指向变量a的地址时,pa为一级指针变量
	
	int** ppa = &pa;//同时pa也是个变量||地址,此时ppa去指向pa的地址时,ppa为二级指针变量


	int*** pppa = &ppa;//此时pppa为三级指针变量
	
	//当然还有四级指针、五级指针...。语法是支持的,但是并不常用(三级指针也很少用到)

	//怎么通过ppa找到a -> *ppa <=> pa, *pa <=> a, **ppa <=> a
	printf("%d\\n", **(ppa));
	return 0;
}

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

七、指针数组

#include<stdio. h>
int main()
{
	int arr[10];//整型数组 - 存放整型的数组就是整型数组
	char ch[5];//字符数组 - 存放字符的数组就是字符数组
	
	//指针数组 - 存放指针的数组就是指针数组
	int* parr1[5];//整型指针数组
	char* parr2[5];//字符指针数组
	return 0;
}

八、指针进阶

这里将附上对于指针更深层次的文章
指针进阶篇

以上是关于C语言精华 - 指针初识篇的主要内容,如果未能解决你的问题,请参考以下文章

C语言进阶之旅(11)指针进阶上 精华篇

知识分享:C语言知识系列——指针篇

从初识到进阶,硬核解说C语言< 初识篇 2 >

C语言从入门到入土(入门篇P3)

《C语言深度剖析》第四章 指针和数组 p2 C语言从入门到入土(进阶篇)

初识c语言