目录
一、什么是指针
1.内存
2.地址的生成
3.数据的储存
4、指针变量
5.解引用操作符*
二、指针变量类型
1.指针加减整数
2.指针的解引用
三、野指针
1.野指针的成因
2.避免野指针的方法
四、指针运算
1.指针加减整数
2.指针减指针
3.指针的关系运算
五、指针和数组
六、二级指针
七、字符指针
1.字符指针的使用
2.笔试题
八、指针数组
九、数组指针
1.数组指针的定义和创建
2.数组名和&数组名
3.数组指针的使用
十、数组与指针传参
1.一维数组传参
2.二维数组传参
3.一级指针传参
4.二级指针传参
十一、函数指针
1.函数指针
2.函数指针实现计算器
十二、函数指针数组
1.函数指针数组的定义
2.函数指针数组简化代码
十三、指向函数指针数组的指针
十四、回调函数
1.qsort函数
2.利用冒泡排序思想模拟实现qsort函数
一、什么是指针
1.内存
程序的运行需要储存信息,而信息储存在内存中,我们为了有效地使用内存,就需要将内存划分为一个个小的内存单元,每一个单元的大小是一个字节。(一个字节比较合理,这个内存单元太小也不好,太大也不好)为了能够有效地使用每个内存单元,我们给每一个单元都定了一个编号,这个编号就叫做这个内存单元的地址。假如内存是一幢楼房,就像楼中的门牌号,通过门牌号我们可以找到对应的房间。同样在计算机中使用这样的方式,我们可以轻松地找到对应的字节位置,而不需要一个一个去比对。
内存 | 编号(取十六进制数) |
1byte | 0x00000001(十进制:1) |
1byte | 0x00000002(十进制:2) |
1byte | 0x00000003(十进制:3) |
1byte | 0x00000004(十进制:4) |
1byte | 0x00000005(十进制:5) |
1byte | 0x00000006(十进制:6) |
1byte | …… |
2.地址的生成
我们的电脑中都有硬件电路,用于生成地址的电线叫地址线。当电路中有电路通过时,会产生正负脉冲,从而表示0与1.此处我们以三十二位电脑为例,它在生成地址时32根地址线同时产生电信号表示1或0,当每一个地址线组合起来时就有了许许多多的不同的排列组合方式
我可以三十二个全为0:00000000000000000000000000000000——对应0
我也可以前三十一个全为0:00000000000000000000000000000001——对应1
……
这样的排序方式一共有2的32次方种,也就是说它有是4294967296种排序,内存中就一共有这么多个字节的空间。
但是这个数字不是很直观,我们先对它除以1024得到4194304个KB,再除1024得到4096个MB,再除以1024得到4GB,也就是说在早期的三十二位电脑内存中一共有4GB的内存空间。
3.数据的储存
这里我们打开VS2022,输入以下代码,可以通过调试找到a的地址
#include int main(){int a = 10;&a;//取出num的地址,&为取地址符号//这里的num共有4个字节,每个字节都有地址,但我们取出的是第一个字节的地址(较小的地址)printf("%p\n", a);//打印地址,%p是以地址的形式打印return 0; }
具象化的话,就用以下内容表示:
内存 | 编号 |
1byte | 0xFFFFFFFF |
1byte | 0xFFFFFFFE |
1byte | …… |
a | 0x0012FF47 |
0x0012FF46 | |
0x0012FF45 | |
0x0012FF44 | |
1byte | …… |
1byte | 0x0000002 |
1byte | 0x0000001 |
我们实际上取出的只有0x0012FF47这个地址
我们在VS上的内存窗口上可以看到三行,包含以下内容:
地址 | 内存中的数据(补码) | 内存数据的文本解析 |
0x0012FF47 | a0 00 00 00(小端存储) | ????(内容不定,没什么用处) |
我们a的值为10,用二进制表示即为:0000 0000 0000 0000 0000 0000 0000 1010(二进制的数字表达最后一位表示2的0次方,倒数第二位就表示2的1次方,以此类推,十就是2的3次方加2的一次方也就是1010),在这个时候我们以每个四位为一组,就可以得到数据的表示方法:00 00 00 0a(在16进制数中,a表示10,b表示11,c表示12,d表示13,e表示14,f表示15)
0000 0000 0000 0000 0000 0000 0000 1010
0 0 0 0 0 0 0 a
4、指针变量
请看以下代码:
#includeint main(){int a = 10;int* p = &a;//我们把a这个变量的地址储存在这个变量p中,这个p就叫做指针变量,类型为int*return 0;}
编号代表地址,而地址也可以成为指针,有一定的指向作用,所以叫做指针变量。简单地说指针就是地址。
那我们如何理解这个int* p = &a;呢?
(1)中间的*表示p是个指针变量,注意指针变量是p,而不是*p
(2)int表示指针指向的对象是整形
(3)p为指针变量,接受&a的内容,也就是变量的首地址
在了解这些后,我们也可以创建其它类型的指针变量,比如:
#include int main(){char ch = 'w';char* pc = &ch;//字符型变量的指针*pc = 'q';printf("%c\n", ch);return 0; }//输出:q
5.解引用操作符*
int main(){int a = 10;int* p = &a;printf("%d", *p);//这里的*p表示对指针变量p解引用,找到其对应的内容return 0;}//输出:10
注意:int*p = &a;中的*不表示解引用,通过解引用符号,我们可以轻易地找到指针地址的对应值
6.指针变量的大小
#include int main(){printf("%d\n", sizeof(char *));printf("%d\n", sizeof(short *));printf("%d\n", sizeof(int *));printf("%d\n", sizeof(double *));return 0; }
你可能认为输出结果是:1 2 4 8
但实际上是:4\8 4\8 4\8 4\8(4或8)
原因:指针变量储存的是地址,也就是说指针变量的大小取决于存放一个地址需要的空间的大小,32位平台下地址是32个bit位(即4个字节),而64位平台下地址是64个bit位(即8个字节),所以指针变量的大小就是4或8.
结论:指针大小在32位平台是4个字节,64位平台是8个字节。
二、指针变量类型
1.指针加减整数
#include int main(){int n = 10;char *pc = (char*)&n;int *pi = &n;printf("%p\n", &n);printf("%p\n", pc);printf("%p\n", pc+1);printf("%p\n", pi);printf("%p\n", pi+1);return 0; }//结果://007FFC20//007FFC20//007FFC21//007FFC20//007FFC24
在这里我们可以看到,不管打印int*还是char*指针对应的地址,它们的结果都是一样的。
那么我们为什么不能将所有的指针统一为一个数据类型呢?
然而,当我们观察两种类型的指针加一后的地址时,不难发现,int*类型的地址向后移动了4字节,char*类型的地址向后移动了1字节.
(1)总结:指针的类型决定了指针向前或者向后走一步有多大
2.指针的解引用
#include int main(){int n = 0x11223344;char* pc = (char*)&n;int* pi = &n;*pc = 0;printf("%x\n", n);*pi = 0; printf("%x\n", n);return 0;}//结果://11223300//0
在内存中,0x11223344这个数字以小端字节序存储(44 33 22 11),先用char* 的指针解引用只能访问一个字节,所以会把第一个字节改为0,也就会打印11223300;而 int* 的指针解引用能访问四个字节,所以会把第四个字节都改为0,也就会打印0
(2)总结:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
三、野指针
野指针就是指针指向的位置是不可知的,就像一条没有拴住的恶犬,接近它是会受伤的。
1.野指针的成因
(1)指针没有初始化
#includeint main(){int* p;//没有初始化*p = 20;//不清楚地址在哪里,为野指针return 0;}
(2)指针的越界访问
#includeint main(){int arr[10]={1,2,3,4,5,6,7,8,9,10};int i = 0;int* p = arr;for(i=0; i<=10; i++){printf("%d ",*(p+i));//可以读取到arr[10],越界访问,为野指针}return 0;}
(3)指针指向的空间被释放
#includeint* add(int x,int y){int d = x+y;int* p = &d;return p;//返回和的地址}int main(){int a = 10;int b = 20;int* c = add(a,b);//因为退出了函数,原来p指针这个地址也就不在程序的作用域内了,c就成为了野指针printf("%d ",*c);//这个解引用虽然数值是正确的,但由于这个空间不在程序的作用域内,//我们无法确定是否会有其他的操作会改变它的值return 0;}
2.避免野指针的方法
(1)指针初始化
(2)小心指针越界
(3)指针指向空间释放即使置NULL
(4)避免返回局部变量的地址
(5)指针使用之前检查有效性
可以在使用指针时加上下面的代码:
#include int main(){int a = 10;p = &a;//使用的指针一定要有准确的地址 int *p = NULL;//不用的指针记得置空if(p != NULL) {*p = 20; }//空指针不能解引用,加上判断return 0; }
四、指针运算
1.指针加减整数
#define N_VALUES 5float values[N_VALUES];float *vp;//指针+-整数;指针的关系运算for (vp = &values[0]; vp < &values[N_VALUES];)//这里虽然下标为五的元素属于越界访问,但是并没有读取它的内容,不算越界访问{ *vp++ = 0;//vp先解引用,然后再++}
指针加减整数可以让地址向后或向前移动对应的字节数。
2.指针减指针
int my_strlen(char *s) {char *p = s;while(*p != '\0' )p++;return p-s; }
指针减指针可以求出中间所差的类型对应字节数的个数
3.指针的关系运算
for(vp = &values[N_VALUES]; vp > &values[0];){*--vp = 0; }
指针的比较实际上就是十六进制数字的比较
五、指针和数组
#include int main(){int arr[10] = {1,2,3,4,5,6,7,8,9,0};printf("%p\n", arr);printf("%p\n", &arr[0]);int i = 0;for(i=0; i<10; i++){printf("&arr[%d] = 0x%p",i,&arr[i])}return 0; }//结果://00DBFD88//00DBFD88//&arr[0] = 0x00DBFD88 = p+0//&arr[1] = 0x00DBFD8C = p+1//&arr[2] = 0x00DBFD90 = p+2//&arr[3] = 0x00DBFD94 = p+3//&arr[4] = 0x00DBFD98 = p+4//&arr[5] = 0x00DBFD9C = p+5//&arr[6] = 0x00DBFDA0 = p+6//&arr[7] = 0x00DBFDA4 = p+7//&arr[8] = 0x00DBFDA8 = p+8//&arr[9] = 0x00DBFDAC = p+9
数组名为数组首元素地址:arr = 00DBFD88,arr[0] = 00DBFD88
&arr[0] = 0x00DBFD88 = p+0
&arr[1] = 0x00DBFD8C = p+1
&arr[2] = 0x00DBFD90 = p+2
&arr[3] = 0x00DBFD94 = p+3
&arr[4] = 0x00DBFD98 = p+4
&arr[5] = 0x00DBFD9C = p+5
&arr[6] = 0x00DBFDA0 = p+6
&arr[7] = 0x00DBFDA4 = p+7
&arr[8] = 0x00DBFDA8 = p+8
&arr[9] = 0x00DBFDAC = p+9
从这些结果我们得到arr[i]等同于*(p+i),这也就解释了数组中元素的访问其实使用了指针的思想,也让我们了解到数组元素的访问可以使用指针。
六、二级指针
1.指针变量也是变量,是变量就有地址,那指针变量的地址就可以储存在二级指针内 。
2.二级指针解引用需要两次才能找到变量
#includeint main(){int a = 0;int* p1 = &a;int** p2 = &p;//二级指针,存放指针变量的地址//可以看作int* *p2,前面的int*表示指向的对象为int*类型,后面的*表示p2为指针变量printf("%d",**p2);//p2解引用一次得到指针变量p1,再解引用得到areturn 0;}
七、字符指针
1.字符指针的使用
#includeint main(){char a = 'w';char* p = &a;printf("%c",*p);//这是最简单的使用方法const char* pstr = "hello bit.";//这里指针保存了这个字符串的首字符地址而不是整个字符串printf("%s\n", pstr);//按字符串打印内存中的数据会打印到\0终止return 0;}
2.笔试题
#include 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 the same\n");elseprintf("str1 and str2 are not the same\n");if(str3 ==str4)printf("str3 and str4 are the same\n");elseprintf("str3 and str4 are not the same\n");return 0; }//结果://str1 and str2 are not same//str3 and str4 are same
这段代码中的str1与str2是两个不同的数组,元素的内容都是hello world.,而str3与str4是两个指针变量,内容都是hello world.的首字符地址。
数组储存在栈区,每创建一个新的数组都需要占用内存空间存储,所以两个地址不同;字符串常量储存在静态区,不需要储存多个这样的字符串,所以两个指针变量都指向了同一个地址。
八、指针数组
指针数组也是数组,只是存放的元素是指针。
#includeint main(){int a = 0;int b = 0;int c = 0;int* arr[3]={&a,&b,&c};//这就是一个简单的指针数组return 0;}
九、数组指针
1.数组指针的定义和创建
定义:指向数组的指针
#includeint main(){int arr[10] = {1,2,3,4,5,6,7,8,9,10};//int arr[10]是一个数组,我们首先去掉数组名//int [10]在中间写上指针变量名p,再写上*表示p为指针变量//最后为了防止被解析为指针数组再加上括号:int (*p)[10],这就是一个指向数组的指针//[10]表示指向的数组有是个元素,前面的int表示数组的元素为int类型return 0;}
2.数组名和&数组名
#include 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 = 00EFFCDC//&arr= 00EFFCDC//arr+1 = 00EFFCE0//&arr+1= 00EFFD04
arr = 00EFFCDC,&arr= 00EFFCDC二者虽然在内容上是一样的,但是arr+1 = 00EFFCE0跳过了4字节,&arr+1= 00EFFD04跳过了整个数组的40字节,二者加一跳过的字节数不同。
3.数组指针的使用
重要思想:一个二维数组可以看作多个一维数组的组合,但二维数组在内存中是连续存放的。
#includevoid print1(int arr[3][5], int r, int c)//二维数组传参可以直接写数组{int i = 0;for (i = 0; i < r; i++){int j = 0;for (j = 0;j < c;j++){printf("%d ", arr[i][j]);}printf("\n");}}void print2(int (*arr)[5], int r, int c)//但是在本质上,用数组指针接收会更好//由于arr[1]=*(p+1),我们用指针的思想改变代码{int i = 0;for (i = 0; i < r; i++){int j = 0;for (j = 0;j < c;j++){//printf("%d ",arr[i][j]);//printf("%d ",*((arr[i])+j));printf("%d ", *(*(arr+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 };printf("print1\n");print1(arr, 3, 5);//二维数组的数组名也是首元素地址,但是这个首元素是首个一位数组的地址printf("print2\n");print2(arr, 3, 5);return 0;}//结果://print1//1 2 3 4 5//2 3 4 5 6//3 4 5 6 7//print2//1 2 3 4 5//2 3 4 5 6//3 4 5 6 7
请看下面的代码,注意理解数据的类型
#includeint main(){//当去掉变量名时剩下的就是数据的类型int parr[5];//整形数组,共有五个元素int* parr1[10];//整型指针的数组,共有十个元素int(*parr2)[10];//数组指针,指向的数组有十个整型元素,指针的类型为int(*)[10]int(*parr3[10])[5];//指针数组,包含十个数组指针,指向的是个数组都是有五个整型元素return 0;}
十、数组与指针传参
1.一维数组传参
#include void test(int arr[10])//可以直接写整形数组{}void test(int arr[])//数组的元素个数可以省略{}void test(int* arr)//本质上数组名是指针{}void test2(int* arr[20])//这是个整型指针数组,不符合{}void test2(int** arr)//这是个二级指针,也不符合{}int main(){int arr[10] = { 0 };test(arr);test2(arr);}
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])//这是一个整型数组指针,符合{}void test(int** arr)//这是一个二级指针,不符合{}int main(){int arr[3][5] = { 0 };test(arr);}
3.一级指针传参
#include 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; }
4.二级指针传参
#include 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;}
十一、函数指针
1.函数指针
函数指针是指向函数的指针
#includevoid print(int n){}int main(){//首先函数的定义去掉函数名:void (int n)//在中间加上(*)表明它是指针变量:void (*)(int)//写上变量名,也可以删去内部参数的变量名:void (*p1)(int)void (*p1)(int) = print;//初始化void (*p2)(int) = &print;printf("0x%p\n", p1);printf("0x%p\n", p2);return 0;}//结果://0x009013F2//0x009013F2
(1)函数名与&函数名都是函数的地址
(2)这个函数地址储存在函数区里,所以函数的地址与它是否被调用无关
2.函数指针实现计算器
#includeint 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 menu(){printf("**************************\n");printf("*** 1.Add ****** 2.Sub ***\n");printf("*** 3.Mul ****** 4.Div ***\n");printf("********* 0.exit *********\n");printf("**************************\n");}//打印初始界面void calu(int (*f)(int, int))//接收函数{int ret = 0;int a = 0;int b = 0;printf("请输入两个值:");scanf("%d %d", &a, &b);ret = f(a, b);//调用相应函数printf("结果为:%d\n", ret);}//通过函数指针可以简化代码int main(){menu();int input = 0;do{printf("请输入:");scanf("%d", &input);switch(input)//根据input的值判断加减乘除{case 0:{printf("退出程序");break;}case 1:{calu(Add);//传参相应的函数指针break;}case 2:{calu(Sub);break;}case 3:{calu(Mul);break;}case 4:{calu(Div);break;}default:{printf("请重新输入\n");break;}}}while (input);return 0;}
十二、函数指针数组
1.函数指针数组的定义
函数指针数组是存储函数指针的数组
#includeint 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 (*arr[5])(int, int) = { Add,Sub,Mul,Div };//函数指针数组,元素类型为int (*)(int, int)//内部写上数组名和元素个数:int (*arr[5])(int, int)int (**p[5])(int, int)return 0;}
2.函数指针数组简化代码
#includeint 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 menu(){printf("**************************\n");printf("*** 1.Add ****** 2.Sub ***\n");printf("*** 3.Mul ****** 4.Div ***\n");printf("********* 0.exit *********\n");printf("**************************\n");}void calu(int (*f)(int, int)){int ret = 0;int a = 0;int b = 0;printf("请输入两个值:");scanf("%d %d", &a, &b);ret = f(a, b);printf("结果为:%d\n", ret);}int main(){menu();int input = 0;int (*arr[5])(int, int) = { 0,Add,Sub,Mul,Div };//函数指针数组,对应数字对应函数do{printf("请输入:");scanf("%d", &input);if(input>=1 && input<=4){calu(arr[input]);//对应函数,大量简化代码}else if (input == 0){printf("退出程序");}else{printf("请重新输入\n");}} while (input);return 0;}
十三、指向函数指针数组的指针
指向函数指针数组的指针就是指向函数指针数组的指针
禁止套娃~
是不是已经晕了,其实这样可以无限套娃。了解这些定义方法是帮我们认识这些指针与数组的定义方法。
void test(const char* str) {printf("%s\n", str);}int main(){//函数指针pfunvoid (*pfun)(const char*) = test;//函数指针的数组pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函数指针数组pfunArr的指针ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0; }
十四、回调函数
定义:回调函数就是一个被作为参数传递的函数,一个函数作为另一个函数参数。
在下面,我们通过qsort(快速排序)函数来讲解
1.qsort函数
(1)qsort函数定义在stdlib.h的头文件中,注意包含头文件。
(2)qsort一共有四个参数,需要排序元素的首地址(void*),元素的个数(size_t),每个元素的大小(size_t),用于定义判定大小方式的compare函数,也就是回调函数的使用。
(3)compare函数需要满足参数为void*的指针,两个元素相减的结果为正数,前大于后;两个元素相减的结果为负数,后大于前;两个元素相减的结果为零,二者相等。
(4)qsort函数可以排序任何类型的数据且默认排升序。
#include#includeint compare(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,0 };qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare);int i = 0;for (i = 0; i < 10; i++){printf("%d ", arr[i]);}return 0;}
2.利用冒泡排序思想模拟实现qsort函数
//使用冒泡排序思想模拟实现qsort函数#includeint int_compare(const void* e1, const void* e2)//设计的函数,这只是int类型的比较,还可以编写其他函数排序其他数据{return (*(int*)e1 - *(int*)e2);}void exchange(char* p1, char* p2, int sz)//一个字节一个字节交换{int i = 0;for (i = 0; i < sz; i++){int temp = 0;temp = *(p1+i);*(p1+i) = *(p2+i);*(p2 + i) = temp;}}void my_sort(void* p, int n, int sz, int compare(const void*, const void*)){char* arr = (char*)p;int i = 0;for (i=0; i<n-1; i++){int j = 0;for (j=0; j<n-i-1; j++){if (compare(arr + sz * j, arr + sz * (j + 1)))//以自己设计的函数的返回值确定先后顺序{exchange(arr + sz * j, arr + sz * (j + 1), sz);}}}}int main(){int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };my_sort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(arr[0]),int_compare);int i = 0;for (i = 0; i < 10; i++){printf("%d ", arr[i]);}return 0;}//结果:1 2 3 4 5 6 7 8 9 10