建议收藏两万字深度解读 指针 ,学好指针看这一篇文章就够了
Posted 努力学习的少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了建议收藏两万字深度解读 指针 ,学好指针看这一篇文章就够了相关的知识,希望对你有一定的参考价值。
前言
大家好,我是努力学习的少年,今天这篇文章是专门写关于指针的知识点,因为指针内容比较多,所以我将指针的这篇文章我将它分为两部分,第一部分是基础篇,是从零开始学习一些基本概念,第二部分是进阶篇,如果你指针基础学得差不多了,你可以尝试学习进阶篇的指针,这部分的内容相对较难一些,学完这部分内容,你的指针知识点基本就学的差不多了,最后还有指针的笔试题,这部分的题需要通过我们学到的指针的知识去笔算,这样有利于巩固我们的知识,并有一个更深的理解。
大纲如下:
目录
指针初阶
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;
}
最后输出的是:
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入门,两万字肝爆,建议收藏!
想要彻底搞懂大厂是如何实现Redis高可用的?看这篇文章就够了!(1.2W字,建议收藏)