【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=#
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是数组名,表示首元素的地址,也就是数组的地址,这就牵扯到了数组指针,数组指针又是什么呢?
...................................
........................
...................
结尾祝福语
风带来故事的种子,时间使之发芽,本章就到这里,博主会尽快更新下一章!!!!感谢大家支持!!!