1.字符指针
#include<stdio.h> int fun(int arr[])//本质上是一个指针 { return sizeof(arr)/sizeof(arr[0]); //指针大小/整型元素的大小,32位系统,指针大小4,得到结果为1 //64位系统指针大小8个字节,得到结果为2 } int main() { int arr[10]; fun(arr); printf("%d",fun(arr)); return 0; }
将一个字符串存到数组中,数组名赋给pc,pc相当于指向了字符数组(arr)
int main() { char arr[]="abcdef"; char* pc=arr; printf("%s\n",arr); printf("%s\n",pc); return 0; }
结果相同
abcdef
abcdef
注:在32位系统中,指针只有四个字节,无法存放”abcded\0“这7个字节,所以”abcdef“放到p中这样的理解是错误的,如何理解
p中存放的是a的地址(把字符串首字符的地址赋值到p),即*p=a;
int main() { char* p="abcdef";//常量字符串 printf("%c\n",*p);//输出结果为a printf("%s\n",p);//输出结果为abcdef return 0; }
a的地址是0x0012ff44,p中存放这个地址,就能找到这个常量字符串,通过这个指针能找到字符串的空间,因为字符串的结尾是\0,打印时遇到后则停止打印。
"abcdef"是常量字符串,既然是常量字符串,就不能被改动,就会报错(segmentfault)
int main() { const char* p="abcdef";//这里最准确的写法是加上const *p='W'; printf("%s\n",p); return 0; }
面试题
int main() { char arr1[]="abcdef"; char arr2[]="abcdef"; const char* p1="abcdef"; const char* p2="abcdef"; /* if(arr1==arr2) printf("1\n"); else printf("2"); */ if(p1==p2) printf("1\n"); else printf("2"); return 0; }
arr1!=arr2:两个不同的数组,占有不同的空间
p1==p2:为了节省空间,p1和p2都指向同一个存储空间的起始位置
注p1和p2还是两个独立的空间,所以p1改变,p2不会受到影响
2.指针数组和数组指针
(1)指针数组
int main() { int arr1[]={1,2,3,4,5}; int arr2[]={2,3,4,5,6}; int arr3[]={3,4,5,6,7}; int* parr[]={arr1,arr2,arr3}; for(int i=0;i<3;i++) { for(int j=0;j<5;j++) { printf("%d ",*(parr[i]+j));//parr[i]:是数组的地址,就是该数组首元素的地址 //i表示第几行,j表示改行的偏移值 } printf("\n"); } return 0; }
注意:指向的是每个数组首元素的地址
(2)数组指针
数组名绝大多数情况下指的是首元素地址
取地址数组名(&arr)取出的是数组的地址
arr+1和&arr+1
#include<stdio.h> int main() { int arr[10]={0}; printf("arr=%p\n",arr); printf("&arr=%p\n",&arr); printf("arr+1=%p\n",arr+1); printf("arr+1=%p\n",&arr+1); return 0; }
结果
一个数组名加1,跳过一个元素,取地址数组名+1跳过一个数组
指针的运用
int main() { int arr[10]={1,2,3,4,5,6,7,8,9,10}; int *p=arr; //指向数组的首元素 for(int i=0;i<10;i++) { printf("%d",*(p+i)); } /* int (*pa)[10]=&arr; int i=0; for(i=0;i<10;i++) { printf("%d",*(*pa+i));//*pa=arr,arr相当于数组名,数组名相当于首元素地址 } */ /* for(i=0;i<10;i++) { printf("%d",(*pa)[i]);//pa表示整个数组的地址,*pa表示整个数组的值,(*pa)[i]表示数组的第几个元素 } */ return 0; } 数组指针一般用在二维数组比较方便 //参数是数组类型 void print1(int arr[3][5],int x,int y) { int i=0; int j=0; for(i=0;i<x;i++) { for(j=0;j<y;j++) { printf("%d ",a[i][j]); } printf("\n"); } } //参数是指针类型 void print2(int (*p)[5], int x,int y)//指向的是一行,就是一维数组 { for(int i=0;i<x;i++) { for(int j=0;j<y;j++) { printf("%d ",*(*(p+i)+j));//j表示每一行数组的偏移值*(*(p+i)+j) //printf("%d ",(*(p+i))[j]);//与上面代码效果相同 //printf("%d ",*(p[i]+j)); //printf("%d ",p[i][j]); } printf("\n"); } } int main() { int arr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}}; print1(arr,3,5); //数组名传参是整个数组 ,arr--数组名--数组的首元素地址,对于二维数组而言, 在讨论二维数组首元素时,要把其想像为一维数组,就是把第一行当作第一个元素,第二行第二个元素..., 所以二维数组中数组名就表示首元素的地址,就是第一行的地址,第一行是一个一维数组, 那么数组地址要放到数组指针当中,指针中放的是一个含有5个元素的一维数组,类型是int print2(arr,3,5); return 0; } p指向的数组为5个元素的话,所以p+1,就跳过一行,所以对*(p+i)进行遍历,*(p+i)=该行的数组名,即该行首元素,再用 j 代表偏移值,找到某行的某一元素 void print2(int (*p)[5], int x,int y)//指向的是一行,就是一维数组 { for(int i=0;i<x;i++) { for(int j=0;j<y;j++) { printf("%d ",*(*(p+i)+j));//j表示每一行数组的偏移值*(*(p+i)+j) //printf("%d ",(*(p+i))[j]);//与上面代码效果相同 //printf("%d ",*(p[i]+j)); //printf("%d ",p[i][j]); } printf("\n"); } } #include<stdio.h> int main() { int arr[10]={1,2,3,4,5,6,7,8,9,10}; int i=0; int* p = arr; for(int i=0;i<10;i++) { printf("%d ",*(p+i));//int* p,指向数组首元素地址,*(p+i)表示遍历各个元素 printf("%d ",*(arr+i));//arr表示数组首元素地址,(arr+i)表示遍历各个元素,*(arr+i)表示取各个元素的值 printf("%d ",p[i]); printf("%d ",arr[i]);//arr[i]==*(arr+i)=*(p+i)==p[i]...等价 } return 0; }
回顾字符指针,数组指针和指针数组
3. 数组参数和指针参数
(1)一维数组传参
(2)二维数组传参
void test(int arr[3][5]) {} void test2(int arr[][5])//可以把行省略 {} void test3(int arr[3][])//不可以把列省略 //必须知道一行有多少列才方便计算 //错误的 void test4(int *arr)//二维数组的数组名,表示首元素的话,首元素应该是第一行,第一行不应该放在整型指针里面,整型指针里面应该放整型地址 {} void test4(int **arr)//一维数组的地址不能放到二级指针当中,一级指针的地址才能放到二级指针当中 {} void test4(int *p[5])//这是一个指针数组,但是二维数组传过来的是第一行的地址,指针里面才能存放地址 //正确的 void test4(int (*p)[5])//指针指向的是一维数组,数组中有5个元素,每个元素是int类型 int main() { int arr[3][5]={0}; test(arr);//二维数组传参 test2(arr); test3(arr); test4(arr); return 0; }
对于void test4(int **arr){}这里特别说明一下
一级指针,指向的是数组的元素,一级指针才能指向数组的首元素,不能是二级指针
int a[5] = {1, 2, 3, 4, 5};
int **s0, *s1, **s2;
s0 = &a;
s1 = a;//首元素地址
s1 = &a[2];
s2 = &s1;
s0 是二级指针,它指向一维数组
s1 是一级指针,它指向一维数组的元素
s2 是二级指针,它指向一级指针
下面这几种情况才能被二级指针接受,下面指针传参也会提到
void test(int **p) {} int main() { int *ptr; int** pp=&ptr; int* arr[10]; test(arr);//这个数组名代表的是int *的地址,一级指针的地址可以用二级指针接收 test(&ptr); test(pp); return 0; }
(3)一级指针传参(指针传参,指针接受)
#include<stdio.h> void print(int *p,int sz) { int i=0; for(i=0;i<sz;i++) { printf("%d\n",*(p+i)); } } int main() { int arr[10]={1,2,3,4,5,6,7,8,9}; int *p=arr; int sz=sizeof(arr)/sizeof(arr[0]); //一级指针p,传给函数 printf(p,sz); return 0; }
思考
当一个函数的参数部分为一级指针的时候,函数能接收什么参数
例如
(4)二级指针传参(参数接受一级指针变量的地址或者是二级指针,也可以传存放一级指针的数组(char* arr[])的数组名)
#include<stdio.h> void test(int** ptr) { printf("num=%d\n",**ptr); } int main() { int n=10; int *p=&n; int **pp=&p; test(pp); test(&p); return 0; }
补充
void test(int **p) {} int main() { int *ptr; int** pp=&ptr; int* arr[10]; test(arr);//这个数组名代表的是int *的地址,一级指针的地址可以用二级指针接收 test(&ptr); test(pp); return 0; }
4.函数指针
数组指针---指向数组的指针
函数指针---指向函数的指针---存放函数地址的一个指针
int Add(int x,int y) { int z=0; z=x+y; return z; } #include<stdio.h> int main() { int a=10; int b=20; //printf("%d\n",Add(a,b)); //Add(a,b); printf("%p\n",&Add);//取地址Add,就是函数的地址 printf("%p\n",Add);//与上面的结果一样 //在数组中&arr,表示整个数组的地址,arr表示首元素的地址,而在函数中&Add和Add是相同的,因为没有函数首地址这样的概念 return 0; }
结果相同
int Add(int x,int y) { int z=0; z=x+y; return z; } #include<stdio.h> int main() { int a=10; int b=20; //printf("%d\n",Add(a,b)); //Add(a,b); /* printf("%p\n",&Add);//取地址Add,就是函数的地址 printf("%p\n",Add);//与上面的结果一样 //在数组中&arr,表示整个数组的地址,arr表示首元素的地址,而在函数中&Add和Add是相同的,因为没有函数首地址这样的概念 */ int *pa(int,int)=Add;//这里的pa是一个函数名而已,不是指针,不能存放函数地址 //pa是一个函数名,后面的(int,int)是参数类型,前面的int *是返回类型,这是不正确的,我们希望pa是一个指针,指针才能存放地址,正确的写法为 int (*pa)(int,int)=Add;//这是一个函数指针,最前面的int表示函数的返回类型,(int,int)表示参数类型 (*pa)(2,3);//调用这个函数,再将(2,3)传入这个函数 //结果为5 pa=Add; return 0; }
再来看一个例子
void Print(char *str) { printf("%s\n",str); } int main() { void (*p)(char*)=Print; (*p)("hello");//调用这个函数,并传入一个char类型元素,或者说让char* 指向char类型的元素 return 0; }
两段有趣的代码
代码1
(*(void(*) () )0)();
void(*p)(char *)=Print;//去掉指针变量名p后,就是指针类型void (*p)(char *),这里一段一段分析
void(*)():这是一个指针类型,指向的是一个(),即一个函数,返回类型是void 所以 void(*) ():函数指针类型 那么 ( void(*) () ):就是强制类型转换 ((void(*)())0):对0进行强制类型转换,即把0作为某函数的地址 (*(void(*)())0):通过*找到这个函数 (*(void(*)())0)():调用这个函数,这个函数的参数是(),即无参 这个代码是一次调用,调用0地址处的参数为无参,返回类型为void的函数
代码2:
void (*signal(int,void(*)(int)))(int);
signal(int,void(*)(int)) 函数有两个参数,一个是int类型,一个是函数指针类型,函数的参数是int,返回值是void 与void Add(int,int)相同,将Add(int,int)去掉,剩下的就是函数返回类型 所以signal函数的返回类型是void(*)(int):函数指针类型,参数是int,返回类型是void 这是一次函数申明,signal是一个函数,函数的第一个参数是整型,第二个参数是函数指针(void(*)(int)),函数的返回类型也是一个函数指针(void(*)(int)) 注意不能这样写 void(*)(int) signal( int, void(*)(int) ) *号一定要和函数指针变量名signal在一起
怎么写的更加简洁?
void (* signal( int,void(*)(int) ) )(int)
typedef void(*pfun_t)(int);//pfun_t要放在*旁边
可以写为
pfun_t signal(int,pfun_t);
int main() { int a=10; int b=20; int (*pa)(int,int)=Add; printf("%d\n",pa(2,3));//pa是Add这个函数的地址,调用这个地址,指向这个函数 printf("%d\n",Add(2,3)); printf("%d\n",(*pa)(2,3));//指针解引用找到对象 printf("%d\n",(**pa)(2,3)); printf("%d\n",(***pa)(2,3)); //*就没有太大的价值,这些输出都相同,并且如果加上*则必须用括号括起来,如(*pa),否则无法编译 return 0; }
5.函数指针数组
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 main() { //指针数组 int *arr[5]; int (*pa)(int,int)=Add; //因为Add,Sub,Div,Mul这几个函数的函数类型和返回值都相同,所以可以表示这几个函数的地址,那么怎么区分呢 //需要一个数组,这个数组可以存放四个函数的地址--函数指针的数组 int(*pa[4])(int,int)={Add,Sub,Mul,Div};//pa首先跟[]结果成为一个数组,数组有4个元素,每个元素的类型是函数指针 int i=0; for(i=0;i<4;i++) { printf("%d\n",parr[i](2,3)); //parr[i]可以不写解引用(*),直接传参 //依次调用这几个函数5,-1,6,0 } return 0; }
举例
//计算器 void menu() { printf("*******************\n"); printf("** 1.add 2.sub**\n"); printf("** 3.add 4.div**\n"); printf("** 0.exit **\n"); printf("*******************\n"); } 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 main() { int x=0; int y=0; do { menu(); printf("请选择:>"); scanf("%d",&input); switch(input) { case 1: printf("请输入两个操作数:>") scanf("%d %d",&x,&y); printf("%d\n",Add(x,y)); break; case 2: printf("请输入两个操作数:>") scanf("%d %d",&x,&y); printf("%d\n",Sub(x,y)); break; case 3: printf("请输入两个操作数:>") scanf("%d %d",&x,&y); printf("%d\n",Mul(x,y)); break; case 4: printf("请输入两个操作数:>") scanf("%d %d",&x,&y); printf("%d\n",Div(x,y)); break; case 0: printf("退出\n"); break; default: printf("选择错误!\n"); break; } }while(input); }
以上的代码冗余过多,改进后可写为
/计算器 void menu() { printf("*******************\n"); printf("** 1.add 2.sub**\n"); printf("** 3.add 4.div**\n"); printf("** 0.exit **\n"); printf("*******************\n"); } 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 main() { int x=0; int y=0; //pf是一个函数指针数组----转移表 int (*pf[5])(int,int)={0,Add,Sub,Mul,Div};//Add的下标刚好为1,Sub的下标刚好为2.... do { menu(); printf("请选择:>"); scanf("%d",&input); if(input>=1 && input<=4) { printf("请输入两个操作数:>") scanf("%d %d",&x,&y); int ret=pf[input](x,y);//接受返回值 printf("%d\n",ret); } else if(input==0) { printf("退出\n"); } else { printf("选择错误\n"); } }while(input); } 6.指向函数指针数组的指针 int Add(int x,int y) { return x+y; } int main() { int arr[10]={0}; int (*p)[10]=&arr;//取出数组的地址 int (*pf)(int,int);//函数指针 int (*pf[4])(int,int);//pf是一个数组-函数指针的数组 //ppf是一个指向[函数指针数组]的指针 int (*(*ppf)[4])(int,int)=&pf;//ppf是一个数组指针,指针指向的数组有4个元素 //指向的数组的每一个元素的类型是一个函数指针int (*)(int,int) return 0; }
7.回调函数
不断调用Calc(int(*pf)(int,int))
接收函数的地址,通过收到的地址,调用相应的函数,那么Calc函数可以实现不同的功能
//计算器 void menu() { printf("*******************\n"); printf("** 1.add 2.sub**\n"); printf("** 3.add 4.div**\n"); printf("** 0.exit **\n"); printf("*******************\n"); } 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 Calc(int (*pf)(int,int))//传函数名,就是函数地址(指针) { int x=0; int y=0; printf("请输入两个操作数:>") scanf("%d %d",&x,&y); printf("%d\n",pf(x,y)); } int main() { do { menu(); printf("请选择:>"); scanf("%d",&input); switch(input) { case 1: Calc(Add); break; case 2: Calc(Sub); break; case 3: Calc(Mul); break; case 4: printf("请输入两个操作数:>") scanf("%d %d",&x,&y); printf("%d\n",Div(x,y)); break; case 0: printf("退出\n"); break; default: printf("选择错误!\n"); break; } }while(input); }
回调函数最简单的示例
void print(char *str) { printf("hehe:%s",str); } void test(void (*p)(char *)) { printf("test\n"); p("bit"); //char类型的指针指向char类型的变量 } int main() { test(print); return 0; }
输出结果
test
hehe:bit
回调函数示例:冒泡排序
经典冒泡排序的代码不够通用
void bubble_sort(int arr[],int sz) { int i=0; for(i=0;i<sz-1;i++) { int j=0; for(j=0;j<sz-i-1;j++) { if(arr[j]>arr[j+1]) { int tmp=arr[j]; arr[j]=arr[i]; arr[j+1]=tmp; } } } } struct Stu { char name[20]; int age; }; int main() { int arr[10]={9,8,7,6,5,4,3,2,1,0}; int sz=sizeof(arr)/sizeof(arr[0]); struct Stu s[3]={{"张三",20}{"李四",30}{"王五",10}}; bubble_sort(arr,sz);//不能用简单的冒泡排序进行比较 int i=0; for(i=0;i<sz;i++) { printf("%d",arr[i]); } return 0; }
需要运用qsort函数处理更为复杂的冒泡排序
void qsort(void *base,//待排序数组的首元素
size_t num,//待排序数组的元素个数
size_t width,//待排序数组的每个元素的大小,单位是字节
int(*cmp)(const void *e1,const void *e2)//函数指针,比较两个元素的方法的函数的地址,这个函数需要自己实现
函数指针的两个参数,是待比较两个元素的地址
);
补充
void*:能够接受任意类型元素的指针
int main() { int a=10; int* pa=&a; char* pa=&a;//发出警报 void* p=&a;//正确,不发出警报 *p =0;//非法的间接寻址,void *指针是不能进行解引用操作的,因为不知道他是是什么类型的指针,不知道访问多少个字节 p++;//也报错,不知道向后迈几个字节 return 0; }
● 当第一个元素小于第二个元素时,返回一个小于0的数字
●当第一个元素等于第二个元素时,返回0
●当第一个元素大于第二个元素时,返回一个大于0的数字
运用上述知识,得到最终代码为:
#include<stdio.h> #include<stdlib.h>//qsort函数的头文件 #include<string.h> void bubble_sort(int arr[],int sz) { int i=0; for(i=0;i<sz-1;i++) { int j=0; for(j=0;j<sz-i-1;j++) { if(arr[j]>arr[j+1]) { int tmp=arr[j]; arr[j]=arr[i]; arr[j+1]=tmp; } } } } struct Stu { char name[20]; int age; }; /* void qsort(void *base, size_t num, size_t width, int(*cmp)(const void *e1,const void *e2)//比较 ); */ //*(int*)e1 强制类型转换为int *,进行解引用 int cmp_int(const void *e1,const void *e2)//比较两个整数值,e1和e2是要比较的两个参数的地址 { return *(int*)e1-*(int*)e2; } void test1() { int arr[10]={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); int i=0; for(i=0;i<sz;i++) { printf("%d",arr[i]); } } int cmp_float(const float* e1,const float* e2) { if(*(float*)e1==*(float*)e2) return 0; else if(*(float*)e1>*(float*)e2) return 1; else return -1; /* 也可以直接进行强制类型转换 return ((int)(*(float*)e1-*(float*)e2)); */ } void test2() { float f[]={9.0,8.0,7.0,6.0,5.0,4.0}; int sz=sizeof(f)/sizeof(f[0]); qsort(f,sz,sizeof(f[0]),cmp_float); int j=0; for(j=0;j<sz;j++) { printf("%f",f[j]); } } //按照年龄排序 cmp_stu_by_age(const void* e1,const void* e2) { return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;//强制转换为结构体指针 } com_stu_by_name(const void* e1,const void* e2) { //比较字符串,不能直接使用大于小于等于符号,应该用strcmp函数 return strcmp( ((struct Stu*)e1)->name ((struct Stu*)e2)->name ); //strcmp函数:第一个大于第二个,返回大于0数字,第一个小于第二个返回小于0数字,等于则返回0 //和qsort函数规则相同,可直接返回 } void test3() { struct Stu s[3]={{"张三",20}{"李四",30}{"王五",10}}; int sz=sizeof(s)/sizeof(s[0]); qsort(s,sz,sizeof(s[0]),cmp_stu_by_age); } void test4() { struct Stu s[3]={{"张三",20}{"李四",30}{"王五",10}}; int sz=sizeof(s)/sizeof(s[0]); qsort(s,sz,sizeof(s[0]),cmp_stu_by_name); } int main() { test1(); test2(); test3(); test4(); //bubble_sort(arr,sz);//不能用简单的冒泡排序进行比较 return 0; } 改造冒泡排序的数组 #include<stdio.h> Swap(char* buf1,char* buf2,int width)//交换width,由buf1和buf2指向的字符,交换每一对字节,两两对应 { int i=0; for(i=0;i<width;i++) { char tmp=*buf1; *buf1=*buf2; *buf2=tmp; buf1++; buf2++; } } //可以接收任意想要比较的两个元素 void bubble_sort(void* base,int sz,int width,int (*cmp)(void *e1,void *e2)) { int i=0; //趟数 for(int i=0;i<sz-1;i++) { //每一趟比较的对数 int j=0; for(j=0;i<sz-i-1;j++) { //两个元素的比较 if(cmp((char*)base+width*j,(char*)base+width*(j+1))>0) //cmp(base,base+1):错误,base是void*类型 //cmp((char*)base+width*j,(char*)base+width*(j+1)):char是一个字节,+width表示跳过宽度个字节 Swap((char*)base+width*j,(char*)base+width*(j+1),width); } } } int cmp_int(const void *e1,const void *e2)//比较两个整数值,e1和e2是要比较的两个参数的地址 { return *(int*)e1-*(int*)e2; } com_stu_by_name(const void* e1,const void* e2) { //比较字符串,不能直接使用大于小于等于符号,应该用strcmp函数 return strcmp( ((struct Stu*)e1)->name ((struct Stu*)e2)->name ); //strcmp函数:第一个大于第二个,返回大于0数字,第一个小于第二个返回小于0数字,等于则返回0 //和qsort函数规则相同,可直接返回 } void test() { int arr[10]={9,8,7,6,5,4,3,2,1,0}; int sz=sizeof(arr)/sizeof(arr[0]); //使用这个函数的程序员一定知道自己排序的是什么数据 //就应该知道如何排序待排序的元素 bubble_sort(arr,sz,sizeof(arr[0]),cmp_int);//根据地址调用相应的比较函数 } void test2() { struct Stu s[3]={{"张三",20}{"李四",30}{"王五",10}}; int sz=sizeof(s)/sizeof(s[0]); bubble_sort(s,sz,sizeof(s[0]),cmp_stu_by_name); } int main() { test(); test2(); return 0; }