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

news2024/9/24 21:21:58

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 I
	qsort(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 7

bool 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.coloradmin.cn/o/822801.html

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

相关文章

基于C语言 --- 自己写一个扫雷小游戏

C语言程序设计笔记---020 初阶扫雷小游戏(开源)1、arr_main2.c程序大纲2、arr_game2.h3、arr_game2.c3.1、 自定义初化函数 InitBoard( ) 和 自定义显示函数 DisPlayBoard( )3.2、 自定义布置雷函数 SetMine( )3.4、 自定义排查雷函数 FindMine( ) 4、结束语 初阶扫雷小游戏(开…

Redis安装部署(基于windows平台)

redis简介 键值对存储数据库是NoSQL数据库的一种类型&#xff0c;也是最简单的NoSQL数据库。顾名思义&#xff0c;键值对存储数据库中的数据是以键值对的形式来存储的。常见的键值对存储数据库有Redis、Tokyo Cabinet/Tyrant、Voldemort以及Oracle BDB数据库。 Remote Diction…

1.4 信息安全管理

数据参考&#xff1a;CISP官方 目录 信息安全管理基础信息安全管理体系信息安全管理实践 一、信息安全管理基础 1、信息 信息是一种资产&#xff0c;与其他重要的业务资产一样&#xff0c;对组织业务必不可少&#xff0c;因此需要得到适当的保护。 2、信息的价值 企业…

数据结构和算法入门(时间/空间复杂度介绍--java版)

数据结构和算法入门&#xff08;时间/空间复杂度介绍–java版&#xff09; write in front 作者&#xff1a; 向大佬学习 专栏&#xff1a; 数据结构&#xff08;java版&#xff09; 作者简介&#xff1a;大二学生 希望能学习其同学和大佬的经验&#xff01; 本篇博客简介&…

定时任务调度 xxl-job

框架地址 https://gitee.com/jiaruiguo/xxl-job.git项目说明 调度管理系统 xxl-job-admin 定时任务实现系统 普通系统&#xff1a; xxl-job-executor-sample-frameless 微服务系统&#xff1a;xxl-job-executor-sample-springboot 配置说明 工程名&#xff1a;xxl-job-execut…

中国农村程序员学习此【JavaScript教程】购买大平层,开上帕拉梅拉,迎娶白富美出任CEO走上人生巅峰

注&#xff1a;最后有面试挑战&#xff0c;看看自己掌握了吗 文章目录 在 Switch 语句添加多个相同选项从函数返回布尔值--聪明方法undefined创建 JavaScript 对象通过点号表示法访问对象属性使用方括号表示法访问对象属性通过变量访问对象属性给 JavaScript 对象添加新属性删除…

AD21 PCB设计的高级应用(八)Draftsman的应用

&#xff08;八&#xff09;Draftsman的应用 1.创建Draftsman文档2.Draftsman页面选项设置3.放置绘图数据3.1 装配图3.2 板制造图3.3 钻孔图和钻孔列表3.4 图层堆栈图例3.5 BOM3.6 标注、注释、测量尺寸 4.文档输出4.1 打印或者导出为PDF4.2 添加到Output job Draftsman 是为电…

windows基础命令

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

【编程语言 · C语言 · 共用体】

【编程语言 C语言 共用体】https://mp.weixin.qq.com/s?__bizMzg4NTE5MDAzOA&mid2247491502&idx1&snd531f724641b18619225de4bbcd02998&chksmcfade357f8da6a41f514ba72d817cc029f8f2a89d3753bfe5c547801abb3d2e080554e67d677&payreadticketHJqAIlk_6GWs…

Uncaught SyntaxError: ‘‘ string literal contains an unescaped line break

今天在修改前端页面的时候&#xff0c;页面报错了&#xff0c;提示了这个信息 Uncaught SyntaxError: string literal contains an unescaped line break 问题指向这行代码&#xff0c;这就是通过JS渲染一个easyui的搜索框&#xff0c;仔细确认之后&#xff0c;发现没有任何问…

Go学习第一天

闲聊两句 从事java后端开发8年多&#xff0c;期间也曾零星看过Go语言、Python、Erlang等等&#xff0c;但都未曾认真学习过&#xff0c;恰好公司最近项目需要&#xff0c;之前用Go开发的项目因为同事离职&#xff0c;暂未人来接手&#xff0c;所以老大就找到我和另外一个同事&…

算法自学__背包动态规划

例1 P5020 [NOIP2018 提高组] 货币系统 题目描述 在网友的国度中共有 n n n 种不同面额的货币&#xff0c;第 i i i 种货币的面额为 a [ i ] a[i] a[i]&#xff0c;你可以假设每一种货币都有无穷多张。为了方便&#xff0c;我们把货币种数为 n n n、面额数组为 a [ 1.. …

unity 使用Vuforia扫描物体( ModelTarget 模型目标)

1、下载vuforia插件vufora 2、下载模型生成器Model Target Generator 3、将vuforia插件导入到unity &#xff0c;我使用的unity是2021版本&#xff0c;导出插件时&#xff0c;只显示有两个文件&#xff0c;导入后&#xff0c;会有一个弹框 让更新插件&#xff0c;点击updata&am…

【编程语言 · C语言 · calloc和realloc】

【编程语言 C语言 calloc和realloc】https://mp.weixin.qq.com/s?__bizMzg4NTE5MDAzOA&mid2247491544&idx1&sn72d8f9931cfa7ce7441a3248475ab619&chksmcfade321f8da6a374a5935bb46441a03a007c0589db6b8afa8c1991854d632a3201553e37b0b&payreadticketHGy…

[算法很美打卡第四天] 字符串篇(中)

文章目录 压缩字符串代码 判断两字符串的字符集是否相同代码 旋转词代码 反转单词代码 回文串验证代码 去掉字符串中连接出现的k次的0代码 压缩字符串 代码 package 每日算法学习打卡.算法打卡.八月份;public class test1 {public static void main(String[] args) {String s …

替换linux的文泉驿正黑fonts-wqy-zenhei字体 替换linux默认中文字体

WSL 怎么替换 linux 的文泉驿正黑 fonts-wqy-zenhei 字体 WSL 怎么替换 linux 默认中文字体 在 wsl 中默认是没有 gnome 界面或者 xface 的&#xff0c;但是我需要使用 wsl 开发 electron 或者使用 chrome 浏览器。这个时候系统就会调用默认的系统字体了。 我使用的是 debian…

国产分布式数据库——TDSQL性能分析工具

一、TDSQL概述 TDSQL是腾讯研发的一款兼容MySQL协议的国产分布式数据库&#xff0c;适用于大并发、高性能、大容量的OLTP类场景。TDSQL分为集中式和分布式版本&#xff0c;分布式版可支持分布式事务&#xff0c;但性能不如单机事务&#xff0c;性能会有一定的损耗&#xff0c;…

Android系统的进程管理(创建->优先级->回收)

一、进程的创建 1、概述 Android系统以Linux内核为基础&#xff0c;所以对于进程的管理自然离不开Linux本身提供的机制。例如&#xff1a; 通过fork来创建进行通过信号量来管理进程通过proc文件系统来查询和调整进程状态 等 对于Android来说&#xff0c;进程管理的主要内容…

EMC VNX1系列存储电池状态说明

SPS电池正常的状态为“Present”。 SPS电池故障时的状态为“Faulted”。 更换SPS后&#xff0c;SPS开始充电&#xff0c;此时状态显示为“Not Ready”状态。 充电完成后显示为Present状态。如果充电完成后状态前面有“F”标记&#xff0c;则需要重启对应的控制器以更新SPS…

2023年最新智能优化算法之——切诺贝利灾难优化器 (CDO),附MATLAB代码和文献

切诺贝利灾难优化器Chernobyl Disaster Optimizer (CDO)是H. Shehadeh于2023年提出的新型智能优化算法。该方法是受到切尔诺贝利核反应堆堆芯爆炸而来的启发。在CDO方法中&#xff0c;放射性的发生是由于核的不稳定性&#xff0c;核爆炸会发出不同类型的辐射。这些辐射中最常见…