目录

一、什么是指针

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