当前位置:网站首页>【C语言-指针进阶】挖掘指针更深一层的知识
【C语言-指针进阶】挖掘指针更深一层的知识
2022-06-11 08:46:00 【BaconZzz】
前言
这篇文章的知识由初阶指针发散,提及更多指针的概念和基础用法,干货超多!快去接杯水!
1. 字符指针
提一种容易犯错的用法:
int main()
{
const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}
把字符串放到指针里的操作是:把字符串首字符地址放到指针
#include <stdio.h>
int main()
{
char arr1[] = "bacon";
char arr2[] = "bacon";
const char* arr1 = "bacon";
const char* arr2 = "bacon";
if (p1 == p2)
printf("p1 and p2 are same\n");
else
printf("p1 and p2 are not same\n");
if (arr1 == arr2)
printf("arr1 and arr2 are same\n");
else
printf("arr1 and arr2 are not same\n");
return 0;
}
结果:
p1 and p2 are same
arr1 and arr2 are not same
解析:p1和p2都存放在 只读数据区,两个相同的常量字符串不重复存储;arr1 和 arr2则分别在栈区创建自己的内存空间。abcdef 是常量字符串,const防止常量字符串被修改。
2. 指针数组
提及定义和用法:
2.1 指针数组的定义
指针数组:存放指针的数组
基本形式:
int* arr1[10];
type* 数组名[n]
2.2 指针数组的用法
- 指针数组用法之一:组合大法!
把不同位置的一维数组组合成二维数组
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 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
//printf("%d ", parr[i][j]);
printf("%d ", *(parr[i] + j));
}
printf("\n");
}
return 0;
}
3. 数组指针
提及定义、数组名和用法:
3.1 数组指针的定义
数组指针:指向数组的指针
来做一个区分
int *p1[10];//指针数组
int (*p2)[10];//数组指针
解析:这里涉及到优先级的问题,[ ] 的优先级高于 *
p1和 [ ] 结合,前面的 int* 是数组元素的类型
p2和 * 结合,代表p2是一个指针,指向的对象类型是:元素个数为10的整形数组
3.2 数组名的事儿
对于
int arr[10];
有这样的说法:
arr 作为数组名,代表首元素的地址
&arr 取出的是整个数组的地址
看看区别
int main()
{
int arr[10] = {
0 };
printf("%p\n", arr);//首元素地址
printf("%p\n", &arr);//整个数组地址
return 0;
}
结果:
005CF86C
005CF86C
可以看到它们的地址一样。
但即便数值是一样的,意义却不同
int main()
{
int arr[10] = {
0 };
printf("%p\n", arr);//首元素地址
printf("%p\n", &arr);//整个数组地址
printf("%p\n", arr+1);//首元素+1地址
printf("%p\n", &arr+1);//整个数组+1地址
return 0;
}
结果:
0318F61C
0318F61C
0318F620
0318F644
arr+1 跳过了4个字节(一个元素)
&arr+1 跳过了40个字节(整个数组)
这也印证了我上文的说法
3.3 数组指针的用法
直接看实例
void print(int(*p)[5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; 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} };
print(arr, 3, 5);
return 0;
}
解析:这里用一个一维数组指针来接受二维数组的数组名(第一行的地址)。相当于,p接收了第一行的地址;p+1,就是第二行。而*(p+1)是每一行一维数组的数组名,又表示首元素地址,给它+1,*(p+1)+1,相当于第二行的第二个元素
3.4 分辨指针数组和数组指针
仔细分析,才能体味到…
int arr[5];//整形数组
int *parr1[10];//存放整形指针的数组
int (*parr2)[10];//指向 有5个元素的整形数组 的指针
int (*parr3[10])[5];//存放 5个指向整型数组的数组指针 的数组
!!!
我有个办法:根据优先级,拿掉 变量名/数组名 ,剩下的就是类型了
!!!
int arr[5];
int [5] 就是类型
int *parr1[10];
int *[10]就是类型
int (*parr2)[10];
int (*)[10]就是类型
int (*parr3[10])[5];
int (*)[5]就是类型
4. 形参:数组和指针
数组和指针是怎样传参的?
…
4.1 一维数组传参
看实例
//一维数组作实参,用一维数组作形参接收,没问题
void test1(int arr[])
{
}
//数组名的本质是首元素地址,拿一个指针接收首元素地址,没问题
void test1(int* parr)
{
}
//一维数组作实参,却用指针数组接收,错
void test2(int* arr[])
{
}
//一维数组作实参,却用二级指针接收
//二级指针是用来存放一级指针变量的地址的,错
void test2(int** arr)
{
}
int main()
{
int arr1[10] = {
0 };
int* arr2[10] = {
0 };
test1(arr1);
test2(arr2);
return 0;
}
4.2 二维数组传参
看实例
二维数组做实参,形参的设计只能省略第一个[ ]的数字
//错
void test(int arr[][])
{
}
//错
void test(int arr[3][])
{
}
//对
void test(int arr[][5])
{
}
//--------------------------------------------
二维数组的数组名,表示的是第一行(一维数组)的地址
//不能简单地用整型指针接收 ,错
void test(int* arr)
{
}
二维数组做实参,却用整型指针数组接收, 错
void test(int* arr[5])
{
}
二维数组做实参,用数组指针接收数组名(第一行的地址),arr可以指向每一行
也就是二维数组的每一个元素,没问题
void test(int(*arr)[5])
{
}
二维数组做实参,却用二级指针接收
二级指针是用来存放一级指针变量的地址的,错
void test(int** arr)
{
}
int main()
{
int arr[3][5] = {
0 };
test(arr);
return 0;
}
4.3 一级指针传参
看实例:
- 一级指针传参实例
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[10] = {
1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
return 0;
}
适时的逆向思维,能让我们更进一步
当一个函数的参数部分是 一级指针,函数能接收什么参数?
void print(int* p)
{
}
int main()
{
int a = 0;
int* pa = &a;
int arr[10] = {
0 };
print(&a);
print(pa);
print(arr);
return 0;
}
当函数的参数为二级指针的时候,可以接收什么参数?
void print(int** p)
{
}
int main()
{
int a = 0;
int* pa = &a;
int** ppa = &pa;
print(ppa);
print(&pa);
return 0;
}
总结:只要传过去的实参本质上是地址/一级指针变量的地址,就没问题
5. 函数指针
先了解一下:
对于函数来说, &函数名 和 函数名 ,都是函数的地址
int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", Add);
printf("%p\n", &Add);
return 0;
}
结果:
00ED13B6
00ED13B6
5.1 函数指针的定义
*返回类型 (指针名)(形参类型,形参类型,…)
int(*pf)(int, int)
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = &Add;
//int ret = (*pf)(10, 20);
int ret = pf(10, 20);
printf("%d\n", ret);
return 0;
}
结果:
30
5.2 函数指针的用法
int Add(int x, int y)
{
return x + y;
}
int calc(int(*pf)(int, int), int x, int y)
{
return pf(x, y);
}
int main()
{
int ret = calc(Add, 3, 5);
printf("%d\n", ret);
return 0;
}
!!!
函数指针的意义:道理和普通指针一样,可以通过指针调用函数,也就是说,可以用函数作实参
!!!
6. 函数指针数组
6.1 函数指针数组的定义
定义一个存放10个函数指针的数组,每个函数指针指向的函数返回类型为int, 无参数
int(*parr[10])()
- 如何理解?
parr 先和 [ ] 结合,我们再拿掉 parr[10] ,就得到数组的类型: int(*)() - 函数指针
6.2 函数指针数组的用法
用法之一:转移表
=================转移表===============
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;
int x = 0;
int y = 0;
int ret = 0;
int (*pfarr[])(int, int) = {
Add,Sub,Mul,Div };
do
{
menu();
printf("请选择:");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器......\n");
break;
}
else if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:");
scanf("%d%d", &x, &y);
ret = pfarr[input-1](x, y);//通过input来访问具体函数
printf("%d\n", ret);
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}
函数指针数组在这省去了冗余的代码,不信来看看普通实现:
int main()
{
int input = 0;
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);
return 0;
}
7. 指向函数指针数组的指针
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pf)(int, int) = Add;//函数指针
int(*parr[1])(int, int) = {
pf };//函数指针数组
int(*(*pparr)[1])(int, int) = &parr;//指向函数指针数组的指针
return 0;
}
我算是找到规律了:
- balabala指针:把指针变量和*括起来 —— bala (*p) bala
- balabal数组: 让数组名和 [ ] 挨在一起 —— bala arr[10] bala
8. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
- 不是由实现方调用
- 由函数指针调用
库函数 qsort 的使用,就用到了回调函数的知识
并且 qosrt 的设计逻辑,有很多值得学习的地方
由此,咱们来研究研究 qsort !
8.1 qsort
要研究一个函数,不得先研究函数的 使用场景、返回类型、参数 嘛!
- 使用场景:也许研究了才知道?
- 返回类型:void,人家就是排个序嘛
- 参数:这个须得好好看看!
8.1.1 qsort的参数
qsort 的参数是这样:
void qsort
(
void *base,//要排序的数据的起始位置
size_t num, //元素个数
size_t width,//每个元素的大小(单位:字节)
int (__cdecl//C语言的函数调用约定,不用深究
*compare )//“比较函数”的指针
(const void *elem1, const void *elem2 )
);
现在就来解析为什么这么设计:
- 为什么要起始位置、元素个数、元素大小…这么多参数,不麻烦嘛?
这样的参数,能使 qsort 更通用,管你什么类型,咱都能排
- void* ?
void* ,无确切类型的指针,也因此可以把不同类型的指针传给他,也是为了 通用性
但是,正因为没有确切类型,无法 解引用 和 +- 整数 (不知道访问多大空间,也不知道跳过多少字节)
- num?width?
想想,只知道要排序的数据的 起始位置,你能找到每个元素嘛?
起始位置 + 元素个数 + 元素大小 —> 精准找到每个元素
- 比较函数?
还是一样,服务于 通用性, qsort 的使用者,根据自己要排序的数据,自实现一个比较函数
int 可以用 > 比较,字符串呢?结构体呢?所以根据需要自己实现更妥
- 要求:e1>e2 返回一个>0的数,e1=e2 返回0,e1<e2 返回一个 <0的数
哇!指针在我心中的地位直线上升,妙!
分析了 qsort 的参数,再来尝试使用一下:
8.2 qsort 的使用& qsort 中的回调函数
#include<stdlib.h>
#include<stdio.h>
int cmp(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
int main()
{
int arr[10] = {
9,8,7,6,5,4,3,2,1 };
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;
}
结果:
0 1 2 3 4 5 6 7 8 9
- cmp 的实现不是确定的,因此,我们可以按照自己的想法实现,得以比较不同数据
哟?这里的 cmp?哟?qsort调用了cmp?
cmp是指针
qosrt 在排序过程中调用了 cmp指针 指向的比较函数 —— 不是实现方调用哦!
这不就和回调函数的定义没差嘛!通过 qsort , 我们不仅学到 指针的妙用 , 还学到回调函数
qsort总结:
- 利用void* 指针可以接收各种指针的特性,提升通用性
- 把部分功能作为参数,通过函数指针,灵活地传递函数,进一步提升通用性
8.3 取qsort之长,补bubble之短
- 我们已经学过基础的冒泡排序:
void BubbleSort(int arr[], int sz)
{
int i = 0;
int j = 0;
int flag = 1;
for (i = 0; i < sz - 1; i++)
{
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[i] > arr[i + 1])
{
int tmp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = tmp;
flag = 0;
}
}
if(1 == flag)
break;
}
}
但是,已经吸收了qsort的精华后,我们可不会满足于此了:
只能排整型,这个冒泡太笨了!
仿照着 qsort 的参数设计,看看能不能也让笨笨冒泡通用起来
- 改良冒泡排序
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void Swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)//根据类型交换每个元素的字节
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void BubbleSort(void* base, int sz, int width, int(*cmp)(const void*, const void*))
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;//如果排好序了
for (j = 0; j < sz - 1 - i; j++)
{
//base是void*:不能+-整数 ;
// 如果强制转换成 int* 可能一下跳过太多,无法达到预期效果
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0
{
//交换
//这里传个width过去,直接交换字节,不用考虑类型了
//为什么交换字节? 如果交换变量,临时变量不好创建(不知道什么类型)
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}int main()
{
int arr[10] = {
9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
//qsort(arr, sz, sizeof(arr[0]), cmp);
BubbleSort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
结果:
0 1 2 3 4 5 6 7 8 9
吸取了精华的 Bubblesort ,已经聪明多了!
- 不信咱们再让他排个结构体数据:
struct stu
{
char name[20];
int id;
};
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
int cmp_stu_by_id(const void* e1, const void* e2)
{
return ((struct stu*)e1)->id - ((struct stu*)e2)->id;
}
void Swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void BubbleSort(void* base, int sz, int width, int(*cmp)(const void*, const void*))
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;//如果排好序了
for (j = 0; j < sz - 1 - i; j++)
{
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)//base是void*:不能+-整数 ; 如果强制转换成 int* 可能一下跳过太多,无法达到预期效果
{
//交换
//这里传个width过去,直接交换字节,不用考虑类型了
//为什么交换字节? 如果交换变量,临时变量不好创建(不知道什么类型)
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}
void test()
{
struct stu s[] = {
{
"zhangsan",110},{
"lisi",120},{
"wangwu",911} };
int sz = sizeof(s) / sizeof(s[0]);
BubbleSort(s, sz, sizeof(s[0]), cmp_stu_by_name);
BubbleSort(s, sz, sizeof(s[0]), cmp_stu_by_id);
}
int main()
{
test();
return 0;
}
调试发现,s数组先以name排成升序,再以id排成升序
*复杂的指针类型
- 判断:
根据优先级,把 变量名/数组名 拿掉,剩下的就是类型
- 定义
从最基础的指针/数组开始,一层层套娃
奇怪又有趣?
你能试着分析这两句代码是什么意思吗?
1.
(*(void (*)())0)();
2.
void (*signal(int , void(*)(int)))(int);
分析1:
- 把 0 强制类型转换成:无参,返回类型是void的函数的指针
- 解引用 这个奇怪的函数指针
- 调用这个函数指针指向的函数
:把 0 这个值,转换成指针,再解引用,找到这个地址处的函数并调用它
分析2:
- 首先根据优先级:signal是函数名
- signal后的圆括号里放的是类型,所以这句代码是一次函数声明
- 用咱们的经典手法:把signal(int, void(*)(int))拿开
- 剩下 void(*)(int)还能是什么呢?不就是函数的返回类型嘛
: 声明一个函数signal,函数的返回类型是 void(*)(int),参数是 int 和 void(*)(int)
9. 已知指针用法总结
这里的用法,会不断增加
好的用法希望大家大方分享,共同进步;拙劣的地方希望大家指出,并帮助我优化,感谢
9.1 普通指针
- 指针可以用来找到 数据、地址…
- void* 可以提升通用性
9.2 指针数组
- 可以把不同内存区域的数据组合在一起(有点链表的感觉?)
9.3 数组指针
- 一维数组指针接收二维数组名
9.4 函数指针
- 可以实现 函数传参,提高通用性
- 回调函数
9.5 函数指针数组
- 转移表
未完……
笔者水平有限,一定有不妥不恰、有待提升的地方,希望大家指出
这里是培根的blog,和你共同进步!
边栏推荐
- Go language: string connection, digital conversion string
- AS 3744.1标准中提及ISO8191测试,两者测试一样吗?
- Matlab学习9-图像处理之非线性锐化滤波
- Matlab r2022a installation tutorial
- 端口占用问题,10000端口
- K8S应用(四)—— 搭建redis5 集群(可供外部直接访问)
- In place reversal of a LinkedList
- Iso8191 test is mentioned in as 3744.1. Are the two tests the same?
- Redis cluster in Linux system
- Introduction to database system experiment report answer Experiment 6: advanced query of data table
猜你喜欢

知识图谱入门之---yedda标注

Web design and website planning assignment 14 add background music to the video

一些学习记录i=

马志强:语音识别技术研究进展和应用落地分享丨RTC Dev Meetup

领导让我重写测试代码,我也要照办嘛?

B+ super tree helps you know the underlying structure of MySQL

leetcode - 518. 零钱兑换 II
![[software tool] installation ffmpeg](/img/ac/4fbfb3f1a540ec181b94999d9029fb.jpg)
[software tool] installation ffmpeg

(一)aac开篇-核心组件原理之Lifecycle、LiveData、ViewModel与源码分析技巧(转载)
![[Clickhouse column] user initialization of new library role](/img/00/a11fb1a8e38ed4e0634839160c8ead.png)
[Clickhouse column] user initialization of new library role
随机推荐
经典图论,深度优先和广度优先,拓扑,Prim和Krukal,该来温习啦
leetcode - 230. 二叉搜索树中第K小的元素
[software tool] the hacker matrix special effect software CMatrix
SAP ODATA 开发教程
Codetop - sort odd ascending even descending linked list
BS 7176软垫家具阻燃防火测试
Analysis of EN 45545 R24 oxygen index test method
SAP abap内表分类与增删改查操作
leetcode - 460. LFU cache
ActiveMQ简单教程,适合初学者,学习笔记yyds
Installation (detailed illustration) and use of SVN
Supplementary provision plan codeworks round 760 (Div. 3)
Are the two flame retardant standards of European furniture en 597-1 and en 597-2 the same?
Is the result too different from the goal? With the help of target management, you can reach the target accurately!
Swagger study notes
E. X的放大与缩小(运算符重载)
领导让我重写测试代码,我也要照办嘛?
BS 7176 fire resistance test for upholstered furniture
2022 Niuke winter vacation 3
MySQL死锁问题如何解决?背诵版