图解:
通过访问数组指针的时候,把
[ ]当成运算符:
parr[i][j]等价于*(*(parr + i) + j)
🐾数组指针
🌅数组指针的定义
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。浮点型指针: float * pf;能够指向浮点型数据的指针。
打个比方:好孩子,主语应该是孩子,所以通过类比我们知道
数组指针:能够指向数组的指针
为了方便我们理解一个复杂的指针类型,我总结出一个小法宝,屡试不爽。
一个复杂的指针类型通常是由多个运算符组合 而成,我们首要目的就是要捋清楚运算符的优先级,哪个先哪个后,那指针的类型不就手到擒来吗?
指针里的运算符主要有三种,它们的优先级是:
()> [ ] > *变量首次与
*结合,就会变成指针,与[ ]结合就会变成数组。可以结合
()变成函数,也可以通过()来改变优先级。
接下来,我们实战一下吧
int\* p[10];
int(\*p)[10];
int p(int);
Int (\*p)(int);
int \*(\*p(int))[3];
//上述的p分别是什么?
解释
int* p1[10]
解释:p1和[ ]先结合,说明p1是个数组名,数组有十个元素,每个元素是int*。所以p1是一个数组,里面存放的是指针,叫整型指针数组
int (*p)[10]
解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫整型数组指针。这里要注意:
[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
int p(int)
解释:从P 处起,先与( )结合,说明P 是一个函数,然后进入( )里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明则是参数为整型,返回值是整型的函数
Int (*p)(int)
解释:P 先与*结合,说明P 是一个指针,然后与( )结合,说明指针指向的是一个函数,函数的参数是int整型,返回值也是int,所以P 是一个指向有一个整型参数且返回类型为整型的函数指针
int *(*p(int))[3]
解释:P先与( )结合,说明P 是一个参数为int的函数,然后与*结合,说明函数返回的是一个指针,再与[ ]结合,说明返回的指针指向的是一个数组,再与*结合,说明数组里的元素是指针,最后与int 结合,说明指针指向的内容是整型数据。
所以P 是一个参数为int型且返回一个指向由整型指针组成的数组指针函数.
🌅浅探究指针
指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四个方面,下面我们继续探究🛫
🌳指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:
int\*ptr;//指针的类型是int\*
char\*ptr;//指针的类型是char\*
int\*\*ptr;//指针的类型是int\*\*
int(\*ptr)[3];//指针的类型是int(\*)[3]
int\*(\*ptr)[4];//指针的类型是int\*(\*)[4]
怎么样?找出指针的类型的方法是不是很简单🥸?
🌳指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的,指针名字和名字左边的指针声明符*去掉剩下的就是指针所指向的类型。例如:
int\*ptr; //指针所指向的类型是int
char\*ptr; //指针所指向的的类型是char
int\*\*ptr; //指针所指向的的类型是int\*
int(\*ptr)[3]; //指针所指向的的类型是int()[3]
int\*(\*ptr)[4]; //指针所指向的的类型是int\*()[4]
我们找到规律:
二者通过加减*可以互推
🌳指针的值
指针的值:指针里存放的地址
int a = 8;
int \*p = &a;
\*p = 0;
printf("%d\n",a);
指针的值:p本身的值,p里存放这变量a的内存的起始地址。
指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?(重点注意)
🌳指针占据的大小
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。
32位平台下占4个字节,64位平台占8个字节
🌅&数组名VS数组名
int arr[10];
arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥? 我们看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0; }
结果如下:
可见数组名和&数组名打印的地址是一样的。
难道两个是一样的吗?
我们再看一段代码:
#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; }
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型
数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.
通常情况下,数组名都是数组首元素的地址
🌍敲黑板~重点:有两个例外
① sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
② &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
③除这两种外,数组名都是数组首元素的地址。
🌅数组指针的使用
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
void print1(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//形参写成指针的形式
void print2(int \*arr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", \*(arr+i));
}
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]);
print1(arr, sz);
print2(arr, sz);
return 0;
}
结果如下👇🏻:
当我们把数组传给函数时,有两种方法:👇🏻
int arr[ ]数组接收,其本质也是指针,因为编译器会把数组转化成指针!指针接收,接收的数组名是首元素的地址。
当我们用数组指针来接收:👇🏻
void print1(int(\*p)[10], int sz)
{
int i = 0;
for (i = 0; i < 10; i++)
{
//\*p 相当于数组名,数组名又是首元素的地址,所以\*p就是&arr[0]
printf("%d ", \*((\*p) + i));
}
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]);
print1(&arr, sz);
return 0;
}
但是我很不推荐用这种方法,这种写法比较别扭, 有一种脱裤子放屁、画蛇添足的感觉。
所以数组指针很少应用在一维数组上
我们通常会用数组指针来接收二维数组👇🏻
void print\_arr1(int arr[3][5], int row, int col) {
int i = 0;
for (i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print\_arr2(int(\*arr)[5], int row, int col) {
int i = 0;
for (i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
//arr+i是指向第i行的
//\*(arr+i)相当于拿到了第i行,也相当于第i行的数组名
//数组名表示首元素的地址,(\*arr+i)也就是第i行第一个元素的地址
//printf("%d ", (\*(arr+i)+j));
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5, 6,7,8,9,10, 11,12,13,14,15 };
print\_arr1(arr, 3, 5);
print\_arr2(arr, 3, 5);
return 0;
}
数组名arr,表示首元素的地址
但是二维数组的首元素是二维数组的第一行
所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
可以数组指针来接收
结果如下:👇🏻
所以最优解就是:使用数组指针来接收二维数组
📜 小练习
学习完了指针数组和数组指针,我们来一起回顾一下并看看下面代码的意思:👇🏻
int arr[5];
int \*parr1[10];
int (\*parr2)[10];
int (\*parr3[10])[5];
arr是一个整型数组,每个元素是
int类型的,有5个元素
.
parr1 首先和[ ]结合,所以是一个数组,数组10个元素,每个元素的类型是int*
.
parr2 首先和*结合,所以是一个指针, 指向的数组有10个元素,每个元素的类型是int
.
parr3 首先和[ ],所以是一个数组,数组有10个元素,每个元素的类型是:int(*)[5]
parr3 是存放数组指针的数组
🐾数组参数和指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
🌅 一维数组传参
判断以下接收方式“河里”吗?👇🏻
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int arr[100])//ok?
{}
void test(int \*arr)//ok?
{}
void test2(int \*arr[20])//ok?
{}
void test2(int \*\*arr)//ok?
{}
int main()
{
int arr[10] = {0};
int \*arr2[20] = {0};
test(arr);
test2(arr2);
}
以上方式都合理,接下来我们逐一分析一波:
形参写成数组形式:形参部分的数组大小可以省略
形参写成指针形式:要注意传的元素的类型,若传的是一级指针,则用二级指针来接收
🌅 二维数组传参
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
分析如下:
那我们是不是有疑问说:为什么可以省略二维数组的行数,但不能省略列数❓
答:二维数组在内存中的地址排列方式是按行排列的连续存放,第一行排列完之后再排列第二行,以此类推
如果我们不知道列数,也就不知道一行能放多少个。
只要知道了列数,放完后就会知道放了多少行
继续判断以下接收方式“河里”吗?👇🏻
void test(int \*arr)//ok?
{}
void test(int\* arr[5])//ok?
{}
void test(int (\*arr)[5])//ok?
{}
void test(int \*\*arr)//ok?
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
分析如下:
总结:
二维数组传参,函数形参的设计只能省略第一个[]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。形参类型要相匹配:指向数组首行的地址,类型要相匹配
开个小灶:
int arr[10];
test(arr);
🤔形参为数组时,其本质还是指针 !编译器会把数组转化成指针
🌅 一级指针传参
#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,传给函数
print(p, sz);
return 0; }
一级指针传参,形参可以用
一级指针接收,也可以用数组接收(但不推荐)
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
比如:
void test1(int\* p)//test1函数能接收什么参数?
{
//...
}
int main()
{
int a = 10;
int\* p = &a;
int arr[10];
test1(arr);
test1(&a);
test1(p);
return 0;
}
分析如下:
形参为一级指针,实参可以是
数组,也可以是一级指针(地址)
🌅 二级指针传参
void test(int\*\* ptr) //二级指针接收
{
printf("num = %d\n", \*\*ptr);
}
int main()
{
int n = 10;
int\* p = &n;
int\*\* pp = &p;//ppa是一个二级指针
test(pp);//二级指针
test(&p);//取地址一级指针,类型为二级指针
return 0;
}
二级指针传参时,形参最好用二级指针来接收(指针数组不推荐)
思考:
当函数的参数为二级指针的时候,可以接收什么参数?
比如:
void test(char\*\* p) {
}
int main()
{
char c = 'b';
char\* pc = &c;
char\*\* ppc = &pc;
char\* arr[10];
test(&pc);//取地址一级指针,类型是二级指针
test(ppc);//传的是二级指针
test(arr);//Ok? ok 因为arr数组名是首元素,char\*的地址——类型为char\*\*
return 0;
}
提问:如果arr2是个二维数组,可以吗?
不可以,要写成char(*p)[5]才可以。
形参为二级指针时,传参可以是二级指针,也可以是数组指针首元素的地址
🐾函数指针
🌅 函数指针的定义
数组指针:是指向数组的指针。
函数指针:类比可知是指向函数的指针,存放函数地址的指针。
int Add(int x, int y)
{
return x + y;
}
int main()
{
int arr[10];
int(\*p)[10] = &arr;//p是一个数组指针变量
printf("%p\n", &Add);
printf("%p\n", Add);
}
我们可以得知:函数名 == &函数名(完全等价)
都可以用来表示函数地址
数组名 != &数组名(完全不同)
函数名 == &函数名(完全等价)
🌅 函数指针的类型
了解了函数指针的定义,那么函数指针类型该怎么样写呢?
int (\*pf)(int x, int y) = &Add;//函数指针变量
分析:
接下来,我们试一下这个:
int test(char* str)的函数指针咋写?
int test(char\* str)
{}
int main()
{
int (\*pf)(char\*) = &test;
}
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (\*pfun1)();
void \*pfun2();
首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:
pfun1可以存放。pfun1先和
*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
阅读两段有趣的代码:
//代码1
(\*(void (\*)())0)();
//代码2
void (\*signal(int , void(\*)(int)))(int);
我们慢慢把代码逐层剖析:
代码1: ①:首先是把0强制类型转换成一个函数指针类型,这就意味着0地址处放着一个返回类型是void,无参的一个函数
②:调用0地址处的这个函数
上面的代码是不是太废眼睛了呢?我们接下来用重定义typedef简化一下吧:
typedef void(\*pf\_t)(int);
//给函数指针类型void(\*)(int)重新起名叫:pf\_t
pf\_t signal(int, pf\_t);
//替换后等效于void (\*signal(int, void(\*)(int)))(int)
注 :推荐《C陷阱和缺陷》,这本书中提及这两个代码。
🐾函数指针数组
🌅 函数指针数组的定义
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,
比如:
int \*arr[10];
//数组的每个元素是int\*
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (\*parr1[10])();
int \*parr2[10]();
int (\*)() parr3[10];
答案是:parr1
parr1 先和[ ]结合,说明 parr1是数组,数组的内容是什么呢?
是int (*)( )类型的函数指针。
函数指针数组的用途:转移表
🌅 函数指针数组的实现
例子:(计算器)
void menu()
{
printf("\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\n");
printf("\*\* 1. add 2. sub \*\*\n");
printf("\*\* 3. mul 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 input = 0;
do{
menu();
int x = 0;
int y = 0;
int ret = 0;
printf("请选择:> ");
scanf("%d", &input);
printf("请输入2个操作数:> ");
scanf("%d %d", &x, &y);
switch (input)
{
case 1:
ret = Add(x, y);
break;
case 2:
ret = Div(x, y);
break;
case 3:
ret = Mul(x, y);
break;
case 4:
ret = Div(x, y);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("重新选择\n");
break;
}
printf("ret = %d\n", ret);
} while (input);
return 0;
}
我在测试中发现了,有bug!
当input等于0和5的时候,发现程序出错了
接下来我们对此进行修改:
把以下代码加入到switch语句的case中:
printf("请输入2个操作数:> ");
scanf("%d %d", &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);
switch (input)
{
case 1:
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = Add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = Sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = Mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = Div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
但是我觉得还不够好:
代码冗余,出现了多次的printf 、scanf(每个case中都有)
可读性低
对此我们利用函数数组指针来优化,通过函数数组的下标来进行跳转到目标函数:
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;
//转移表
int (\*pfArr[])(int, int) = {0, Add, Sub, Mul, Div};
//pfArr就是函数数组指针
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
}
else if(input >= 1 && input<=4)
{
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = pfArr[input](x, y);
printf("ret = %d\n", ret);
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}
上面应用了函数指针数组,通过函数数组的下标来进行跳转到对应的目标函数
我们把这种函数指针称为转移表
🐾指向函数指针数组的指针
指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针 ;
如何定义?
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;
}
🐾回调函数
🌅回调函数的定义
回调函数就是一个通过函数指针调用的函数
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
举个简单的例子:
void test()
{
printf("hehe\n");
}
void print\_hehe(void (\*p)())//函数指针接收,该形参与test函数类型相同
{
if (1)
p();
}
int main()
{
print\_hehe(test);
return 0;
}
图例分析如下:
把
test的地址传给了print_hehe函数,通过print_hehe函数来调用test函数,所以我们称print_hehe函数为回调函数
🌅回调函数的应用
我们拿switch版本的计算机来实践:
void menu()
{
printf("\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\n");
printf("\*\* 1. add 2. sub \*\*\n");
printf("\*\* 3. mul 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("请输入2个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", pf(x, y));
}
int main()
{
int input = 0;
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:
Calc(Div);
break;
case 0:
printf("退出\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
把冗余的代码封装成一个
Calc函数。
把输入的函数地址传给Calc,Calc通过传入的地址,从而找到了目标函数
🐾sqort函数
🌅sqort函数的定义
sqort是一个包含在**<stdlib.h>** 头文件下的库函数,主要根据一定的比较条件进行快速排序。
也可以对所有类型的数据进行排序,一个函数解决所有类型的排序问题,不需要根据不同的类型些不同的函数,提高效率!🔥
接下来我们来回顾一下冒泡排序:
void bubble\_sort(int arr[], int sz)
{
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-1-i; j++)//一次的冒泡对换
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}



**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://gitee.com/vip204888)**