建议收藏两万字深度解读 指针 ,学好指针看这一篇文章就够了

Posted 努力学习的少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了建议收藏两万字深度解读 指针 ,学好指针看这一篇文章就够了相关的知识,希望对你有一定的参考价值。

前言

大家好,我是努力学习的少年,今天这篇文章是专门写关于指针的知识点,因为指针内容比较多,所以我将指针的这篇文章我将它分为两部分,第一部分是基础篇,是从零开始学习一些基本概念,第二部分是进阶篇,如果你指针基础学得差不多了,你可以尝试学习进阶篇的指针,这部分的内容相对较难一些,学完这部分内容,你的指针知识点基本就学的差不多了,最后还有指针的笔试题,这部分的题需要通过我们学到的指针的知识去笔算,这样有利于巩固我们的知识,并有一个更深的理解。

大纲如下:

 

目录

前言

 指针初阶

1.地址和指针        

2.指针的定义

 3.取地址操作符:&

4.取内容运算符

5.指针的类型

6.指向指针的指针

7.指针与数组

8.指针运算 

8.1指针与整数的加减

8.2相同类型指针的减法运算

8.3指针关系运算

8.4指针类型的强制类型转换

9.void* 指针 

10.空指针

11.野指针

12.指针与const

12.1常量指针

 12.3指向常量的指针:

12.4指向常量的常量指针 

进阶篇

1.字符指针和字符串

2.指针数组和数组指针

3.指针与多维数组

4.&数组名vs数组名

5.函数指针

6.函数指针数组

7.指向函数指针数组的指针

8.回调函数

9.qsort的使用以及它的底层原理

指针练习题及解析

训练一

训练二

题一

 题二

 题三

题四

 题五

题六

题七

题八


 指针初阶

1.地址和指针        

数据在程序运行过程中存储在计算机内存中,而内存是以字节为基本单位的连续存储空间,为了能够标识内存中不同的存储单元,每一个存储单元都有一个编号,这个编号就是内存单元的的”地址“。由于内存单元是连续的,所以内存地址也是连续的。

指针是“指向”另外一种类型的复合类型。指针是用来存储变量的地址,本身就是一个对象,允许对指针进行赋值和拷贝,而且指针的生命周期内它可以先后指向不同的对象。准确的说指针就是一个变量,是用来存放地址的变量。

pa可以根据地址去找到变量x的存储单元,这种方式为“间接访问”。 

在内存中,一个字节的空间大小,对应一个地址。 

2.指针的定义

指针变量的定义:
类型说名符* 变量名1,*变量名2,......;

	int a, b;//定义两个int类型变量
	int* c, * d;//定义两个int*指针变量
	int e, * f;//e为int类型变量,fint*指针变量

  1.指针本身就是一个变量,它也有自己的地址  

  2.定义指针变量需要在前面加一个*,但它不是变量名的组成部分,只是说明后面的变量为指针

 3.取地址操作符:&

我们知道指针后,我们还需要知道变量的地址怎么取出来?

“ & ”为取地址运算符

取地址运算符是单目运算符,其作用是返回其后的变量(包括数组元素)的地址。register存储类型的变量是不能使用“&”返回地址。

	int i = 10;
	int* pi = &i;//取出变量i的地址,为int* ,然后赋值给pa变量

对指针变量进行赋值时,要求右边的表达式的地址地址类型与指针变量的类型相同,如果不相同编译器会发生警告,甚至是发生错误。

4.取内容运算符

取内容运算符为“ * ”,  当我们有一个地址后,“ * ”能够通过该地址去访问相应的内存单元

* 指针表达式

指针表达式要求结果是一个“地址”,例如:

	printf("%d\\n", *pi);//输出10:*pi等价于i
	*pi = 100;//通过指针变量间接访问了i这个变量,并将i变量改为100
	printf("%d\\n", *pi);//输出100

5.指针的类型

指针变量和其它内置类型一样,也有int*,char*,double*等类型,那么它们的类型代表的大小为多少
我们看下面的例子:

	int i = 0;
	char c = 'a';
	double d = 1.11;
	int* pi = &i;
	char* pc = &c;
	double* pd = &d;
	printf("pi:%d  pc:%d  pd: %d", sizeof(pi),sizeof(pc),sizeof(pd));

sizeof运算符是计算变量的大小,单位为字节。

输出结果:pi:4  pc:4  pd: 4

可见,不同类型的指针变量它们的大小都为4个字节。其实指针变量的大小与它的类型无关,只与我们的机器平台有关

在32位机器上,指针变量的大小为4个字节。

在64位机器上,指针变量的大小为8个字节。

那么指针变量的类型到底有什么意义呢?我们再来看这个例子:

	int i = 0x11223344;
	int* pi=&i;
	char* pc = (char*)(&i);//将i的指针强制转换为(char*)
	printf("%x\\n", *pi);//输出11223344
	printf("%x\\n", *pc);//输出44

%x是按十六进制进行打印数据,0x11223344是十六进制的整形常量,有效整数为11223344.

11223344每两个数字为一个字节,则*pi则访问了4个字节,*pc则访问一个字节。

总结:指针的类型决定了指针能够访问多大的空间。如int*能够访问一个int类型大小的空间(4个字节),char*能够访问一个char类型的空间(为一个字节)

也有同学有有点疑惑,为什么数据倒着存放的,这涉及到数据大小端存储的问题。 

 那什么是数据存储的大小端呢?

大端是高字节存放到内存的低地址

小端是高字节存放到内存的高地址

由于我的机器是小端存储,所以高字节数据放在低地址处,如上述。

        

我们再来看一个例子:

	int i = 0;
	char c = 'a';
	int* pi = &i;
	char* pc = &c;
	printf("%p\\n", pi );
	printf("%p\\n", pi + 1);
	printf("%p\\n", pc);
	printf("%p\\n", pc+1);

 %p是打印出地址的符号。

输出:0137FB00
            0137FB04
            0137FAF7
            0137FAF8

可以看到pi指针+1走了4个字节,pc指针+1走了一个字节。

所以,指针类型决定了指针走一步的距离有多大,例如:int*指针类型+1向后走4个字节的距离,

double*指针+1向后走8个字节的距离

6.指向指针的指针

指针是内存中的对象,同样指针也有地址,因此,允许把指针的地址在存放到另一个指针中。

 通过*的个数可以区别指针的级别,例如 **表示指向指针的指针,***表示指向指针的指针的指针。

int a=10;
int *pa=&a;
int** ppa=&pa;//ppa是指向pa的指针,为二级指针
int*** pppa=&ppa;//pppa是指向ppa的指针,为三级指针

7.指针与数组

每个变量都有地址,数组中包含若干个元素,每个元素都占用内存单元,它们都有自己相应的地址,

数组元素的指针就是数组元素的地址。

例如:


	int arr[5] = {0];
	int* pa = &arr[3];//指针pa指向arr数组下标为3的元素
	int* pb = arr;//指针pa指向arr数组下标为0的元素

数组名存放的是数组首元素的地址,即arr相当于&arr[0].

数组元素的访问有两种方式:

(1)下标法:arr[3] 或pb[3]都可以访问到数组下标为3的元素。

(2)指针法:*(arr+3)或*(pb+3)也可以访问到数组下标为3的元素。

arr[3]等价于 *(arr+3),pa是数组下标为3的元素,pa[1]等价于*(pa+1),

所以pa[1]是访问到数组下标为4的元素。

例题:打印数组中所有的元素

#include<stdio.h>
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int sz = sizeof(arr) / sizeof(arr[0]);//计算出数组有多少个元素
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

sizeof运算符能够计算变量有多少个字节。

sizeof(arr)是计算出整个数组有多少个字节,sizeof(arr[0])计算出数组中第一个个元素有多少个字节(相当于计算数组中每个元素有多少个字节)

sizeof(arr)/sizeof(arr[0])计算出数组中有多少个元素 


 

8.指针运算 

8.1指针与整数的加减

指针可以加减一个整形数据。

那么指针加减一个数据有什么意义呢?我们来看一下例子:

	int arr[5] = { 0 };
	printf("arr:%p\\n", arr);
	printf("arr+1:%p\\n", arr+1);
	char str[5] = "0";
	printf("str:%p\\n", str);
	printf("str+1:%p", str + 1);

   

 数组名为数组首元素的地址,如arr表示的是arr数组首元素的地址,为int* 类型,

  str表示的是str数组首元素的地址,为char*类型。

arr+1跳过1个int类型的字节数到下一个地址(跳过4个字节)。

str+1跳过1个char类型的字节数到下一个地址(跳过1个字节)

 假设指针有一个指针为p:

p+n=p+p指向的数据类型的字节数×n

p-n=p-p指向的数据类型的字节数×n;

其中n为整数。

8.2相同类型指针的减法运算

假设有两个指针,一个p和q;

其中p和q为相同类型的指针表达式,相减的结果是两个地址之间间隔的数据。

例如:

	int arr[10] = { 0 };
	printf("%d\\n", &arr[0] - &arr[9]);//输出-9
	printf("%d", &arr[9] - &arr[0]);//输出9

arr数组的各个元素是连续存放的,元素arr[0]是元素arr[9]前面的第9个元素,因此arr[0]-arr[9]的结果为-9.

8.3指针关系运算

关系运算符= =和!=用于判断两个指针是否指向同一个内存单元,例如有这两个指针变量:
                                                         int* p,int*q;

如果p==q结果为1(为真),则表明p和q指针指向同一块内存单元,为0(假)表示指向不同的内存单元。

8.4指针类型的强制类型转换

对指针变量进行强制类型转换的一般形式:

int a=0;

int* pa=&a;

char* pc=(char*)pa; 

将pa保存的int*类型指针强制转换为char*类型指针后赋值给pc,其中pa还是为int*,没有改变。

9.void* 指针 

void*指针是一种特殊类型的指针,它能存放任意类型的的地址,一个void* 指针存放一个地址,这与其它类型的的指针是一样的。但是我们不知道该指针是存放什么类型的地址,也就是说我们无法知道它指向的对象是什么类型,所以我们就无法对它指向的对象进行操作。

	int i = 0;
	char c = 'a';
	void* pi = &i;
	void* pc = &c;

10.空指针

         指针变量跟我们的内置类型一样,被定义出来后,如果没有对它进行初始化,则指针变量的值使随机的,指针变量存储的地址时不确定的,这时它存储的地址由可能是用户程序内存区的一个地址。如果直接使用 该指针区间接修改对应内存地址中的数据,会导致不可预料的错误,甚至导致系统不能正常进行。

         为了避免上诉问题的出现,所以我们在定义指针变量时需要对指针进行初始化,使指针指向一个合法单元,

 如果指针定义出来后,如果暂时不知道它要指向哪块空间,那么我们可以把指针赋值为0,表示该指针不指向任何

一块空间,值为0的指针称为”空指针“,为了提高代码的可读性,c语言在stdio.h这个头文件定义了如下常量符号:

                #define NULL 0

所以,在c语言中,定义指针变量为空指针由以下两种方法:

                  int* pa=0;

                  int* pa=NULL;

11.野指针

概念:野指针是指向的空间是不可知,如上面的指针未初始化,这个指针就是就是野指针。

访问野指针,相当于去访问一个本不存在的位置上本不存在的变量。所以我们需要避免野指针的产生。

野指针产生有三种方式:

    int* pa;//指针未初始化,pa为野指针
    int* pb = (int*)malloc(sizeof(int));
    free(pb);//释放空间后,pb没有置成NULL,pb为野指针

    int arr[5] = { 0 };
    arr[5] = 10;//指针越界访问,&arr[5]为野指针

pa指针未初始化,那么存储的地址是随机的,也就是说pa指向哪块空间我们是不知道。

所以我们定义指针需要对指针初始化。 

pb是malloc的空间释放掉,但pb指针还在,pb指针指向的内容是已经归还给系统,那么系

统再分配这块空间我们是不知道的,此时的pb指针已经没有意义了。(malloc涉及到动态内存开辟的知识)。

所以我们将空间free掉时,需要对相应的指针置成空指针。

pc指针是访问数组的以外的空间,系统只给数组分配5个int类型大小的内存,我们直接去访问数组以外的 空间是我们是不知道的,所以&arr[5]是野指针,我们在使用数组时尽量避免指针越界。 

野指针的产生是一件很可怕的事情,它常常会使我们的程序崩溃,作为一名合格的程序员,我们需要避免野指针的产生。

12.指针与const

const修饰的变量则该变量中的值则不能被修改,为一个常变量,如:
    const int a = 10;
    a = 20;//错误:a是一个常变量,不能被修改

指针也是一个变量,它也可以被const修饰,const修饰指针可以分为三种:

第一种是修饰指针本身;称为常量指针

 第二种是修饰指针指向的对象;称为指向常量的指针

第三种是既是修饰指针本身由修饰指针指向的对象。称为指向常量的常量指针。

12.1常量指针

常量指针是是const修饰指针,即指针本身是一个常变量,不能被修改,

它的定义方式:

类型* const 变量名

例如:int* const p;注意const在*的右边。

const变量在定义的同时必须进行初始化,

    int a = 10,b=20;
    int* const pa = &a;
    pa = &b;//错误:pa是常变量指针,不能被修改
    *pa=b;//正确,指针指向的值可以被修改

 12.3指向常量的指针:

指向常量的指针是指const修饰指针指向的变量,即不能通过指针去修改它指向的变量

定义方式:

const 类型* 变量名 或者 类型 const* 变量名 

注意const在*的左边

    
	const int i = 10;
    const int a= 20;
	int* pi = &i;//错误:pi是一个普通的指针,不能指向一个常变量
	const int* pi1 = &i;//正确:pi1是一个指向常量的指针
	*pi1 = 20;//错误:pi1指向的值不能修改
    pi1=&a;//正确,指针本身的值可以被修改

 指向常量的指针可以指向一个非常量变量

	int a = 10;
	const int* pa = &a;//正确,但是不能通过pa指针去修改a的值

12.4指向常量的常量指针 

指向常量的常量指针即指针本身不能被修改,而且指向的值即不能被修改

定义方式:

const 类型* const 变量名 或者 类型 const* const 变量名

例如:const int* const pa;


	int a = 10;
	int b = 20;
	const int* const pa = &a;
	*pa = 20;//错误:pa是指向常量的指针,即指向的值不能被修改
	pa = &b;//错误:pa又是一个常量指针,即指针本身的值不能被修改

进阶篇

1.字符指针和字符串

c语言中把字符串存放在字符数组中,通过数组名可以访问字符串或字符符串中的某个元素。使用字符指针访问字符串是需要把字符串的地址(第一个字符的地址)存放到字符指针变量中。

字符指针变量的初始化方式:

                   char* pc = "abcdef";
其中abcdef不是存储到指针变量里,而是将首元素的地址存储到pc中,此时称字符指针指向字符串第一个元素。此时的字符串是一个字符串常量,只能读取字符串常量中的值,不能对字符串进行修改。如果要在程序中修改字符串内容,需要把字符串放在一个数组里面,像这样: 

                  char str[ ] = "abcdef";

用”abcdef“初始化并定义str数组中。

有这样一道经典题:

#include <stdio.h>
int main()
{
	char str1[] = "hello sjp.";
	char str2[] = "hello sjp.";
	char* str3 = "hello sjp.";
	char* str4 = "hello sjp.";
	if (str1 == str2)
		printf("str1 and str2 are same\\n");
	else
		printf("str1 and str2 are not same\\n");
	if (str3 == str4)
		printf("str3 and str4 are same\\n");
	else
		printf("str3 and str4 are not same\\n");
	return 0;
}

最后输出的是:

str1 and str2 are not same
str3 and str4 are same

2.指针数组和数组指针

            指针数组和数组指针看起来没什么区别,其实这两个是完全不同的概念,指针数组本质是一个数

   组, 是用来存放指针的数组,而数组指针本质是指针,是指向数组的指针。这看起来还是有一点难以理解,

   那么我将带大家去区分这两个概念。        

指针数组:一个数组存储的元素均为指针类型型的数据,称其为指针数组。 

数组指针:指向一个数组的的指针

我们来看下它们的区别:

	int* arr[5] = { 0 };//arr是指针数组,能够存放5个int*的指针
	//pa是数组指针,存放的是一个地址,这个指针指向的是一个能够存放5个int型的数组
	int(*pa)[5]=&arr;

注意:1.*和变量名跟括号括一起的为数组指针, 如果*和变量名没有括号括起来为指针数组,因为[ ]的优先级比*高,所以变量名会与[ ]先结合,确认为数组,*和变量名括号括一起了,则变量名会先与*结合,确认为指针。这点对于我们区分是数组还是指针是十分重要的。

            2.定义数组指针时,数组指针的类型和长度与数组的类型长度必须相同。

例子 :

int arr[5];//整形数组
int* parr1[5];//指针数组,存放5个int*指针变量
int(*parr2)[5];//数组指针,指向的数组是一个能够存放5个int型的数据

int(*parr3[5])[5];//指针数组,存放5个指针,且这两个指针指向的数组能够存放5个int型的数据

对于parr3 ,由于" [ ] " 的优先级比” * “高,所以parr3先与“ [ ] "结合,所以parr3为数组,我们把parr3[5]去掉,则只剩下int (* )[5],所以parr3数组存储的数据类型为int (* )[5],这个数据类型为数组指针,指针指向的数组能存储5个int类型的数据。

3.指针与多维数组

指针变量可以指向一维数组中的元素,也可以指向多维数组中的元素。

数组名代表数组的首地址,是一个地址常量,在二维数组中这一规则同样有效。

例如:

                                int arr[3][4];

我们可以把数组arr理解成有arr[0],arr[1],arr[2]三个元素组成的一维数组,而arr[0],arr[1],arr[2]又可以理解成由4个int类型组成的一维数组。

        其中arr代表的是二维数组的首元素的地址,为&arr[0],注意&arr[0]的类型不是int*,而是int* [4]类型的指针数组

则arr+1则代表的是下一个一维数组的的地址&arr[1].

      arr[0]、arr[1]、arr[3]可以认为是二维数组中每一行中的一维数组的数组名。所以它们分别代表3个一维数组的首地址。

arr[0]的值是&arr[0][0],arr[1]代表的是&arr[1][0],arr[2]的值代表的是&arr[2][0]它们的类型为int*

a[i][j]的地址有下列几种表示方法:

                     &arr[i][j];

                     *(arr+i)+j;

                     arr[ i ]+j;

数组名a和数组名a[0]代表的地址相同,但是它们的含义相同,数组名a为&a[i],它的类型为int* [4],为数组指针类型,数组名a[0],

为&a[0][0],它的类型为int*,为整形指针类型。

4.&数组名vs数组名

int arr[10]={0};

那么&arr跟arr有什么区别呢?

我们知道arr代表的是首元素的地址。

其实&arr是数组的地址。

它们有什么区别呢?

	int arr[5] = { 0 };
	printf("arr:%p\\n", arr);
	printf("arr+1:%p\\n", arr + 1);
	printf("&arr:%p\\n", &arr);
	printf("&arr+1:%p\\n", &arr+1);

 输出:

我们可以看到:

        arr和&arr的地址相同,但arr+1和&arr+1的地址有很大的区别,arr+1与arr相差4个字节,&arr+1与arr相差20个字节。

因为arr代表的首元素的地址,它的类型为int*,所以+1就跳过一个int类型。

&arr是整个数组的地址,它的类型为int* [5].为数组指针,它+1就向后走5个int类型大小的距离

arr的解引用是指向整个数组的所有元素,而int*指针解引用仅指向数组中的一个元素。

如下图所示:

    只有两种情况数组名表示数组,其它的数组名表示首元素的地址:
   1.&arr表示整个数组的地址

   2.数组名单独放在sizeof内部,计算数组总的大小。

5.函数指针

程序定义函数后,对程序进行编译时,编译系统为函数分配一端存储空间存储二进制代码,这段内存空间的起始地址(也称入口地址)称为函数指针。

函数指针变量的定义:

类型说明符 (* 指针变量名)(函数的形参列表);

int Add(int x, int y)
{
    return x + y;
}


int (*pf)(int x , int y) = Add;//等价于int (*pf)(int, int) = Add

其中,&函数名与函数名都表示相同的意义,都表示函数的地址。

pf为函数指针变量,指向的是Add这个函数。int(* )(int,int)函数指针类型

 实际中函数定义指针定义变量时,函数指针的形参的名字没有实际意义,习惯上省略不写。

上面的pf定义可以这样写:

int (*pf))(int,int)=Add;

 函数指针的类型中形参列表函数的形参列表相同,且返回类型与函数的返回类型相同。

void Swap(double* x, double* y)
{
	double tmp = *x;
	*x = *y;
	*y = tmp;
}

void (*pd)(double*, double*) = Swap;

 定义函数指针pd时,函数指针的类型为形参为两个double*,返回类型为void

    在《c陷阱和缺陷》中有这两段代码,让我们尝试去解读它们:

//代码1 
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

(*(void (*)())0)();的解读:
void(*)()表示的是一种函数指针类型,这个函数指针类型指向的是无参数,且返回类型为void,(void (*)())0是将0这个整形变量强制转换为上面的函数指针类型,0是一个地址,所以(*(void (*)())0)();表示的是调用一个0地址处的函数,且这个函数没有参数,返回类型为void。
 

void (*signal(int , void(*)(int)))(int);解读:

如果我们将signal(int , void(*)(int))提取出来后,我们发现signal其实一个函数声明,且这个函数有两个参数,一个参数为int类型,另一个参数是void(*)(int)类型,返回类型为一个函数指针类型,为void(* )(int),这个函数指针,指向的是函数只有一个参数为int,返回类型为void。

我们发现void (*signal(int , void(*)(int)))(int)这条语句有点难以看懂,那么我们怎样这条语句给简化呢?

       typedef void(* pfun)(int);//给void(*)(int)这个类型取一个别名为pfun
        
       pfun signal(int ,pfun);

给void(*)(int)这个指针函数取pfun别名后,注意这个别名必须在(*)里面,那么void (*signal(int , void(*)(int)))(int)这个代码就可以改为 pfun signal(int ,pfun),这样是不是容易看多了。

通过函数指针去调用函数:

(*函数指针变量){实参列表}或函数指针变量{实参列表};

	int ret=(*pf)(2, 3);//通过函数指针去调用函数
    //或者int ret=pf(2,3);
    //(*pf)(2, 3)等价于Add(2,3)

上面的语句中调用函数指针pf指向的函数,实参为2和3,返回赋值给变量c。

6.函数指针数组

函数指针是一个变量,那么变量就可以放在一个数组里。相同类型函数指针放在一个数组里,则这个数组称为函数指针数组

函数指针数组里元素必须为相同类型的函数指针。

定义:函数指针类型  数组名[ ] 

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int(*parr[2])(int, int) = { Add,Sub };//parr为函数指针数组

我们之前说过“ [ ]"的优先级比” * “比要高,所以parr先与[ ]结合,所以parr为函数指针数组,这个数组存储的元素的是类型函数指针类型,为int(* )(int,int)。

例题:通过函数指针数组写一个简单的计算器;

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 Menu()
{
	printf("#######################\\n");
	printf("##1.Add     2.Sub    ##\\n");
	printf("##3.Mul     4.Div    ##\\n");
	printf("##     0.exit        ##\\n");
	printf("#######################\\n");

}


int main()
{
	int (* parr[5])(int, int) = { 0,Add,Sub,Mul,Div };//将函数指针存在parr数组里
	int input = 0;
	int x = 0, y = 0;
	do
	{
		Menu();
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出成功");
			break;
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个值:\\n");
			scanf("%d %d", &x, &y);
			int ret = arr[input](x, y);
			printf("%d\\n", ret);
		}
		else
		{
			printf("输入错误,请重新选择\\n");
		}
	} while (input);
	return 0;
}

7.指向函数指针数组的指针

既然有函数指针数组,那么就有指向函数指针数组的指针。

指向函数指针数组的指针定义:

    int(*parr[2])(int, int) = { Add,Sub };//函数指针数组

    int(*(*pparr)[2])(int, int) = parr;//指向函数指针数组的指针

" * " 先与pparr结合,确定pparr为指针,指向的是一个存储函数指针类型的数组,且这个数组有两个元素。

8.回调函数

回调函数就是一个 通过函数指针调用的函数 。如果 你把函数的指针(地址)作为参数传递给另一
个函数 当这个指针被用来调用其所指向的函数时,我们就说这是回调函数 。回调函数不是由该
函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或
条件进行响应。
例如:

例题:利用回调函数去写一个简单的计算器;

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 Menu()
{
	printf("#######################\\n");
	printf("##1.Add     2.Sub    ##\\n");
	printf("##3.Mul     4.Div    ##\\n");
	printf("##     0.exit        ##\\n");
	printf("#######################\\n");

}

void Cal(int(* p)(int,int))
{
	int x = 0, y = 0;
	int ret = 0;
	printf("请输入两个数:");
	scanf("%d %d", &x, &y);
	ret = p(x, y);
	printf("%d\\n", ret);
}

int main()
{
	int input = 0;
	do
	{
		Menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			Cal(Add);//Cal通过Add指针去调用Add函数
			break;
		case 2:
			Cal(Sub);//Cal通过Sub指针去调用Add函数
			break;
		case 3:
			Cal(Mul);//Cal通过Mul指针去调用Add函数
			break;
		case 4:
			Cal(Div);//Cal通过Div指针去调用Add函数
			break;
		case 0:
			printf("退出成功\\n");
			break;
		default:
			printf("选择错误,请重新选择\\n");
			break;
		}
	} while (input);
	return 0;
}

9.qsort的使用以及它的底层原理

在c语言中,有这样一个qsort函数,它可以排序任意类型的数组,其中它这个函数就使用了函数回调的方法。

它的参数如下:

void qsort( void *base, 
	        size_t num, 
			size_t width,
		    int ( *compare )(const void *elem1, const void *elem2 ) );

之前说过void*可以接受任意类型的指针,为了排序任意类型的数组,所以void*指针是很有必要的。

      其中的base是要排序的数组的首元素的指针,num是数组中有多少个元素,width是数组中元素的宽度,compare是比较函数的函数指针(你想用什么方法比较,你就自己写一个比较函数,)qosort函数会通过这个函数去调用这个compare这个函数。

那么我们来看一下qsort这个函数怎么使用:

struct person
{
	int age;
	char ch;
};

//stuct person类型的数组
struct person str[3] = { {20,'b'},{30,'c'},{25,'a'} };

假设我们要对str数组进行排序,那么我们有两种方式对它排序,一种是按age比较进行排序,一种是按ch比较进行排序,这得根据我们写的compare是对数组以什么样的方式排序。

 例如,我们想要按age的比较的方式,则我们可以写这样一个compare的函数:

int cmp_int(const void* e1, const void* e2)
{
	return ((struct person*)e1)->age - ((struct person*)e2)->age;
}

则我们先将e1和e2的类型强制转换为struct person*的类型,然后将解引用找到age,再对它们进行比较,

  如果compar返回值小于0(< 0),那么p1所指向元素会被排在p2所指向元素的前面

  如果compar返回值等于0(= 0),那么p1所指向元素与p2所指向元素的顺序不确定

  如果compar返回值大于0(> 0),那么p1所指向元素会被排在p2所指向元素的后面

如果我们想排一个升序(从小到大),则可以这样写:

return ((struct person*)e1)->age - ((struct person*)e2)->age;

如果排一个逆序(从大到小:则可以这样写:

return ((struct person*)e2)->age - ((struct person*)e1)->age;

那么我们将cmp_int传给qosrt,让它对我们进行排序,则:

	struct person str[3] = { {20,'b'},{30,'c'},{25,'a'} };
	int sz = sizeof(str) / sizeof(str[0]);//计算出数字有多少个元素变量
	qsort(str, sz, sizeof(str[0]), cmp_int);

 运行结果:

结果是按age从小到大排序。

若我们想要按ch的比较的方式来排序,则可以:

int cmp_char(const void* e1, const void* e2)
{
	return ((struct person*)e1)->ch - ((struct person*)e2)->ch;
}

 则运行结果为:

我们可以看到,运行结果则按ch从小到大进行排序。

好了,既然我们知道qsort怎样使用后,那么我们用冒泡排序的思想去实现一个类似qsort的函数,能够排任意类型的函数。

(qsort的底层是快速排序的思想,冒泡排序的思想较容易理解)

那么什么是冒泡排序思想是什么呢?

两两比较,然后将最大的数放在最后一个,其次在找出第二大的数,放在最后第二个........

直到排序完成。

         

 我们再来模拟实现:

void Swap(char* p1, char* p2,size_t width)
{
	for (int i = 0; i < width; i++)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
}

void Bubble_sort(void* base, size_t num, size_t width, int cmp(const void* elem1, const void* elem2))
{
	for (int i = 0; i < num-1; i++)//第一趟比较
	{
		for (int j = 0; j < num - i-1; j++)//每一趟比较的次数
		{
			if (cmp((char*)base + j * width, (char*)base + (j+1)* width)>0 )
			{
				Swap((char*)base + j * width, (char*)base + (j+1)* width,width);
			}
		}
	}	
}

num-1是数组需要进行多少趟的比较。

例如:有一个数组的元素个数为10,那么它就需要进行9趟的比较。

num-i-1是数组每一趟比较需要进行多少次的比较。

例如:有一个数组的元素个数为10,它的第一趟比较的次数就是选出最大的数放在最后面,i是0,所以第一趟的比较次数是9次。

我们再来看这个cmp:

cmp((char*)base + j * width, (char*)base + j * width+ width)

首先,将base指针转换为(char*)指针,因为base是void*指针,而且char*指针为最小单位指针,指针加减整数以一个字节

进行移动,width大小能够让指针指向下一个数据时需要走多少个字节,如int类型,指向下一个数据时需要走4个字节,j代表的

是位于数组下标第几个元素,,(char*)base+j*width代表的是指向数组下标为j的元素的指针,(char*)base + (j+1)* width代表

的指向是数组下标为j+1的元素的指针。

接下来我们再看Swap:

Swap((char*)base + j * width, (char*)base + (j+1)* width,width)

     既然我们知道元素的地址,但我们要交换任意类型的数据,所以我们通过一个字节一个字节的交换整个元素,所以我们就需要

元素的宽度。

通过代码我们可以发现,无论我们传什么类型的元素的数组,我们都可以将它们进行排序,不过这就需要要我们写的比较函数,同时我们发现

void*指针,和回调函数发挥了它们应有的作用,如果没有这两个,则任意类型的排序就可能实现不了。

指针练习题及解析

训练一

int a[] = {1,2,3,4};
printf("%d\\n",sizeof(a));
printf("%d\\n",sizeof(a+0));
printf("%d\\n",sizeof(*a));
printf("%d\\n",sizeof(a+1));
printf("%d\\n",sizeof(a[1]));
printf("%d\\n",sizeof(&a));
printf("%d\\n",sizeof(*&a));
printf("%d\\n",sizeof(&a+1));
printf("%d\\n",sizeof(&a[0]));
printf("%d\\n",sizeof(&a[0]+1));

答案:

16,数组名单独放在sizeof里面是计算整个数组的大小,所以为16个字节

4/8,a+0代表的是数组首元素的地址,在32位平台的机器下是4个字节,在64位平台下是8个字节。

4,*a代表的是数组第一个元素,为4个字节。

4/8,a+1代表的是数组第二个元素的地址。

4,a[4]代表的是数组第二个元素。

4/8,&a代表的是整个数组的地址.

16,*&a代表整个元素。

4/8,&a代表的是整个数组的地址,&a+1则跳过整个数组,是下一块16个字节的地址

 4/8,代表的数组第一个元素的地址。

 4/8,代表数组第二个元素的地址。

//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\\n", sizeof(arr));
printf("%d\\n", sizeof(arr+0));
printf("%d\\n", sizeof(*arr));
printf("%d\\n", sizeof(arr[1]));
printf("%d\\n", sizeof(&arr));
printf("%d\\n", sizeof(&arr+1));
printf("%d\\n", sizeof(&arr[0]+1));

printf("%d\\n", strlen(arr));
printf("%d\\n", strlen(arr+0));
printf("%d\\n", strlen(*arr));
printf("%d\\n", strlen(arr[1]));
printf("%d\\n", strl

以上是关于建议收藏两万字深度解读 指针 ,学好指针看这一篇文章就够了的主要内容,如果未能解决你的问题,请参考以下文章

一篇博文:带你TypeScript入门,两万字肝爆,建议收藏!

学会Java输入输出流,看这一篇就够了,建议收藏!

考试C语言,指针不会?看这一篇博客就够了

想要彻底搞懂大厂是如何实现Redis高可用的?看这篇文章就够了!(1.2W字,建议收藏)

两万字博文教你python爬虫requests库,看完还不会我把我女朋友都给你❤️熬夜整理&建议收藏❤️

两万字博文教你python爬虫requests库,看完还不会我把我女朋友都给你❤️熬夜整理&建议收藏❤️