C++学习day--18 空指针和函数指针、引用

1、void 类型指针

void => 空类型
void* => 空类型指针, 只存储地址的值,丢失类型,无法访问,要访问其值,我们必须对这个指
针做出正确的类型转换,然后再间接引用指针 。 所有其它类型的指针都可以隐式自动转换成 void 类型指针,反之需要强制转换。
#include <stdio.h>
#include <stdlib.h>
int main(void) {int arr[] = { 1, 2, 3, 4, 5 };char ch = 'a';void* p = arr;//定义了一个void 类型的指针//p++; //报错, void * 指针不允许进行算术运算//printf("数组第一个元素: %d\n", *p); //报错,不可以进行访问p = &ch; //其它类型可以自动转换成void * 指针printf("p: 0x%p ch: 0x%p\n", p, &ch);char* p1 = (char*)p;//强制类型转化printf("p1 指向的字符是: %c\n", *p1);return 0;
}

运行结果:

 重点说几点:空类型指针不能直接访问,不管它初始化为什么,都不能直接访问!!这是它核心的一个点。除了不能直接访问,还不能做加减运算,由于没有指定类型,因此你也不知道它会跳变几个字节。

要想访问空类型指针,必须强转空类型指针才能访问。空指针就这几点要注意,那么有个问题:

很多人在这里有疑问,为什么要定义空指针,再强制类型转换? 直接定义同类型指针不就行了吗?其实不然,很多库函数的实现都用了void类型指针,只是在这里 体现不到它的精妙之处!! 我们下面马上讲函数指针,讲完函数指针,再与空指针结合实现C语言库函数的快速排序!

2、函数指针

函数地址:

所谓函数指针就是函数名的地址,原来函数名也有地址。

#include<stdio.h>int add(int a, int b) {return a + b;
}int main() {printf("%p\n", add);printf("%p\n", &add);
}
运行结果:

这个结果说明,函数指针和不同指针的区别,函数名本身就是一个地址,函数名的地址等于地址的地址还是本身!这就是和普通指针的区别!!

函数指针:

顾名思义,函数指针就是用来保存函数地址的变量。函数指针定义如下:

函数指针的定义 把函数声明移过来,把函数名改成 (* 函数指针名)

案例:

#include<stdio.h>int add(int a, int b) {return a + b;
}
//函数指针的定义 把函数声明移过来,把函数名改成 (* 函数指针名)
int main() {int (*fp)(int, int) = &add;int (*fp1)(int, int) = add;printf("%p\n", fp);printf("%p\n", *fp);printf("%p\n", fp1);printf("%p\n", *fp1);printf("%d\n", (*fp)(2, 3));printf("%d\n", fp(2, 3));printf("%d\n", (*fp1)(2, 3));printf("%d\n", fp1(2, 3));
}

运行结果:

 函数指针和普通指针区别非常明显,赋值可以不用加&,使用可以不加解引用符*,这是有历史原因的:

    贝尔实验室的C和UNIX的开发者采用第1种形式,而伯克利的UNIX推广者却采用第2
    种形式,最后ANSI标准C 兼容了两种方式
    fp = &compare_int; 
  
 (*fp)(&x, &y); //第1种,按普通指针解引的放式进行调用,(*fp) 等同于compare_int
    fp(&x, &y); //第2种 直接调用

函数指针的应用:快速排序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>int compare_int(const void* a, const void* b) {int* a1 = (int*)a;  //空类型指针强转int* b1 = (int*)b;return *b1 - *a1;
}int main(){//qsort 对整形数组排序int arr[] = { 2, 10, 30, 1, 11, 8, 7, 111, 520 };qsort(arr, sizeof(arr) / sizeof(int), sizeof(int), &compare_int);for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {printf("%d ", arr[i]);}return 0;
}

运行结果:

 这是降序排序,升序排序只需要把compare_int函数改一下即可如下:

即只需把返回值改为: *a1 - *b1 ,就是升序排序

说一下qsort()函数:

第一个参数是待排序的数组

第二个参数是数组长度

第三个参数是每个元素的大小(所占字节)

第四个参数是函数指针,用来声明排序规则的。当然这个参数不用加取地址符&也可以。

用qsort()函数实现char类型数组按升序排序:要求对大小写不敏感,即不区分大小写字母(有难度)。 

 代码实现如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>int compare_char(const void* e1, const void* e2) {char a1 = *((char*)e1);char b1 = *((char*)e2);if (a1 >= 'a'&& a1 <= 'z') {a1 = *((char*)e1) - 32;}if (b1 >= 'a' && b1 <= 'z') {b1 = *((char*)e2) - 32;}return a1 - b1;
}int main(){char arr[] = "abcdefghiABCDEFGHI";//要求最终的排序结果要为:a A b B c C d D e E f F g G h H i Iqsort(arr, sizeof(arr) / sizeof(char) - 1, sizeof(char), compare_char);for (int i = 0; i < strlen(arr); i++) {printf("%c ", arr[i]);}return 0;
}

运行结果:

 

区分大小写排序,思考为什么是这么实现的。这个代码第一次可能接受起来困难,理解如下 :因为我们要区分大小写,我们在传递进去的时候把在大写字母(65~90)加上32变成小写字母,这样进行排序的时候,大写字母被看为小写字母进行排序。
qsort()函数是C语言特有的快速排序函数,现在对qsort()函数具体说一下几点:

1、第四个参数函数指针,实现该函数返回值只能是int 类型,因此所有指针都是强转为int

2、实现的函数,参数是空类型指针,且有const关键字修饰,根据前面所学知识,我们知道const修饰的是void,即所指向的空间的内容不能改变! 

3、对字符串,因为C语言默认有结束符'\0',虽然不计入长度,但是它会占据一个字节,因此第二参数要减去1。

3、引用

变量名回顾
变量名实质上是一段连续存储空间的别名,是一个标号 ( 门牌号 )
程序中通过变量来申请并命名内存空间
通过变量的名字可以使用存储空间
问题 1 :对一段连续的内存空间只能取一个别名吗?
问题2:指针传参能提高效率,但往往可读性差,有没有更好的传参方式?

 引用的概念:

a) 在C++中新增加了引用的概念
b) 引用可以看作一个已定义变量的别名
c) 引用的语法:Type& name = var;
d) 引用做函数参数那?(引用作为函数参数声明时不用初始化,但是其他情况必须初始化),不理解先看下面,再回头来看。

看个代码:

 

#include <stdio.h>
#include <stdlib.h>
int main(void) {int a = 666;float c = 10.0;int& b = a;float& d = c;//不能直接定义没有指向的别名int& e = a;printf("a: %d, b: %d e:%d\n", a, b,e);b = 888;printf("a:%d, b: %d e: %d\n", a, b,e);printf("a 的地址:%p, b 的地址:%p e的地址:%p\n", &a, &b,&e);printf("c = %f d = %f c的地址 = %p d的地址 = %p", c, d, &c, &d);return 0;
}

运行结果:

 我们可以画个图:

 之前学指针时,指针是另外一块单独开辟的空间,值是0X200,但是引用不一样,如下:

变量a、b、e都是指同一块内存空间!!因此对b、e操作就是对a操作。 

对引用我们首先要知道,而且非常清晰的知道, 一旦定义就必须初始化!!否则报错!!如下图: 

 引用时C++新引入的,C语言里没有引用,因此本代码在C编译器中是编译不通过的!

引用做形参(重点)

实现两数交换,要求把之前学过的方法也实现对比一下,每个方法的不同点

#include <stdio.h>
#include <stdlib.h>
//初学者的错误做法
void swap0(int a, int b)
{int tmp = a;a = b;b = tmp;
}//方式一, 使用指针
void swap1(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}//方式二, 使用引用
void swap2(int& a, int& b)
{//int &c; //报错,引用一旦定义就必须初始化int tmp = a;a = b;b = tmp;
}int main(void) {int x = 10, y = 100;//swap1(&x, &y);//指针实现两数交换swap2(x, y);//引用实现两数交换printf("x: %d, y: %d\n", x, y);system("pause");return 0;
}

swap0()这个不用多说,初学者的错误做法,不能实现两数交换

swap1(),指针实现两数交换

swap2(),引用实现两数交换

真的实现交换了吗?运行试试:

果然实现了两数交换,这是为什么?我们画个图:

形参就是对实参,只是改了改了下变量名的名字。通过引用特性可知,对形参的操作 就是对实参的操作!

 引用的本质:

1 )引用在 C++ 中的内部实现是一个常指针 :     Type& name    -->   Type* const name
2 C++ 编译器在编译过程中使用常指针作为引用的内部实现, 因此引用所占用的空间大
小与指针相同
3 )从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是 C++
为了实用性而做出的细节隐藏

  引用总结:

(1) 当实参传给形参引用的时候,只不过是 c++ 编译器帮我们程序员手工取了一个实参地址,传给了形参引用(常量指针)
(2) 当我们 使用引用 语法的时,我们不去关心编译器引用是怎么做的,当我们 分析奇怪的语法现象 的时,我们才去考虑 c++ 编译器是怎么做的

指针引用:

回顾前面那个想通过指针把子函数的局部变量的值带出来,怎么用指针实现,当时用一级指针是不行的,用了二级指针实现的。

#include <stdio.h>
#include <stdlib.h>//二级指针实现
void boy_home0(int** meipo) {static int boy = 23;*meipo = &boy;
}//引用实现
void boy_home1(int*& meipo) {static int boy = 23;meipo = &boy;
}int main(void) {int* meipo = NULL;//boy_home(&meipo);//想通过指针把子函数的局部变量带出来得用二级指针!!boy_home1(meipo);//而引用可以直接用一级指针就把子函数的局部变量的值带出来printf("boy: %d\n", *meipo);system("pause");return 0;
}

运行结果:

对形参指针的操作就是对实参的指针的操作,这就是引用的好处!!也是它的价值所在。

 常引用:

C++ 中可以声明 const 引用 ,语法: const Type& name = var;
const 引用让变量拥有只读属性,分两种情况:
1. 用变量初始化常引用
2. 用字面量初始化常量引用

 

#include <stdio.h>
#include <stdlib.h>int main(void) {int a = 10;//1.用变量初始化常引用const int& b = a;//b = 100; //报错!!常引用是让变量引用变成只读,不能通过引用对变量进行修改printf("a: %d\n", a);//2.用字面量初始化常量引用const int c1 = 10;//c1 = 90;//报错!!c1是常变量const int& c2 = 10; //这个是在 C++中,编译器会对这样的定义的引用分配内存,这算是一个特例int c3 = c2;//c2 = 100;//不能修改return 0;
}

常引用的本质就是:对原变量添加了const关键字修饰。从而变成了常变量!常引用使得变量只能读取,不能修改!

常引用总结:

1 const & int e 相当于 const int * const e
2 普通引用 相当于 int *const e1
3 )当使用常量(字面量)对 const 引用进行初始化时, C++编译器会为常量值分配空间,
并将引用名作为这段空间的别名
4 )使用字面量对 const 引用初始化后,将生成一个只读变量

 4、最后,常见的一些错误总结:

1、使用未初始化的指针

 使用未初始化的指针现在基本编译都通不过,之前的一些编译器语法比较简单,还有些能运行,但是使用未初始化的指针很危险。

2、将值当做地址赋值给指针

 变量的值不能赋值给指针,常量的值也不能赋值给指针。指针只能赋值相应级别变量的地址。

3、忘记解引直接访问内存

 因为数组在内存中是从低地址到高地址存放的,因此p1不可能大于p2.这就是忘记解引用的结果,但是编译是没什么问题的。

4. 再次使用忽略重新赋初值

应该在每次循环结束后更新p1的值,使其重新指向数组input。 

 

5、项目实现:

需求: 编写程序找出最近一段时间每个号码出现的次数并把结果保存到一个数组,供其它
分析模块调用,往期数据保存在一个名为 ball.txt 中:

 

算法设计
1) 将双色球往期数据从文件读入一维数组 ;
2) 逐行遍历一维数组的每个元素,统计前六个球在 1-33 范围内出现的总次数
代码实现:
#define  _CRT_SECURE_NO_WARNINGS 1#include <iostream>
#include <fstream>
#include <string>
using namespace std;
#define NUM 7bool statistics(const char* path, int* ball_16, int ball_16_len) {int result[NUM];ifstream file;if (!path) return false; file.open(path);if (file.fail()) {cerr << "打开输入文件出错." << strerror(errno) << endl;return false;}//从数据文件读数据到数组,一行必须能读取 7 个do {int i = 0;for (i = 0; i < NUM; i++) {file >> result[i];if (file.eof()) { //判断文件是否达到结尾break;}if (file.fail()) {cerr << "读取文件失败, 原因:" << strerror(errno) << endl;break;}}if (i == 0) break;//记录正常结束//如果最后未满 7 个if (i < (NUM - 1)) {cerr << "仅读到" << i << "个记录,预期读取 7 个.";break;}for (i = 0; i < NUM; i++) {printf("%3d ",result[i]);}cout << endl;//对读入的数据进行统计for (i = 0; i < NUM; i++) {int index = *(result + i) - 1;if (index >= 0 && index < ball_16_len) {*(ball_16 + index) += 1;}}} while (1);//关闭文件file.close();return true;
}int main() {string filename;int ball_1_6[33] = { 0 };cout << "请输入文件名.\n";cin >> filename;if (!statistics(filename.c_str(), ball_1_6, 33)) { //c_str()函数可以将 const string* 类型转化为 const char* 类型cerr << "统计出错!" << endl;}int k = 0;for (int i = 0; i < 33; i++) {if (k == 2) {printf("%-3d出现的次数为%-8d\n", i+1,ball_1_6[i]);}k = (k++) % 3;printf("%-3d出现的次数为%-8d", i+1,ball_1_6[i]);}return 0;
}

ball.txt:

运行结果:(VS2022上运行)

 

 

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

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

相关文章

郑州https数字证书

很多注重隐私的网站都注重网站信息的安全&#xff0c;比如购物网站就需要对客户的账户信息以及支付信息进行安全保护&#xff0c;否则信息泄露&#xff0c;客户与网站都有损失&#xff0c;网站也会因此流失大量客户。而网站使用https证书为客户端与服务器之间传输的信息加了一个…

python学到什么程度算入门,python从入门到精通好吗

本篇文章给大家谈谈python学到什么程度算入门&#xff0c;以及python从入门到精通好吗&#xff0c;希望对各位有所帮助&#xff0c;不要忘了收藏本站喔。 学习 Python 之 进阶学习 一切皆对象 1. 变量和函数皆对象2. 模块和类皆对象3. 对象的基本操作 (1). 可以赋值给变量(2). …

保护云数据库实用指南

在数字化转型时代&#xff0c;越来越多的企业将运营转移到云端&#xff0c;导致对云数据库的依赖越来越大。虽然它们提供了可扩展性和可访问性等显着优势&#xff0c;但它们也带来了独特的安全挑战&#xff0c;需要解决这些挑战以保护敏感数据免受各种威胁。 在本文中&#x…

【MybBatis高级篇】MyBatis 拦截器

【MybBatis高级篇】MyBatis 拦截器 拦截器介绍实现拦截器注册拦截器应用ymlDynamicSqlDao 层代码xml启动类拦截器核心代码代码测试 拦截器应用场景 MyBatis 是一个流行的 Java 持久层框架&#xff0c;它提供了灵活的 SQL 映射和执行功能。有时候我们可能需要在运行时动态地修改…

超细详解,接口自动化测试-JSON和JsonPath提取数据(实战)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 JSON(JavaScript …

kafka总结

Kafka是一种高吞吐量的分布式发布订阅消息系统&#xff08;消息引擎系统&#xff09;&#xff0c;它可以处理消费者在网站中的所有动作流数据。 消息队列应用场景 缓存/削峰 :处理突然激增的大量数据&#xff0c;先放入消息队列&#xff0c;再按照速度去处理&#xff0c; 解…

Redis中的缓存雪崩、击穿、穿透的原因以及解决办法

redis的缓存 雪崩 击穿1.缓存雪崩双11访问很大,比如说redis设置缓存时间为3小时&#xff0c;当购物超过3小时之后 首页redis 在一瞬间全部失效,导致所有请求都打在db上.造成db在响应不及时直接就挂掉了 这个时候首页就不能立马对外响应服务了redis的key大面积失效 导致前端直接…

一个完整的http请求响应过程

一、 HTTP请求和响应步骤 图片来自&#xff1a;理解Http请求与响应 以上完整表示了HTTP请求和响应的7个步骤&#xff0c;下面从TCP/IP协议模型的角度来理解HTTP请求和响应如何传递的。 二、TCP/IP协议 TCP/IP协议模型&#xff08;Transmission Control Protocol/Internet Pr…

windows基础命令

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一.目录和文件的操作 1.cd 命令 切换到d盘 2.目录分为相对路径和绝对路径 3. dir命令 用于显示目录和文件列表 4. md 或 mkdir 创建目录 5. rd 用于删…

数据结构——AVL树

文章目录 一.AVL树的定义二.AVL树的插入三.插入后更新平衡因子四.AVL树的旋转1.左单旋2.右单旋3.先左单旋再右单旋4.先右单旋再左单旋 五.检查是否满足AVL树六.源码 一.AVL树的定义 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支…

智慧水务和物联网智能水表在农村供水工程中的应用

摘 要&#xff1a;随着社会的进步和各项事业的飞速发展&#xff0c;人民生活水平的逐步提升&#xff0c;国家对农村饮水安全有了更高的要求&#xff0c;为了进一步提升农村供水服务的质量&#xff0c;利用现代化、信息化科学技术提升农村供水服务质量&#xff0c;提高用水管理效…

基于高通QCC5171的对讲机音频数据传输系统设计

一 研发资料准备 二 设计方法 蓝牙连接与配对&#xff1a;使用QCC5171的蓝牙功能&#xff0c;实现设备之间的蓝牙连接和配对。确保设备能够相互识别并建立起稳定的蓝牙连接。 音频采集与处理&#xff1a;将麦克风采集到的音频数据通过QCC5171的ADC&#xff08;模数转换器&…

upload-labs详解------持续更新

目录 注&#xff1a; 搭建&#xff1a; pass-01&#xff08;前端绕过&#xff09; pass-02&#xff08;后缀绕过&#xff09; pass-03&#xff08;黑名单绕过&#xff09; pass-04&#xff08;Apache解析漏洞\.htaccess文件绕过&#xff09; 注&#xff1a; 本项目提供的…

如祺出行冲刺自动驾驶商业化,人少的地方机会多?

网约车&#xff0c;正在迎来让人“不明觉厉”的新一轮竞赛。 网约车监管信息交互系统的数据显示&#xff0c;截至今年6月30日&#xff0c;全国共有318家网约车平台公司取得网约车平台经营许可&#xff0c;环比增加5家&#xff1b;网约车监管信息交互系统6月份共收到订单信息7.…

爬虫原理详解及requests抓包工具用法介绍

文章目录 一、什么是爬虫&#xff1f;二、爬虫的分类三、网址的构成四、爬虫的基本步骤五、动态页面和静态页面六、伪装请求头七、requests库介绍1. 概念&#xff1a;2. 安装方式&#xff08;使用镜像源&#xff09;&#xff1a;3. 基本使用&#xff1a;4. response对象对应的方…

使用Express部署Vue项目

使用Express部署Vue项目 目录 1. 背景 2. 配置Vue CLI 1.1 安装nodejs 1.2 创建vue-cli 1.3 创建vue项目 1.4 构建vue项目3. 配置Express 2.1 安装express 2.2 创建项目4. 使用express部署vue项目 1&#xff0c;背景 我们想要做一个前后端分离的课程项目&#xff0c;前端…

eclipse版本与jdk版本对应关系

官网&#xff1a;Eclipse/Installation - Eclipsepedia eclipse历史版本&#xff08;2007-&#xff09;&#xff1a;Older Versions Of Eclipse - Eclipsepedia Eclipse Packaging Project (EPP) Releases | Eclipse Packages

记一次centos 磁盘挂载过程

前言 最近买了云服务器磁盘&#xff0c;需要挂载&#xff0c;一下就由大猿来记录这次过程。 挂载过程 查看磁盘挂载情况 查看物理硬盘 lsblkfdisk -l标记分区 fdisk /dev/vdb格式化分区 xfs mkfs.xfs /dev/vdb mkfs.xfs -f /dev/vdbext4 mkfs.ext4 /dev/vdbxfs 和 ex…

一起学算法(顺序表篇)

概念&#xff1a; 1.顺序表的定义 用一段地址连续的存储单元依次存储数据的线性表被称为数据表&#xff0c;在Java中顺序表一般是数组或者是ArrayList实现的 先把代码放这里&#xff0c;接下来一一给大家进行讲解&#xff1a; public class SeqList {private Object[] data;…

网络基础-认识每层的设备和每层的特点用途

目录 网络层次常见设备各层介绍数据链路层网络层传输层应用层 网络层次 常见设备 各层介绍 数据链路层 有了MAC地址。数据链路层工作在局域网中的&#xff0c;以帧为单位进行传输和处理数据。 网络层 网络层有了IP。不同的网络通过路由器连接成为互联网 路由器的功能:   …