本文主要是介绍【c语言】——两万字深度解读 指针 ,学好指针看这一篇文章就够了,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前言
大家好,我是努力学习的少年,今天这篇文章是专门写关于指针的知识点,因为指针内容比较多,所以我将指针的这篇文章我将它分为两部分,第一部分是基础篇,是从零开始学习一些基本概念,第二部分是进阶篇,如果你指针基础学得差不多了,你可以尝试学习进阶篇的指针,这部分的内容相对较难一些,学完这部分内容,你的指针知识点基本就学的差不多了,最后还有指针的笔试题,这部分的题需要通过我们学到的指针的知识去笔算,这样有利于巩固我们的知识,并有一个更深的理解。
大纲如下:
目录
前言
指针初阶
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变量改为100printf("%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);//输出11223344printf("%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]);//输出-9printf("%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");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("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)这个类型取一个别名为pfunpfun 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]+1代表的是第二个元素的地址。
随机值,arr数组中没有\0,strlen遇到\0才停下。
随机值
错误,*arr是’ a ',ascll码值为97,strlen是一个函数,它的参数为const char*,则传参的时候会把97强制转换为char*类型,由于我们不知道地址97指向是什么值,所以传进去的参数为野指针,所以会发生错误
错误,同上
随机值,
随机值,strlen(&arr))的值比 srlen(&arr+1))大
随机值,strlen(&arr))的值比strlen(&arr[0]+1))的值大6.
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
答案:
4/8,p为字符串”abcdef“首元素的地址。
4/8,p+1为字符串”abcdef“第二个元素的地址。
1,*p为字符串”abcdef“第一个元素
1,p[0]为字符串”abcdef“第一个元素。
4/8,代表的是p的地址。
4/8,&p+1则代表的是指向p后面的一个变量。
4/8,指向的是b的地址。
6,从字符a开始计算个数,直到遇到\0就结束
5,从字符b开始计算个数,直到遇到\0就结束
错误。野指针
错误。野指针
随机值,因为p后面不知道什么时候有\0,
随机值,strlen(&p)的值比strlen(&p+1)的值大1.
总结:sizeof(操作数)计算操作数的所占空间大小,计算大小的时候,不在乎内存中放的值
strlen只使用:字符串,字符数组
5,从字符b开始计算个数,直到遇到\0就结束
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
答案:
48, 数组名a放在sizeof里面,计算整个二维数组的大小。
4, a[0][0]代表的是二维数组中第一行第一个元素
16, a[0]代表的是第一行的数组名,数组名单独放在sizeof,计算的是整个数组的大小,所以计算二维数组中第一行数组的大小
4/8,a[0]是第一行的数组名,没有单独放在sizeof里面,所以a[0]是第一行第一个元素的地址,a[0]+1是第一行第二个元素的地址
4,*(a[0]+1)代表的是二维数组第一行第二个元素
4/8,a是二维数组的数组名,没有单独放在sizeof里面,所以代表二维数组首元素的地址,a+1代表的是二维数组的第一个元素的地址,
及第二行数组的地址。
16,*(a+1)计算二维数组第二行数组的大小。
4/8, &a[0]为二维数组中第一行的地址,&a[0]+1为二维数组中第二行的地址。
16,*(&a[0]+1))计算二维数组第二行数组的大小
*a,计算二维数组第一行的大小
16,此处不越界访问,sizeof内部是不进行运算的,所以arr[3]在sizeof是没有进行访问的,arr[3]就是第4行的数组名,只确定arr[3]的大小,
然后计算它的总大小。
训练二
题一
int main()
{int a[5] = { 1, 2, 3, 4, 5 };int *ptr = (int *)(&a + 1);printf( "%d,%d", *(a + 1), *(ptr - 1));return 0; }
//程序的结果是什么?笔算
运行结果为:2,5
a为数组名,代表首元素的地址,为int*,a+1是第二个元素的地址,即2的地址,*(a+1)就是2
&a是整个数组的地址,&a+1则会跳过一整个数组,则ptr则数组5后面一位的地址,由于ptr为int*类型,所以ptr-1向前走4个字节的单位,即指向5的地址
题二:
//已知该结构体的大小为32个字节。
struct Test
{int Num;char* pcName;short sDate;char cha[2];short sBa[4];
}*p;int main()
{p = (struct Test*)0x100000;printf("%p\n", p + 0x1);printf("%p\n", (unsigned long)p + 0x1);printf("%p\n", (unsigned int*)p + 0x1);return 0;
}
%p是按指针形式进行十六进制进行打印数据,(在64位机器上,如果输出不满16个数,则左边补0,在32位机器上,如果输出不满8个数,则左边补0。)
输出:
0000000000100020
0000000000100001
0000000000100004
0x1代表的是1,因为p是struct Test*类型,所以+1跳过32个字节,就相当于加上0x100000+32,32的十六进制是0x20,因为输出结果是按指针形式的十六进制打印数据,所以第一个输出结果0000000000100020
(unsigned long)p呗转换为整数,整数+1就是+1,则输出结果为0000000000100001
(unsigned long*)p则转换为整形指针,则+1会跳过4个字节,就相当于0x100000+4,所以输出
0000000000100004
题三:
int main()
{int a[4] = { 1, 2, 3, 4 };int *ptr1 = (int *)(&a + 1);int *ptr2 = (int *)((int)a + 1);printf( "%x,%x", ptr1[-1], *ptr2);return 0;}
输出:
4, 2000000
ptr1是数组后一位值的地址,ptr[-1]相当于*(ptr+(-1)),由于ptr是int*类型,所以-1是向前走一个int类型大小,指向4的位置的地址,在解引用就是4.
假设数组a的地址是0x10000002,则该地址转换为整形后再加1,则变为0x10000002,则跳过一个字节,在把它转化为int*赋值给ptr2,如下图:
(假设我的机器是32位,且为小端存存储)
下面按16进制进行存储,每一个代表一个字节。
*ptr2,取出ptr2中的值,由于ptr*为int*类型指针,所以取出的是4个字节的值,小端存储,取出来也是按小端取出来,即 02 00 00 00,由于%x如果前面有0则会被省略掉,则打印2000000
题四:
#include <stdio.h>
int main()
{int a[3][2] = { (0, 1), (2, 3), (4, 5) };int *p;p = a[0];printf( "%d", p[0]);return 0;
}
输出:1
(0,1)(2,3)(4,5)括号里面是一个逗号表达式,运算结果分别是1,3,5,所以二维数组存储的数据是1,3,5,即如图所示。a[0]代表的是第一行首元素地址,即1的地址,即结果输出1.
题五:
int main()
{int a[5][5];int(*p)[4];p = a;printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);return 0;
}
运行结果为:
FFFF FFFC
-4
p为int(*)[4]类型指针,+1能够向后走4个int类型个字节,p[4][2]相当于*(*(p+4)+2),(p+4)即向后走16个int类型大小个字节,(*(p+4)+2)即跳过18个int类型大小个字节,所以&(*(p+4)+2)是a[3][2]的地址,指针减指针表示这中间相差多少个元素,所以&a[3][2]-&a[4][2]为-4, %p是按指针形式进行十六进制进行打印数据
假设在32位机器上:
-4的二进制
原码:10000000 00000000 00000000 00000100
反码:11111111 11111111 11111111 11111011
补码:11111111 11111111 11111111 11111100
按十六进制进行打印结果为:
FFFF FFFC
题六:
int main()
{int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };int *ptr1 = (int *)(&aa + 1);int *ptr2 = (int *)(*(aa + 1));printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));return 0;}
输出:10,5
&aa是二维数组的指针,&aa+1则跳过整个数组,所以ptr1是二维数组后一位数,且ptr1为int*类型,所以ptr-1则向前走4个字节,指向10的位置,aa是二维数组首元素的地址,即第一行的地址,
所以aa+1则是第二行的地址,*(aa+1)则代表的是a[1]为第二行的数组名,代表的是第二行第一个元素的地址,即为6的地址,所以ptr2-1向前走4个字节,指向5的的地址。
题七:
int main()
{char *a[] = {"work","at","alibaba"};char**pa = a;pa++;printf("%s\n", *pa);return 0;
}
输出:at
a存储的是3个char*类型的指针数组,存储的是“work”,“at”,“alibaba”三个字符串首元素的地址。pa是存储数组a第一个元素,pa++相当于pa=pa+1,所以pa指向第二个元素,*pa相当于就是 相当于找到“at”的首元素的地址。并且%s是传char*类型参数, 输出到\0为止.
题八:
int main()
{char *c[] = {"ENTER","NEW","POINT","FIRST"};char**cp[] = {c+3,c+2,c+1,c};char***cpp = cp;printf("%s\n", **++cpp);printf("%s\n", *--*++cpp+3);printf("%s\n", *cpp[-2]+3);printf("%s\n", cpp[-1][-1]+1);return 0;}
输出:
POINT
ER
ST
EW
++cpp后 cpp就指向c+2。
*cpp为c+2,**cpp则为“POINT"的首元素的地址,所以输出POINT。
再++cpp,cpp则指向c+1,*++cpp为c+1
--*++cpp代表的是c+1指向c数组中第一个元素,如下图
*--*++cpp代表的是字符串”ENTER"首元素的地址,*--*++cpp+3代表的是字符串”ENTER"中E的地址,所以输出为ER。
cpp[-2]等价于*(cpp-2),cpp-2指向cp数组中第一个元素,所以cpp[-2]为c+3
*cpp[-2]代表的是“FIRST”字符串首元素的地址, *cpp[-2]+3是“FIRST”字符串中的字符S的地址,
所以输出ST
cpp[-1][-1]相当于*(*(cpp-1)-1),其中cpp-1指向c+2。
*(cpp-1)为c+2,(cpp-1)-1,则代表的是c+2指向c数组的第二个元素,*(*(cpp-1)-1)是
则为“NEW”字符串首元素的地址,所以cpp[-1][-1]+1是字符串“NEW”中字符E的地址,所以输出EW。
好啦,今天的分享就到这里了,由于这篇文章比较长,其中可能会出现一些错误,如果你发现有错误,请及时指正,如果这篇文章对你有用的话,希望你可以给我个赞或收藏,你的点赞、收藏、关注将是我分享的巨大的动力,我后面将总结更多的知识点给大家。谢谢你们能够看到这里。
这篇关于【c语言】——两万字深度解读 指针 ,学好指针看这一篇文章就够了的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!