系列文章目录
速通C语言系列
速通C语言第一站 一篇博客带你初识C语言 http://t.csdn.cn/N57xl
速通C语言第二站 一篇博客带你搞定分支循环 http://t.csdn.cn/Uwn7W
速通C语言第三站 一篇博客带你搞定函数 http://t.csdn.cn/bfrUM
速通C语言第四站 一篇博客带你学会数组 http://t.csdn.cn/Ol3lz
速通C语言第五站 一篇博客带你详解操作符 http://t.csdn.cn/OOUBr
速通C语言第六站 一篇博客带你掌握指针初阶 http://t.csdn.cn/7ykR0
速通C语言第七站 一篇博客带你掌握数据的存储 http://t.csdn.cn/qkerU
感谢佬们支持!
文章目录
只看目录,大家就能知道这篇博客有多高能了
前言
上一篇博客中我们了解了数据的存储,这篇博客是指针进阶,就是指针的天花板,很有难度,大家系好安全带准备上车啦~
一、字符指针
原来我们使用时为
char ch='q';
char*pc=ch; //单个字符
现在我们
char*ps="hello YiGang";
他可以把hello YiGang的首字符地址h存了,这波就像数组的访问形式一样
我们给到一个题:
char str1[] = "hello YiGang";
char str2[] = "hello YiGang";
if (str1 == str2)
{
printf("same\n");
}
else
{
printf("nope\n");
}
char*str3= "hello YiGang";
char* str4 = "hello YiGang";
if (str3 == str4)
{
printf("same\n");
}
else
{
printf("nope\n");
}
这个程序将输出什么呢?
为什么呢?
刚才我们讲过,字符指针可以存字符串首字符的地址,由于hello YiGang 是常量字符串,所以其首字符的地址是固定的,所以str3==str4.
而str1和str2分别为两个数组,所以字符串为各自存的元素,存的地址不同,所以其首字符的地址也不同。
二、指针数组
之前我们学的数组指针为
//每个元素为int*
int* arr[3];
还有:
int a = 10; int b = 20; int c = 30;
int* arr[3] = { &a,&b,&c };
显然,上面的写法很挫
我们可以将数组的地址存入数组
int a[5] = { 1,2,3,4,5 };
int b[5] = { 2,3,4,5,6 };
int c[5] = { 3,4,5,6,7 };
int* arr[3] = { a,b,c };
由于数组名是首元素的地址,所以我们直接将a b c存进去
由此,我们就模拟出了二维数组一样的东西,但是相较于二维数组,这个东西不连续
我们打印一下
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 5; ++j)
{
printf("%d ", *(arr[i] + j));// -> printf("%d ",arr[i][j]);
}
printf("\n");
}
成功打印
三、数组指针
是一种指针,类比整形指针是指向整形的指针,数组指针是指向数组的指针
例:
int arr[10] = { 1,2,3,4,5 };
int(*parr)[10] = &arr;
此处,parr就是一个数组指针,他存的是arr的地址,即arr[0]的地址
同理
double* d[5];//每个元素为double*
double*( (*pd) [5]) = &d;
那我们如何通过数组指针访问数组的每个元素?
例:
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*parr)[10] = &arr;
for (int i = 0; i < 10; i++)
{
printf("%d ", *( (*parr) + i));
}
即(*parr)拿到arr,然后通过arr+i 来访问每个元素。
(成功打印)
但是,相比直接打印,这种方法太恶心了。
数组指针放在二维数组中会好用很多
例:
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
我们写一个打印函数来打印他
//将首行的地址、行数、列数传为参数
Print(arr, 3, 5);
void Print(int arr[3][5], int row,int col)
{
for (int i = 0; i < row; ++i)
{
for (int j = 0; j < col; ++j)
{
printf("%d ", *((*arr+i)+j));
}
printf("\n");
}
}
arr为首行的地址,解引用拿到首行地址 后可以通过+i 来拿到 第二行、第三行的地址
拿到每行的地址后再通过 加 j来拿到每个元素。
画一波图
当然,我们也可以这么打印
printf("%d ",arr[i][j]);
四、数组传参和指针传参
写代码时难免要把数组/指针传给函数,那函数参数应该如何设计?
一维数组传参
假设我们要传一个一维数组
int arr[10] = { 0 };
test1(arr);
那么该用什么参数接收呢?
1
void test1(int arr[]) //彳亍?
彳亍,由于传过去的仅为首元素地址,我们可以不写 [ ] 中的10.
2
void test1(int arr[10]) //彳亍?
彳亍,写上10也可以
3
void test1(int *arr) //彳亍?
彳亍,传首元素地址,当然可以用一级指针接收
给一个指针数组的例子
int *arr2[20]={0};
test2(arr2);
那么这个又该用什么参数接收呢?
1
仿照刚才的操作
void test2(int *arr[20])
void test2(int *arr[])
这样肯定是可以的
2
那么该如何用指针接收呢?
这个指针数组的元素为一级指针,取一个一级指针的地址,我们应该用二级指针来接收,即
void test2(int **arr)
注:在我们学习数据结构的单链表时,将会用到这个操作
二维数组传参
int arr[3][5] = { 0 };
test3(arr);
传二维数组,传过去的为第一行的地址,所以我们的参数要匹配的是第一行的地址。
1
显然,我们依然可以无脑直接传
void test3(int arr[3][5])
2
当然,我们也可以省略传,但是这波有一个注意的点,就是行省列不省
什么意思呢?
就是传参的时候第一个 [ ] 的3可以省略不写,但第二个 [ ] 的5必须写。
为什么?
因为不写行只写列,即告诉编译器每列5个元素,他依然可以根据你的元素个数推演出你有几行,反之则不行
所以
void test3(int arr[][5])
//彳亍
3
那么该如何用指针接收呢?
由于我们传过去的为第一行的地址,而第一行可视为一个一维数组,所以我们需要的指针要指向一个一维数组。只想一个数组的指针就是我们上面学到的数组指针。即
void test3(int(*arr)[5])
//彳亍
一级指针传参
一级指针传参,我们要用一级指针接收
例:
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;//p是一级指针
Print(p, sz);
void Print(int *ptr,int sz)
{
int i = 0;
for (i = 0; i < sz; ++i)
{
printf("%d ", *ptr + i);
}
}
(成功打印)
那么假设函数的参数是一级指针,我们可以传什么过去呢?
1 一级指针
test(p);
2 数组
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
test(arr);
3 传地址
int a = 10;
test(&a);
二级指针传参
二级指针传参,当然也是二级指针接收
例:
int a = 10;
int *pa = &a;
int** ppa = &pa;
test(ppa);
void test(int** p2)
{
**p2 = 20;
}
那么假设函数的参数是二级指针,我们可以传什么过去呢?
1 二级指针
test(ppa);
2 一级指针的地址
test(&pa);
3 存一级指针的数组
int* arr[10] = { 0 };
test(arr);
五、函数指针
指向函数的指针 / 存放函数指针的指针
例:
我们先搞一个函数
int Add(int x, int y)
{
return x + y;
}
&Add即为Add函数的地址
printf("%p\n", &Add);
printf("%p\n", Add);
结果竟然一样!
这波其实很类似数组
啊但是!
虽然打印结果一样,但我们知道本质上arr和&arr不同
但是函数名是完全等于&函数名的
怎么写?
此时,pf就是一个函数指针
如何调用?
int ret = (*pf)(3, 5);
由于函数名是完全等于&函数名
我们可以直接
int ret = pf(3, 5);
两段有趣的代码
(*( (void ( * )()) 0)();
刚看到这行代码,我们的第一反应肯定是把电脑扔了,立即推放弃学C语言
先别急,别的我们不认识,0我们肯定认识
0前面有半个括号,前面我们找见剩下半个括号,中间的东西是
void (*) ()
好像就是我们刚刚才学过的函数指针,返回类型为 void,没有参数
既然括号里是函数指针,说明他想用括号离得东西对0强转,所以0变成了一个函数
再在外面加 * 解引用,就是对这个函数进行调用,由于是void 类型,所以调用时传不了参,所以在最后补一个 ()
总结
1 void(*)() 表示一个 没有参数的函数指针类型
2(void(*)())0 对0 强转,转换为一种函数的地址
3 *((void ( * )()0 再加一颗 *,即为进行解引用操作,也就是找到了这个函数
4 (*((void ( * )()0)(); 调用0地址的函数,由于函数没有参数,所以我们最后一个括号没东西。
void (*signal(int,void(*) (int))) (int);
这次我们很明显就能看出,外面为
void (* ~ ) (int);
显然,外面是一个函数指针
接下来我们看看里面是什么,signal 和()先结合,说明signal是函数名,通过里面的那个逗号,我们知道signal函数有两个参数,第一个是int,而第二个为
void (*) (int)
显然是一个函数指针,其返回类型为void,参数为int 。
总结
1 signal 和()先结合,说明signal是函数名
2 signal的第一个参数为int,第二个为一个函数指针,这个函数指针指向一个参数为int,返回类型为void的函数
3 signal的函数返回类型也是一个函数指针,该函数指针指向一个参数为int,返回类型为void的函数
4 由于最后为分号,所以这个东西是函数的声明。
六、函数指针数组
存放函数指针的数组
例:
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
//pf1就是一个指向函数的指针
int(*pf1)(int, int) = Add;
int(*pf2)(int, int) = Sub;
int (*pfArr[2])(int, int) = { Add,Sub };
由于Add和Sub返回类型、参数一样,所以同类型,可以存在一个数组里
怎么用?
我们实现一个对整形变量加减乘除的计算器
先把不重要的目录和加减函数的实现搞出来
//加
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("1 Add 2 Sub\n");
printf("3 Mul 4 Div\n");
printf("0 exit\n");
}
按我们之前的思路,实现计算器必然是用一个do~while循环,里面再用一个switch~case来分支不同运算。
但是每个运算都写个case实在是太冗余了
所以这个时候我们用一个函数指针来操作
int (*pfArr[5])(int, int) = { NULL,Add,Sub,Mul,Div };
(为了和目录匹配,我们在第一个元素补一个NULL)
nt main()
{
int input = 0;
do
{
//目录
menu();
//函数指针数组
int (*pfArr[5])(int, int) = { NULL,Add,Sub,Mul,Div };
printf("请输入\n");
//输入
scanf("%d", &input);
//操作数
int x = 0;
int y = 0;
scanf("%d %d", &x, &y);
//结果
int ret = 0;
ret = (pfArr[input])(x, y);
printf("%d", ret);
} while (input);
return 0;
}
这样,我们就可以通过下标来访问函数
例:
补充:在《C和指针》这本书中,称这种操作为转移表
函数指针数组的一个最经典的应用,就是C++中的虚函数表,这个等我们到继承多态的时候就能知道辣~
七、指向函数指针数组的指针
函数指针数组是个数组,所以我们可以取出他的地址
例:
我们先给一个函数指针数组
int(*parrf[4]) (int, int);
我们用p3取出他的地址
那我们该如何书写呢?在上面的基础上操作,由于parrf会结合 [ 4 ]成为数组,所以我们应该加一个括号不让parrf和 [ 4 ]结合,然后再补一个*即可
int (*(*p3)[4]) (int,int)=&parrf;
八、回调函数
回调函数就是一个通过函数指针调用的函数,如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其指向的函数时,我们就说这是回调函数
通俗来讲,我写了个A函数,但是我不直接调用!我把A函数的地址给了B函数,
然后我通过B函数来调用A函数,那B函数的形参就是函数指针,我们称A函数为回调函数
例:我们不用函数指针数组搞计算器了,继续用switch~case,那如何解决每个case的冗余问题?
写一个calc 函数来封装输入输出的操作,然后将4个运算函数的地址作为函数参数传入,每个case对应不同的运算函数。
比如:
case 1;
//加
ret = calc(Add);
printf("%d",ret);
break;
实现一波calc函数
int calc(int* (pf)(int x, int y))
{
int x, y = 0;
printf("输入两个数\n");
scanf("%d %d", &x, &y);
return pf(x, y);
}
尝试一波
(成功)
其他运算函数也同理,我们也将他们分装到4个case中,大家可以仿照上面自己敲一敲。
再例:qsort快速排序(库函数)
qsort可以排任何类型,整形、字符串,甚至结构体也可以。
第一个参数代表首元素地址,第二个为元素个数,第三个为一个元素为几个字节
那第四个参数是什么?
由于qsort可以排任何类型的数据,但是库函数的实现者怎么知道我们以什么方式比较?所以我们需要自己写两个元素的函数。
例:我们写给一个数组的例子
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);
针对 cmp_int 的实现
定义第一个数大于第二个数我们就返回 大于0的数
定义第一个数等于第二个数我们就返回 等于0的数
定义第一个数小于第二个数我们就返回 小于0的数
(升序:前减后 降序:后减前)
所以
int cmp_int(const void*e1,const void *e2)
{
return *(int*)e1 - *(int*)e2;
}
打印一波试试
for (int i = 0; i < 10; ++i)
{
printf("%d ", arr[i]);
}
(成功排序)
我们再举一个结构体的例子
先创、初始化一个结构体
struct stu
{
char name[20];
int age;
};
void test4()
{
struct stu s[] = { {"WenXiao",69}{"ZhangYiGang",20},
{"DiaoMao",18},{"ZhaoZhiMing",6} };
}
怎么排序?
我们可以按年龄排
实现一波比较函数
int sort_age(const void* e1, const void* e2)
{
return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}
排序:
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), sort_age);
运行后
我们还可以按名字首字母排
qsort(s, sz, sizeof(s[0]), sort_name);
比较字符串时,我们要用strcmp函数
int sort_name(const void* e1, const void* e2)
{
return strcmp (((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
运行一波
总结
做总结,今天这篇博客带大家上到了指针的天花板,各种抽象的套娃概念,大家要仔细看过
而指针的恶心题目也是面试题中喜欢出的,下篇博客我将给大家带来 指针练习题。
水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。