深入解剖指针篇(2)

目录

指针的使用

strlen的模拟实现

传值调用和传址调用

数组名的理解

使用指针访问数组 

一维数组传参的本质

冒泡排序


个人主页(找往期文章):我要学编程(ಥ_ಥ)-CSDN博客

 

指针的使用

strlen的模拟实现

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

函数原型:

知道了上面这些,我们就直接开是写代码 。

#include <stdio.h>
int my_strlen(const char* p)
{int count = 0;while (*p != '\0'){count++;p++;}return count;
}
int main()
{char arr[] = "abcdef";int len = my_strlen(arr);printf("%d\n", len);return 0;
}

如果要真正相同的话,这个函数的返回类型也应该改一改。

#include <stdio.h>
size_t my_strlen(const char* p)
{size_t count = 0;while (*p != '\0'){count++;p++;}return count;
}
int main()
{char arr[] = "abcdef";size_t len = my_strlen(arr);printf("%zd\n", len);return 0;
}

因为函数返回类型改了,那么那个返回的值(count)的类型也应该变,接收的,打印的都要变。 

传值调用和传址调用

 学习指针的目的是使用指针解决问题,那什么问题,非指针不可呢?

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

#include <stdio.h>
void swap(int a, int b)
{int tmp = 0;tmp = a;a = b;b = tmp;
}
int main()
{int a = 0;int b = 0;scanf("%d%d", &a, &b);printf("交换前:a=%d,b=%d\n", a, b);swap(a, b);printf("交换后:a=%d,b=%d\n", a, b);return 0;
}

我们去运行这个代码会发现,交换前后a与b的值根本就没有发生变化。

这到底是什么原因导致的呢?我们可以尝试调试一下(因为这里我形参和实参都是设置a和b,不好观察,我就把形参改成x和y了): 

通过上面两幅图,我们可以看到x与y的值,虽然交换了,但是却没有影响到a与b。我们在通过指针来深入观察:

我们可以看到a,b与x,y的地址不是一样的,相当于x和y是独立的空间,那么在swap函数内部交换x和y的值, 自然不会影响a和b,当swap函数调用结束后回到main函数,a和b的没法交换。swap函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,这种叫传值调用。

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

通过前面指针的学习,我们知道可以通过指针来寻找到它所指向的对象,并且可以修改这个对象的值。在main函数中将a和b的地址传递给swap函数,swap 函数里边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

#include <stdio.h>
void swap(int* x, int* y)
{int tmp = 0;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);swap(&a, &b);printf("交换后:a=%d,b=%d\n", a, b);return 0;
}

上面代码是将a与b的地址传给了函数swap,这种叫做传址调用。

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

数组名的理解

在使用指针访问数组的内容时,有这样的代码:

上面两种写法都是对的,用代码验证一下。

&arr[0]的写法:

#include <stdiio.h>
void Print(int* p, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(p + i));}
}
int main()
{int arr[] = { 1,2,3,4,5 };int sz = sizeof(arr) / sizeof(arr[0]);Print(&arr[0], sz);return 0;
}

arr的写法:

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

我们可以看到这两个代码的结果是一模一样的。就说明&arr[0]与arr是一样的。换句话说,数组名就是数组首元素的地址。但是有两种情况是例外:

• sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小, 单位是字节

• &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的) 除此之外,任何地方使用数组名,数组名都表示首元素的地址。

我们可以通过打印的结果知道: sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小。

这些是十六进制,通过计算可以得知:&arr[0]和&arr[0]+1相差4个字节,是因为&arr[0] 都是首元素的地址,+1就是跳过一个元素。但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。

但是在用代码证明时,有的小伙伴,可能会写成下面的代码,从而无法证明。

之所以会这样,是因为这个&arr,是指整个数组,而这个p1是一个指针变量,只能存放一个地址,不能将整个地址给存放。如果强行这样做,就导致整个p1,只是存了第一个元素的地址。达不到我们的预期。

使用指针访问数组 

有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了。

练习:用指针实现数组的输入和输出。

#include <stdio.h>
int main()
{int arr[10] = { 0 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;int i = 0;for (i = 0; i < sz; i++){scanf("%d", p + i);}for (i = 0; i < sz; i++){printf("%d ", *(p + i));}
}

我们再分析一下,数组名arr是数组首元素的地址,可以赋值给p,其实数组名arr和p在这里是等价的。那我们可以使用arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢? 

所以本质上p[i] 是等价于 *(p+i)。我们可以理解为[ ] == * 。

一维数组传参的本质

数组是可以传递给函数的,我们讨论一下数组传参的本质。 首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给一个函数后,函数内部求数组的元素个数吗?

我们发现在函数内部是没有正确获得数组的元素个数。 这就要学习数组传参的本质了,上面验证了:数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组首元素的地址。 所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写 sizeof(arr) 计算的是一个指针的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。 而这个sz2的值是和32位平台还是64位有关。因为指针变量的大小在32位平台下是4个字节,64位是8个字节。

总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

冒泡排序

冒泡排序的核心思想就是:两两相邻的元素进行比较。

冒泡排序是一种算法,用来解决数组内部元素有序的问题。比如:有一个数组,内部元素杂乱无章,但是我们要的是一个降序的数组。这时就可以采用冒泡排序的方法。具体怎么实现呢?我就用画图的方式给大家展现出来。

到这里这个代码也就可以写出来了。

#include <stdio.h>
void bubble_sort(int* p, int sz)
{int i = 0;for (i = 0; i < sz - 1; i++)//趟数{int j = 0;for (j = 0; j < sz - 1; j++)//每一趟{if (*(p + j) < *(p + j + 1)){int tmp = *(p + j);*(p + j) = *(p + j + 1);*(p + j + 1) = tmp;}}}
}
void Print(int* p, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(p + i));}
}
int main()
{int arr[10] = { 0 };int sz = sizeof(arr) / sizeof(arr[0]);Init(arr, sz);bubble_sort(arr, sz);//改成降序Print(arr, sz);return 0;
}

但是如果我们再仔细分析的话,就会发现在每一趟的元素比较中,需要比较的元素个数是随着趟数的增加,变得越来越少。举例:第一趟要比较10个数,得出一个数是最小的之后,第二趟来比较时,就不需要和那个第一趟比较出的数再来比较了。因为第一趟比较出的数之所以最小,是因为它在这是个元素中是最小的,那么第二趟比较出的那个最小数,一定比那个第一趟比较出的那个数要大才行。以此类推,第二趟比较9个数就是j到7就可以了(j+1等于8,第九个数的下标是8),第三趟比较8个数就是j到6就可以了。那么最终的规律就是j<sz-1-i。

改进的代码:

#include <stdio.h>
void bubble_sort(int* p, int sz)
{int i = 0;for (i = 0; i < sz - 1; i++)//趟数{int j = 0;for (j = 0; j < sz - 1 - i; j++)//每一趟{if (*(p + j) < *(p + j + 1)){int tmp = *(p + j);*(p + j) = *(p + j + 1);*(p + j + 1) = tmp;}}}
}
void Print(int* p, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(p + i));}
}
int main()
{int arr[10] = { 0 };int sz = sizeof(arr) / sizeof(arr[0]);Init(arr, sz);bubble_sort(arr, sz);//改成降序Print(arr, sz);return 0;
}

其实到这里了,这个代码还能够优化一点:如果我们的数组里,就只有一个元素不是有序的,其余的都是有顺序的,因此我们只需要比较一次就可以了。

如果这个数组是9 8 7 6 5 4 3 2 1 10,这个想要变成降序,就需要走9趟了。因为第一趟就只能把1和10换位置。还剩下其它的数要换,就只能走9趟 

那么怎么判断这个数组比较过后是有序还是无序呢?

法一:可以定义一个flag变量,初始化为1。如果这个有序了,啥也不干,那么就令它为0;否则就是1。每走完一趟之后就可以根据flag的值,判断是否有序。如果是0,就说明这个数组有序,跳出循环。

#include <stdio.h>
//void init(int* p, int sz)
//{
//	int i = 0;
//	for (i = 0; i < sz; i++)
//	{
//		scanf("%d", (p + i));
//	}
//}
void bubble_sort(int* p, int sz)
{int i = 0;for (i = 0; i < sz - 1; i++)//趟数{int flag = 1;//注意这个定义的位置。int j = 0;for (j = 0; j < sz - 1 - i; j++)//每一趟{if (*(p + j) < *(p + j + 1)){int tmp = *(p + j);*(p + j) = *(p + j + 1);*(p + j + 1) = tmp;flag = 0;//一旦进入就变为0。}	}if (flag == 1)//等于1,代表if一次也没有执行。{break;}}
}
void print(int* p, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(p + i));}
}
int main()
{int arr[10] = { 9,10,8,7,6,5,4,3,2,1 };int sz = sizeof(arr) / sizeof(arr[0]);//init(arr, sz);bubble_sort(arr, sz);//改成降序print(arr, sz);return 0;
}

法二:可以定义一个变量count,如果这个一趟里面每没有执行一次,count就++。一趟走完之后,如果count==sz-1-i,那么说明这个数组已经有序,就跳出循环。

#include <stdio.h>
//void Init(int* p, int sz)
//{
//	int i = 0;
//	for (i = 0; i < sz; i++)
//	{
//		scanf("%d", (p + i));
//	}
//}
void bubble_sort(int* p, int sz)
{int i = 0;for (i = 0; i < sz - 1; i++)//趟数{int j = 0;int count = 0;//注意这个定义的位置,如果定义在趟数的外面,这个count就会累加for (j = 0; j < sz - 1 - i; j++)//每一趟{if (*(p + j) < *(p + j + 1)){int tmp = *(p + j);*(p + j) = *(p + j + 1);*(p + j + 1) = tmp;}else{count++;}}if (count == sz - 1 - i)如果count等于这个,就说明if一次也没有执行{break;}}
}
void Print(int* p, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(p + i));}
}
int main()
{int arr[10] = { 9,10,8,7,6,5,4,3,2,1 };int sz = sizeof(arr) / sizeof(arr[0]);//Init(arr, sz);bubble_sort(arr, sz);//改成降序Print(arr, sz);return 0;
}

上面有些代码被注释,是为了更好的调试观察。当然大家可以把那些注释去掉。 

上面两种优化可以通过调试来观察是否优化成功(VS的调试方法在我往期的文章里,可以去主页里找) 。

 感觉四篇文章可能写不完指针的所有内容。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/661862.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1683. 无效的推文

说在前面 &#x1f388;不知道大家对于算法的学习是一个怎样的心态呢&#xff1f;为了面试还是因为兴趣&#xff1f;不管是出于什么原因&#xff0c;算法学习需要持续保持。 题目描述 表&#xff1a;Tweets ------------------------- | Column Name | Type | --------…

PySpark(二)RDD基础、RDD常见算子

目录 RDD RDD五大特性 RDD创建 RDD算子 常见的Transformation算子 map flatMap mapValues reduceByKey groupBy filter distinct union join intersection glom groupByKey groupByKey和reduceByKey的区别 ? sortBy sortByKey 常见的action算子 countByKey…

MySQL入门篇(10)-聚合函数的应用

MySQL数据库聚合函数的应用 在MySQL数据库中&#xff0c;聚合函数用于计算一组数据的统计值并返回结果。这些函数可以应用于查询语句中&#xff0c;对数据进行汇总、计数、平均值计算等操作。本文将介绍一些常用的MySQL聚合函数及其应用。 1. COUNT函数 COUNT函数用于计算指…

软考 系统分析师系列知识点之需求管理(2)

接前一篇文章&#xff1a;软考 系统分析师系列知识点之需求管理&#xff08;1&#xff09; 所属章节&#xff1a; 第11章. 软件需求工程 第8节. 需求管理 11.8.2 需求风险管理 人们做事情总希望一帆风顺&#xff0c;做项目也是如此&#xff0c;总是希望项目进展顺利&#xff…

Ubuntu18配置Docker

1.基本过程 1.更新软件源列表 sudo apt update2.安装软件包依赖 sudo apt install apt-transport-https ca-certificates curl software-properties-common3.在系统中添加Docker的官方密钥 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - …

新概念英语第二册(49)上

【New words and expressions】生词和短语&#xff08;13&#xff09; tired adj. 厌烦的 real adj. 真正的 owner n. 主人 spring n. 弹簧 mattress …

erlang (erlang 操作模块)学习笔记(四)

map_size 1> map_size(#{a>1, b>2, c>3}). 3返回一个整数&#xff0c;即键值对的数量 max 2> max(1, 2). 2 3> max(1.0, 1). 1.0 4> max(1, 1.0). 1 5> max("abc", "b"). "b"返回 Term1 和 Term2 中最大的值。如果这…

前端JavaScript篇之ES6中数组新增了哪些扩展?

目录 ES6中数组新增了哪些扩展?1. **箭头函数&#xff1a;**2. **扩展运算符&#xff08;Spread Operator&#xff09;&#xff1a;**3. **解构赋值&#xff1a;**4. **Array.from()方法&#xff1a;**5. **Array.of()方法&#xff1a;**6. **find()和findIndex()方法&#xf…

Python入门到精通(七)——Python文件操作

Python文件操作 一、文件的编码 二、文件的读取 1、操作汇总 2、model 常用的三种基础访问模式 三、文件的写入 四、文件的追加 五、综合案例 一、文件的编码 1、什么是编码&#xff1f; 编码就是一种规则集合&#xff0c;记录了内容和二进制间进行相互转换的逻辑。编…

Flink1.14新版KafkaSource和KafkaSink实践使用(自定义反序列化器、Topic选择器、序列化器、分区器)

前言 在官方文档的描述中&#xff0c;API FlinkKafkaConsumer和FlinkKafkaProducer将在后续版本陆续弃用、移除&#xff0c;所以在未来生产中有版本升级的情况下&#xff0c;新API KafkaSource和KafkaSink还是有必要学会使用的。下面介绍下基于新API的一些自定义类以及主程序的…

解析Excel文件内容,按每列首行元素名打印出某个字符串的统计占比(超详细)

1.示例&#xff1a; 开发需求&#xff1a;读取Excel文件&#xff0c;统计第3列到第5列中每列的"False"字段占比&#xff0c;统计第6列中的"Pass"字段占比&#xff0c;并按每列首行元素名打印出统计占比 1.1 实现代码1&#xff1a;列数为常量 请确保替换y…

测试access和trunk口的区别(华为)

思科设备参考&#xff1a;测试access和trunk口的区别&#xff08;思科&#xff09; 一&#xff0c;实验目的 实现同一 Vlan 内的主机互通&#xff0c;不同 Vlan 间的主机隔离。 二&#xff0c;配置前测试 PC1分别ping PC2、PC3、PC4都能通&#xff0c;因为四台PC默认同处于v…

NatCross免费内网穿透工具:助你快速搭建内网服务

NatCross是一款免费的内网穿透工具&#xff0c;产品基于分布式集群服务架构&#xff0c;为需要远程连接的企业和个人提供更加稳定和简单的解决方案。 场景主要包括出差在外或分支机构访问公司的OA、CRM、ERP系统、利用ADSL等宽带连接实现远程视频监控、在办公室或者家里搭建网站…

Android native层c++调用java层API

在Android开发中&#xff0c;从本地&#xff08;native&#xff09;代码调用Java层的接口是一个常见的需求&#xff0c;尤其是在使用JNI&#xff08;Java Native Interface&#xff09;进行混合编程时。以下是一个基本的步骤指南&#xff0c;展示如何从C代码调用Java方法&#…

一文掌握SpringBoot注解之@Configuration知识文集(2)

&#x1f3c6;作者简介&#xff0c;普修罗双战士&#xff0c;一直追求不断学习和成长&#xff0c;在技术的道路上持续探索和实践。 &#x1f3c6;多年互联网行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f389;欢迎 &#x1f44d;点赞✍评论…

【JAVA】单例模式的线程安全性

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;JAVA ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 正文 我的其他博客 正文 老生常谈的问题了&#xff0c;首先要说的是单例模式的线程安全意味着&#xff1a;某个类的实例在多线程环境 下只会被…

main函数中参数argc和argv用法解析

1 基础 argc 是 argument count 的缩写&#xff0c;表示传入main函数的参数个数&#xff1b; argv 是 argument vector 的缩写&#xff0c;表示传入main函数的参数序列或指针&#xff0c;并且第一个参数argv[0]一定是程序的名称&#xff0c;并且包含了程序所在的完整路径&…

YOLO系列详解(YOLO1-YOLO5)

YOLO&#xff08;You Only Look Once&#xff09;系列是一组流行的实时对象检测算法&#xff0c;它们因其速度快和准确率高而闻名。YOLO 系列从 YOLOv1 发展到了后续的多个版本&#xff0c;每个版本都在性能、准确度和速度方面进行了改进。 YOLOv1 发布年份&#xff1a;2016主…

2024年美赛赛题浅析

MCM 赛题难度B>A>C 选题人数C>A>B Problem A: Resource Availability and Sex Ratios 数据收集多元分析定性分析&#xff08;语文建模&#xff09;稳定性分析相关分析 七鳃鳗寄生虫生态系统的数据收集 Problem B: Searching for Submersibles 数据收集预测模…

深度解读NVMe计算存储协议-2

近日&#xff0c;NVME协议组织为了解决这些性能问题并为供应商提供标准化机制&#xff0c;在其架构中集成优化的计算功能&#xff0c;开发了NVM Express (NVMe) 计算存储特性。 计算存储的核心特性包括两个命令集&#xff1a;计算程序集和子系统本地内存。 其中&#xff0c;计算…