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

wheel down ~~ 2024-10-09 09:05:02 阅读 80

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

目录

一、指针运算

1.1指针+-整数

1.2.指针-指针

1.3.指针的关系运算

二、野指针

一.野指针成因

1.指针未初始化

 2.指针越界访问

3.指针指向的空间释放

  三、规避野指针

1.小心指针越界

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

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

assert断言

四、指针与数组的关系

1.数组名

2.使用指针访问数组

五、字符指针

1.字符指针隐藏秘密

2.常量字符串

《剑指offer》笔试题

六、二级指针

七、指针数组

用指针数组模拟二维数组

结尾祝福语


一、指针运算

1.1指针+-整数

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

<code>#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.指针未初始化

<code>//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的大小,

<code>#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函数如果后面的参数是数组名,表示的是整个数组,取出来的是整个数组,计算时算出来的是整个数组的字节数

&数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址虽然数值一样,但还是有区别的)

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

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

分析代码

<code>#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.使用指针访问数组

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

<code>#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* ,存放的是字符的地址,比如:

<code>#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》:中收录了⼀道和字符串相关的笔试题,我们⼀起来学习⼀下:

<code>#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。

六、二级指针

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

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

<code>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所在的地址是相同的,所以表明了,二级指针存放的是一级指针的地址

七、指针数组

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

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

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

用指针数组模拟二维数组

<code>#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是数组名,表示首元素的地址,也就是数组的地址,这就牵扯到了数组指针,数组指针又是什么呢?

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

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

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

结尾祝福语

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



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。