小言_互联网的博客

【C语言基础学习笔记】六、初始指针(重点必看)(详细讲解+代码举例+练习巩固)

173人阅读  评论(0)

本章内容是初始指针的全部内容,希望小伙伴们能静下心来,仔细看完并看懂,一定会收获满满~
说明:在后续的数据结构与算法、C语言进阶或者C++的学习当中,指针以及结构体这部分内容至关重要!如果这两大块内容没有学好,学懂,真正融合贯通去运用,那么在后续的数据结构与算法、C语言进阶或者C++的学习会非常艰难,尤其是数据结构和算法!



一、指针是什么?

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

 #include <stdio.h>
 int main({
   
		int a = 10;  //在内存中开辟一块空间
		int* p = &a; //这里我们对变量a,取出它的地址,可以使用&操作符。
					 //将a的地址存放在p变量中,p就是一个之指针变量。
		return 0;
	}

总结∶指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

思考以下的问题∶
(1)一个小的单元到底是多大 ? (1个字节)
(2)如何编址 ?

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电 / 负电(1或者0)
那么32根地址线产生的地址就会是∶
00000000 00000000 00000000 00000000
0000000 00000000 00000000 00000001
…….
11111111 1111111111111111 11111111

这里就有2的32次方个地址。 每个地址标识一个字节,那我们就可以给(2 ^ 32Byte == 2 ^ 32 / 1024KB == 2 ^
32 / 1024 / 1024MB == 2 ^ 32 / 1024 / 1024 / 1024GB == 4GB)4G的空闲进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。

这里我们就明白 :
(1)在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
(2)那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

总结 :
(1)指针是用来存放地址的,地址是唯一标示一块地址空间的。
(2)指针的大小在32位平台是4个字节,在64位平台是8个字节。


二、指针和指针类型

我们先看这样一段代码的执行效果:

可以看到不管什么类型的指针,大小都是4,这时候我们心里可能产生疑问,为什么大小都是4,还要区分不同类型的指针,那这种区分是不是没有意义的?不同类型的指针是不是可以互用?为了验证这些问题,请看以下代码及运行结果。

我们可以先屏蔽其它代码内容,保留前三行内容,再加上 * p = 0; 即调用指针来更改指针指向地址中的内容。通过F10调试,打开窗口-- - 内存监视器(类似变量监视窗口),可以看到a的内容确实改变了

现在我们将char * pc来重复上面的操作,看能否实现相应变更指针指向地址内容的功能。
通过F10调试,打开窗口-- - 内存监视器(类似变量监视窗口),我们发现这里仅前两个变成了00,后面几位没有变化。
通过这个我们可以发现指针的类型还是意义的,其意义在于解引用操作时,对字节的操作数量不同,比如int * 类型的指针,可以改动4个字节的内容,而char * 类型的指针只能更改一个字节的内容。

指针的意义1:指针的解引用

总结:指针类型决定了指针进行解引用操作的时候,能够访问空间的大小

举例:

int* p; *p能够访问4个字节
char* p; *p能够访问1个字节
double* p; *p能够访问8个字节


指针的意义2:指针 + -整数
看下面这个例子:pa + 1 实际地址 + 4, pc + 1 实际地址 + 1

总结:指针的类型决定了指针向前或者向后走一步有多大(距离、指针的步长)。

举例:

int* p; p + 1 – > 4
char* p; p + 1 – > 1
double* p; p + 1 – > 8


思考:那么指针这两个意义的价值是什么呢?

如果我们将int * p = arr; 改成char* pc = arr;会发生什么呢?
是不是只会将10个字节改成1 而我们数组int arr[10]有40个字节 10个字节的大小相当于2个半int类型的大小,我们可以打开内存窗口调试,看一下是不是这样的情况。


三、野指针

看到野指针,我们的第一想法或者说印象是什么?会联想到什么?是不是野猫、野狗,它们是没有主人养的的宠物。那么野指针又是什么呢?
野指针概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
那么为什么会产生野指针呢?
1.指针未初始化

  #include<stdio.h>
  int main()
	{
   
		int a;//局部变量不初始化,默认是随机值
		int* p;//局部的指针变量不初始化,默认也会给随机值
		*p = 20;//这时候对指针进行解引用操作,实际上并不知道更改的内存是在哪里
		return 0;
	}

2.指针越界访问

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

3.指针指向的空间释放

    #include<stdio.h>
	int* test()
	{
   
		int a = 10;
		return &a;
	}
	//1.a 创建的空间在函数结束的时候返回给系统了,不属于当前程序的内容
	int main()
	{
   
		int* p = test();
		//2.这时候通过*p 去访问一个返还系统的空间,该空间有可能已经存放其它内容了
		//3.所以p是一个野指针
		*p = 20;
		return 0;
	}

那么我们如何能够避免野指针呢?
1.指针初始化
2.小心指针越界
3.指针指向空间释放即使置NULL
4.指针使用之前检查有效性


四、指针运算

指针运算有三种,分别为:
①指针 + -整数
②指针 - 指针
③指针的关系运算


下面对这三种运算进行详细的讲解:
①指针 + -整数

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

这里我们使用的时指针 + 1,那么指针 + 2或指针 + 3是什么样子的呢?请看下面的举例;


指针除了可以 + 整数,也可以 - 整数,比如:


②指针 - 指针
我们知道指针变量是用于存放地址的,那么指针 - 指针 就是 地址 - 地址,那指针 - 指针的结果又是什么呢?请看下面的例子:

打印 & arr[9] - &arr[0] 结果为9,代表从arr[0]到arr[9], 其中间有9个元素。

总结:指针 - 指针得到的结果是中间元素的个数

当然如果我们将 & arr[9] - &arr[0] 的顺序调换以下,变成 & arr[0] - &arr[9],得到的又是什么呢?

结果是: - 9
所以如果我们要得到元素的个数,应该用大地址 - 小地址。


下面我们来做一个破坏性实验:

千万不要犯类似与 & arr[9] - &ch[0]这样的错误!给自己徒增 bug!


指针 - 指针实际应用举例:

    #include<stdio.h>
	int my_strlen(char* str)
	{
   
		char* start = str;//指向首地址
		char* end = str;//指向尾地址 \0
		while (*end != '\0')
		{
   
			*end++;
		}
		return end - start;//end - start 就是中间元素的个数
	}
	int main()
	{
   
		char arr[] = "hello c";
		int len = my_strlen(arr);
		printf("%d\n", len);
		return 0;
	}

③指针的关系运算
关系运算,简单来说就是比较大小,有 > 、 >= 、 == 、 != 、 < 、 <=
看下面例子:
float valuea[5];
for (vp = &values[5]; vp > &values[0]; )
{
*–vp = 0;
}
代码简化, 这将代码修改如下︰
for (vp = &values[4]; vp >= &values[0]; vp–)
{
*vp = 0;
}
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定∶
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

看到这里,可能你还是不太明白,所以我们就画个图来演示一下:
(学编程时,画图演示帮助理解很重要!想想学数学的时候,为什么会有数形结合的方式来帮助思考求函数的根就知道了)


五、指针和数组

数组名是什么?
在之前的学习中,我们可能已经知道了数组名就是首元素的地址,这里我们验证一下这个说法是否正确,请看下面这个例子:

可以看到arr 和& arr[0]结果是一样,说明“数组名就是首元素的地址”是正确的!
但是,数组名就一直是首元素地址吗?

数组名在绝大多数情况下是首元素地址,但有两个例外:
1、& arr-- - &数组名 - 数组名不是首元素的地址,数组名表示整个数组 &
数组名 取出的是整个数组的地址。
这个时候,我们可能会问:整个数组的地址和首元素地址有什么区别呢?这个问题我们待会再讨论。
2、sizeof(arr)----sizeof(数组名)-- - 数组名表示的整个数组–sizeof(数组名)计算的是整个数组的大小。


我们回到刚刚说的问题:整个数组的地址和首元素地址有什么区别呢?在打印的时候我们发现打印出来的值是相等的。

解析:
整个数组的地址等于首元素的地址,从地址值来看两者相等,但是从它们的意义和使用来说,两者是有区别的。
那么如何来理解它们的区别呢?我们看下面的举例:

举例:我们让它们都进行 + 1, 首元素地址加1就到了第二个元素的地址,而数组地址 + 1则是直接跳过该数组


上面我们说了 数组名就是首元素的地址,那么我们通过使用指针来访问数组就成为了可能:

    #include<stdio.h>
	int main()
	{
   
		int arr[] = {
    1,2,3,4,5,6,7,8,9,10 };
		int* p = arr;
		int i = 0;
		int sz = sizeof(arr) / sizeof(arr[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其实计算的是数组arr下标为i的地址。那么我们就可以直接通过指针来访问数组,比如:


六、二级指针

指针变量也是变量,是变量就有存储空间,就有地址,那么指针变量的地址存放在哪里那?
这就是二级指针,指向内容是存放地址的指针。
通常我们所说的指向数组的指针是一级指针。

    #include<stdio.h>
	int main()
	{
   
		int a = 10;
		int* pa = &a;//pa 一级指针
		int** ppa = &pa;//ppa 二级指针 ......后面还有 三级指针、四级指针....n级指针
		return 0;
	}

这里我们进行画图演示:


好了,现在我们对二级指针应该理解清楚了,但是二级指针怎么应用呢?如果我们对ppa进行解引用操作* ppa就能找到pa 也就是a的地址,再次进行解引用操作** ppa,就能找到a了!
如下图:


七、指针数组

在我们学习指针和数组的概念时,会接触到两个这样的概念
1.指针数组-- - 数组-- - 存放指针的数组
2.数组指针-- - 指针

那么我们怎么来理解1.指针数组呢?我们来看下面的这个问题。

   #include<stdio.h>
	int main()
	{
   
		int a = 10;
		int b = 20;
		int c = 30;
		int* pa = &a;
		int* pb = &b;
		int* pc = &c;
		//这里只有三个变量,我们创建了三个指针分别存放它们的地址
		//假如有10个变量,我们要一口气创建10个指针来存放它们的地址吗?
		return 0;
	}

这个时候我们就会思考,这10个指针变量能不能利用数组的方式来创建呢,就跟10个int 类型的变量一样,通过创建一个 int arr[10]就能实现。
整型数组-- - 存放整型
字符数组-- - 存放字符
指针数组-- - 存放指针
按照这个思路,我们创建一个指针数组 int* arr[3] = { &a,&b,&c };
我们可以通过指针数组访问其内容并打印:


结尾:以上就是C语言初始指针的全部内容,有关指针的进阶内容会在后面的【C语言进阶学习笔记】有关指针的部分详细讲解!


转载:https://blog.csdn.net/QIYICat/article/details/116852942
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场