当前位置: 首页 > 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.lryc.cn/news/611519.html

相关文章:

  • Kaggle 经典竞赛泰坦尼克号:超级无敌爆炸详细基础逐行讲解Pytorch实现代码,看完保证你也会!!!
  • 霍尔传感器
  • 碰撞问题的分析
  • 什么是CDN, 它为什么更快
  • 《算法导论》第 7 章 - 快速排序
  • 概率/期望 DP Jon and Orbs
  • 机器学习④【算法详解:从决策树到随机森林】
  • 一周学会Matplotlib3 Python 数据可视化-图形的组成部分
  • 场外期权的卖方是什么策略?
  • Python包管理新利器:uv全面解析与Conda对比指南
  • 从 LinkedIn 到 Apache:Kafka 的架构设计与应用场景
  • KafKa 项目 -- GitHub 学习
  • 【第6话:相机模型2】相机标定在自动驾驶中的作用、相机标定方法详解及代码说明
  • 在Word和WPS文字中如何输入汉字的偏旁部首
  • SELinux加固Linux安全2
  • docker安装FFmpeg
  • SmartMediaKit 模块化音视频框架实战指南:场景链路 + 能力矩阵全解析
  • Flink CDC如何保障数据的一致性?
  • 力扣经典算法篇-44-组合总和(回溯问题)
  • Ubuntu20.04 离线安装 FFmpeg 静态编译包
  • 【unity实战】用unity实现一个3D俯视角暗杀潜行恐怖类游戏,主要是实现视野范围可视化效果
  • X86-ubuntu22.04远程桌面只有1/4无法正常操作
  • 问题定位排查手记1 | 从Windows端快速检查连接状态
  • 分布式文件系统07-小文件系统的请求异步化高并发性能优化
  • ubuntu 22.04 中安装python3.11 和 3.11 的 pip
  • STM32U5 外部中断不响应问题分析
  • Ubuntu设置
  • DevOps时代的知识基座革命:Gitee Wiki如何重构研发协作范式
  • 基于51单片机的温控风扇Protues仿真设计
  • 【面试场景题】电商秒杀系统的库存管理设计实战