- 🌈🌈🌈这里是say-fall分享,感兴趣欢迎三连与评论区留言
- 🔥🔥🔥专栏:《C语言入门知识点》、《C语言底层》
- 💪💪💪格言:今天多敲一行代码,明天少吃一份苦头
前言:
本篇继续补充指针篇的知识点,包括指针运算、野指针(包含悬垂指针)、assert断言和指针传参,感兴趣的小伙伴们快做好笔记!!!
文章目录
- 前言:
- 正文:
- 5. 指针运算
- 5.1 指针 ± 整数
- 5.2 指针 - 指针
- 5.3 指针的关系运算
- 6. 野指针
- 6.1 初始化指针和野指针
- 6.1.1 初始化指针
- 6.1.2 指针越界访问
- 6.1.2 指针越界访问
- 6.2 如何规避野指针
- 6.2.1 初始化野指针时赋值或置于NULL
- 6.2.2 小心指针越界
- 6.2.3 指针不再使用时,及时置0,使用前检查指针有效性
- 6.2.4 避免指针返回局部变量地址
- 7. assert断言
- 8. 指针的使用与传址调用
- 8.1 strlen的模拟实现
- 8.2 传值调用与传址调用
正文:
5. 指针运算
指针基本运算包括三种:
- 指针 ± 整数
- 指针 - 指针
- 指针的关系运算
5.1 指针 ± 整数
在前文《指针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));
- 可以看得到,打印的两个结果是完全相同的。也就是说,指针通过+整数(移动了4个字节)的方式,访问到了数组的元素。
跑题一下,我们了解一个数组和指针的“小秘密”:
其实数组名就是一个数组第一个元素的指针
即int* arr = arr[0]
验证一下:
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
printf("%d ", *arr);
return 0;
}
运行结果:
既然如此的话,我们发现数组其实有两种表示方法:
- 1.指针表示法
- 2.数组表示法
指针表示法其实相对来说更底层一点,因为它对内存的应用更明显。
5.2 指针 - 指针
明白了指针与整数的运算,不妨先来猜一下指针 - 指针实际上是在做什么吧
先贴一段代码,想想他的运行结果
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!我们成功了!
5.3 指针的关系运算
我们这次直接来看代码
//指针的关系运算
#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;
}
这段代码中就用到了指针间的大小比较,通过这串代码输出了数组的元素:
6. 野指针
6.1 初始化指针和野指针
首先来说一下初始化指针,当我们知道这个指针是指向那里的时候,我们一般直接给指针一个地址,如果不知道的话我们给指针一个NULL,NULL和0是等价的,也就是让这个指针指向0。而如果我们什么都不初始化,直接定义一个指针而不赋值,在这个指针就被称为野指针,有的时候指针在运行时跑出作用范围也被称为野指针。
6.1.1 初始化指针
#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运行结果:
6.1.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就是野指针
*(p++) = i;
}
return 0;
}
这个数组的范围为10,而指针访问时最后到达12才停止,导致报错:
6.1.2 指针越界访问
代码来啦!
#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;也有可能是随机值;也可能导致程序崩溃(内存非法访问)。
- 第2、3种方法都会产生悬垂指针
6.2 如何规避野指针
其实上面的三种野指针成因也就对应了规避野指针的方法
6.2.1 初始化野指针时赋值或置于NULL
#include <stdio.h>
int main()
{
int* p0 = NULL;
int* p1 = 0;
printf("%p\n", p0);
printf("%p\n", p1);
return 0;
}
6.2.2 小心指针越界
在调整指针指向的位置时,不要让指针超出访问的空间,超出空间时,指针就越界访问了
6.2.3 指针不再使用时,及时置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;
}
6.2.4 避免指针返回局部变量地址
像是上面的例子,指针不能返回局部变量地址,会产生不可预料的结果。
7. assert断言
我们上文中提到了判断指针p是否置于NULL,如果为真就继续执行,不为真就不执行,这里我们介绍一种用于判断然后决定是否继续运行的宏:assert()。
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
- PS:宏:是由编译器或解释器预处理的代码替换规则,在程序运行前(预处理阶段)会被自动替换为定义好的代码片段。
我们在这里这样用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 版本不影响⽤⼾使⽤时程序的效率
8. 指针的使用与传址调用
8.1 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;
}
运行结果依旧正确:
8.2 传值调用与传址调用
我们不用指针写一个交换数值的函数:
#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;
}
运行一下:
- 欸,为啥没换过来
试试调试:
- 可以看得到,在出函数的前一刻,xy的地址和ab的地址是不一样的,出函数以后,xy作为局部变量就会被销毁,也就导致了虽然x和y交换数值了,但是和ab有什么关系呢?
上面我们用的是函数的传值调用,试试用指针呢
#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的值了。
运行结果:
很好,通过两个函数的对比,我们就了解了什么时候必须要用指针,这也就大大提高了指针存在的必要性。
- 本节完…















3321

被折叠的 条评论
为什么被折叠?



