C语言指针初阶

本文声明>由于作者水平有限,本文难免有理解不正确之处,如有发现,恳请读者批评指出。


一、指针的概念

在计算机科学中,指针是编程语言中的一个对象。它直接指向存在电脑存储器中另一个地方的值,通过它可以直接找到所需的变量单元,而在内存中,我们通过地址也可以直接找到变量单元,因此,又将地址形象化的称为“指针”。

要真正的理解指针的概念,我们要先了解变量在内存中如何存储的。变量在内存中存储时,内存被分为一块一块的,每一块都有一个特有的编号,而这个编号,就是内存的地址,也可以暂时理解为指针。

1.1 变量与地址

先看一段代码

int main()
{
	int a = 10, b = 20;

	return 0;
}

以上是十分简单的一段代码,声明了两个变量a和b,并分别赋值为10和20。那a和b在内存中是如何存储的呢?
在这里插入图片描述
从上图我们可以更加形象的看出内存是一块连续的空间,每块空间都有一个特有的编号,这个编号也称为地址,那么我们就可以说地址就是内存单元的编号

那当我们改变a和b的值,内存中是如何变化的呢?

int main()
{
	int a = 10, b = 20;

	a = 30;
	b = 50;

	printf("a = %d, b = %d\n",a, b);

	return 0;
}

在这里插入图片描述
可以看出变量的本质其实就是一块可以操纵的内存空间,当我们改变a和b这两个变量的值时候,我们本质上是改变的是内存空间里的值。

1.2 指针与地址

指针我们暂时可以理解为就是地址,而地址是内存单元的编号,那么指针也就是指向了某一块的内存空间。

1.3 指针的大小

  • 在32位平台上是4个字节
  • 在64位平台上是8个字节

在这里插入图片描述

1.3 指针变量和指针的类型

指针变量就是一个变量,它存储的内容是一块内存单元的编号,从而指向了这块内存单元,那么通过这个指针变量我们就可以找到这块内存单元。通常情况下,我们习惯把指针变量叫做了指针。

当我们定义一个变量的时候要确定它的类型。如:

int x = 0 ;
char ch = 10;
float f = 0.0f ;

在定义指针变量的时候,也是一样的,必须确定指针变量的类型。通常情况下,什么类型变量的指针,就用什么类型的指针来存储,即,int类型变量的指针需要用int类型的指针存储。

	// 分别声明了int、float、char类型的指针变量
	int *pi;
	float *pf;
	char *pc;

注意上面的指针变量名分别是pi 、pf、pc,而并非*pi、*pf、*pc。

定义指针变量并初始化

int main()
{
	int a = 10, b = 20;

	// 定义两个指针变量分别存储变量a和b的地址
	int* pa = &a;
	int* pb = &b;

	// 初始化为NULL
	char* pc = NULL;

	// 连续定义多个指针
	int *px, *py, *pz;

	return 0;
}

注意定义连续多个指针变量,并非int px, py, pz ;这样定义 py、pz只是普通的int类型变量。*

&这个符号是用来取操作对象的地址,叫做取地址运算符;例如 &a 为取变量a的地址。对于常量表达式、寄存器变量不能取地址。

相对于 & 这个运算符 ,还有一个和它互为逆运算的运算符,间接访问运算符 * ,作用是通过操作对象的地址,获取存储的内容,例如:int* pa = &a ; 指针变量pa存储的是a的地址,*pa则为通过a的地址获取a的内容。

int main()
{
	int a = 10, b = 20;

	// 定义两个指针变量分别存储变量a和b的地址
	int* pa = &a;
	int* pb = &b;

	// 通过间接访问运算符*来获得变量a的内容并修改为50
	*pa = 50;  

	printf("a = %d, b = %d\n", *pa, *pb);

	return 0;
}

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

上面我们知道指针的定义方式是:类型 + * 。 并且也知道了,通常情况下,什么类型变量的地址就用什么类型的指针变量来存储。那么,这个指针的类型到底有什么意义呢?如果int类型变量的地址,我用 char* 类型的指针来存储会怎么样呢? 接下来我们来看看。

如下图,我们使用char*可以接收int类型变量的地址,那在内存中的情况是怎样的呢?
在这里插入图片描述
接下来我们在看一组操作

使用int类型接收int类型变量地址
在这里插入图片描述
使用char
类型接收int类型变量地址

在这里插入图片描述

我们可以看到只是改变了int类型变量的一个字节的大小。

由此我们可以发现了指针类型的第一个意义就是,但我们进行间接访问的时候,指针的类型决定了一次可以访问几个字节

我们再看一段代码
在这里插入图片描述

通过以上代码我们可以看出指针的类型也确定了指针向前或者向后的步长,即向前或者向后走一步有多大的距离

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

总结:

指针类型决定了

  1. 指针解引用(间接访问)操作的时候,一次可以访问多少个字节
  2. 指针加减整数的步长

二、 野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不确定的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的。指针变量值可能是别的变量的地址,这也就意味着指针指向了一个地址是不确定的变量,此时间接访问操作就是去访问了一个不确定的地址,所以结果是不可知的。(百度百科)

2.1 野指针的成因

指针未初始化

int main()
{
	// 局部指针变量未初始化,其值是随机的
	int* ptr; 

	// 当我们间接访问这块内存时,就造成了非法的访问
	// 原因就是这个地址是随机的,那么这个空间就不属于我们当前的程序
	*ptr = 100;

	return 0;
}

指针的越界访问

在这里插入图片描述

2.2 如何避免野指针

  1. 指针一定要初始化
  2. 小心指针越界
  3. 指针指向的空间释放就将其置为NULL
  4. 指针使用之前检查有效性

三、指针的运算

3.1 指针 + - 整数

在这里插入图片描述

3.2 指针 - 指针

观察如下代码
在这里插入图片描述
图示:
在这里插入图片描述
如图所示,指针-指针的到的是两个指针间的元素个数。

利用指针-指针来模拟实现strlen函数

int my_strlen(char* str)
{
	char* start = str;

	while (*str != '\0')
	{
		str++;
	}

	return str - start;
}
int main()
{
	char ch[] = "abcdef";

	int len = my_strlen(ch);

	printf("len = %d\n", len);

	return 0;
}

四、指针与数组

数组:相同数据类型元素的集合,是一块连续的内存空间

4.1 数组名

当我们定义一个数组的时候,它的数组名到底是什么?

观察如下代码:

int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};

	printf("%p\n", arr);
	printf("%p\n", &arr[0]);

	return 0;
}

结果:
在这里插入图片描述
我们发现数组名和数组的首元素地址是一样的。

那么我们也就可以说,数组名其实代表的就是数组的首元素地址

那么我们就可以这样来写代码:

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* ptr = arr;

既然可以把数组名当成数组首元素地址存放在指针变量中,使得我们使用指针来访问数组元素也就成为了可能。

int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int* ptr = 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, ptr + i);
	}

	return 0;
}

结果:
在这里插入图片描述
通过以上代码可以发现,其实 p + i 计算的就是数组arr下标为i的地址,如果我们再进行间接访问,那么也就访问到了数组的每个元素的值。

如下:

int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int* ptr = arr;

	int sz = sizeof(arr) / sizeof(arr[0]);

	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(ptr + i));
	}

	return 0;
}

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

4.2 简单认识二级指针

指针变量,说白了也是一个变量,是变量就会在内存中开辟空间,那么它就会有地址,那如果我们把指针变量在内存中开辟空间的地址存放起来,应该存放在哪里呢?答案是:二级指针

在这里插入图片描述
当我们*ppa操作的时候就可以找到pa,**ppa操作的时候,我们就找到了a

int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	**ppa = 20;

	printf("a = %d\n", a);

	return 0;
}

结果:

20

4.3 指针数组

首先,我们思考一个问题,指针数组本质是指针还是数组?

答案:是数组,是存放指针的数组。

定义一个数组我们信手拈来

int main()
{
	// 整型数组
	int arr1[5]; 
	// 字符型数组
	char arr2[5];

	return 0;
}

第一个是我们定义了一个可以存放5个int型数据的整型数组,第二个是我们定义了一个可以存放5个char型数据的字符型数组

在这里插入图片描述
那指针数组是怎样的呢?

那我们先来猜想一下,既然指针数组本质上是个数组,我们又知道数组是相同数据类型元素的集合。那指针数组里面存放的是什么数据类型呢?

没错,就是指针类型!

int main()
{
	// 声明一个指针数组
	int* arr[5];

	return 0;
}

以上代码: arr首先和[]结合,表明是一个数组,有5个元素,每个元素的数据类型是整型指针。

在这里插入图片描述


版权声明:本文为zk_lar原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。