当前位置: 首页 > article >正文

【C语言】(指针系列2)指针运算+指针与数组的关系+二级指针+指针数组+《剑指offer面试题》

前言:开始之前先感谢一位大佬,清风~徐~来-CSDN博客,由于是时间久远,博主指针的系列忘的差不多了,所以有些部分借鉴了该播主的,有些地方如果解释的不到位,请翻看这位大佬的,感谢大家!!!!!!

目录

一、指针运算

1.1指针+-整数

1.2.指针-指针

1.3.指针的关系运算

二、野指针

一.野指针成因

1.指针未初始化

 2.指针越界访问

3.指针指向的空间释放

  三、规避野指针

1.小心指针越界

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

3.指针变量不再使用是置为NULL,使用时检查其有效性

assert断言

四、指针与数组的关系

1.数组名

2.使用指针访问数组

五、字符指针

1.字符指针隐藏秘密

2.常量字符串

《剑指offer》笔试题

六、二级指针

七、指针数组

用指针数组模拟二维数组

结尾祝福语


一、指针运算

1.1指针+-整数

指针是一个存放地址的变量,这些我们都知道,但是对于一个指针来说,他的运算是怎么样的?我们可以看看。我们都知道数组在内存中是连续存放的,只要知道首地址,我们就可以知道后面几个元素的地址。

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

我们观察发现

  • p存放的是arr[0]的地址,p+1跳过4个字节,也就是1个整形,所以p+1指向整形元素arr[1]
     
  • p一次访问4个字节,也就是一个整形,得到arr[0]
     
  • 同理*(p+1)得到arr[1],按照指针的方法就可以打印数组所有的元素
     
  • &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址虽然数值一样,但还是有区别的)
     
  • 除此之外,任何地方使用数组名,数组名都表示首元素的地址。
     

1.2.指针-指针

大指针 - 小指针得到的是指针之间元素的个数,仅限于它们是同一块空间 还有小指针 - 大指针得到的就是负数

#include <stdio.h>
//指针 - 指针
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p1 = &arr[0];
	int* p2 = &arr[6];
	printf("%d\n", p1 - p2);//-6
	printf("%d\n", p2 - p1);//6
	return 0;
}
  • 数组再内存中是连续存放的,且是由低地址向高地址存放的。

1.3.指针的关系运算

指针还能够比较大小,指针本质是地址,地址以16进制显现出来的,所以本质就是比较两个数的大小

	while (p < arr + sz) //指针的大小比较 
	{
		printf("%d ", *p);//打印数组所有的元素
		p++;
	}

!!这还有一个需要注意的点是:

二、野指针

野指针就是指向的位置是不可知的(危险的,未知的,没有明确限制的),是非常危险的

一.野指针成因

1.指针未初始化

//1.指针未初始化
#include <stdio.h>
int main()
{
	int*p//局部指针变量未初始化,没有明确的指向,默认值为随机值
*p=20;//!!!非法访问了,p成了野指针
//随机将p指向的对象改变是非常危险的。
	return 0;
}

 2.指针越界访问

//2.指针越界访问
#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i <= 11; i++)
	{
//当指针的指向的范围超出了arr的范围,这就是越界了,也算非法访问
//我们没有权利访问和修改超出的空间

		*(p++)=i;
	}
	return 0;
}

3.指针指向的空间释放

//3.返回局部变量的地址(生命周期结束后使用)
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
/*p要存a的地址,函数返回的时候已经把a还给操作系统了,p没有权限访问这块空间,所以p是野指针
但是内存里的这块空间还在,只是不属于当前程序,没有使用的权限*/

	printf("%d\n", *p);//通过非法的地址,如果这块空间没有被使用(覆盖),还能找到10,但是不属于我们。
	return 0;
}

  三、规避野指针

如果知道这块指针指向的哪里就直接将这块地址赋值给指针,如果不知道指针只想哪里,就赋值给NULL

NULL是C语言当中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错

#include<stdio.h>
int main()
{
int num=10;
int*p1=&num;
int*p2=NULL;
return 0;
}

1.小心指针越界

  • ⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。导致野指针。

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

  • 如造成野指针的第3个例子,不要返回局部变量的地址

3.指针变量不再使用是置为NULL,使用时检查其有效性

  • 当一个指针变量指向一块区域时,我们可以通过指针访问这块区域,但是我们如果后期不再使用时,我们置为NULL,这样就不用害怕成野指针了,为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使用前要判断指针是不是NULL
  • 我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起来
  • 不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;对于指针也是,在使用之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使用,如果不是我们再去使用。
#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL 
	p = NULL;
	//下次使⽤的时候,判断p不为NULL的时候再使⽤ 
	//...

	p = &arr[0];//重新让p获得地址 
	if (p != NULL) //判断 
	{
		//...
	}
	return 0;
}

assert断言

assert.h头文件定义了宏assert(),用于在运行程序时判断程序是否符合条件,,如果不符合就终止运行,直接报错!!!。这个宏常常被称为“断言”    

 assert(p!=NULL);

验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运行,否则就会终止运行,并且给出报错信息提示

四、指针与数组的关系

1.数组名

大多数人认为,数组名只不过是一个代号罢了,没有什么实际的意义,没什么大用,如果你怎么想,那就大错特错了,当初祖师爷在设计的时候,将数组名设计了一个特殊的角色---------数组的首地址 !!!!

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

运行可以发现:两个的地址是完全一样的,所以数组名就是数组的首地址

我们再来看一段代码,看一下arr的大小,

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

这不紧让我们引发了思考:我们微微皱眉,arr既然是元素的首地址,应该是指针变量呀,返回的是应该是(4/8)呀,为什么会返回40哪?

这是因为:

arr是元素的首地址是对的,但是有两个意外:

  • sizeof(数组名)表示sizeof函数如果后面的参数是数组名,表示的是整个数组,取出来的是整个数组,计算时算出来的是整个数组的字节数
  • &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址虽然数值一样,但还是有区别的)

  • 除此之外,任何地方使用数组名,数组名都表示首元素的地址。

再度思考:那么数组的地址,数组名与数组首元素的地址这三种又存在什么关系呢?以下的代码将使你清晰理解这三者的联系:

分析代码

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

 

  • &arr[0]和&arr[0]+1相差4个字节,arr和arr+1相差4个字节,是因为&arr[0]和arr都是首元素的地址,+1就是跳过⼀个元素,也就是4个字节,而每个字节都有对应的地址,且地址相差1,所以它们的地址就相差4。
     
  • &arr和&arr+1相差40个字节,这就是因为&arr是数组的地址,+1操作是跳过整个数组,就是40个字节,地址相差(0x26),到这里大家应该搞清楚数组名的意义了吧。

2.使用指针访问数组

  有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组元素了,如下代码:

#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;

	return 0;
}

将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i]是等价于*(p+i)。
同理arr[i]应该等价于*(arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的。
还可以这么写*(i+arr),以及这么写i[arr],是不是很奇妙啊,了解一下就行了,不推荐这么写。

思考为什么可以使用指针来访问数组呢?
总结:

  • 数组在内存是一块连续的空间,存放的是相同类型的元素。
     
  • 指针变量是一个变量,是存放地址的变量,数组和指针不是一回事,但是可以利用指针来访问数组,指针进行不断地+1,解引用可以很方便地遍历数组,取出数组的内容。

  我们发现在函数内部是没有正确获得数组的元素个数,这又是为什么呢?你也许会想,指针怎么这么…(此处省略一万字),要尝试先接受它,以后学习多了自然都解释地清了。

  • 这就要学习数组传参的本质了,上个小节我们学习了:数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组首元素的地址。所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。
  • 那么在函数内部我们写sizeof(arr) 计算的是⼀个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
  • 那形参为什么可以写成数组的形式呢?这是因为C语言考虑到了学者的感受,在学习数组的时候,如果一来就传地址,形参用指针变量来接收,学者会非常地疑惑的。所以说C语言并不是这么冷若冰霜的。


总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

五、字符指针

1.字符指针隐藏秘密

  在指针的类型中我们知道有⼀种指针类型为字符指针 char* ,存放的是字符的地址,比如:

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

还有一种

#include<stdio.h>
int main()
{
	const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
	printf("%s\n", pstr);
	return 0;
}

代码 const char* pstr = “hello bit.”;特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是不妨考虑一下指针存放的是地址,怎么可能会存放字符串呢?

其实本质是把常量字符串 hello bit. 首字符(h)的地址放到了指针变量pstr中

2.常量字符串

常量字符串,字面意思,就是该字符串不能被修改,接下来看一个代码:

#include<stdio.h>
int main()
{
	char arr[] = "abcdef";
	char* p1 = arr;
	*p1 = 'b';
	printf("%s\n", arr);

	char* p2= "abcdef";
	*p2 = 'b';
	printf("%s\n", p2);
	return 0;
}

可以发现指针p1指向的空间可以修改,而修改指针p2指向的空间则报错:写入访问权限冲突。这是因为p2是常量字符串,它还有更重要的特点,接下来带我慢慢为你分析一二,请看以下的笔试题。

《剑指offer》笔试题

《剑指offer》:中收录了⼀道和字符串相关的笔试题,我们⼀起来学习⼀下:

#include <stdio.h>
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	const char* str3 = "hello bit.";
	const char* str4 = "hello bit.";

	if (str1 == str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");

	if (str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");

	return 0;
}

这个题的答案是不是很意外,str1和str2是两个字符数组,储存的字符为"hello world",是两个不同的空间,str1和str2表示的是首地址,由于是两个不同的空间,所以str1和str2不相等。

str3和str4是被const修饰的字符指针,都是指向“hello world”字符串的首地址的,所以str3和str4是相等的

总结:

str1和str2是两个数组,数组的操作方式是将右边常量字符串的内容拷贝进来,所以他们是两个空间,只是内容相同,所以str1 != str2。
而str3和str4是两个指针, 编译器在处理的时候,会将相同的常量字符串做成同一个地址,所以,str3和str指向的是同一个常量字符串,所以str3 == str4。

六、二级指针

问题:我们知道指针是存放元素地址的变量,但是指针的地址我们可以存放吗?

可以的,指针的地址可以用另一个不同的指针变量来存放,我们一般将这样的指针叫做二级指针

	int a = 10;
	int* p = &a;
	int** ppa = &p;
	int* l = 0;
	
	*p = 20;
	printf("%d\n", a);
	l = *ppa;
		printf("%p\n", l);
		printf("%p\n", p);
	**ppa = 30;
	printf("%d\n", a);

	return 0;

运行发现,二级指针往往需要进行两层解引用,我们用一层解引用发现,一级解引用二级指针的结果和指针变量p所在的地址是相同的,所以表明了,二级指针存放的是一级指针的地址

七、指针数组

思考一下:指针数组是指针还是数组?好好思考这个问题,有助于跟后面的学习区分开仔细想想:

  • 整型数组是存放整形的数组,字符数组是存放字符类型的数组。
  • 那么指针数组一定是存放指针的数组。指针数组的每一个元素都是用来存放地址的。指针数组的元素是地址,而每一个地址都可以指向一块区域。

所以,我们可以先来看一道题

用指针数组模拟二维数组

#include<stdio.h>
int main()
{
	int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
	int arr2[] = { 1,2,3,4,5,6,7,8,9,10 };
	int arr3[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* parr[] = { arr1,arr2,arr3 };
	int i = 0, j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j< 10; j++)
		{
			printf("%d", parr[i][j]);
		}
		printf("\n");
	}
}

 

parr是数组名,表示首元素的地址,也就是数组的地址,这就牵扯到了数组指针,数组指针又是什么呢?

...................................

........................

...................

结尾祝福语

风带来故事的种子,时间使之发芽,本章就到这里,博主会尽快更新下一章!!!!感谢大家支持!!!


http://www.kler.cn/a/307237.html

相关文章:

  • ️️一篇快速上手 AJAX 异步前后端交互
  • Ubuntu安装MySQL8
  • 超市里的货物架调整(算法解析)|豆包MarsCode AI刷题
  • Java 堆内存管理详解:`-Xms` 和 `-Xmx` 参数的使用与默认内存设置
  • 软件设计师-计算机网络
  • 谷歌浏览器的自动翻译功能如何开启
  • 重塑科普展厅魅力,以用户体验为核心的策略性规划新探索!
  • 『功能项目』切换职业面板【48】
  • php部署到apach服务器上遇到的问题
  • 萤石举办2024清洁机器人新品发布会 多维智能再造行业标杆
  • 2024.9.15周报
  • Kubernetes标签与标签选择器
  • 容器技术--Docker应用部署
  • Redis——常用数据类型List
  • C# 入坑JAVA 潜规则 大小写敏感文件名和类名 枚举等 入门系列2
  • 机器学习--神经网络
  • 【裸机装机系列】4.kali(ubuntu)-配置个人用户的sudo权限并进行bashrc的其他配置
  • Qt常用控件——QComboBox
  • Redisson分布式锁实现及原理详解
  • Visual Studio(vs)下载安装C/C++运行环境配置和基本使用注意事项
  • 深度学习之微积分预备知识点
  • HTTP 的请求方式有哪些和有什么区别
  • MATLAB 可视化基础:绘图命令与应用
  • C#笔记8 线程是什么?多线程怎么实现和操作?
  • Python精选200Tips:121-125
  • Spring Boot集成Akka Cluster实现在分布式节点中执行任务