手把手带你拿捏C指针(2)(含冒泡排序)

在这里插入图片描述

文章目录

  • 一、数组名的理解
  • 二、使用指针访问数组
  • 三、一维数组传参本质
  • 四、冒泡排序
  • 五、二级指针
  • 六、指针数组
  • 七、指针数组模拟二维数组

一、数组名的理解

在上⼀个章节我们在使⽤指针访问数组的内容时,有这样的代码:

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];

    这⾥我们使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且是数组⾸元素的地址,我们来做个测试

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0] = %p\n", &arr[0]);printf("arr = %p\n", arr);return 0;
}

输出结果:
在这里插入图片描述
    我们发现数组名和数组⾸元素的地址打印出的结果⼀模⼀样,数组名就是数组首元素(第⼀个元素)的地址
    这时候有同学会有疑问?数组名如果是数组⾸元素的地址,那下⾯的代码怎么理解呢?

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("%d\n", sizeof(arr));return 0;
}

    输出的结果是:40,如果arr是数组⾸元素的地址,那输出应该的应该是4/8才对。
    其实数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:

  • sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的大小,单位是字节
  • &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)

除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址

这时有好奇的同学,再试⼀下这个代码:

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0] = %p\n", &arr[0]);printf("arr     = %p\n", arr);printf("&arr    = %p\n", &arr);return 0;
}

运行结果:
在这里插入图片描述
    我们发现它们三个打印出来居然是一样的,那arr和&arr有什么区别呢?我们看以下的一个例子:

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0]   = %p\n", &arr[0]);printf("&arr[0]+1 = %p\n", &arr[0]+1);printf("arr       = %p\n", arr);printf("arr+1     = %p\n", arr+1);printf("&arr      = %p\n", &arr);printf("&arr+1    = %p\n", &arr+1);return 0;
}

运行结果如下:
在这里插入图片描述
    这⾥我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素
    这⾥我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素
    总结:数组名一般是数组首元素地址,只有两个例外,一个是它在sizeof中一个是&arr

二、使用指针访问数组

    有了前面知识的基础,我们用指针访问数组就显得简单多了,当我们要对数组进行输入时,我们还是使用循环,scanf后面的参数我们就可以写成arr+i,因为i=0时,arr+0就是首元素的地址,i=1时,arr+1就是第二个元素的地址,依此类推
    输出数组时也是同理,就是对原本的指针进行解引用,如下例:

#include <stdio.h>
int main()
{int arr[10] = {0};//输⼊int i = 0;int sz = sizeof(arr)/sizeof(arr[0]);//输⼊int* p = arr;for(i=0; i<sz; i++){scanf("%d", p+i);
//也可以这样写:
//scanf("%d", arr+i);}//输出for(i=0; i<sz; i++){printf("%d ", *(p+i));}return 0;
}

    这个代码搞明⽩后,我们再试⼀下,如果我们再分析⼀下,数组名arr是数组⾸元素的地址,可以赋值给p,其实数组名arr和p在这⾥是等价的。那我们现在可以大胆想象一下,可以使⽤arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢?如下代码:

 for(i=0; i<sz; i++){printf("%d ", p[i]);}

我们来看看代码运行结果:
在这里插入图片描述
    可以发现确实是这样,将 * (p+i)换成p[i]也是能够正常打印的,因为本质上p[i] 是等价于 * (p+i)。
    同理arr[i] 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的
    随后我们可以继续思考,既然arr[i]就等价于 * (arr+i),我们可以想一下,它是否 就等于 * (i+arr),很明显这是肯定的,那arr[i]是否就可以写成i[arr]呢?p[i]是否可以写成i[p]?这个确实有点匪夷所思,实践出真知,我们接下来就进入实验,如下代码:

int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;for (i = 0; i < sz; i++){printf("%d ", i[arr]);}printf("\n");for (i = 0; i < sz; i++){printf("%d ", i[p]);}return 0;
}

我们来看看运行结果:
在这里插入图片描述
    我们可以看到这样确实可以,是不是很震惊,我刚开始学到这里也是这样的,但是也确实很有趣。
    从这个例子我们也可以得出,下标访问操作符[]它的实际作用就是将它的两个操作数转换成指针的形式,比如将arr[i]转换为*(arr+i),如果是i[arr]就转换成 * (i+arr),这两个东西是等价的,所以我们将i和arr交换位置才没有问题

三、一维数组传参本质

    数组我们学过了,之前也讲了,数组是可以传递给函数的,这个⼩节我们讨论⼀下数组传参的本质。⾸先从⼀个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给⼀个函数后,函数内部求数组的元素个数吗?

#include <stdio.h>
void test(int arr[])
{int sz2 = sizeof(arr)/sizeof(arr[0]);printf("sz2 = %d\n", sz2);
}
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int sz1 = sizeof(arr)/sizeof(arr[0]);printf("sz1 = %d\n", sz1);test(arr);return 0;
}

我们来看看运行结果:
在这里插入图片描述
    我们发现在函数内部是没有正确获得数组的元素个数
    这就要学习数组传参的本质了,上个⼩节我们学习了:数组名是数组⾸元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组⾸元素的地址
    所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写sizeof(arr) 计算的是⼀个地址的大小(单位字节)而不是数组的⼤小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的
    随后我们也能推导出,既然一维数组传参是传的首元素的地址,那么我们是否就可以用指针接收,接下来看另一个例子:

#include <stdio.h>
void test1(int arr[])//参数写成数组形式,本质上还是指针
{printf("%d\n", sizeof(arr));
}void test2(int* p)//参数写成指针形式
{printf("%d\n", sizeof(p));//计算⼀个指针变量的⼤⼩
}int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };test1(arr);test2(arr);return 0;
}

我们来看一下运行结果:
在这里插入图片描述
    总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式

四、冒泡排序

    冒泡排序就是模拟冒泡的样子,数据不同,那么泡泡的大小就不同,小泡泡就会慢慢浮上去,按这个理解,冒泡排序默认是升序的,今天我们写一个冒泡排序,既可以升序,也可以降序
    冒泡排序的原理就是比较一堆数中相邻的两个数,如果升序的话就是把小的那个数换到前面,如果是降序的话,就是把大的数换到前面
接下来我们开始设计冒泡排序函数:

  1. 函数命名:推荐:Bubble_sort,可以自行取名
  2. 函数参数:由于我们要对一堆数进行排序,所以我们需要一个数组帮我们存储这些数,随后我们需要这个数组的元素个数,最后由于我们设计的冒泡函数既有升序又有降序,所以我们可以将第三个参数用于辨别是升序还是降序,在这里我们就定义:第三个参数是0就是升序,第三个参数是1就是降序
  3. 函数实现:
    (1)冒泡排序的中心思想就是比较相邻的两个数,看它们的大小比较,然后适时交换,现在我们以升序举例,如果左边的数大于右边的数,那么就对它们进行交换
    (2)接着我们思考一下需要交换多少次,我们现在举一个比较极端的例子,如下图所示:
    在这里插入图片描述
        可以看到在这个例子中,7一直在做交换,那它最多做几次交换呢?经过推算,我们知道,在第七次交换时,7就已经交换好了,也就是总共8个数,需要交换8-1次,n个数就要交换n-1次,当然,这是最差的情况
    (3)我们将7这个数换到了它正确的地方,经过了多次交换,我们就叫它一趟冒泡排序,一趟冒泡排序可以排好一个数字,那么一共有8个数字就需要7趟冒泡排序,因为如果把7个数字放在正确位置上了,那么第8个数字一定就在正确的位置上
    (4)所以经过分析,我们知道了我们需要进行多趟冒泡排序,一趟冒泡排序可能有多次交换,所以我们需要两层循环,外层负责多趟,内层负责一趟的多次交换
    (5)那么需要多少趟呢?在上面的例子中,我们了解到应该需要n-1趟,那每一趟可能需要交换多少次呢?这个就会随着循环的变化而变化,比如第一趟时需要n-1趟,又比如我们已经进行了一趟冒泡排序,那么就有1个数字排到了正确位置,这个时候就最多只需要n–1-1次交换,所以一趟需要交换多少次是会变化的,每完成一趟就少一次交换,所以我们可以写成n-i-1
    (6)最后就是对数组中挨着的两个元素进行比较大小,我们可以设计一个if进行判断,如果第三个参数是0,那么进行升序排序,如果是1,就升序,这里用升序排序举例,如果左边大一些,那么就把两个数交换,否则不做任何修改
    (7)有可能当我们只排序两三趟就完成了排序,后面的判断就有点浪费,所以我们可以创建一个变量flag作为标志,我们将其设置为1,含义是排序已经完成,然后每次进入交换时就把它置为0,如果没有产生交换就说明排序已经完成,就可以结束,以上就是整个冒泡排序的思路
    (8)代码:
//冒泡排序:
void Bubble_sort(int arr[], int sz, int x)
{int i = 0;int j = 0;for (i = 0; i < sz-1; i++){//假设已经排序完成int flag = 1;for (j = 0; j < sz-i-1; j++){if (x == 0){if (arr[j] > arr[j + 1]){int exg = arr[j];arr[j] = arr[j + 1];arr[j + 1] = exg;flag = 0;}}else if (x == 1) {if (arr[j] < arr[j + 1]){int exg = arr[j];arr[j] = arr[j + 1];arr[j + 1] = exg;flag = 0;}}}if (flag == 1){break;}}
}void print(int arr[], int sz)
{for (int i = 0; i < sz; i++){printf("%d ", arr[i]);}
}int main()
{int arr[10] = { 2,5,8,4,6,1,9,3,7,10 };int sz = sizeof(arr) / sizeof(arr[0]);Bubble_sort(arr, sz, 0);//第三个参数是0就升序//是1就是降序print(arr, sz);return 0;
}
  1. 最后我们来运行一下这个代码,看看我们的冒泡排序是否成功:
    升序:
    在这里插入图片描述
    降序:
    在这里插入图片描述

五、二级指针

    指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥?
    这就是二级指针,二级指针就是存放指针变量的地址,创建方式如下:

#include <stdio.h>
int main()
{int a = 0;int* pa = &a;int** ppa = &pa;return 0;
}

在这里插入图片描述
对于⼆级指针的运算有:

  1. *ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa , *ppa 其实访问的就是 pa,如下例:
int b = 20;
*ppa = &b;//等价于 pa = &b;
  1. **ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa ,那找到的是 a,如下例:
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

二级指针用的也比较少,后面会举例讲解,现在了解一下

六、指针数组

    指针数组是指针还是数组?
    我们类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组
那指针数组呢?是存放指针的数组
在这里插入图片描述
    指针数组的每个元素都是⽤来存放地址(指针)的,如下图:
在这里插入图片描述
    指针数组的每个元素是地址,分别指向⼀块区域

七、指针数组模拟二维数组

    我们可以创建几个数组,然后将数组的地址分别存入一个指针数组,如下:

int arr1[] = { 1,2,3,4,5,6 };
int arr2[] = { 2,3,4,5,6,7 };
int arr3[] = { 3,4,5,6,7,8 };
int* parr[] = {arr1,arr2,arr3};

    然后现在当我们访问指针数组parr的第一个元素时,我们发现parr[0]就是arr1的地址

    我们之前讲过,我们要访问数组的元素,不一定必须写出诸如arr[i]的样式,只要是arr首元素的地址都可以,比如假设有一个指针变量p存放了数组arr的首元素地址,那么可以使用p[i]来访问数组

    这里也是同理parr[0]就是第一个数组的数组名,也是该数组首元素地址,所以为了方便理解,我们将parr[0]想象成数组arr1的数组名,那么arr1的第一个元素表示为arr1[0],即parr[0][0],第二个元素为arr1[1],即parr[0][1]

    然后同理parr[1]就是第二个数组的数组名,也是该数组首元素地址,所以为了方便理解,我们也可以将parr[1]想像成arr2的数组名,那么arr2的第一个元素表示为arr2[0],即parr[1][0],第二个元素为arr2[1],即parr[1][1]

    经过上面的讲解,聪明的你是否已经发现,我们通过指针数组存放若干个数组地址,通过访问指针数组来访问原数组,实现了类似于二维数组的效果,在上例中,arr1相当于这个二维数组的第一行,arr2相当于这个二维数组的第二行,arr3相当于第三行

    接下来我们来看看这个完整过程是怎样的,以及它的运行结果:

#include <stdio.h>
int main()
{int arr1[] = { 1,2,3,4,5,6 };int arr2[] = { 2,3,4,5,6,7 };int arr3[] = { 3,4,5,6,7,8 };int* parr[] = {arr1,arr2,arr3};int i = 0;int j = 0;for (i = 0; i < 3; i++){for (j = 0; j < 6; j++){printf("%d ", parr[i][j]);}printf("\n");}return 0;
}

运行结果:
在这里插入图片描述
    可以看到确实通过指针数组,我们模拟实现了二维数组,今天的内容就到这里,你是否醍醐灌顶了呢?
    敬请期待下一篇指针(3)吧!

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

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

相关文章

工信部【信创认证】全面解读,包含信创集成项目管理师,信创规划管理师等

行业背景 国资委发布79号文件&#xff1a;详细规划了中央企业和国有企业信创国产化的实施路径和时间表&#xff0c;明确提出了到2027年100%完成信创“替代”的宏伟目标。这一政策不仅涵盖了芯片、基础软件、操作系统、中间件等重要领域&#xff0c;更意味着从2023年起&#xf…

5天涨粉3W!26个视频12.2W粉!AI做这种视频这么火嘛?

前几天刷到一个AI视频的账号&#xff0c;当时刷到时候才9W粉丝&#xff0c;今天又刷到他&#xff0c;已经12.2W了&#xff01;这涨粉速度&#xff0c;简直了&#xff01;&#xff01; 26个视频&#xff0c;12.2万粉丝 在这个看脸的时代&#xff0c;内容创作者们为了吸引眼球&a…

3.js - 着色器设置点材质(螺旋星系特效)

上图 着色器设置点材质时&#xff0c;在顶点着色器中&#xff0c;最好设置gl_PointSize&#xff0c;不然看不到你在页面中添加的点 main.js import * as THREE from three import { OrbitControls } from three/examples/jsm/controls/OrbitControlsimport gsap from gsapimp…

【截图服务 +打包】pkg打包 puppeteer

目录 最后结论 windows打包成服务 定制executablePath 用程序来查找chrome.exe 代替上面的写配置文件 服务遇到的问题 使用java开一个线程启动 遇到的问题与解决 版本匹配问题 打出包后的运行报错问题 linux下的安装 安装n 库缺少 程序运行后的报错 制作 运行报…

化工机械如何精准地进行网络营销推广?

合作咨询联系竑图 hongtu201988 化工机械行业该如何做网络推广&#xff0c;让销量和利润都有明显的提升呢&#xff1f;湖南竑图网络来为大家分析分析&#xff1a; 一、产品的用户是谁&#xff1f; 在传统行业中&#xff0c;用户群体的多样性不容忽视。比如机械设备有很多种&am…

Java 后端接口入参 - 联合前端VUE 使用AES完成入参出参加密解密

加密效果&#xff1a; 解密后的数据就是正常数据&#xff1a; 后端&#xff1a;使用的是spring-cloud框架&#xff0c;在gateway模块进行操作 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30…

Kamailio-超强dispatcher负载均衡模块

Kamailio 负载均衡的功能主要依靠 Dispatcher 模块完成&#xff0c;模块官方文档参看 为什么要引入负载均衡&#xff1f;如何使用&#xff1f; 引入和配置功能路由调用命令行指令 为什么要引入负载均衡&#xff1f; Q: 如果单台VOIP服务的性能不能满足业务需求了&#xff0…

C++中的I/O流

本节主要看代码理解 I/O流继承关系 iostream 主要类 cin cout cerr clog while&#xff08;cin>>str&#xff09; &#xff5b; //处理 &#xff5d; 当接收ctrl z 或 ctrl c时&#xff0c;会停止&#xff0c; 原理&#xff1a;重载操作符bool&#xff0c;令指定istr…

Meta:大语言模型可以通过自我批判取得大幅提升!

夕小瑶科技说 原创 作者 | 谢年年 论文的审稿模式想必大家都不会陌生&#xff0c;一篇论文除了分配多个评审&#xff0c;最后还将由PC综合评估各位审稿人的reviews撰写meta-review。 最近&#xff0c;来自Meta的研究团队将这一模式引进到大模型的对齐训练中。模型同时扮演 执…

poker (GuanDan)

poker &#xff08;GuanDan&#xff09; 掼蛋 基础比大小规则: ①单牌 2最小与以往不太一样&#xff08;2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A < Joker&#xff09; 如果本级打9&#xff0c;那么9就比A大&#xff0c;…

安装Anaconda(过程)

Anaconda是一个开源的Python发行版本&#xff0c;用来管理Python相关的包&#xff0c;安装Anaconda可以很方便的切换不同的环境&#xff0c;使用不同的深度学习框架开发项目&#xff0c;本文将详细介绍Anaconda的安装。 一、安装 1、安装方式 官网&#xff1a;“https://www.…

JVM - GC垃圾回收

文章目录 目录 文章目录 1. 自动垃圾回收 1.1 垃圾回收区域 2. 方法区回收 3. 堆回收 3.1 对象已死&#xff1f; 3.1.1 引用计数算法 3.1.2 可达性分析算法 3.1.3 再谈引用 强引用 软引用 弱引用 虚引用 3.2 垃圾收集算法 3.2.1 分代收集理论 3.2.2 垃圾回收算…

FlinkCDC 3.2.0 新增优点 Pattern Replacement in routing rules

新增优点&#xff1a;Pattern Replacement in routing rules flinkcdc 3.2.0版本相较于3.1.0版本&#xff0c;避免了多表多sink多次写 route 路由的麻烦&#xff0c;类似于统一前后缀的形式多表多sink&#xff0c;通过<>正则&#xff0c;大大减少了书写 官网&#xff1…

初始Linux 和 各种常见指令

目录 Linux背景 1. 发展史 Linux发展历史 1.历史 2. 开源 Linux下基本指令 01. ls 指令 02. pwd命令 03. cd 指令 04. touch指令 05.mkdir指令&#xff08;重要&#xff09;&#xff1a; 06.rmdir指令 && rm 指令&#xff08;重要&#xff09;&#xff1a; …

【Vue3】自动化路由配置:Vue3与unplugin-vue-router的完美结合

引言 在Vue3项目中&#xff0c;路由管理是构建复杂应用不可或缺的一部分。传统上&#xff0c;手动编写路由配置既繁琐又容易出错。本文将介绍如何利用unplugin-vue-router插件&#xff0c;实现Vue3项目的自动化路由配置&#xff0c;从而提升开发效率和准确性。 unplugin-vue-…

基于SpringBoot+Vue+MySQL的考研互助交流平台

系统展示 用户前台界面 管理员后台界面 系统背景 本文设计并实现了一个基于SpringBoot、Vue.js和MySQL的考研互助交流平台。该平台旨在为广大考研学子提供一个集资源共享、学习交流、经验分享、心理辅导等功能于一体的综合性在线社区。通过SpringBoot构建高效稳定的后端服务&am…

轻松实现游戏串流,内网穿透

一、部署Gemini Gemini使用教程 二、部署Moonlight 过程大概说一下&#xff0c;网上有太多太多moonlight的东西了 需要运行游戏的机器上安装GFE&#xff08;GeForce Experience&#xff09;&#xff0c;登录并开启GAMESTREAM&#xff08;游戏串流&#xff09;功能 注&…

网络安全 L2 Introduction to Cryptography 密码学

Definitions 1. crypto - hidden/secret grafia - writing 2. “the science and study of secret writing” 3. Cryptography is the science of protecting data, which provides means of converting data into unreadable form, so that 1. the data cannot be ac…

vue + Element UI table动态合并单元格

一、功能需求 1、根据名称相同的合并工作阶段和主要任务合并这两列&#xff0c;但主要任务内容一样&#xff0c;但要考虑主要任务一样&#xff0c;但工作阶段不一样的情况。&#xff08;枞向合并&#xff09; 2、落实情况里的定量内容和定性内容值一样则合并。&#xff08;横向…

9.11 QT ( Day 4)

一、作业 1.Widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimerEvent> //定时器类 #include <QTime> #include <QtTextToSpeech> //文本转语音类QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEcl…