指针就是地址,而凡是存储在内存中的值都会有属于自己的地址,指针指向地址,这样我们就能通过指针间接操作变量。我们在指针初阶中介绍了指针的基本概念:如指针大小、野指针问题、指针间的关系运算等,在我们的指针进阶中,将会对指针进行进一步剖析,见识更深的指针!🎊🎊
图片来源:新浪网
我们将在指针进阶中学习各种各样指针,比如字符指针、数组指针、函数指针等,这些指针种类虽多,但能力都很强大,作为进阶系列文章,涉及知识多多少少有点难度,但我们相信无论多么大的困难都无法阻挡我们的学习之路,因为每个人的潜力都是无限的,相信自己!
我们先从步长最短的字符型指针开始,字符指针就是用来存放字符变量(或字符串变量)的指针,当存储字符串变量时,会像数组一样只存入首字母的地址,然后在解引用时可以根据首地址依次往后访问并解引用,直到遇到结束标志 '\0' ,由此看来指针貌似和数组有点相似。
//字符指针
int main()
{
char a = 'X';
char* pa = &a;
char* pc = "Hello";//取出字符串首地址
printf("字符指针打印:%c\n", *pa);
printf("字符串指针打印:%s\n", pc);
return 0;
}
这题主要是考察两组数组名(内容完全相同)是否一致、两组字符指针(指向同一个字符串常量)是否一致的问题,下面是题解及源代码:
//字符指针笔试题
int main()
{
char arr1[] = { "Hello World" };
char arr2[] = { "Hello World" };
const char* str1 = "Hello World";
const char* str2 = "Hello World";
if (arr1 == arr2)
printf("arr1 and arr2 are same\n");
else
printf("arr1 and arr2 are not same\n");
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
return 0;
}
这两兄弟(其实没啥关系),虽然名字很像,但一个本质上是数组(指针数组),而另一个则是指针(数组指针)。 如果分不清楚也没关系,记住一个原则就行了:主语在后面,前面的都是形容词,再配合具体形式进行记忆即可。
指针数组是数组,是存放指针(地址)的数组,以前我们的数组中是放具体的值,而现在我们的数组中可以存放地址,好处有很多,节省空间就是一个大优势(因为在32位平台下,指针大小都是4字节),如果我们需要访问到具体元素,就需要下标+解引用操作符的配合了。
//指针数组
int main()
{
int a = 1, b = 2, c = 3;
int* pa = &a;
int* pb = &b;
int* pc = &c;
int* arr[3] = { pa,pb,pc };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("地址为:%p\n", arr[i]);
printf("具体值为:%d\n", *arr[i]);
}
return 0;
}
前面说过数组指针是指针,我们可以这样理解,把数组看成一队列的小货车,货车中装的就是我们的元素,领头车就是首元素地址,我们可以用一个数组指针指向这个数组(也就是首地址),我们可以通过数组指针对数组进行操作,再比如把数组比作麋鹿(圣诞老人的坐骑🎄),把圣诞老人当作我们的数组指针,手中的绳子对应着不同的数组,能对它们进行操作。可能有些抽象,但配合例子就好理解了。
//数组指针
int main()
{
int arr[10] = { 1,2,3 };
int ch[10] = { 0 };
int(*parr)[10] = &arr;
int(*pch)[10] = &ch;
printf("数组的地址:%p %p\n", arr,ch);
printf("数组指针指向的地址:%p %p\n", parr,pch);
return 0;
}
数组指针(&数组名)与数组名之间的区别: 两者最大区别就是操作权限(移动步长)不同,比如将数组名+1,会跳到下一个元素处,而&数组名+1会跳过整个数组,下面看看示例
//&数组名与数组名
int main()
{
int arr[5] = { 1,2,3,4,5 };
int(*pa)[5] = &arr;
printf("这是起始地址:%p %p\n", arr, pa);
printf("这是+1后的地址:%p %p\n", arr + 1, pa + 1);
return 0;
}
数组指针的应用场景:
我们的数组指针一般用来接收二维数组传参,而这种形参是唯二正确方法之一,当然还有一种很普通的形参形式,我们后面会介绍到,下面来看看示例
//数组指针的应用
void print(int(*pa)[3],int r,int c)//这里的3是列,不能少
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", *(*(pa + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][3] = { {1,2,3},{2,3,4},{3,4,5} };
print(arr,3,3);//把二维数组传过去
return 0;
}
既然提到了传参,我们就来好好总结一下各种数组和指针的传参方式吧!
//一维数组传参
void test1(int arr[])
{}//一维数组可以省略元素数
void test1(int arr[10])
{}//当然形参也可以写清楚
void test1(int*pa)
{}//用一级指针接收一维数组,合情合理
void test2(int*arr2[10])
{}//形参用指针数组接收指针数组传参
void test2(int**ppa)
{}//指针数组本质上是二级指针,这样也可以
int main()
{
int arr1[10] = { 0 };
int* arr2[10] = { 0 };
test1(arr1);
test2(arr2);
return 0;
}
//二维数组传参
void test(int arr[3][5])
{}//完整化接收
void test(int arr[][5])
{}//省略行接收,是可行的
void test(int(*pa)[5])
{}//用我们前面的数组指针接收
void test(int** pa)
{}//这种形式是错误的,不能使用
int main()
{
int arr[3][3] = { 0 };
test(arr);
return 0;
}
//一级指针传参
void test1(int*pa,int sz)
{}//传数组名,用指针接收
void test2(int*pa,int sz)
{}//传指针,用指针接收
int main()
{
int arr[3] = { 1,2,3 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* pa = arr;
test1(arr, sz);
test2(pa, sz);
return 0;
}
//二级指针传参
void test1(int**pa)
{}//接收的是二级指针
void test2(int**pa)
{}//接收的一级指针的地址
int main()
{
int a = 10;
int* pa = &a;//这是一个一级指针
int** ppa = &pa;//二级指针
test1(ppa);//直接传二级指针
test2(&pa);//把一级指针的地址取出来
return 0;
}
是的,我们函数也有指针,跟数组一样,函数名就是地址,不过函数名不必区分首地址等,因为一个函数名就只有一个地址,函数指针的形式也比较奇怪,需要多看看加深记忆。
函数指针由三部分组成:类型、指针、形参,类型和形参都允许为空,当我们想要调用函数时,只需要通过指针,并传入参数,就能正常使用函数。
//函数指针
int add(const int x, const int y)
{
return (x)+(y);
}
int main()
{
int (*pa)(const int x, const int y)=&add;
printf("%d\n", pa(2, 3));
return 0;
}
以下是两段比较复杂的代码,均用到了函数指针的知识。
代码一:
//代码一
int main()
{
(*(void(*)())0)();
return 0;
}
代码二:
//代码二
typedef void(*pfun_t)(int);
//此时 pfun_t == void(*)(int)
int main()
{
void (*signal(int, void(*)(int)))(int);
return 0;
}
前面已经提到过指针数组的概念了,本质上是一个数组,用来存放指针的数组,既然我们可以得到函数的地址,因此我们就可以将一些函数地址存入数组中,这样我们就得到了函数指针数组。
函数指针数组中的函数形式要一致,即形参要一致,返回类型也要一致,创建好函数指针数组后就可以把符合条件的函数地址存入数组中了,下面是使用示例:
//函数指针数组
int add(const int x, const int y)
{
return (x)+(y);
}
int sub(const int x, const int y)
{
return (x)-(y);
}
int main()
{
int(*pfun[2])(const int x, const int y) = { add,sub };
printf("add(2,3)=%d\n", pfun[0](2, 3));
printf("sub(2,3)=%d\n", pfun[1](2, 3));
return 0;
}
上面已经展示了加和减两个函数构成的函数指针数组,并成功运行,既然如此,我们可以制作一个简易整型计算器,将另外两个函数 乘与除也放进去,这样就不必要借助 switch 语句分通道进入,可以节省很多空间,下面是原码:
//简易整型计算器
#include<stdio.h>
void menu()
{
printf("***********************\n");
printf("****简易整型计算器*****\n");
printf("*****1.加 2.减********\n");
printf("*****3.乘 4.除********\n");
printf("*******0.退出**********\n");
printf("***********************\n");
}
int add(const int x, const int y)
{
return (x)+(y);
}
int sub(const int x, const int y)
{
return (x)-(y);
}
int mul(const int x, const int y)
{
return (x)*(y);
}
int div(const int x, const int y)
{
return (x)/(y);
}
int main()
{
int input = 1;
int(*calc[5])(const int x, const int y) = { 0,add,sub,mul,div };
//这里放0的原因是和菜单中的序号对应上
while (input)
{
menu();
printf("请输入你的选择:>");
scanf("%d", &input);
if (input > 0&&input < 5)
{
int x = 0, y = 0;
printf("请输入两个数:>");
scanf("%d %d", &x, &y);
printf("计算结果为%d\n", calc[input](x, y));
}
else if (input >= 5)
printf("选择错误,请重新选择!\n");
}
printf("退出计算器\n");
return 0;
}
函数指针数组要求比较多,一般是用于转移表中。
数组与指针间的套娃关系开始了,不用慌,直接看主语,是指针,即指向函数指针数组的指针,通过这个指针,就能找到函数指针数组,当然肯定也有函数指针数组指针数组。
//函数指针数组的指针
int add(int x, int y)
{
return x + y;
}
int main()
{
//这是函数指针数组
int (*pa[5])(int x, int y) = { add };
//这是函数指针数组的指针,需要取出地址
int(*(*ppa)[5])(int x, int y) = &pa;
printf("这是函数指针数组的指针%p\n", ppa);
printf("这是&函数指针数组后的地址%p\n", &pa);
return 0;
}
回调函数的特点是当特定的事件和条件发生时由另外一方调用目标函数,比如进网吧是一个函数,小明是一个主函数,只有当他满18岁时才会发生这件事,回调函数可以这样理解。
这是一个库函数,头文件是 stdlib,这个库函数的使用方法在下面,qsort函数可以进行各种数据的排序,无论是整型、字符型还是浮点型,它都能完成排序任务。
关于qsort中比较函数的返回值
//qsort
#include<stdio.h>
#include<stdlib.h>
int cmp(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
int main()
{
int arr[5] = { 7,3,9,4,6 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp);
int i = 0;
for(i = 0; i < sz; i++)
printf("%d ", arr[i]);
return 0;
}
当然qsort还可以用于其他数据的排序,修改下cmp比较函数就行了。
下面是我写的qsort对各种数据的排序程序,其中的比较函数是关键,可以着重阅读。
//练习使用qsort
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
struct stu
{
char name[10];
int age;
};
int cmp_c(const void* e1, const void* e2)
{
//这是字符型的比较函数
return strcmp((char*)e1, (char*)e2);
}
int cmp_d(const void* e1, const void* e2)
{
//这是整型的比较函数
return *(int*)e1 - *(int*)e2;
}
int cmp_f(const void* e1, const void* e2)
{
//这是浮点型的比较函数
return (int)(*(float*)e1 - *(float*)e2);
}
int cmp_str(const void* e1, const void* e2)
{
//这是结构体型的比较函数
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
int main()
{
int arr_d[] = { 3,7,8,5,2,1,4,6 };
int sz_d = sizeof(arr_d) / sizeof(arr_d[0]);//整型部分
float arr_f[] = { 3.2f,7.6f,8.7f,5.4f,2.1f,1.0f,4.3f,6.5f };
int sz_f = sizeof(arr_f) / sizeof(arr_f[0]);//浮点型部分
char arr_c[] = { "hgfedcba" };
int sz_c = strlen(arr_c);//字符型部分
struct stu s[3] = { {"张三",20 },{"李四",30},{"王二",35} };
int sz_str = sizeof(s) / sizeof(s[0]);//结构体型部分
qsort(arr_c, sz_c, sizeof(arr_c[0]), cmp_c);
qsort(arr_d, sz_d, sizeof(arr_d[0]), cmp_d);
qsort(arr_f, sz_f, sizeof(arr_f[0]), cmp_f);
qsort(s, sz_str, sizeof(s[0]), cmp_str);
int i = 0;
printf("整型排序\n");
for (i = 0; i < sz_d; i++)
printf("%d ", arr_d[i]);
printf("\n浮点型排序\n");
for (i = 0; i < sz_f; i++)
printf("%.2f ", arr_f[i]);
printf("\n字符型排序\n");
for (i = 0; i < sz_c; i++)
printf("%c ", arr_c[i]);
printf("\n结构体型排序\n");
for (i = 0; i < sz_str; i++)
printf("%s %d\n", s[i].name, s[i].age);
return 0;
}
qsort函数中就用到了回调函数的知识,当我们每次使用qsort,它都会去调用比较函数。
我们之前介绍过冒泡排序的相关知识,但是我们当时的冒泡排序只能用于整型数组的排序,我们可以模仿qsort函数,插入比较、交换函数,做一个通用的冒泡排序。
//通用冒泡排序
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
int cmp(const void* buf1, const void* buf2)
{
assert(buf1 && buf2);//断言
return strcmp((char*)buf1, (char*)buf2);
//字符比较需要用到strcmp函数
//其返回值与qsort的返回值吻合
}
void swap(char* buf1, char* buf2, int width)
{
//这里接收时直接用字符型指针接收,合情合理
assert(buf1 && buf2);
int i = 0;
for (i = 0; i < width; i++)
{
//我们需要进行逐渐字节操作,这样能保证通
//用性,因为无论什么类型,基本单位是字节
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;//每次交换完就往后偏移
buf2++;//寻找下一个字节交换
}
}
void bubble_sort_gen(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2))
{
assert(base && cmp);//断言
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)
{
//判断条件利用一个比较函数,同样利用1字节的巧妙关系,访问相邻元素
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
//专门的交换函数,逐字节交换,适用于所以类型
}
}
}
}
void print(char* pa, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
printf("%c ", *(pa + i));
printf("\n");
//一个普通的打印函数
}
int main()
{
char arr[] = { "qwertyuiopasdfghjklzxcvbnm" };
//把键盘上所有字符敲进去
int sz = strlen(arr);//获取长度
bubble_sort_gen(arr, sz, sizeof(arr[0]), cmp);
//同样的模仿qsort函数传参
print(arr, sz);//打印函数
return 0;
}
到这里指针进阶的基本内容已经介绍完了,从不同类型的指针到回调函数的空指针,我们见识到了属于指针的世界,这个能访问到底层地址小玩意,具有无限潜力,只要指针玩的够六,那么C语言就属于精通级别了。当然指针进阶还有很多联系等着我们去挑战,我们的目标很简单——征服C指针,然后去实现我们的梦想!