本篇继续补充指针篇的知识点,包括指针运算、野指针(包含悬垂指针)、assert断言和指针传参,感兴趣的小伙伴们快做好笔记!!!
指针基本运算包括三种:
在前文《指针b篇》我们已经了解过指针 ± 整数(了解详情可点击《指针b篇》回顾),我们知道pointer ± n*sizeof(----),----代表的是变量类型,而且变量类型对应了指针会移动几个字节。
那有什么应用呢?
我们不妨想一下,有哪些数据在内存中是连续存放的?很显然,数组就是这样。 也就是说,我们可以通过指针来访问数组的元素,好,详看代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9};
int* p = &arr[0];
printf("%d\n", arr[2]);
printf("%d\n", *(p + 2));
return 0;
}运行结果:

printf("%d\n", arr[2]);
printf("%d\n", *(p + 2));跑题一下,我们了解一个数组和指针的“小秘密”:
其实数组名就是一个数组第一个元素的指针
即int* arr = arr[0]
验证一下:
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
printf("%d ", *arr);
return 0;
}运行结果:

既然如此的话,我们发现数组其实有两种表示方法:
指针表示法其实相对来说更底层一点,因为它对内存的应用更明显。
明白了指针与整数的运算,不妨先来猜一下指针 - 指针实际上是在做什么吧
先贴一段代码,想想他的运行结果
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int x = 0;
x = *(arr + 4) - *(arr + 1);
printf("%d\n", x);
return 0;
}揭晓答案,运行结果:

答案是3,也就是说指针 - 指针的结果是指针与指针之间所差变量类型的个数
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int x = 0;
x = *(arr + 4) - *(arr + 1);
printf("%d\n", x);
int arr_ch[6] = { 'a','b','c','d','e','f' };
int y = 0;
y = *(arr_ch + 4) - *(arr_ch + 1);
printf("%d\n", y);
return 0;
}运行结果;

看来确实如此。
有什么应用吗?
我记得有一个函数,能返回字符串的长度。strlen()
我们来试着复刻一下这个函数:
int my_strlen(char* s)
{
char *x = s;
while(*x != '\0')
x++;
return x - s;
}
int main()
{
printf("%d", my_strlen("abcd"));
return 0;
}运行结果:

yes!我们成功了!
我们这次直接来看代码
//指针的关系运算
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}这段代码中就用到了指针间的大小比较,通过这串代码输出了数组的元素:

首先来说一下初始化指针,当我们知道这个指针是指向那里的时候,我们一般直接给指针一个地址,如果不知道的话我们给指针一个NULL,NULL和0是等价的,也就是让这个指针指向0。而如果我们什么都不初始化,直接定义一个指针而不赋值,在这个指针就被称为野指针,有的时候指针在运行时跑出作用范围也被称为野指针。
#include <stdio.h>
int main()
{
int* p0 = NULL;
int* p1 = 0;
int* p2;
printf("%p\n", p0);
printf("%p\n", p1);
printf("%p\n", p2);
return 0;
}Dev c++运行结果:

VS 2022运行结果:

代码来啦!
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}这个数组的范围为10,而指针访问时最后到达12才停止,导致报错:

代码来啦!
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}代码一结果:

#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%p\n", p);
printf("%d\n", *p);
return 0;
}代码二结果:

已知
test();返回的是 &n ,但是n是在test()内部的局部变量,如果在test()之外使用&n(p),此时的p是一个悬垂指针,返回的是一个无效的地址,打印的值又可能是100;也有可能是随机值;也可能导致程序崩溃(内存非法访问)。
其实上面的三种野指针成因也就对应了规避野指针的方法
#include <stdio.h>
int main()
{
int* p0 = NULL;
int* p1 = 0;
printf("%p\n", p0);
printf("%p\n", p1);
return 0;
}在调整指针指向的位置时,不要让指针超出访问的空间,超出空间时,指针就越界访问了
在使用指针后,指针停止使用,再次使用时检查。
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}像是上面的例子,指针不能返回局部变量地址,会产生不可预料的结果。
我们上文中提到了判断指针p是否置于NULL,如果为真就继续执行,不为真就不执行,这里我们介绍一种用于判断然后决定是否继续运行的宏:assert()。
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
我们在这里这样用assert():
#include <assert.h>
assert(p != NULL);如果想要开启或者关闭assert()函数呢,我们就在#include assert()前定义一个DNEBUG
#define DNEBUG
#include <assert.h>这样的话,下次运行函数的时候,就会自动屏蔽掉assert(),同理删除后在运行就能开启assert()
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, 在 Release 版本不影响⽤⼾使⽤时程序的效率
前文见过,代码走起!
int my_strlen(char* s)
{
char *x = s;
while(*x != '\0')
x++;
return x - s;
}
int main()
{
printf("%d", my_strlen("abcd"));
return 0;
}运行结果依旧正确:

我们不用指针写一个交换数值的函数:
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}运行一下:

试试调试:

上面我们用的是函数的传值调用,试试用指针呢
#include <stdio.h>
void Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}能看的到和上次不同,我们这次调用的是地址,这样的话直接让地址被交换(即指针),就能交换ab的值了。 运行结果:

很好,通过两个函数的对比,我们就了解了什么时候必须要用指针,这也就大大提高了指针存在的必要性。