文章目录
- 导语:
- 思维导图:
- 1.字符指针
- 2.指针数组
- 3.数组指针
- 3.1 数组指针的定义
- 3.2 &数组名和数组名
- 3.3 数组指针的使用
- 4.数组参数、指针参数
- 4.1 一维数组传参
- 4.2 二维数组传参
- 4.3 一级指针传参
- 4.4 二级指针传参
- 5.函数指针
- 6.函数指针数组
- 7.指向函数指针数组的指针
- 8.回调函数
- 结语
导语:
之前有一篇文章给大家讲解了指针的概念(链接:指针的疑难杂症):
1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
4. 指针的运算。
那本篇文章,讲解深层次的指针概念。
思维导图:
1.字符指针
字符指针的一般用法:
int main()
{
char ch = 'q';
char* pc = &ch;
//*pc = 'q'
return 0;
}
还一种使用方法:
int main()
{
char* p = "abcdef";
return 0;
}
注意,在这里不是"abcdef"赋给了p,"abcdef"是占7个字节(包含\0),而我们知道,指针变量是4个字节,那p肯定是放不下的。
在这里,"abcdef"是一个常量字符串,当常量字符串出现在表达式里面的时候,其实这个字符串的值是首元素的地址。
既然刚刚说了"abcdef"是常量字符串,常量是不能修改的,那么上面的这种写法就是不够标准,应该改为:
int main()
{
const char* p = "abcdef";
return 0;
}
有了这个知识,我们来看一道例题:
#include <stdio.h>
int main()
{
char str1[] = "hello world";
char str2[] = "hello world";
const char* str3 = "hello world";
const char* str4 = "hello world";
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是指针,指向的是一个常量字符串,常量字符串不可被修改,在内存中只会开辟一个空间;
那么在比较的时候,str1和str2比较的是首元素地址,因为是2块空间,所以肯定不一样;str3和str4比较的是首元素地址,指向的是同一块空间。
2.指针数组
指针数组是存放指针的数组!
int main()
{
int* arr1[10]; //整型数组 存放整型的数组
char* arr2[10];//字符数组 存放字符的数组
char ch1[] = "qwe";
char ch2[] = "asd";
char ch3[] = "zxc";
char* arr3[3] = { ch1,ch2,ch3 }; //指针数组 存放指针的数组
//指针数组的访问
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%s\n", arr3[i]);
}
return 0;
}
3.数组指针
3.1 数组指针的定义
数组指针是数组?还是指针?
答案是:指针。
int main()
{
int* pi; //整型指针 -- 存放整型地址的指针 -- 指向整型的指针
char* pc;//字符指针 -- 存放字符地址的指针 -- 指向字符的指针
int arr[10];
int(*pa)[10] = &arr;//数组指针 -- 存放数组地址的指针 -- 指向数组的指针
return 0;
}
在这里,是需要区分这两端代码p1和p2到底代表着什么?
int main()
{
int* p1[10];
int(*p2)[10];
return 0;
}
首先要知道,[ ] 优先级是要高于 * 号。
int* p1[10] ,p1优先和数组结合,那么此时p1就是一个数组,里面存放的内容都是指针类型,所以p1是一个数组,里面存放的内容是指针的地址,叫指针数组。
int(*p2)[10],在这里 *号优先和p2结合,那么p2此时就是一个指针变量,指向的是一个大小为10的整型数组,所以p2是一个指针,指向一个数组,叫数组指针。
3.2 &数组名和数组名
我们知道,数组名就是数组首元素的地址,那么&数组名又是什么意思呢?
通过编译器,我们可以看出数组名和&数组名指向的都是首元素的地址,那他们的意义是一样的吗?
将他们的地址都+1,我们发现arr+1是跳过4个字节,而 ~ &arr+1是跳过40个字节。
那么我们就可以得出结论:
数组名是数组首元素的地址。
&数组名是整个数组的地址。
数组首元素地址虽然和整个数组的地址从值的角度来看,虽然是一样的,但是意义不相同。
这里还一点就是指针类型决定了指针+1到底偏移的是几,这里数组名和&数组名的类型是不一样的。
数组名的类型是 int*,首元素的地址也是int*
而&数组名类型是int(*)[10]
3.3 数组指针的使用
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* pa = arr;
int(*p)[10] = &arr;
int i = 0;
//一般访问
for (i = 0; i < 10; i++)
{
printf("%d ", *(pa + i));
}
printf("\n");
//数组指针使用
for (i = 0; i < 10; i++)
{
printf("%d ", (*p)[i]);
}
return 0;
}
但我们发现在这里使用数组指针特别的别扭,有点脱裤子放屁的感觉,很鸡肋。其实数组指针在一维数组运用比较少,在二维数组的运用会多一点。
int main()
{
int arr[3][4] = { {1,2,3,4 },{2,3,4,5 },{3,4,5,6} };
int(*p)[4] = &arr;
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", (*(p+i))[j]);
//printf("%d ", p[i][j]); 写出这种形式也行
}
printf("\n");
}
return 0;
}
既然了解了指针数组和数组指针的概念,那我们再来看看下面这段代码的意思:
int main()
{
int arr[5]; //整型数组 -- 元素是5个
int* parr1[10];//指针数组 -- 可存放10个整型指针
int(*parr2)[10];//数组指针 -- 指向一个数组 -- 该数组元素为10个,每个元素为整型
//[]的优先级高于*,使用parr3先和[]结合
//parr3是数组,数组有10个元素
//数组的每个元素是int(*)[5]的数组指针类型
int(*parr3[10])[5];
return 0;
}
4.数组参数、指针参数
在写代码的时候难免要把数组或者指针传给函数,那函数的参数该如何设计呢?
4.1 一维数组传参
#include <stdio.h>
void test(int arr[])//形参部分写出数组,可以不指定大小(√)
{}
void test(int arr[10])//形参部分是数组接收,10个元素,与传递过来的数组一致(√)
{}
void test(int* arr)//传过来的是数组名,数组名是首元素地址,用整型指针接收(√)
{}
void test2(int* arr[20])//形参与传递过来的保持一致(√)
{}
void test2(int** arr)//传的是数组名,首元素地址,传过来的类型是int*一级指针,用二级指针接收(√)
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr); //数组传参,传的数组名,也是首元素的地址
test2(arr2);//数组传参,传的数组名,也是首元素的地址
return 0;
}
4.2 二维数组传参
//数组形式
void test(int arr[3][5])//形参与传递过来的参数保持一致,标准写法 (√)
{}
void test(int arr[][])//二维数组行可以省略,但列不能省略,该处都省略了 (×)
{}
void test(int arr[][5])//二维数组可以省略,但列不能省略 (√)
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
//指针形式
void test(int* arr)//形参写成了整型一维数组的接收方式 (×)
{}
void test(int* arr[5])//形参是指针数组,是数组,但不是二维数组;能存指针,但不是指针 (×)
{}
void test(int(*arr)[5])//形参是数组指针,数组5个元素,每个元素是int类型(√)
{}
void test(int** arr)// 传过来的是首行的地址,而二级指针是接收一级指针的地址 (×)
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);//传的是二维数组
}
4.3 一级指针传参
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,传给函数
print(p, sz);
return 0;
}
那我们想一想,当一个函数的参数部分为一级指针的时候,函数能接收什么参数呢?
void test(int* p)
{}
int main()
{
int a = 10;
int* p = &a;
int arr[5] = { 1,2,3,4,5 };
test(&a);//整型变量的地址
test(p);//一级指针
test(arr);//一维数组的数组名
return 0;
}
4.4 二级指针传参
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;
int* arr[5];
int arr[3][3];
test(ptr);//二级指针
test(&pp);//一级指针的地址
test(arr);//指针数组的数组名
return 0;
}
5.函数指针
在上面我们讲了,数组指针是指向数组的指针;那么函数指针顾名思义就是指向函数的指针。
函数还有地址吗?我们通过编译器,看看是否能将函数的地址取出:
我们发现确实可以拿到函数的地址,那么函数的地址是否存起来呢?答案是肯定的,是地址,我们就可以存起来,语法规则如下:
int Add(int x,int y)
{
return x + y;
}
int main()
{
int(*pf)(int, int) = &Add;
//pf是存放一个函数地址的变量 -- 函数指针
//int(*pf)(int, int) = Add;
//&函数名和函数名都代表函数的地址,这里 没 有 区 别!
return 0;
}
函数指针与数组指针的书写十分类似
数组指针书写:
int arr[10];
int(*pa)[10] = &arr;
函数指针的书写:
int(*pf)(int, int) = &Add;
既然能拿到函数的地址,也能将函数的地址存起来,那是否能通过地址调用这块函数呢?答案也是肯定的。
通过调试,可以观察到确实可以通过pf指针去调用这个函数。
另外,既然&函数名和函数名一样,都代表着函数的地址,那么其实我们的写法可以更加简便一点:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pf)(int, int) = Add;
//Add赋给pf,那pf就和Add其实是一回事
//int ret = Add(1,2); //原先调用Add函数的写法
//那么pf也可以这样写
int ret = pf(1, 2);
printf("%d\n", ret);
return 0;
}
注意:(*pf)这里的 * 可以不写,但是如果要写上,一定要加上括号(*pf)
阅读(坑X)两段有趣的代码:
//代码1
(*(void (*)())0)();
该代码是一次函数调用;
首先将0先强制类型转换成一个函数指针类型,把0当成某个函数的地址,那个函数没有参数,返回类型为void;
然后*解引用调用函数,函数是无参的,所以最后面直接写括号,没有参数。
//代码2
void (*signal(int , void(*)(int)))(int);
该代码是一次函数声明;
声明的函数名字叫signal;
signal有2个参数,一个是int类型,一个是函数指针类型,该函数指针指向的函数参数为int,返回类型为void;
signal函数的返回类型是一个函数指针,该函数指针指向的函数参数为int,返回类型为void。
其实在这里,代码还是十分的难以理解,我们可以将其简化:
//将函数指针类型改名为pf_t
typedef void(*pf_t)(int);
//signal一个参数为int,另一个为pf_t,返回类型也是pf_t
pf_t signal(int, pf_t);
6.函数指针数组
函数指针数组,见名知意,是存放函数指针的数组,既然了解了函数指针的概念,那么这个写法,在函数指针的基础上再改造一下:
void test1()
{}
void test2()
{}
void test3()
{}
int main()
{
//函数指针
void(*pf1)() = &test1;
void(*pf2)() = &test2;
void(*pf3)() = &test3;
//函数指针数组
//pf先与[]结合,说明是一个数组,该数组里面存放的内容是函数指针类型
void(*pf[3])() = { pf1,pf2,pf3 };//存放了3个函数的地址
return 0;
}
函数指针数组的用途:转移表
我们在这里模拟一个简易计算器的程序,这是一般写法:
void menu()
{
printf("*********************\n");
printf("* 1.add 2.sub *\n");
printf("* 3.mul 4.div *\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()
{
menu();
int input = 0;
int a = 0;
int b = 0;
do
{
printf("请选择:");
scanf("%d", &input);
switch(input)
{
case 1:
printf("请输入两个操作数:");
scanf("%d %d", &a,&b);
printf("%d\n", add(a, b));
break;
case 2:
printf("请输入两个操作数:");
scanf("%d %d", &a, &b);
printf("%d\n", sub(a, b));
break;
case 3:
printf("请输入两个操作数:");
scanf("%d %d", &a, &b);
printf("%d\n", mul(a, b));
break;
case 4:
printf("请输入两个操作数:");
scanf("%d %d", &a, &b);
printf("%d\n", div(a, b));
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
这个写法其实是十分的冗余,我们了解函数指针数组之后,就可以简化很多:
void menu()
{
printf("*********************\n");
printf("* 1.add 2.sub *\n");
printf("* 3.mul 4.div *\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()
{
menu();
int input = 0;
int a = 0;
int b = 0;
//将函数的地址全部放入函数指针中去
int(*pf[5])(int, int) = { 0,add,sub,mul,div };
do
{
printf("请选择:");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
break;
}
else if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:");
scanf("%d %d", &a, &b);
int ret = pf[input](a,b);
printf("%d\n", ret);
}
else
{
printf("输入错误,重新输入\n");
}
} while (input);
return 0;
}
7.指向函数指针数组的指针
看到这个是不是感觉有点套娃的感觉,如果不是很好理解,其实先可以巧记,我们就看最后两个字是数组还是指针,指向函数指针数组的指针,它是一个指针。
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
将这段代码拆分就会更好理解一点,这里要一步一步的写,这样不容易出错,思路也会更清晰一点。
8.回调函数
前面的函数指针、函数指针数组的内容,其实基本上会运用于回调函数的实现。
回调函数的概念:
1.回调函数就是一个通过函数指针调用的函数。
2.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
3.回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
还是以简易计算器为例:
红框里面的代码,相似度非常高,逻辑也是十分的类似,那么其实可以将其封装为一个函数:
void menu()
{
printf("*********************\n");
printf("* 1.add 2.sub *\n");
printf("* 3.mul 4.div *\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 a = 0;
int b = 0;
printf("输入2个操作数:");
scanf("%d %d", &a, &b);
printf("%d", pf(a, b));
}
int main()
{
menu();
int input = 0;
do
{
printf("请选择:");
scanf("%d", &input);
switch(input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
回调逻辑:
在这里确实并没有直接去调用add函数,而是把函数的地址传递给另一个函数,然后在这个函数内部通过函数指针去调用add函数。
换言之,回调函数是一个中介,现实生活中,通过找中介办事,这样自己会轻松很多,但同时会被赚差价。那么运用到这里,就是代码的通用性更加高,但是调来调去,运行速度,自然会降低。
结语
~~
指针是C语言的重点内容,不要想的太复杂,也不要不敢面对,指针它就是个地址,没什么高科技,将内容梳理一下,采用零敲牛皮糖战术,一点一点的消化。
~~
那么本期方向就到这里,有帮助的话,三连支持一下,谢谢,再见!