深入理解指针(三)

一、指针运算

1.1指针+-整数

下面我们来看一个指针加整数的例子:

#include<stdio.h>

int main()

{

int arr10 = { 1,2,3,4,5,6,7,8,9,10 };

int* p = &arr0;

int i = 0;

int sz = sizeof(arr) / sizeof(arr0);

for (i = 0; i < sz; i++)

{

printf("%d ", *(p + i));

}

return 0;

}

我们可以看到p加上i,再对他们取地址,可以作为数组的下标,相减也同理。

1.2指针-指针

下面我们来看一个指针-指针的例子:

#include<stdio.h>

int my_strlen(char* s)

{

char* p = s;

while (*p != '\0')

{

p++;

}

return p - s;

}

int main()

{

printf("%d\n", my_strlen("abc"));

return 0;

}

​ 这是模拟函数strlen,来计算字符串长度,我们将p减去原长度s,所得到的就是字符串的长度。

1.3指针的关系运算

#include<stdio.h>

int main()

{

int arr10 = { 1,2,3,4,5,6,7,8,9,10 };

int* p = &arr0;

int i = 0;

int sz = sizeof(arr) / sizeof(arr0);

while (p < arr + sz)//指针的大小比较

{

printf("%d ", *p);

p++;

}

return 0;

}

我们将for循环换成while循环,在里面将p和arr+sz进行比较,p<arr+sz其实就是1.1中的i<sz。

二、野指针

概念:野指针就是至臻纸箱的位置时不可知的(随机的、不正确的)

2.1野指针的成因

2.1.1.指针未初始化

#include<stdio.h>

int main()

{

int* p;//局部变量未初始化,默认随机值

*p = 20;

return 0;

}

2.1.2指针越界访问

#include<stdio.h>

int main()

{

int arr10 = { 0 };

int* p = &arr0;

int i = 0;

for (i = 0; i <= 11; i++)

{

//当指针指向的范围超出数组arr的范围时,p就是野指针

*(p++) = i;

}

return 0;

}

2.1.3指针指向的空间释放

#include<stdio.h>

int* test()

{

int n = 100;

return &n;

}

int main()

{

int* p = test();

printf("%d\n", *p);

return 0;

}

2.2如何规避野指针

2.2.1指针初始化

如果明确知道指针指向哪里就直接赋值,如果不知道,可以给指针赋值NULL。NULL是C语言中定义的一个标识符常亮,值是0,0也是地址,这个地址是无法使用的,读写改地址会报错。

初始化如下:

#include<stdio.h>

int main()

{

int num = 10;

int* p1 = #

int* p2 = NULL;

return 0;

}

2.2.2小心指针越界

一个程序向内存申请了哪些空间,就只能访问哪些,不能超出范围去访问,超出了就是越界访问。

2.2.3指针变量不再使用时,及时置NULL,指针使用之前检查有效性。

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL。

我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起来。不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;对于指针也是,在使用之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使用,如果不是我们再去使用。

#include<stdio.h>

int main()

{

int arr10 = { 1,2,3,4,5,6,7,8,9,10 };

int* p = &arr0;

int i = 0;

for (i = 0; i < 10; i++)

{

*(p++) = i;

}

//此时p已经越界了,可以把p置为NULL

p = NULL;

//下次使用的时候,判断p不为NULL的时候再使用

p = &arr0;//重新让p获得地址

if (p != NULL)//判断

{

//...

}

return 0;

}

2.2.4避免返回局部变量的地址

2.1.3中的例子,不要返回局部变量的地址。

三、assert断言

assert.h这个头文件中定义了宏assert(),用于在运行时确保符合指定条件,如果不符合就报错终止运行。这个宏常常被称为"断言"。

assert(p != NULL);

在程序运行到上述代码时,如果p不等于NULL,程序继续运行,如果等于,就会终止运行,并且给出报错信息提示。

assert() 宏接受一个表达式作为参数。如果该表达式为真(返回值非零), assert() 不会产生

任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误

流 stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

assert() 的使用对程序员是非常友好的,使用 assert() 有几个好处:它不仅能自动标识文件和

出问题的行号,还有一种无需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问

题,不需要再做断言,就在 #include <assert.h> 语句的前面,定义⼀个宏 NDEBUG 。

#define NDBUG

#include<assert.h>

然后,重新编译程序,编译器就会禁用文件中所有的 assert() 语句。如果程序又出现问题,可以移除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启用了 assert() 语

句。

assert() 的缺点是,因为引入了额外的检查,增加了程序的运行时间。一般我们可以在 Debug 中使用在 Release 版本中选择禁用 assert 就行,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响用户使用时程序的效率。

四、指针的使用和传址调用

例:写出一个函数,交换两个整型变量的值

#include<stdio.h>

void Swap(int x, int y)

{

int tmp = x;

x = y;

y = tmp;

}

int main()

{

int a = 10;

int b = 20;

printf("交换前:a=%d,b=%d\n", a, b);

Swap(a, b);

printf("交换后:a=%d,b=%d\n", a, b);

return 0;

}

代码运行的结果如下:

这是为什么呢?

我们发现在main函数内部,创建了a和b,a的地址是0x0093fe58,b的地址是0x0093fe4c,在调用Swap函数时,将a和b传递给了Swap函数,在Swap函数内部创建了形参x和y接收a和b的值,但是x的地址是0x0093fd74,y的地址是0x0093fd78,x和y确实接收到了a和b的值,不过x的地址和a的地址不一样,y的地址和b的地址不一样,相当于x和y是独立的空间,那么在Swap函数内部交换x和y的值,自然不会影响a和b,当Swap函数调用结束后回到main函数,a和b的没法交换。Swap函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,这种叫传值调用。

结论:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。

那怎么办呢?

我们现在要解决的就是当调用Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就可以使用指针了,在main函数中将a和b的地址传递给Swap函数,Swap函数里边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

#include<stdio.h>

void Swap(int* px, int* py)

{

int tmp = 0;

tmp = *px;

*px = *py;

*py = tmp;

}

int main()

{

int a = 10;

int b = 20;

printf("交换前:a=%d,b=%d\n", a, b);

Swap(&a, &b);

printf("交换后:a=%d,b=%d\n", a, b);

return 0;

}

我们可以看到实现成Swap的方式,顺利完成了任务,这里调用Swap函数的时候是将变量的地址传

递给了函数,这种函数调用方式叫:传址调用。

相关推荐
Zane19942 分钟前
ClassName() 只是一步?拆开看 __new__ 和 __init__ 各自在干什么
后端·python
geovindu3 分钟前
java: Memento Pattern
java·开发语言·后端·备忘录模式·行为模式
星哥的编程之路6 分钟前
万字深度解析 Agent 学习路线
后端
程序边界9 分钟前
SQL Server数据迁移这件事,远比你想的复杂——但也远比你想的简单(下)
后端
Zane199411 分钟前
JRE 去哪了?一次讲清楚 JDK、JRE、JVM 到底是什么关系?
java·后端
JavaPub15 分钟前
Ontology 本体论是什么?从哲学概念到 AI、知识图谱与软件工程
后端
liuxiaocheng19 分钟前
聊聊 Vercel AI SDK 的流式协议:前后端到底是怎么"边想边说"的
前端·后端
得物技术31 分钟前
得物知识问答:复合检索 Agent 的系统设计实践
人工智能·后端·ai编程
andongni20341 分钟前
SpringBoot 入门实验报告
java·spring boot·后端
李广坤1 小时前
Agent 记忆系统详解:从"金鱼脑"到"过目不忘"的进化之路
后端