资讯专栏INFORMATION COLUMN

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

zhkai / 1423人阅读

摘要:在位机器上,指针变量的大小为个字节。指针类型的强制类型转换对指针变量进行强制类型转换的一般形式将保存的类型指针强制转换为类型指针后赋值给,其中还是为,没有改变。

前言

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

大纲如下:

 

目录

前言

 指针初阶

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的元素。

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

#includeint 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 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)();//代码2void (*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", strlen(&arr));printf("%d/n", strlen(&arr+1));printf("%d/n", strlen(&arr[0]+1));

答案:

6,数组名多带带放在sizeof里面是计算整个数组的大小

4/8,arr+0为数组首元素的地址

1,*arr代表的是数组第一个元素,char的类型为一个字节

1,a[1]代表的是数组第一个元素。

4/8,&arr代表的是数组的地址。

4/8,&arr+1代表的是跳过整个数组,指向下一块6个字节内存空间的地址

4/8,&arr[0]&

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/119406.html

相关文章

  • 一篇够了建议收藏)——超详解sizeof与strlen的用法

    摘要:万字详解与的用法数组名的意义一维数组用法字符数组用法的用法字符串数组用法的用法指针与字符串用法用法二维数组数组名的意义在讲所有东西之前,需要先明确一个关键问题数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单 ...

    Taonce 评论0 收藏0
  • 糊涂算法之「八大排序」总结——用两万,8张动图,450行代码跨过排序这道坎(建议收藏

    摘要:今天,一条就带大家彻底跨过排序算法这道坎,保姆级教程建议收藏。利用递归算法,对分治后的子数组进行排序。基本思想堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为,它也是不稳定排序。 ...

    greatwhole 评论0 收藏0
  • 【转】成为Java顶尖程序员 ,看这10本书够了

    摘要:实战高并发程序设计这本书是目前点评推荐比较多的书,其特色是案例小,好实践代码有场景,实用。想要学习多线程的朋友,这本书是我大力推荐的,我的个人博客里面二十多篇的多线程博文都是基于此书,并且在这本书的基础上进行提炼和总结而写出来的。 学习的最好途径就是看书,这是我自己学习并且小有了一定的积累之后的第一体会。个人认为看书有两点好处:showImg(/img/bVr5S5);  1.能出版出...

    DTeam 评论0 收藏0
  • ❤️学懂C语言文件操作读这篇够了(万总结,附习题)❤️

    目录 ​​​ 一,写在前面 二,为什么使用文件 1,原因 2,数据流 3,缓冲区(Buffer) 4,C语言中带缓冲区的文件处理 5,文件类型 6,文件存取方式 三,什么是文件 1,程序文件  2,数据文件 3,文件名 四,文件的打开和关闭  1,文件指针  2,文件的打开和关闭 五,文件的顺序读写 1,功能 2,代码实现 六,文件的随机读写 1,fseek 2,ftell 3,rewind 七,...

    Genng 评论0 收藏0
  • Lombok 看这够了

    摘要:注解在类上为类提供一个全参的构造方法,加了这个注解后,类中不提供默认构造方法了。这个注解用在类上,使用类中所有带有注解的或者带有修饰的成员变量生成对应的构造方法。 转载请注明原创地址:http://www.54tianzhisheng.cn/2018/01/07/lombok/ showImg(http://ohfk1r827.bkt.clouddn.com/blog/180107/7...

    LeanCloud 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<