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 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;
}

运行结果:


区分大小写排序,思考为什么是这么实现的。这个代码第一次可能接受起来困难,理解如下 :因为我们要区分大小写,我们在传递进去的时候把在小写字母(97~122)减去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上运行)

相关推荐
cuisidong199712 分钟前
5G学习笔记三之物理层、数据链路层、RRC层协议
笔记·学习·5g
雷神乐乐12 分钟前
File.separator与File.separatorChar的区别
java·路径分隔符
小刘|17 分钟前
《Java 实现希尔排序:原理剖析与代码详解》
java·算法·排序算法
南宫理的日知录19 分钟前
99、Python并发编程:多线程的问题、临界资源以及同步机制
开发语言·python·学习·编程学习
逊嘘36 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
van叶~38 分钟前
算法妙妙屋-------1.递归的深邃回响:二叉树的奇妙剪枝
c++·算法
morris13143 分钟前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
knighthood20011 小时前
解决:ros进行gazebo仿真,rviz没有显示传感器数据
c++·ubuntu·ros
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员1 小时前
java导出word文件(手绘)
java·开发语言·word