前言
这篇文章的知识由初阶指针发散,提及更多指针的概念和基础用法,干货超多!快去接杯水!
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* p1 = "bacon";
const char* p2 = "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];
arr跟 [] 结合,是数组,5个元素,拿掉arr[5],int就是数组存放的元素的类型
arr的类型是 int[5]
int *parr1[10];
parr1跟 [] 结合,是数组,10个元素,拿掉parr[10],int* 就是数组存放的元素的类型
parr的类型是 int* [10],* 和 int 结合,表示是数组类型,每个元素是 int*
int (*parr2)[10];
parr2和 * 结合,是指针,拿掉*parr2,int[10]就是指向的元素类型——存放10个整型的数组
parr2的类型是 int[10] *
int (*parr3[10])[5];
parr3和 [] 结合,是数组,10个元素,拿掉parr3[10],int(*)[5]就是数组存放的元素的类型
int(*)[5],* 用 () 括起来了,标准表示是:int[5] *,说明是指针类型,指向5个整型的指针类型
int (*) [5]中的 * 有括号,放到存放的数据的类型的最后,所以数组存放的类型是 int[5] *——指向存放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,和你共同进步!