目录
什么类型的变量能存放地址?这个变量可以通过地址来访问地址下的数据。答案就是指针变量。
前言
C语言基础:初步入门,会函数,数组即可。
指针一:初步介绍指针相关知识,比如指针变量,指针类型,指针如何使用,指针作为函数参数。
指针二:围绕数组,主要讲指针与一维数组。包括如何用指针访问数组,二级指针,冒泡排序,还有指针数组。
指针三:难度较高,围绕指针,说明各种类型的指针,从字符指针引入,到数组指针,函数指针,到函数指针数组。
指针(进阶一):了解回调函数的概念,并学会使用排序函数qsort,最后模拟用冒泡排序模拟实现qsort。
指针(进阶二):待更。
1.指针(一)
1.1内存和地址
先引入一个生活案例,假设你住在一个宿舍楼,你住的宿舍水龙头坏了。此时,你需要去楼下填表,届时就会用修理师傅上门。修理师傅是如何找上门的?是因为你留了房间号,他就知道那间水龙头坏了。那么这里的房间号类比到计算机,这就是指针。
为什么学习指针,先要了解内存?因为指针是用来访问内存的。内存编码==指针==地址。
0x开头的数字默认为16进制,如0x11223344。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 10;
printf("%p\n", &a);
return 0;
}
上述变量创建
int a = 10;//本质是向内存申请4个字节的空间。int的大小是4个字节。
1.2 指针变量
什么类型的变量能存放地址?这个变量可以通过地址来访问地址下的数据。答案就是指针变量。
两个重要操作符&和*
int* p= &a;
p是变量名,p前面加上*,*说明p是指针变量。int* p说明p的类型为int*型(后面会再提到)
int说明 p指向的对象是int 类型。
&a------& 取地址操作符,上述语句,表示把a的地址取出来赋值给指针变量p。
首先,上述运行结果表示,a的地址确实赋值给到了p。
其次,说明解引用操作符---* ,*p 间接访问了a,并通过赋值*p=0,改变了a的值。因此,解引用操作符,通过p的地址,找到变量a,这为我们提供了另一个改变变量的途径。
1.3 指针类型
(type)* p
1.p是变量名,*说明p是指针变量,p的类型是type* ,p指向的对象是type类型。
2.这里的 type 是泛指,如1.2所说的int,可以是 short,long,float, double等。
指针在内存空间的大小
为什么结果统一是4呢?
接下来我们谈一下地址如何产生的。CPU和内存要用线连起来,这里我们关心地址总线,在32位机器里,每根线有0/1,一根线有两种可能,那么一共有2^32种可能,每种就代表一个地址。所以我们得出结论,地址是32个0/1二进制序列。储存这样的地址要32个比特位(=4个字节),所以类型大小是4个字节。这是X86下进行,X64下为8个字节。
结论:指针变量大小为4/8个字节。指针类型大小跟平台(X86 X64)有关。
指针类型的作用
看来上面的内容,感觉指针类型大小很统一,这里先给结论,具体请看1.5指针运算->指针与整数的运算。
结论:指针类型决定了指针进行解引用操作时,它能访问多少个字节,也可以说指针类型决定了指针访问权限。
1.4 const
const修饰变量
在声明变量语句前加上const修饰,变量的值不可修改了。
const修饰指针变量
int * const p;
int const * p;
const int * p;
以下讨论这三种情况并给出相应结论。
结论:
const 在*左,本身可改,指向不能改。如p1,p3。
const 在*右,本身不可改,指向可改。如p2。
1.5 指针运算
指针+-整数
指针加减整数结果仍为指针。因为数组在内存中连续存放,随下标增长地址由低变高。
我们下面以一维数组举例。
每次p+1,地址加4,相当与走了四个字节的长度(1个int的大小)。
这次p+1,地址加1,等于走了一个字节(1个char类型的大小)。
对于 int* p;p+1就跳过4个字节。
对于 char* p;p+1就跳过1个字节。
结论:p+1 ->跳过 1*sizeof(type);
p+n ->跳过 n*sizeof(type);
指针-指针
1.两个指针必须指向一段连续空间,比如一个一维数组。
2.指针-指针运算,减完的结果的绝对值是之间相差元素个数。
3.不存在指针+指针运算。
指针是地址,指针减指针相当于地址减地址,地址是16进制,减出来结果必然是多少字节,在换算成相应类型的元素个数。指针加指针相当于地址加地址,16进制数加16进制数,结果无法想象,因此特定条件可以用指针与指针减法满足,但指针之间的加法运算不存在。
void* 指针
void* 可以接受任何类型的指针,但不可解引用(void*访问权限未知),不能进行上述的指针运算。
1.6 野指针
概念:野指针就是指针指向的位置随机的,不可知的。
成因
1.指针未初始化。
2.指针越界访问
3.指针向已经释放空间的内存访问。
如何避免野指针(空指针介绍)
创建指针变量,在不确定赋值时,习惯给它赋值成空指针NULL。
#define NULL ((void* )0) //NULL具体意义为0(0x00000000)的地址。
int* p = NULL;//0也是地址,但这个地址我们无法使用,否则会报错。
注意:程序只能访问分配给自己的内存单元,否则就是非法访问了,不能随便访问内存空间的。
小心越界访问。指针只访问已经申请内存的空间,不能超出范围访问。
指针变量不再使用后,最终赋值为NULL(空指针)。
1.7 assert介绍
头文件:assert.h
assert不是函数,是类函数宏,这个宏被称为"断言"。
这个宏接受一个表达式,assert(表达式),若表达式结果为真(非0),程序将会继续执行下去,否则会在屏幕中写入错误信息,显示哪个表达式没通过。
#include<stdio.h>
#include<assert.h>
int main()
{
assert(0);//表达式为假,会报错指明错误文件位置。
return 0;
}
assert能 自动标识文件和出问题的行号 (见上图),还有⼀种无需更改代码就能开启或关闭 assert() 的机制。如果程序都没问题了,那么把#define NDEBUG 语句在 #include<assert.h>前,会禁用所有assert语句。
#include<stdio.h>
#include<assert.h>
int main()
{
int* p = NULL;
assert(p != NULL);
return 0;
}
1.8 指针的使用
strlen
strlen函数,相信大家已经不陌生了。
头文件:string.h
功能:计算字符串的长度。
size_t strlen ( const char * str );类型 size_t 无符号整型。
从参数知,它接受字符串的起始地址,然后统计字符串'\0'之前的字符数。
结果符合。
模拟实现strlen
那么如何模拟实现strlen?
我们可以从起始位置,遍历字符串,如果字符!='\0'那么继续执行,否则结束循环返回个数。
#include<stdio.h>
#include<string.h>//调用库函数strlen需引用的头文件。
size_t my_strlen(const char* p);//函数声明
int main()
{
char arr[] = "Hello World!";
size_t ret = strlen(&arr[0]);
printf("%zd\n", ret);
printf("%zd\n", my_strlen(&arr[0]));//将起始地址传给my_strlen.
return 0;
}
size_t my_strlen(const char* p)//接受一个字符串的起始地址
{
size_t ret = 0;//创建变量储存循环次数,也就是字符数
while (*p != '\0')//每次判断
{
p++;//指针往后走,检查一下个字符
ret++;//循环一次,'\0'前的字符数加一。
}
return ret;//返回'\0'前的字符数。
}
结果与strlen函数调用一致。
函数递归实现strlen
#include<stdio.h>
#include<string.h>//调用库函数strlen需引用的头文件。
size_t my_strlen(const char* p);//函数声明
int main()
{
char arr[] = "Hello World!";
size_t ret = strlen(&arr[0]);
printf("%zd\n", ret);
printf("%zd\n", my_strlen(&arr[0]));//将起始地址传给my_strlen.
return 0;
}
size_t my_strlen(const char* p)//接受一个字符串的起始地址
{
if (*p != '\0')
{
p++;
return 1 + my_strlen(p);
}
else
return 0;
}
传值和传址调用
问题:如何创建函数来实现变量交换?
//在主函数中这样交换变量
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("交换前:a = %d , b = %d\n", a, b);
int temp = a;//创建临时变量交换两个数。
a = b;
b = temp;
printf("交换后:a = %d , b = %d\n", a, b);
}
可能的想法:把交换部分的代码封装成函数不就可以了吗。结果真能如此吗?
为什么做不到交换的效果呢?我们来调试一下。
1.x和y确实交换了,但a和b没有交换。因为swap函数在创建局部变量x,y时向内存申请空间,分别把a,b的值拷贝了一份,地址不同。交换x,y做不到交换a,b的效果。
那么要在swap函数操作main函数中的局部变量a,b就要指针变量为参数,通过指针解引用的操作来交换a,b。
#include<stdio.h>
void swap(int* x,int* y)
{
int temp = *x;//创建临时变量交换两个数。
*x = *y;
*y = temp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a = %d , b = %d\n", a, b);
swap(&a, &b);//传a和b的地址。
printf("交换后:a = %d , b = %d\n", a, b);
}
指针间接访问,a与b交换了。
总结:
上面的操作,分别叫函数的传值调用和传址调用。函数调用过程形参是实参的一份临时拷贝。如果只想操作变量存储的数据就进行传值调用,若想通过形参来改变实参就必须用指针的形式,用解引用操作符* ,这是传址调用。
2.指针(二)
2.1 数组名的新认识
先下结论:数组名是数组首元素的地址。
两个例外:
1.sizeof(arr)中,arr表示整个数组。
2.&arr,arr表示整个数组。
数组名是数组首元素的地址,即arr=&arr[0];上面展示所有的&arr[0]均可替换为arr。
图示结果符合结论。
接下来说明的两个例外。
sizeof(arr)中,arr表示整个数组。
sizeof(),单独放一个数组名,才代表整个数组。如sizeof(arr+0),这个arr就理解为首元素的地址了。
#include<stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
/*printf("&arr[0] = %p\n", &arr[0]);
printf(" arr = %p\n", arr);*/
printf("sizeof(arr) = %zd", sizeof(arr));
return 0;
}
这里的arr表示是整个数组,这个数组有10个int 的元素,10*4,结果是40(字节)。
事实上,可以这么理解,sizeof(),这个单目操作符(),无论放入类型还是变量,都会判断()为什么类型并返回结果。我们知道数组是有类型的,去掉数组名就是它的类型,int [10]就是这个数组的类型,sizeof(int [10])结果是40,很好理解吧。
&arr,arr表示整个数组。
以上面的 int arr[10]举例
一、arr是数组名,也是数组首元素的地址,类型是int* 。
二、&arr 是这个数组的地址,类型是int* [],这是我们后面要提到的数组指针,这里先留个印象。
arr和&arr的地址是一样的,但由于指针类型不同,+1后移动的字节不同。因为arr是地址,arr可以执行指针的运算,但arr不是指针变量,不能进行赋值和自增等运算。
arr+1跳过了4个字节。
&arr+1 跳过了40个字节(整个数组),(E-C)*16+(4-0)=40。
因为类型的不同,指针+-1跳过的字节不同。1.5指针运算
2.2 使用指针访问数组
#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));
//printf("%d ",*(arr+i));
}
return 0;
}
arr是地址,可以赋值给指针变量。
所以arr+i和p+i等价。先前学习数组时,我们用[]操作符,即arr[i]来访问数组元素。
那么p[i]是否也能用来访问数组元素?
p[i]换成*(p+i)也行,本质上,p[i]==*(p+i)。事实上,用arr[i],编译器会理解成*(arr+i)
*(arr+i)==arr[i]==i[arr]--->(不推荐),右边两个都会转换成*(arr+i),所以写成任意形式效果是一样的。
结论
数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。
数组在内存中连续存放,指针+偏移量可以很好访问整个数组。
2.3 一维数组传参本质
//数组形式作为函数参数
//void print_arr(int arr[], int sz)
//{
// int i = 0;
// for (i = 0; i < sz; i++)
// {
// printf("%d ", arr + i);
// }
// return;
//}
//指针形式作为函数参数
void print_arr(int* arr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
return;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
print_arr(arr, sz);
return 0;
}
前面说过arr是地址,所以无论是前面的数组形式还是指针形式,本质都是把地址传给函数。
一维数组传参的本质是传递首元素的地址。
学习数组和函数的两个问题
1.为什么元素个数要在main函数(主调函数)中计算?
2.为什么在被调函数中修改数组,能改变原来的数组?
第一个问题:
sz预期是10,但结果是1,与预期不符。
因为print_arr中的arr是指针变量,类型是int* ,sizeof(int* )=4,sizeof(arr[0])=4,结果必然是一。
main函数中sizeof(arr),arr表示整个数组;但是print_arr中arr是形参,本质为指针变量。
第二个问题:
因为传参传的是地址,操作的是同一个数组,不存在创建一个新数组拷贝原来的数组对应数据的说法。
2.4冒泡排序
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])//拍成升序数组
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
for (i = 0; i < sz; i++)
{
printf("%d\n", arr[i]);
}
return 0;
}
冒泡排序的核心思想:两两相邻元素比较
分析:
{9,8,7,6,5,4,3,2,1,0}
要把这数组排成升序,先看第一个数和第二个数
9比8大所以交换
{8,9,7,6,5,4,3,2,1,0} //第一次比较
{8,7,9,6,5,4,3,2,1,0} //第二次比较
{8,7,6,9,5,4,3,2,1,0} //第三次比较
...............
{8,7,6,5,4,3,2,1,0,9}//第九次比较,确定了最大值9
至此,一趟冒泡排序结束了。
但还剩8个数待排序,于是要进行第二躺冒泡排序。
{7,6,5,4,3,2,1,0,8,9}//第二趟冒泡排序,确定了次大值8
第二趟进行了8对数的比较。
经过上面分析,10个数的降序数组要排成升序,要进行9躺冒泡排序。
第一趟比较9对数
第二趟比较8对数......
第九趟比较1对数
因此,我们可以给上面的代码进行解释。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;//趟数
for (i = 0; i < sz-1; i++)//循环sz-1次,即进行sz-1趟冒泡排序
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)//进行第i+1趟的冒排,就要减少i对
{
if (arr[j] > arr[j + 1])//结果有右比左大,不满足就交换。
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
for (i = 0; i < sz; i++)//遍历打印
{
printf("%d\n", arr[i]);
}
return 0;
}
下面试着用冒泡排序写一个排序函数吧
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void bubble_sort(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (p[j] > p[j + 1])
{
int tmp = p[j];
p[j] = p[j + 1];
p[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0};
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
for (int i = 0; i < sz; i++)//遍历打印
{
printf("%d\n", arr[i]);
}
return 0;
}
2.5 二级指针
pp==&p;p==&a;
前面提过指针指向类型决定了指针+1是跳过多少大小的字节。
同样 pp是二级指针,指向类型是指针类型。pp+1就跳过4/8字节。
2.6 指针数组
指针数组引入
了解指针数组前,先回顾我们已经学过的数组类型。
整型数组--->存放整型的数组。
字符数组--->存放字符的数组。
由此我们可以推出,指针数组是存放指针的数组,本质上还是数组,只不过数组内元素数据类型是指针。
其实,区分函数指针,指针函数,指针数组,数组指针,指针函数,函数指针数组到底是什么。简单理解最后两个字理解为名词,前面理解为形容词。比如,指针数组翻译为指针的数组,即存放指针的数组。再比如说,函数指针数组,理解为函数的指针的数组(也可以理解为专门存放函数地址的数组,是一类特殊的指针的数组),存放函数的指针的数组具体看3.4-函数指针
先创建一个数组
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int* arr[3] = { arr1,arr2,arr3 };
return 0;
}
arr存放了三个一维数组的地址。
用指针数组模拟二维数组
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int* arr[3] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
}
return 0;
}
第一层for循环嵌套,访问arr数组三个元素,arr三个元素又是地址再通过下标引用操作符访问各个数组的元素。
1.为什么可以用arr[i][j]的形式?
前面提过arr[i]被编译器理解为*(arr + i)。
所以arr[i][j],编译器会转化成 *(*(arr + i) + j)。(arr+i)是arr数组元素的地址,解引用后找到对应数组的地址,在通过偏移访问arr1,arr2,arr3的元素。
2.指针数组等价与二维数组吗?
不等于。
第一,二维数组虽然分行列,但仍然在内存中连续储存。上面的指针数组arr中三个一维数组的地址不一定连续。
第二,指针数组可以存其它类型的地址,不一定是数组的地址。
以下是arr1,arr2,arr3的整形数组分配的内存,显然不连续。
二维数组在内存中连续存放
3.指针(三)
3.1 字符指针
指针有一种类型 char*
int main()
{
char ch = 'Y';
char* p = &ch;
*p = 'Q';
return 0;
}
字符数组和字符串
int main()
{
char arr[] = "hello world";
//这两种写法有什么区别?
char* p1 = "hello world";
char* p2 = arr;
return 0;
}
arr是一个字符数组,数组意味着数组的元素可以修改。
char* p1="hello world";C语言内置数据类型都没有能存放字符串的,这句话意思是把字符串的首元素地址赋值给了p1。这种字符串为常量字符串,它不能被修改。
arr数组会向内存申请空间,把这些字符连续存放在栈区。
常量字符串是存储在静态存储区,只是把地址传给了p1。
字符数组的内容可修改,常量字符串的内容不可修改。
思考一下下面的问题
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
解释 :
str1,str2是两个字符数组,尽管内容一致,但各自数组在内存申请空间储存(开辟的空间不同),所以地址不同。两个字符数组首元素的地址并不相同。
str3和str4指向的是⼀个同一个常量字符串。C语言会把常量字符串存储到单独的⼀个内存区域(静态区)。因此,几个指针指向同⼀个字符串的时候,它们实际会指向同⼀块内存。
3.2 数组指针
数组指针是什么?指针,存放的是数组的地址,能够指向数组的指针变量。
回忆学过的指针类型 :char* int* double*。
那么如何创建一个简单的数组指针?
[]下标引用操作符优先级高于*解引用操作符。
p是一个变量名。
由于优先级 (*p)就是一个指针变量。
int (*p)[5] 就是一个简单数组指针。
p的类型是int (*)[5]
*p即p指向的类型为int [5]
Q1:p1,p2,谁是数组指针和指针数组。
#include<stdio.h>
int main()
{
int* p1[10];
int(*p2)[10];
return 0;
}
p1是指针数组,p2是数组指针。
强调一遍:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。
数组指针初始化
数组指针加一跳过了40个字节符合指针运算。存放整个数组的地址用数组指针。
3.3 二维数组传参本质
前面提过一维数组的数组名首元素的地址,二维数组同样满足。
学习二维数组的时候,提到二维数组的元素是一维数组。
二维数组的首元素看做是一个一维数组,每个元素都是一维数组。那么二维数组首元素的地址就是第一行一维数组的地址
void print(int a[3][5], int r, int c)//二维数组传参。
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", a[i][j]);
}
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
print(arr, 3, 5);
return 0;
}
前面提一维数组的本质时,形参可以写成数组形式也可以写成指针形式。
根据数组名是数组⾸元素的地址,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀ 维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[5] 。说明二维数组传参本质也是传递了地址,只不过时数组的地址。
void print(int (*a)[5], int r, int c)//第一个形参时是数组指针
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", a[i][j]);
}
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
print(arr, 3, 5);
return 0;
}
无论写成数组形式还是指针形式都是传地址。
结论:二维数组作为形参,既可以写成数组形式,也可以写成数组指针形式。
3.4 函数指针
函数有地址吗?
#include<stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf(" test = %p\n", test);
printf("&test = %p\n", &test);
return 0;
}
类比数组,函数名就是函数的地址,那么&函数名也可以获得函数地址,结果也是函数的地址。
函数名和&函数名都是函数的地址,没区别。
什么类型的变量能存储函数的地址,没错,就是函数指针变量。
类比数组指针的写法,如下
int (*paff)(int x, int y);
//或者 int (*paff)(int,int);
//形参名可以省略。
//(*paff)为指针变量
//int 表明函数的返回类型。
//(int ,int) 表明函数有两个参数,都是int类型
可以通过函数指针来调用函数,以下两种写法均可。
函数指针数组
如果把函数指针储存在一个数组,那么这个数组就叫做函数指针数组。
int (*puf[5])(int,int)
puf先和[]结合,说明puf是一个数组,*表明这是一个指针数组,外面int (int,int)为函数类型,说明这个是函数指针数组。
//以下四个函数类型相同,可以放进同一个函数指针数组
int ADD(int x, int y)//加法函数
{
return x + y;
}
int SUB(int x, int y) //减法函数
{
return x - y;
}
int MUL(int x, int y) //乘法函数
{
return x * y;
}
int DIV(int x, int y) //除法函数
{
return x / y;
}
int (*paff[4])(int, int) = { ADD,SUB,MUL,DIV };
转移表(函数指针数组的简单应用)
#include<stdio.h>
int ADD(int x, int y)
{
return x + y;
}
int SUB(int x, int y) {
return x - y;
}
int MUL(int x, int y)
{
return x * y;
}
int DIV(int x, int y)
{
return x / y;
}
int (*paff[5])(int, int) = { 0,ADD,SUB,MUL,DIV };
void menu()
{
printf("***********************\n");
printf("***1.ADD 2.SUB***\n");
printf("***3.MUL 4.DIV***\n");
printf("***0.EXIT ***\n");
printf("***********************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
case 2:
case 3:
case 4:
printf("请输入两个数:>");
scanf("%d%d", &x, &y);
ret = paff[input](x, y);
printf("结果是: %d \n", ret);
break;
default:
printf("输入错误\n");
case 0:
break;
}
} while (input);
}
4.指针(进阶一)
4.1 回调函数
回调函数是一个通过函数指针调用的函数。
#include<stdio.h>
typedef int(* Cacl)(int, int);
int ADD(int x, int y)//加法函数
{
return x + y;
}
int SUB(int x, int y) //减法函数
{
return x - y;
}
int MUL(int x, int y) //乘法函数
{
return x * y;
}
int DIV(int x, int y) //除法函数
{
return x / y;
}
void cacl(Cacl p)
{
int x = 0; int y = 0;
printf("请输入两个数:");
scanf("%d%d", &x, &y);
int ret = p(x, y);
printf("ret = %d\n", ret);
}
void menu()
{
printf("***********************\n");
printf("***1.ADD 2.SUB***\n");
printf("***3.MUL 4.DIV***\n");
printf("***0.EXIT ***\n");
printf("***********************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
cacl(ADD);
break;
case 2:
cacl(SUB);
break;
case 3:
cacl(MUL);
break;
case 4:
cacl(DIV);
break;
default:
printf("输入错误\n");
case 0:
break;
}
} while (input);
return 0;
}
这里main函数根据输入,将不同的算术函数的地址传递给cacl函数,cacl函数内部通过这个函数指针调用函数。其中,这些主调函数通过传函数指针在另一个函数调用相应的函数,那么这就称作回调函数。
回调函数不是有主调方直接调用,而是在满足条件下由另一方调用,用作对该事件响应。
4.2 qsort
qsort介绍和使用
网站:legacy.cplusplus.com
qsort
库函数
头文件: stdlib.h
采用的快速排序。qsort共有四个参数
1.void* base 是一个无具体类型的指针,作为形参,可以接受任何类型的参数。这里可以是待排序数组的首元素地址
2.size_t num 待排序的个数,size_t是无符号整形。接受一个无符号整形。
3.size_t size 传入数组中元素类型大小,单位字节。
4.int (*cmp)(const void* e1,const void* e2) 这里是函数指针作为参数,接受int (*)(const void* e1,const void* e2)这类函数类型的指针。因为不知道比较的元素是什么类型,而且也不希望修改它,所以它是void*用const修饰。
下面我们要学习如何创建这个函数。
我们从这个函数指针类型可以知道,这个函数返回类型是int,有两个形参都是void* 指针,可以认为数组中两个相邻之间的元素的指针。
假设比较整型:
可以这样*((int*)e1) - *((int*)e2),注意强制类型转换,因为void* 不能解引用。
如果像上面一样写,那么
表达式结果 >0 交换
表达式结果<=0 不交换
最后会排序结果是升序数组。如果要降序,只需调换e1和e2的位置。
#include<stdio.h>
#include<stdlib.h>//qsort包含的头文件
int cmp_int(const void* e1, const void* e2);//函数声明
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };//把这个数组排成升序。
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组的元素个数
qsort(arr, sz, sizeof(int), cmp_int);
//1.把数组首元素的地址传过去,2.整个数组都要排序(传数组的元素个数),3.数组元素的类型大小用sizeo计算并传参,4.自行构造比较整形的函数然后传函数指针。
//打印观察
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
int cmp_int(const void* e1, const void* e2)
{
//强制类型转换成int* ,再解引用!!!
return *((int*)e1) - *((int*)e2);
}
用冒泡排序模拟实现qsort
回顾一下冒泡排序
qsort可以排序任意类型的数据,尝试思考下面代码要修改哪些部分
//该函数只能接受整型指针,也就是对int 的数据排序
void bubble_sort(int* arr, int sz)
{
//趟数
int i = 0;
for (i = 0; i < sz-1; i++)//第一次for循环可以不用修改
{
//一趟内部两两比较
int j = 0;
for (j = 0; j < sz - 1 - i; j++)//第二层for循环也不用更改
{
//两个整型元素可以直接使用>比较
//两个字符串,两个结构体元素是不使用>比较的!!!
//但我们作为使用者,明确知道要排序的是什么数据类型,所以我们可以自己定义比较函数作为判断条件
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
参数改造理解:
1.void*的指针,无具体类型的指针,它可以作为函数参数接受任何类型的指针。作为bubble_sort的第一个参数。
2.排序要清楚待排的元素个数,int sz 可以保留,但个数非负,写成size_t更严谨。
3.传了指针和排序的元素个数,知道起始位置,但在函数内部不知道它的多少字节。所以必须知道一个元素多大!第三个参数是size_t size。
4.用户知道自己排序的数据类型,根据函数类型要求自定义函数。第四个参数要函数指针为形参。int (*cmp)(const void* e1,const void* e2)。
基本框架有了,
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{
//趟数
int i = 0;
for (i = 0; i < sz-1; i++)
{
int j = 0;//一趟内部相邻两两比较
for (j = 0; j < sz - 1 - i; j++)
{
if(cmp())//这里不再只是整型,用cmp比较;
;//非完整代码
}
}
}
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{
int i = 0;
for (i = 0; i < sz-1; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (cmp((char* )base+j*width,(char* )base+(j+1)*width) > 0)
{
//交换两个元素。
/*
不能这样写了
int tmp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=tmp;
*/
//接下来通过一个字节一个字节的交换
swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
//传两个相邻元素的地址,还要传它们原本的类型大小
}
}
}
}
void swap(char* p1, char* p2, size_t width)
{
int i = 0;
for (i = 0; i < width; i++)//类型多少个字节则交换多少次。
{
//char 大小一个字节,用它作为中间变量,来起到交换作用。
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
return;
}
最后的代码
#include<stdio.h>
#include<stdlib.h>//qsort包含的头文件
//函数声明
void swap(char* p1, char* p2, size_t width);
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2));
int cmp_int(const void* e1, const void* e2);
int main()
{
int arr[] = { 9,11,7,60,8,4,5,6,16,10 };//把这个数组排成升序。
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组的元素个数
bubble_sort(arr, sz, sizeof(int), cmp_int);
//打印观察
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
int cmp_int(const void* e1, const void* e2)
{
return *((int*)e1) - *((int*)e2);
}
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{
int i = 0;
for (i = 0; i < sz-1; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (cmp((char* )base+j*width,(char* )base+(j+1)*width) > 0)
{
//交换两个元素。
/*
不能这样写了
int tmp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=tmp;
*/
//接下来通过一个字节一个字节的交换
swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
//传两个相邻元素的地址,还要传它们原本的类型大小
}
}
}
}
void swap(char* p1, char* p2, size_t width)
{
int i = 0;
for (i = 0; i < width; i++)//类型多少个字节则交换多少次。
{
//char 大小一个字节,用它作为中间变量,来起到交换作用。
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
return;
}
5.结尾
有错指正,私信,谢谢了。
随缘补充内容。
指针进阶二待更。