1、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;
}
运行结果:
重点说几点:空类型指针不能直接访问,不管它初始化为什么,都不能直接访问!!这是它核心的一个点。除了不能直接访问,还不能做加减运算,由于没有指定类型,因此你也不知道它会跳变几个字节。
要想访问空类型指针,必须强转空类型指针才能访问。空指针就这几点要注意,那么有个问题:
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变成小写字母,这样进行排序的时候,大写字母被看为小写字母进行排序。
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 name2 ) C++ 编译器在编译过程中使用常指针作为引用的内部实现, 因此引用所占用的空间大小与指针相同 。3 )从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是 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;
}
运行结果:
对形参指针的操作就是对实参的指针的操作,这就是引用的好处!!也是它的价值所在。
常引用:
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 e2 ) 普通引用 相当于 int *const e13 )当使用常量(字面量)对 const 引用进行初始化时, C++编译器会为常量值分配空间,并将引用名作为这段空间的别名4 )使用字面量对 const 引用初始化后,将生成一个只读变量
4、最后,常见的一些错误总结:
1、使用未初始化的指针
使用未初始化的指针现在基本编译都通不过,之前的一些编译器语法比较简单,还有些能运行,但是使用未初始化的指针很危险。
2、将值当做地址赋值给指针
变量的值不能赋值给指针,常量的值也不能赋值给指针。指针只能赋值相应级别变量的地址。
3、忘记解引直接访问内存
因为数组在内存中是从低地址到高地址存放的,因此p1不可能大于p2.这就是忘记解引用的结果,但是编译是没什么问题的。
4. 再次使用忽略重新赋初值
应该在每次循环结束后更新p1的值,使其重新指向数组input。
5、项目实现:
需求: 编写程序找出最近一段时间每个号码出现的次数并把结果保存到一个数组,供其它分析模块调用,往期数据保存在一个名为 ball.txt 中:
#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上运行)