当前位置: 首页 > news >正文

C语言:指针(1-2)

5. 指针运算

指针的基本运算有三种,分别是:

指针+-整数

指针-指针

指针的关系运算

5.1 指针运算

在上面,我们知道,数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能找到后面的所有元素。

那么,运用这一点,我们就可以写出下面的代码:

#include <stdio.h>int main()
{int i;int arr[] = {1,2,3,4,5,6,7,8,9};int sz = sizeof(arr)/ sizeof(arr[0]);for(i = 0;i < sz;i++){printf("%d ",*(arr + i));}return 0;
}

我们利用指针 arr (数组名即为数组首元素的地址) + i ,访问数组中下标为 i 的元素,并打印出来。而指针与整数运算后跳过的字节数的大小是与数据的类型有关的。例如,上面代码中, arr 数组是整型数组,所以在运算时,会在 arr 的位置,跳过4 * i 个字节,访问到数组中下标为 i 的元素。

5.2 指针 - 指针

上面,我们知道指针可以和整数进行加减运算,那指针是否可以与指针进行加减运算呢?

#include <stdio.h>int main()
{int arr[] = {1,2,3,4,5,6,7,8,9};int a = (arr + 9) - (arr + 3);//正常运行int b = (arr + 9) + (arr + 3);//编译器报错:Invalid operands to binary expression ('int *' and 'int *')printf("%d",a);return 0;
}

将指针加法的那一行代码删去后,我们得到了如下输出:

6
进程已结束,退出代码为 0

输出结果为6,这代表了(arr + 9)与(arr + 3)两个指针之间一共有6个元素。因此,指针的减法运算所得到的结果就是两个地址之间的元素个数。

利用这一点,我们可以自己写出类似于函数 strlen()的效果的代码:

int my_strlen(char* s)
{char *p = s;while(*p != '\0'){p++;}return p - s;
}
#include <stdio.h>
int main()
{char s1[] = "asdf";int a = my_strlen(s1);printf("%d",a);return 0;
}//输出结果
4

我们发现,输出结果为4,正等于字符数组中的字符数。

5.3 指针的关系运算

我们知道,指针就是地址,而地址有高低之分,那指针是否可以比较大小呢?

#include <stdio.h>int main()
{int arr[] = {1,2,3,4,5,6,7,8,9};int sz = sizeof(arr)/ sizeof(arr[0]);int *p = &arr[0];while(p < arr + sz){printf("%d ",*p);p++;}return 0;
}//输出结果
1 2 3 4 5 6 7 8 9 

我们可以发现,循环正常进行,说明表达式是合法有效的,指针可以用来进行比较大小

6. 野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

6.1 产生野指针的原因

6.1.1. 指针未初始化

#include <stdio.h>int main()
{int* p ;*p = 0;//编译器警告:Variable 'p' is uninitialized when used herereturn 0;
}

指针未初始化时,默认为随机值。直接使用可能导致系统报错。

6.1.2 指针越界访问

这种错误可以类比数组访问越界:

#include <stdio.h>int main()
{int i;int arr[10] = {0};int* p = &arr[0];for(i = 0;i < 11;i++){*p = i;p++;}return 0;
}

在这个代码中,当指针指向的范围超出数组范围时,p就会成为野指针,执行预期外的操作。

6.1.3 指针指向的空间释放

当指针所指向的空间已经被释放时,就会导致野指针的产生:

int*  test()
{int n = 1;return &n;
}
#include <stdio.h>int main()
{int* p = test();printf("%p",p);return 0;
}

由于变量n是在函数test中创建,因此函数执行完毕后,变量n的内存也会被回收,空间被释放。此时,程序就会打印出一个无效地址或者程序崩溃。

6.2 如何规避野指针

6.2.1 指针初始化

在创建指针变量时,如果明确知道指针指向哪里就直接赋值地址;如果不知道指针应该指向哪里,可以给指针赋值NULL,再后面使用时再进行赋值。

NULL是C语言中定义的一个标识符常量,值是0,地址也是0,这个地址是无法使用的,读写该地址时程序会报错。

#include <stdio.h>int main()
{int n = 0;int* p1 = &n;int* p2 = NULL;return 0;
}

6.2.2 防止指针越界

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

6.2.3 指针变量不再使用时,及时赋值NULL,指针使用之前检查有效性

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

#include <stdio.h>int main()
{int i;int arr[10] = {0};int* p = &arr[0];for(i = 0;i < 11;i++){*p = i;p++;}//此时,指针已经访问越界p = NULL;//将p赋值为NULL,防止p成为野指针...if(p != NULL)//使用前,检验p是否为空指针{...}return 0;
}

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

如上面的示例,避免返回局部变量的地址,防止使用野指针。

7. assert 断言

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

例如:

assert(p != NULL);

上面代码在程序运行到这⼀行语句时,验证变量p是否为空指针。如果表示,程序正常运行;否则,程序终止运行,并且会给出错误信息。

assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值非零),assert()宏则不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

assert()的使用对程序员非常友好,使用assert()的好处在于:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问题,不需要再做断言,就在 #include <assert.h> 语句前面定义一个 NDEBUG

 #define NDEBUG#include <assert.h>

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

而使用assert()的缺点在于:引入了额外的检查,增加了程序的运行时间。

一般我们可以在Debug中使用,在Release版本中选择禁用assert()就行。这样在debug版本写有利于程序员排查问题,在Release版本不影响用户的使用体验。

8. 指针的使用和传址调用

8.1 strlen的模拟实现

库函数strlen的功能是求字符串长度,统计的是字符串中 '\0' 前的字符数。

函数原型如下:

size_t strlen ( const char * str );

参数str接收一个字符串的起始地址,然后开始统计字符串中 '\0' 之前的字符个数,最终返回长度。

因此,我们模拟就需要从起始地址开始向后逐个检查字符,如果不为 '\0' ,计数器就+1,知道遇到 '\0' 为止。

例如:

#include <stdio.h>
#include <assert.h>int my_strlen(const char* s)
{assert(s);int count = 0;while(*s != '\0'){count++;s++;}return count;
}//输出结果
5

8.2 传值调用和传址调用

学习了指针的知识,现在我们来看看专门用指针来解决的问题。

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

思考之后,我们可能会写出这样的代码:

#include <stdio.h>void Swap(int x,int y)
{int temp;temp = x;x = y;y = temp;
}int main()
{int a = 1,b = 2;printf("交换前:a = %d,b = %d",a,b);Swap(a,b);printf("交换后:a = %d,b = %d",a,b);return 0;
}

但是当我们检查打印结果时:

交换前:a = 1,b = 2
交换后:a = 1,b = 2

我们发现a,b的值并没有和我们预期中一样实现交换,这是为什么呢?

这个时候,我们就要回顾一下前面的知识:形参是实参的一份临时拷贝,也就是说,形参与实参的地址是不同的。在函数内部实现的值的交换只是交换了形参的地址中的值,而实参的地址的值并没有变化。在函数结束后,内存被释放。所以,x和y的值的交换不会影响a和b的值。

像Swap函数这样,在调用函数时传递变量本身的调用方法被称为传值调用

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

因此,这种写法是错误的,那我们应该怎么实现题目要求呢?

我们现在要解决的事情就是,在函数Swap内部实现main函数中变量a和b的值的交换。既然直接传递变量时,形参与实参的地址是不同的,那我们直接传递地址是否能解决这个问题呢?

于是,我们可以得到下面的代码:

#include <stdio.h>void Swap(int* const px,int* const py)
{int temp;temp = * px;* px = * py;* py = temp;
}int main()
{int a = 1,b = 2;printf("交换前:a = %d,b = %d\n",a,b);Swap(&a,&b);printf("交换后:a = %d,b = %d\n",a,b);return 0;
}

此时,我们再检查打印结果:

交换前:a = 1,b = 2
交换后:a = 2,b = 1

可以发现,代码成功实现了值的交换。

而像这样在调用函数时传递变量的地址的调用方式被称为传址调用。

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。

http://www.dtcms.com/a/318660.html

相关文章:

  • 亚马逊新品实现快速起量:如何设置有效的广告竞价策略
  • Java保姆级新手教程第三章(方法与数组)
  • 亚马逊广告进阶指南:广告转化的基本原理
  • 前端性能优化实战:电商首页从 10s 加载到 1s 的踩坑与复盘
  • 大数据存储域——HDFS存储系统
  • 在LLM小型化趋势下,AI Infra需要做出哪些相应调整?
  • 用 “私房钱” 类比闭包:为啥它能访问外部变量?
  • 日记研究:一种深入了解用户真实体验的UX研究方法
  • 【2025CVPR-目标检测方向】FIRE:通过频率引导重建误差对扩散生成的图像进行鲁棒检测
  • 2025AI论文工具测评?个人实测5款AI工具论文写作使用体验对比
  • 【pytorch(02)】Tensor(张量)概述、如何创建、常见属性,切换设备
  • 【0基础PS】PS工具详解--直接选择工具
  • TypeScript 数组类型精简知识点
  • 文本编码扫盲及设计思路总结
  • Mongodb入门介绍
  • [Python 基础课程]学生语文成绩录入和查询需求
  • [假面骑士] 555浅谈
  • AI大语言模型如何重塑软件开发与测试流程
  • Linux操作系统启动项相关研究与总结
  • 高速信号设计之 UPI2.0 篇
  • Spring Security 框架深度集成与开发指南
  • 如何设计一个开放授权平台?
  • 初识神经网络01——认识PyTorch
  • k8s的存储之statefulset控制器
  • 【MyBatis新手避坑】详解 `Could not find resource ...Mapper.xml` 错误
  • Class30数据增广
  • Leetcode刷题营:字符串相关--第35,36题
  • 深度探索:非静态内部类不能定义 static 成员属性和方法 及 静态内部类的必要性
  • 若依前后端分离版学习笔记(六)——JWT
  • K8S、Docker安全漏洞靶场