C语言之初识指针

1.指针的介绍

🐇指针是什么?
指针理解的要点:
1.指针是内存中一个最小单元的编号,也就是地址。
2.平时口语中所说的指针,通常指的是指针变量,用来存放内存地址的变量。
3.内存是电脑上的存储设备,一般为4/8/16G。程序运行的时候会加载到内存中,会使用内存空间

总结:指针就是地址,口语中说的指针通常指的是指针变量🎠🎠🎠

图形理解:
在这里插入图片描述
指针变量
我们可以通过&(取地址操作符)取出变量的起始地址,并把这个地址存放在一个变量中,这个变量就是指针变量。
问题①:一个小的内存单元是多大?
问题②:如何编址?
C语言规定一个字节对应一个地址;对于32位机器·,假设有32根地址线,每根地址线寻址的时候产生高电平(高电压)或者低电平(低电压),在计算机用0或1表示。
那么32根地址线产生的地址如下:在这里插入图片描述
这里有2的32次方个地址,每个地址标识一个字节,我们可以给(2 ^ 32 Byte== 2 ^ 32/1024KB== 2 ^ 32/1024/1024MB == 2 ^ 32/1024/1024/1024 ==4GB) 4G的空间进行编址。
同样的原理,那64位机器,对应64根地址线,能编址16GB的空间。

总结:
①在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就是4个字节。
②在64位机器上有64个地址线,地址得用8个字节的空间来存储,这时一个指针变量的大小就是8个字节。
③指针变量是用来存放地址的,一个地址标示一个内存单元。
④指针变量在32位平台是4个字节,在64位平台是8个字节。
⑤在vs编译器下x86是32位机器的平台,x64是64位机器的平台。

2.指针和指针类型

我们都知道变量有不同的类型,整型、浮点型等。那指针变量有没有类型呢·?
准确来说:有的。
当有这样的代码:

int num=0;
p=#

要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?

	char* pc = NULL;
	int* pi = NULL;
	short* ps = NULL;
	long* pl = NULL;
	float* pf = NULL;
	double* pd = NULL;

这里我们可以看到,指针的定义方式:type(类型)+*。
char * 类型的指针就是为了存放char类型变量的地址。
short * 类型的指针就是为了存放short类型变量的地址。
int * 类型的指针就是为了存放int类型变量的地址。

2.1指针加减整数

eg:

#include<stdio.h>
int main()
{
	int n = 10;
	char* pc = (char*)&n;//把a的地址(int*类型的指针),强制转换为char*类型,char*类型只能访问一个字节。
	int* pi = &n;//int*类型可以访问4个字节
	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
	printf("%p\n", pi);
	printf("%p\n", pi + 1);
	//%p是以十六进制的形式打印地址
	return 0;
}

运行结果如下:

总结: 我们在使用中期望访问几个字节的内存,然后决定用什么样的指针类型去访问,指针类型决定了指针向前或者向后走一步有多大(距离)。

2.2指针解引用操作

eg:

#include<stdio.h>
int main()
{
	int n = 0x11223344;//0x表示十六进制的数
	char* pc = (char*)&n;
	int* pi = &n;//重点观察在调试过程中内存的变化
	*pc = 0;   //访问1个字节
	*pi = 0;   //访问4个字节
	return 0;
}

在这里插入图片描述
调试结果如下:


*pc=0执行后:
在这里插入图片描述
*pi=0执行后:
在这里插入图片描述
总结: 📸指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。比如:char * 的指针解引用只能访问一个字节,而int * 的指针的解引用能访问四个字节。

3.野指针

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

3.1野指针产生的原因

1.指针未初始化

#include<stdio.h>
int main()
{
	int* p;//局部变量指针未初始化,里面的值为随机值
	*p = 20;
	return 0;
}

在这里插入图片描述
注意: 我们在定义指针变量的时候应该进行初始化,即令指针变量等于空指针或者某个变量的地址(int *p=NULL或 int *p=&a)。

2.数组越界访问

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;
		//当指针指向的范围超出数组arr的范围时,p就是野指针
	}
	return 0;
}

程序运行的结果:
在这里插入图片描述
注意: 我们存在指针变量的程序时,应该注意指针越界。

3.指针指向的空间释放

这里指的动态内存函数malloc、calloc、realloc在使用的时候,需要开辟内存空间,函数返回的值用指针接收,使用完之后,应释放掉指针指向的空间,并把指针置为空指针。在这里我们只需要简单了解一下即可,在后面的C语言进阶栏目中我们将详细介绍。

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* ptr = NULL;
	ptr = (int*)malloc(10 * sizeof(int));
	if (NULL != ptr)
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(ptr+ i) = 0;
		}
	}
	free(ptr);//释放掉ptr所指向的动态内存空间
	ptr = NULL;//指针置空
	return 0;
}

3.2如何避免野指针

1.指针初始化。

2.注意指针越界问题。

3.指针指向空间释放,及时置空指针。

4.避免返回局部变量的地址。

eg1:

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

代码运行的结果:
在这里插入图片描述

上面的代码中存在错误,函数test使用完之后,该函数开辟的空间就销毁了,即还给操作系统了。在主函数main中再用int *p来接收a的地址,并把用printf函数打印出来是不合法的。那为什么还能把a=10打印出来呢?这是因为操作系统虽然回收了test函数开辟的空间,但是并没有去使用它,恰好该地址存放的值还是10。
那我们稍微改动一下代码看看结果又如何?

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

代码执行结果如下:
在这里插入图片描述
我们看到曾经a的地址里面现在是一个随机值,这是因为操作系统为int i=20开辟了空间,让曾经a的地址中的值被覆盖掉了。这是有关函数栈帧的内容,感兴趣的可以看一下我的这篇博客:函数栈帧的创建和销毁

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

eg2:

#include<stdio.h>
int main()
{
	int* p = NULL;
	int a = 10;
	p = &a;
	if (p != NULL)//判断p是否为空指针
	{
		*p = 20;
	}
	return 0;
}
#include<stdio.h>
#include<assert.h>
int main()
{
	int* p = NULL;
	int a = 10;
	p = &a;
	assert(p);
	//断言assert中可以放一个表达式
	//表达式的结果为假,就报错,如果为真啥事都不发生
	//空指针NULL的本质是0
	*p = 20;
	return 0;
}

4.指针的运算

4.1指针±指针

eg:

#define N 5
#include<stdio.h>
int main()
{
	float values[N] = { 0 };
	float* vp = NULL;
	for (vp = &values; vp < &values[N];)
	{
		*vp++ = 1;//后置++,先使用,再+1
		//等价于*vp=1;vp++;
	}
	vp = &values;
	int i = 0;
	for (i = 0; i < N; i++)
	{
		printf("%.1f ", *(vp + i));
	}
	return 0;
}

代码运行结果为:
在这里插入图片描述

在这里插入图片描述
总结: 指针加减整数n,指针向前或者向后跳过n个该类型的字节数,指向另一块空间。

4.2指针-指针

eg:

//用指针-指针模拟strlen函数
#include<stdio.h>
int my_strlen(char* p)
{
	char* start = p;
	while (*p)
	{
		p++;
	}
	return p - start;
}
int main()
{
	char arr[] = "abcdef";
	int ret= my_strlen(arr);
	printf("%d", ret);
	return 0;
}

代码执行结果如下:
在这里插入图片描述
在这里插入图片描述
总结: 在两个指针指向同一块空间的前提下,指针减指针得到的是两个指针之间的元素个数;如果两个指针不是指向同一块空间,指针减指针得到的结果无实际意义。

4.3指针的关系运算

eg:

#define N 5
#include<stdio.h>
int main()
{
	int arr[N] = { 1,2,3,4,5 };
	int* p = NULL;
	for (p = &arr[5]; p > &arr[0];)
	{
		*--p = 0;//等价于p--;*p
	}
	p = arr;
	int i = 0;
	for (i = 0; i < N; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

代码原理:
在这里插入图片描述

代码运行结果:
在这里插入图片描述
代码简化eg:

#define N 5
#include<stdio.h>
int main()
{
	int arr[N] = { 1,2,3,4,5 };
	int* p = NULL;
	for (p = &arr[N-1]; p >= &arr[0];p--)
	{
		*p=0;
	}
	p = arr;
	int i = 0;
	for (i = 0; i < N; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

简化代码原理:
在这里插入图片描述
简化代码运行结果:
在这里插入图片描述
简化代码中,虽然容易让人理解并且在绝大部分编译器上是可以顺利完成任务的,然而我们应该避免这样写,因为标准并不保证它可行。就是你觉得它可行,它不一定合法(符合语法)。
标准规定:

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

5.指针和数组

eg1:

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

代码运行结果为:
在这里插入图片描述
①从上面的运行结果可以看出,一维数组的数组名表示数组首元素的地址,但有两种特殊情况如下:

1.sizeof(数组名),这里的数组名指的是整个数组,计算的是整个数组的大小,单位是字节。
2.&数组名,这里的数组名表示整个数组的地址,即取出的是数组的地址。

②指针和数组是不同的对象,指针是一种变量,可以存放地址,大小4/8个字节;数组是一组相同类型元素的集合,可以放多个元素,大小取决于元素个数和元素的类型。
③地址可以存放在指针变量中,可以通过指针变量访问数组。
eg2:

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

代码运行结果:
在这里插入图片描述
④C语言中[ ]是一个双目操作符,i和arr是两个操作数,可以像a+b=b+a一样执行的加法交换律,再从编译器的执行代码的结果,可知arr[i]<=> * (arr+i)<=> * (i+arr)<=>i[arr]。
eg3:

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

代码运行结果:
在这里插入图片描述
⑤代码中p+i计算的是数组下标为i的地址,我们可以通过指针来访问数组。
eg4:

#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;//存放一维数组首元素地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ",*(p+i));
	}
	return 0;
}

代码运行结果:
在这里插入图片描述

6.二级指针

二级指针是什么呢?指针变量也是变量,是变量就有地址,那么二级指针就是用来存放指针变量(一级指针变量)的。
在这里插入图片描述
eg:

#include<stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;//一级指针
	//pa旁边的*说明变量pa是指针,前面的int说明该指针指向的变量是int类型
	int** ppa = &pa;//二级指针
	//ppa旁边的* 说明变量ppa是指针,前面的int*说明该指针指向的变量是int*类型
	int*** pppa = &ppa;//三级指针
	//ppa旁边的* 说明变量pppa是指针,前面的int**说明该指针指向的变量是int**类型
	return 0;

二级指针的解引用运算

  • *ppa通过ppa中的地址进行解引用,这样找到的是pa,*ppa访问的就是pa。
  • **pa先通过*ppa找到pa,然后对pa进行解引用操作,*pa访问就是a。
#include<stdio.h>
int main()
{
	int a = 20;
	int* pa = &a;
	int** ppa = &pa;
	//*ppa<==>pa,**pa<==>*pa<==>a
	**ppa = 10;
	printf("%d\n", **ppa);
	printf("%d\n", a);
	return 0;
}

代码运行的结果如下:
在这里插入图片描述

7.指针数组

指针数组是指针还是数组呢?

那我们类比之前学过的char arr[ ]字符数组—用来存放字符的数组;
int arr[ ]整型数组—用来存放整型的数组;
顾名思义,指针数组就是用来存放指针的数组。

指针数组的形式:

#include<stdio.h>
int main()
{
	int* arr[] = {NULL};
	//arr[]说明arr为数组,int*则说明数组每个元素类型是int*类型
}

为了更好的理解指针数组,下面我们使用一维指针数组,模拟实现二维数组。

#include<stdio.h>
int main()
{
	//模拟一个三行四列的二维数组
	int a[] = { 1,2,3,4 };
	int b[] = { 5,6,7,8 };
	int c[] = { 9,10,11,12 };
	int* arr[3] = { a,b,c };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}return 0;
}

代码运行结果:
在这里插入图片描述
图形理解:
在这里插入图片描述
注意:如果没有把某变量的地址存在指针变量中,则该变量的地址不会占用内存空间(即变量才会占内存空间),就比如int a=10,a的地址本身就存在了,但是a变量的地址没有(int* p=&a)放到指针变量中,所以变量a的地址不会占用内存空间;变量名本身不占内存空间,我们命名变量是给程序员看的,而编译器在执行过程中拿到的是该变量的地址,而不是变量名本身。

总结

本章我们学习了

①什么是指针?
②指针和指针类型
③野指针含义、成因及规避方法
④指针的运算
⑤指针和数组的关系
⑥什么是二级指针?
⑦什么是指针数组?

希望对大家初识指针有些许帮助!若有不对的地方,欢迎大家指正!最后感谢大家的阅读!

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值