目录
一、字符指针
1.字符指针的一般用法
char ch = 'w';
char* pc = &ch;
*pc = 'a';
printf("%c", ch);
1.1字符指针的另一种用法
char* p = (char*)"abcdef"; //没有(char*),则"abcdef"表示的是一个常量字符串,是不能够放进p里面的
//const char* p = "abcdef"; //写成这种形式也是可以的
printf("%c\n", *p);
p是指针变量,在x86的情况下只能存放4个字节
char* p = "abcdef"; 这句话的意思是 p存放的不是 abcdef 这个字符串,而相当于是 a 这个位置的地址,同理既然指向了a,而且字符串在内存中是连续存放的,也就间接的可以指向字符串的全部地址
在vs2022编译器环境下是需要用 这样的形式 char* p = (char*)"abcdef"; 表示强制类型转换,但是原理都是一样的
二、指针数组
概念:指针数组是一个存放指针的数组,有:整型指针的数组,一级指针的数组,二级指针的数组
int* arr[10] ;//存放整型指针的数组
char* arr[5] ;//存放字符指针的数组
举两个指针数组的使用例子:
int a = 10;
int b = 20;
int c = 30;
int* arr[10] = { &a,&b,&c };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", *(arr[i]));
}
这里面的指针数组arr存放的是整型 a b c 的地址,返回类型是int型
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 1,1,3,5,5 };
int arr3[5] = { 1,2,3,9,5 };
int* prr[] = {arr1,arr2,arr3};
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
//printf("%d ", prr[i][j]);
printf("%d ",*(prr[i]+j));
}
printf("\n");
}
这里面的指针数组prr存放的是数组类型的数据,返回类型是int型
三、数组指针
1.数组指针的概念
数组指针是专门用来存放一个数组的地址的
一个数组指针存放一个地址
数组指针是数组还是指针?
答:数组指针是一个指针
下面由一些常见的指针来引入数组指针
整型指针:int * point能够指向整型数据的指针。
浮点型指针:float * pf; 能够指向浮点型数据的指针
那么数组指针应该是指向数组的指针。
数组指针类型是: int (*)[size] ;
位置不能随便换
[size]表示的是 它指向的呢个数组arr,有size个元素,每个元素是int
给它加一则加的是size个元素大小的地址。
例题:下面哪个代码是数组指针?
int *p1[10];
int (*p2)[10];
由指针数组和数组指针的概念可以得到:p1是个数组,为指针数组,p2是一个指针,则为数组指针
又如: char* arr[20];
p = &arr;
//则类型为 char* (*p)[20]; p指向这20个元素,这20个元素全为char*类型
2. &数组名和数组名的比较
有一个数组:int arr[5] = {0};
arr 和 &arr 分别是什么呢?
我们知道arr是数组名,数组名表示数组的首元素地址/
那么&arr数组名到底是什么?
//例子:
int arr[5] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr);
int arr[5] = { 0 };
printf("%p\n", arr+1);
printf("%p\n", &arr+1);
从中我们可以看到,arr表示首元素地址,&arr中的arr表示整个数组的地址
那么在什么情况下数字名才能表示整个数组的地址呢
1.sizeof(数组名),这里面的数组名表示的是整个数组的大小,计算的是整个数组的大小
2.&数组名,这里的数组名表示的是整个数组,取出的是整个数组的地址
3.数组指针的作用实例
1.数组指针的使用
#include<stdio.h>
void print(int (*p)[10], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(*p + i)); //*p 对p进行解引用,相当于拿到了数组的//全部元素,整个数组
// 而整个数组相当与数组的首元素地址 //arr[0]
}
}
//方法一打印print2
void print2(int arr[3][5], int c, int r)
{
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
//方法二打印print2
void print2(int(*p)[5],int c,int r)
{
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ",*(*(p+i)+j)); //(*p+i)表示拿到了第i行的 ,也相当于第i行的数组名,p+i是指向第i行的
//数组名表示首元素地址 *(p+i),表示第i行第1个元素的地址
//再一次加j进行解引用则表示第i行第j个元素
//
//
//写成printf("%d ", arr[i][j]);的形式也可以,是一摸一样的
//原理:arr[i][j];
// arr[i] ---> *(arr+i)
// ---> *(arr+i)[j];
// 进一步表示: ---> *(*(arr+i)+j);
}
printf("\n");
}
}
int main()
{
//int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
写一个函数打印arr数组的内容
//int sz = sizeof(arr) / sizeof(arr[0]);
//print(&arr, sz);// <-- 传入一个数组地址
// 其实有些麻烦
// 而其实数组指针不是这样用的
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
//写一个函数,打印arr数组
//方法一:
//print2(arr, 3, 5);
//方法二:
print2(arr, 3, 5); //二维数组的解释: 每一行代表一个元素 ,所以当二维数组的数组名表示首元素的地址的时候指的是第一行的地址
//而第一行里面是5个整型的一维数组
//print2(arr, 3, 5); 若写成&arr的话则表示的是整个二维数组的地址
return 0;
}
4.指向二维数组的指针
&arr;
int (*ptr)[3][5] = &arr;
四、数组传参和指针传参
1.一维数组的传参
如一个函数 传入数组arr:test(arr);
则其参数形式有:
test(arr); // ---> *形参写成数组形式*
1.void test(int arr[10])
{}
2.void test(int arr[])
{} // 形参部分的数组大小可以省略
3.void test(int arr[100])
{} //不建议,但是没错
//---> *形参写成指针形式* (数组名表示首元素地址)
也可有指针形式的传参
test(arr)
void test(int *p)
{}
int*类型的写成数组形式传参是
test2(arr2); //---> 数组形式 (数组名表示首元素地址)
void test2(int* arr[20])
{}
void test2(int* arr[])
{}
void test2(int* arr[200])
{
}
int*类型的写成指针形式传参是
test2(arr2); //---> 指针形式
void test2(int** p)
{}
2.二维数组的传参
如:
int arr[3][5] = { 0 };
test(arr);
//形参写成数组的形式:
test(int arr[3][5])
{
}
test(int arr[][5]) //行可以省略但是列不可以省略 //行可以不写也可以乱写,但是不建议乱写(仅限范围写大写小)
{
}
//形参写成指针的形式://二维数组的首元素表示的第一行的地址
void test(int (*p)[5])// p 是先是指向的第一行, [5] 表示的是p指向的呢一行有几个元素,类型是int类型
//此处的[5]是不能够省略的,没有5就不知道一行有几个元素了
//二级指针使用来存放一级指针的地址的,数组指针是用来存放数组的地址的!
//所以在二维数组当中不能用二级指针来传参
3.一级指针传参
如
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int *p=arr;
如果想要对元素内容进行打印还需要知道数组元素的个数
int sz = sizeof(arr)/sizeof(arr[0]);
void test(int *ptr,int sz) //一级指针的传参就是用一级指针来接收
//ptr 里面所存放的地址与p相同,也是指向数组首元素的地址
{
int i = 0;
for (i = 0; i < sz; i++)
{ //方法一:
/*printf("%d ", *ptr);
ptr++;*/
//方法二:
printf("%d ", *(ptr++));
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int *p=arr;
//如果想要对元素内容进行打印还需要知道数组元素的个数
int sz = sizeof(arr)/sizeof(arr[0]);
test(p,sz); //p是一级指针
}
4.二级指针的传参
void test1(char** ppc) //二级指针的传参用二级指针来接收 ,没有什么其他的方法。
{
}
int main()
{
char a = 'w';
char* pa = &a;
char** ppa= &pa;
// ppa 就是一个二级指针
test1(ppa);
//思考:
//当一个函数的参数部分是一个二级指针的时候,函数能接收什么参数?
//答:可以把
char ch = 'a';
test1(&ch);//不可以
// &&ch 也不可以 ,因为&是取的变量地址而 &ch 已经是一个常量了,所以不能再用 &&ch 了
char* pc = &ch;
char** ppc = &pc;
test1(&pc);//可以
test1(ppc);//可以
char* arr1[4]; //指针类型的数组
test1(arr1);//可以
//但是二维数组就不可以了
char* arr2[3][5];
test1(arr2);
}
1.二维数组的传参方法
void test1(int(*p)[5])
{
}
int main()
{
int arr3[3][5];
test1(arr3); //这个是吧第一行的地址传过去了
return 0;
}
2.二维数组的二级指针地址传参
void test2(int(*p)[3][5])
{
}
int main()
{
int arr3[3][5];
test2(&arr3); //这个是吧整个的二维数组地址传过去了 -- 一般不会这样写,这样写太麻烦了
return 0;
}
五、函数指针
1.指向函数的指针
int arr4[10];
int(*p)[10] = &arr4; // p 是一个数组指针的变量 ,是用来存放数组的地址的
由数组指针来引入函数指针
&ADD; //取出函数的地址
printf("%p", &ADD);
printf("%p", ADD); //这两个都叫拿到函数的地址,虽然方法不一样,但是意义一样。
如何 保存函数地址呢/
int (* pf)(int, int) = ADD; //这就是函数指针变量.
*pf 说明是个指针 ,那么他指向的是什么呢 ,指向的是() ,里面是 int,int 类型 (ADD函数例子)
返回类型是 int
呢么现在也可以举一例
照猫画虎的举一例
int test(char* str) {}
....
int (*pt)(char*) = test;
2.函数指针的使用
//如上题
int ADD(int x, int y)
{
return x + y;
}
//...
int (*pf)(int, int) = ADD;
int ret = (*pf)(2, 3);
//之前的使用是: int ret = ADD(2,3);
//但是
int ret = pf(2, 3); //这个也是可以的 ,加*或者不加*都可以的,这个*其实是可以没有的,写多个*也都一样
// 这里的*是没有实际意义的
//如果加*的话不能去掉括号
printf(" %d", ret);
//int (*p)[10] ---> int (*)[10] 是数组指针类型 , p 是 类型变量
//int (*pf)(int, int) ---> int (*)(int, int) 是函数指针类型
对此有两个例题:解释这两个代码的含义
//代码一:
(*(void(*)())0)();
//分析:
// ( *( void(*)() )0 )(); --->
//void(*)() //(函数指针类型) --->
//( void(*)() ) //强制类型转换 --->
//(类型)
//( void(*)() ) 0 //对0进行强制类型的转换 // 把0转换成函数指针类型转换 函数类型为void ,参数为空的 --->
// 此时0被当作函数的地址了
// *(void(*)())0 //对0进行解引用 --->
// (*(void(*)())0)(); //调用0地址处的这个函数
//代码2:
int (*signal(int, void(*)(int)))(int);
//解释:signal是一个函数声明
//signal(int, void(*)(int)) //一个函数名为signal的一个参数为int ,一个参数为 函数指针类型
// signal函数的返回类型为: void(* )(int); //void(*)(int)signal(int, void(*)(int)) 可以这样理解,但是这样的写法是不对的
//可以将其简化
//typedef void(*)(int) pf_t; 给这个函数指针类型重新起名为 pf_t ,但是这样写是错误的,下面的是对的
typedef void(*pf_t)(int);
//简化后的结果:
pf_t signal(int, pf_t);
六、函数指针数组
1.函数指针数组的概念
指针数组字符指针数组
char* arr[5];
整型指针数组
int* arr2[4];
由此来引出函数指针数组
// //先创建四个函数
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = Sub;
int (*pf3)(int, int) = Mul;
int (*pf4)(int, int) = Div;
//函数指针数组
int (*pf[4])(int, int) = { Add,Sub,Mul,Div }; //pf 先和[4]结合了,所以他是个数组,类型是 int (*)(int, int)
//数组是一组相同类型的数组集合
2.指针数组的传参和打印
//指针数组的打印和传参;
int i = 0;
for (i = 0; i < 4; i++)
{
int ret=pf[i](8, 2);
printf("%d\n", ret);
}
3. 函数指针数组的使用
设计一个计算器
方法一:
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 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);
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
switch (input)
{
case 1:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("ret=%d\n", ret);
break;
case 2:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("ret=%d\n", ret);
break;
case 3:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("ret=%d\n", ret);
break;
case 4:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = Div(x,y);
printf("ret=%d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误!\n");
break;
}
printf("ret=%d\n", ret);
} while (input);
这个代码过于冗余了,如果再增加新的算法的话会更加的冗余造成不必要的重复写入
代码的改进
方法二:
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
//把这个函数指针称作是 转移表
int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div }; //为什么要放0,原因是在刚才的的功能里面,选择的1是加法,依次
//其目的是为了更好的对应下标
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
}
else if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("ret=%d\n", ret);
}
else
{
printf("选择错误\n");
}
/*printf("ret=%d\n", ret);*/
} while (input);
//好处
//*****如果还想再增加其他的运算只需改动 函数指针数组就行了*****
//局限性,只有函数的参数类型一致时函数才可以使用
return 0;
}
4.指向函数指针数组的指针
概念:指向函数指针的数组的指针是一个指针 指针指向一个数组,数组的元素都是函数指针。
如何定义?
//函数指针
int (*pf)(int, int) = &ADD;
//函数指针数组
int (*pf1[4])(int, int);
int (*(*p3)[4])(int ,int) = &pf1; //p3 就是一个指向函数指针数组的指针
// 数组的地址放到p3的里面,
int i = 0;
for (i = 0; i < 4; i++)
{
int ret = (*p3)[i](3, 4);
//如果要将p3写成[]的形式,那么p3只能写成p3[0]
int ret = p3[0][i](3, 4);
printf("%d\n", ret);
}
七、回调函数
1.回调函数的概念
解释:回调函数就是一个 通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方 直接调用的,而是在特定的事件或条件发生时由另外一方调用的,对于该事件或条件进行响应。
// //例如:写了一个A函数,而没有直接调用A函数,那么B函数里面必须要有一个指针来接收A函数,通过B函数来调用A函数,此时A函数成为回调函数
用回调函数对上述计算机代码进行改进
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
void clc(int(*pf)(int, int))
{
int ret = 0;
int x = 0;
int y = 0;
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = pf(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;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
/*printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);*/
switch (input)
{
case 1:
clc(Add);
break;
case 2:
clc(Sub);
break;
case 3:
clc(Mul);
break;
case 4:
clc(Div);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误!\n");
break;
}
/*printf("ret=%d\n", ret);*/
} while (input);
}
2.演示qsort函数的使用
普通冒泡排序的函数实现
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++) //第i趟
{
int j=0;
for (j = 0; j < sz-1-i; j++) //i的下一趟,会少一项
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
用qsort函数实现
void qsort(void* base,
size_t num,
size_t width,
int (*cmp)(const void* e1, const void* e2) );
// qsort函数的实现
//类型为:
void qsort(void* base, //base是目标数组(待排序数据)的起始位置
size_t num, //num是目标数组的元素个数
size_t width, //一个元素的字节大小
int (*cmp)(const void* e1, const void* e2) //函数指针
//cmp是一个比较函数,e1和e2是待比较两个元素的地址
//比较函数的要求是,qsort函数使用者,自定义的一个比较函数!
//比较类型的情况:
//如果 ,排序的是整型数据,用 > <
// 如果排序的是结构体数据: 可能不方便直接使用><比较了
// 需要使用者根据使用情况,提供一个函数,实现2个数的比较~
//待比较 如果 e1 < e2 则返回 <0 ;如果 e1 = e2 则返回 =0 ;如果e1 > e2 则返回 >0
// void* 是一个无确切类型的指针,是无法直接进行解引用的
//void* 是可以接受任意类型的地址,但是无法对void* 的变量进行解引用或者增量....其他等等
);
qsort函数实现冒泡与模拟实现qsort库函数
函数的解读
void test1()
{ //将上述的冒泡排序定义为test1
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//排序为升序;
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr,sz);
print_arr(arr, sz);
}
//自定义一个函数
int cmp_int(const void* e1,const void* e2)
{
//比较的实现
/*if (*(int*)e1 > *(int*)e2)
{
return 1;
}
else if (*(int*)e1 == *(int*)e2)
{
return 0;
}
else
{
return -1;
}*/ //但是这种写法过于复杂
//可以改为:
return (*(int*)e1 - *(int*)e2);
}
//冒泡排序的改进
void swap(char* buf1, char* buf2, int width)
//如果交换8和7的话
// 8 在内存中存放是: 08 00 00 00 //buf1指向8
// —— // 这四个字节都要交换
// 7 在内存中存放是: 07 00 00 00 //buf2指向7
{
int i = 0;
for (i = 0; i < width; i++)
{
char temp = *buf1;
*buf1 = *buf2;
*buf2 = temp;
buf1++;
buf2++;
}
}
void bubble_sort1(void* base, int num,int width , int (*cmp)(const void* e1, const void* e2))
{
int i = 0;
for (i = 0; i < num - 1; i++) //第i趟
{
int j = 0;
for (j = 0; j < num - 1 - i; j++) //i的下一趟,会少一项
{
//if (arr[j] > arr[j + 1]) //比较
//因为没有arr[]数组了,只有base,所以应该怎么传要比较的数字呢
//用整型来分析
// 9 8 7 6 5 4 3 2 1 0
// base 在9的位置
//则8的位置是在:(char*)base+width
//则如果要比较8的地址和7的地址的话
//我们虽然不能直接知道地址,但是我们知道下标j
//所以 有 8 的地址:(char*)base+j*width
//所以 有 7 的地址:(char*)base+(j+1)*width
if(cmp((char*)base + j * width, (char*)base + (j + 1) * width)>0) //相邻两个地址的交换
{
//交换
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
//void test2()
//{
// int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
// //排序为升序;
// int sz = sizeof(arr) / sizeof(arr[0]);
// qsort(arr, sz, sizeof(arr[0]),cmp_int);
// print_arr(arr, sz);
//}
//
//使用qsort排序结构体
struct Stu
{
char name[20];
int age;
double score;
};
int cmp_stu_by_age(const void* e1, const void* e2) //假设按照年龄来排序
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_stu_by_name(const void* e1, const void* e2) //假设按照名字来排序
{
//升序
return strcmp(((struct Stu*)e1)->name ,((struct Stu*)e2)->name);
//strcmp是用来比较字符串的,按照字典的顺序来比较的 ,如a b的顺序 a在b前面
//降序
//return strcmp(((struct Stu*)e2)->name, ((struct Stu*)e1)->name);
}
void test3()
{
struct Stu arr[3] = { {"zhangsan",20,55.5},{"lisi",30,88.0},{"wangwu",25,90.0}};
int sz = sizeof(arr) / sizeof(arr[0]);
//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
//升序
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
//降序
//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}
void test4()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//排序为升序;
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort1(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
}
void test5()
{
struct Stu arr[3] = { {"zhangsan",20,55.5},{"lisi",30,88.0},{"wangwu",25,90.0} };
int sz = sizeof(arr) / sizeof(arr[0]);
//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
//升序
bubble_sort1(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
//降序
//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}
int main()
{
test4();
return 0;
}
思考
qsort函数的作者,能不能想到我们使用qsort排序什么具体类型的数据呢? 答:什么类型的可以,根本不用想 设计成void*才可以接收任意类型的数据 num :要知道元素个数 width: 要知道一个元素的大小 函数指针:实现比较 为什么要设计成void const*呢? 首先我们并不知道base要放什么样类型的数据,我们只知道这个数据从哪开始 所以我们只进行未知类型的比较,void*也不知道什么类型,const是只让进行比较而不做修改