目录
本章重点
1. 指针是什么
2. 指针和指针类型
3. 野指针
4. 指针运算
5. 指针和数组
6. 二级指针
7. 指针数组
8. 字符指针
9.数组指针
10. 指针数组
11数组传参和指针传参
12. 函数指针
13. 函数指针数组
14. 指向函数指针数组的指针
15. 回调函数
16 指针和数组面试题的解析
1. 指针是什么
我们口语讲到指针,比如p指针,其实想要表达的意思就是p是一个指针变量
取地址操作符&a取出a的地址,看上图代码
给大家画个图让大家初步认识一下学习指针需要用到的基本理论
32位机器上我们有32根物理电线,32根地址线通电后产生的电信号转化为数字信号,随机产生0 1组成的这样的二进制随机序列,一共有2的32次方个全0到全1的二进制序列
2的32次方个字节,每个地址标识一个字节,那我们就可以给2^32Byte == 2^32/1024KB == 232/1024/1024MB==232/1024/1024/1024GB == 4GB,在32位机器上最多能寻址(或者管理)4GB的空间,至于64位机器,大家参照32位机器的计算方式可以自己计算一下
总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是:
一个小的单元到底是多大?(1个字节)
如何编址?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);那么32根地址线产生的地址就会是:
2. 指针和指针类型
这里就有2的32次方个地址。
每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==
232/1024/1024MB==232/1024/1024/1024GB == 4GB) 4G的空间进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。
这里我们就明白:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节
在64位机器上,64个比特位就能存放64个01组成二进制数
指针变量是用来存放地址的,地址是唯一标示一个内存单元的
指针的大小在32位平台是4个字节,在64位平台是8个字节
下面指针类型的讲解,大家先别慌,先跟我的思路走,看下图
在X86环境下,打印的指针大小都是4个字节
在X64环境下,打印的指针大小都是8个字节
我们按住F10调试起来看到a在内存中的存储是倒着存放的,为什么呢,大家可以先不管,如果感兴趣,可以去我的数据在内存中的存储那两篇博客中去看一下,不懂的可以在评论区提问
下图是按住F10调试起来给大家有一些疑惑的地方做了一些注释,希望大家能够理解
我们将*pa赋值为0,可以看出全部改成了0
这是因为指针类型决定了解引用操作的权限,看下图
下面给大家讲解指针±整数
指针±一的细节已经给出大家代码和讲解了,希望大家能够理解
3. 野指针
下面是一个很有意思的知识点:野指针
概念:野指针就是指针指向的位置是不可知的 随机的 不正确的 没有明确限制的
野指针第一种情况
指针未初始化
野指针第二种情况
指针越界访问
当P指向超过数组范围的位置时,P就已经越界访问了,这就是第二钟野指针的情况
第三种情况
*局部变量的作用域进入函数创建,出来函数就销毁,但是return &a把a的地址带回去了,p又去访问,则就是非法访问
我们必须对指针进行严格地初始化,养成良好的代码习惯
例如,指针p使用完了之后需将其置为NULL(空)
4. 指针运算
接下来是第四个部分
指针的第一种和第二种运算已经给大家画出图了,大家自行阅读,理解起来还是比较容易
最后一种运算
对比简化前的代码简化后的代码,大家对比起来看,第二种代码就发生了错误
5. 指针和数组
上次我们已经把前4个部分给大家讲完了,现在我们来讲一下后面三个部分
首先看数组和指针
指针和数组之间是什么关系呢?
指针变量就是指针变量,不是数组,指针变量的的大小是4个或8个字节,专门用来存放地址
数组就是数组,不是指针,数组一块连续的空间,可以存放1个或者多个类型相同的数据
联系:
在数组中,数组名其实就是数组首元素的地址,数组名 == 地址 == 指针
当我们知道数组首元素的地址的时候,因为数组是连续存放的,所以通过指针就可以遍历数访问数组,组是可以通过指针来访问的
下面我们打开VS2019来看代码
当我们知道起始位置的时候,地址加 i 到所在位置的下一个位置的地址,我们打印出来看一下
数组名就是首元素的地址,p指针变量里面放的是数组arr首元素的地址,地址+i想后访问下一个地址,在用*解引用操作找到该地址对应的值,希望大家能够理解
6. 二级指针
看下面代码
对intpp 做一下解释,int是在说明pp指向的是int类型的变量,pp前面的那一颗*是在说明pp是指针变量,这也就对上面代码做了解释,pp就等于p,希望大家能够理解
我们给定三个字符数组,再给定一个字符指针数组,再用一个二级指针存放一级指针的地址
看下图,字符指针数组本质上是数组,,里面是存放的指针(也就是存放的三个字符数组的首元素的地址),二级指针存放一级指针的地址,也就是存放的是数组parr的首元素的地址arr1,希望大家能够理解**
7. 指针数组
再看一个例子,大家用上个例子中相同的方法先去自己分析,然后再看解析,我把解析放在下面供大家参考
来看最后一个知识点,指针数组,其实刚才已经给大家讲到了,指针数组本质上是一个数组,就比如你是一个好孩子,你本质上是一个孩子,指针数组也是同样的道理,本质上是一个数组,数组里面存放的是指针(存放地址(如果是数组,就是表示存放的是数组首元素的地址)),
我们来看最后一行代码
这个代码本质上是模拟出来一个二维数组,不是真正意义上的二维数组,真实的二维数组在内存中是连续存储的,我们来详细讲解一下
这里的模拟出来的二维数组是由三个一位数组维护出来的二维数组,直接用二维数组的打印形式可以,用地址,再用*解引用操作也是可以的,根据自己的喜好
我们来看代码运行结果
8.字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char*
我们来看代码
#include<stdio.h>
int main()
{
char arr[] = "abcdef";
char* pc = arr;
printf("%s\n", arr);
printf("%s\n", pc);
return 0;
}
为什么打印的结果是一样的呢?首先把一个字符串存放到字符数组里面,数组名赋给pc,pc就相当于指向那个字符数组,所以打印的内容是一样的
我们再看下一个代码
#include<stdio.h>
int main()
{
char* p = "abcdef";//"abcdef"是一个常量字符串,这里相当于把字符串首元素的地址a放到了p当中,我们打印出来看效果
printf("%s\n", p);
return 0;
}
我们接着看代码
#include<stdio.h>
int main()
{
char* p = "abcdef";//"abcdef"是一个常量字符串,这里相当于把字符串首元素的地址a放到了p当中,我们打印出来看效果
*p = 'w';
printf("%s\n", p);
return 0;
}
既然我是常量字符串就不能被修改,大家理解一下,是不是只有变量才有被修改的可能
上述代码就报错了,希望大家能够理解,这种代码一律不能被修改,这是当我们联想一下const,我们来看代码
这样我们就完全把这个字符串限制死了,不能被修改
#include<stdio.h>
int main()
{
const char* p = "abcdef";//"abcdef"是一个常量字符串,这里相当于把字符串首元素的地址a放到了p当中,我们打印出来看效果
*p = 'w';
printf("%s\n", p);
return 0;
}
来看下面代码
#include<stdio.h>
int main()
{
char arr1[] = "abcdef";
char arr2[] = "acbdef";
char* p1 = "abcdef";
char* p2 = "abcdef";
if (arr1 == arr2)
{
printf("hehe\n");
}
else
{
printf("haha\n");
}
return 0;
}
打印haha,希望大家能理解,但是大家仔细看一看下一个代码呢
#include<stdio.h>
int main()
{
char arr1[] = "abcdef";
char arr2[] = "acbdef";
char* p1 = "abcdef";
char* p2 = "abcdef";
if (*p1 == *p2)
{
printf("hehe\n");
}
else
{
printf("haha\n");
}
return 0;
}
也正因为这个原因 C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。
#include<stdio.h>
int main()
{
int arr[10] = { 0 };//整型数组
char ch[5] = { 0 };//字符数组
int* parr[4];//存放整型指针的数组--指针数组
char* pch[5];//存放字符指针的数组--指针数组
return 0;
}
解释附在代码后面的了,希望大家能够理解,我们再来看下列代码
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int* arr[4] = { &a,&b,&c,&d };
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d ", *(arr[i]));
}
return 0;
}
虽然我们打印出来了结果,但是这不是指针数组的本质用法,我们来看下列优化过的代码
#include<stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* parr[] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", *(parr[i] + j));
}
printf("\n");
}
return 0;
}
指针数组里面存放的是各个数组的首元素的地址,地址加依次1解引用操作得到后面的数据,就得到了一个二维数组,这就是指针数组的正常操作方式
9.数组指针
数组指针
数组指针的定义
数组指针是指针?还是数组?
答案是:指针
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针
浮点型指针: float * pf; 能够指向浮点型数据的指针
那数组指针应该是:能够指向数组的指针
我们来看代码
#include<stdio.h>
int main()
{
//数组指针--指向数组的指针--存放数组的地址
//arr-首元素的地址
//&arr[0]-首元素的地址
//&arr-数组的地址
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;//这里涉及到了()优先级
return 0;
}
下面代码哪个是数组指针?
int* p1[10];
int(*p2)[10];
//p1, p2分别是什么?
int(*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
我们来看下面代码,(pa)表示这是一个数组指针,数组里面有10个元素,把数组的地址取出来放到pa里面,然后再解引用操作又得到了原来的那个数组int arr[10],大家觉得这个方法是不是多此一举,我们的数组指针用在二维数组里面会更加合适
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*pa)[10] = &arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(*pa + i));//这里的*pa == arr;
}
return 0;
}
我们再来看一下数组指针用于二维数组的情况
#include<stdio.h>
void print1(int arr[3][5], int x, int y)
{
int i = 0;
int j = 0;
for (i = 0; i < x; i++)
{
for (j = 0; j < y; 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} };
print1(arr, 3, 5);
return 0;
}
这是常规操作打印二维数组的情况,数组名传参是可以直接拿一个数组来接收的,代码运行结果我们也可以看到结果正确
下面我们用指针数组来实现,看代码
10.指针数组
这个代码是什么意思呢,首先我们知道二维数组的数组名也是首元素的地址,但是他并不是像一维数组里面的数组名一样是首元素的地址,这时我们想到在初始数组那篇博客讲到的把二维数组看成一个一维数组,所以二维数组的数组名就是首元素的地址也就是第一行一维数组的地址,然后后面我们遍历整个二维数组的时候就是第一行的地址加j先遍历第一行的元素,同理,第二行加j遍历第二行的元素,所以,这就是数组指针应用于二维数组的情况,希望大家能够理解
我把代码给大家,对代码的解释也附在代码后面了
#include<stdio.h>
void print2(int(*p)[5], int x, int y)
{
int i = 0;
for (i = 0; i < x; i++)
{
int j = 0;
for (j = 0; j < y; j++)
{
printf("%d ", * (*(p + 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} };
//print1(arr, 3, 5);//arr--二维数组的数组名--数组名(首元素的地址),对于二维数组来说,它的首元素的地址是谁呢?这里我们应该把二维数组想象成一个一维数组,前面数组的讲解已经给大家讲到了,也就是表示二维数组第一行的地址
print2(arr, 3, 5);
return 0;
}
当然你也可以写成
p[i][j];
*(p[i] + j));
*(*(p + i) + j));
(*(p + i))[j]);
下面给大家四个代码来判断一下
看代码
int arr[5];----arr是一个5个元素的整型数组
int parr1[10];----parr1是一个数组,数组有10元素,每个元素的类型是int,parr1是一个指针数组
int (*parr2)[10];----parr2是一个指针,改指针指向了一个数组(10个元素),每个元素的类型为Int,parr2是一个数组指针
int (*parr3[10])[5];----大家先想一下这是个什么东西,其实parr3是一个数组,有10个元素,每个元素是一个数组指针,这个指针能够指向5个元素的这一个数组,每个元素是Int
11.数组参数、指针参数
我们来看一维数组传参
#include<stdio.h>
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int* arr)//ok?
{}
void test2(int* arr[20])//ok?
{}
void test2(int** arr)//ok?
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
我们来分析一下上面5个传参的方式,首先看第一个,数组名传参可以用一个数组接受,数组的大侠可以省略也可以补省略,数组名就是首元素的地址,也可以用一个指针来接收,对于指针数组来说,数组名传参也可以用一个数组接收,并且数组的每个元素的类型为int*
对于最后一种,数组名是首元素的地址,指针数组的第一个元素的地址是一个一级指针,需要用一个二级指针来接收
我们接着来看二维数组传参
#include<stdio.h>
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int(*arr)[5])//ok?
{}
void test(int** arr)//ok?
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
我们还是来分析一下代码。二维数组的数组名是首元素的地址(第一行的地址,把二维数组想象成一个一维数组即可)二维数组数组名传参可以拿一个二维数组来接收,但是二维数组数组名传参可已拿一个省略了行的二维数组接收,但是不能拿一个省略了列的二维数组接收,也不能拿一个既省略了行又省略了列的二维数组来接收,二维数组名传参传过来的是首元素的地址第一种方式用于接收一维数组首元素的地址,所以排除,第二种方式是用于接收一级指针变量的地址,第三种方式是用于用于接收二维数组首元素的地址,,把第一行的地址传过去用一个指针接收,这个数组里面有5个元素,这种方式可行,希望大家能够理解
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
void test1(int* p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?
大家可以把答案打在评论区,我会在评论区公布答案
那么对于二级指针传参呢?我们看代码
#include <stdio.h>
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;
}
二级指针传参就直接拿一个二级指针接收,对于一级指针传值,在这里需用一个二级指针接收用于存放一级指针的地址(因为我们这里讲的是用二级指针接收)
思考:
当函数的参数为二级指针的时候,可以接收什么参数
12.函数指针
数组指针—指向数组的指针
函数指针—指向函数的指针
函数指针
我们来看代码
#include<stdio.h>
int Add(int x, int y)
{
}
int main()
{
int a = 10;
int b = 20;
int arr[10] = { 0 };
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
这里大家记住,函数不存在什么首元素和首元素的地址,&函数名和函数名都是函数的
接着看下面代码
#include<stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int arr[10] = { 0 };
int(*pa)(int, int) = Add;
printf("%d\n", (*pa)(2, 3));
/*printf("%p\n", &Add);
printf("%p\n", Add);*/
return 0;
}
结果是5,这里给大家解释一下上述代码的意思,首先我们加一个()说明pa是一个指针,把函数的地址当放到pa里面,然后再说明我们要传的值是(2,3),希望大家能够理解
我们接着来看代码
#include<stdio.h>
void Print(char* str)
{
printf("%s\n", str);
}
int main()
{
void(*p)(char*) = Print;
(*p)("hello bit");
return 0;
}
这就是我们函数指针的概念
输出的是两个地址,这两个地址是 test 函数的地址。 那我们的函数的地址要想保存起来,怎么保存? 下面我们看代码:
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void* pfun2();
首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针? 答案是:
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回 值类型为void
pfun2首先和()结合,他不是一个指针,只是一个函数名而已
13.函数指针数组
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
我们来看一行代码
int (*parr1[10])();
int* parr2[10]();
int (*)() parr3[10];
哪一个才是我们函数指针数组的标准定义呢???,大家先思考一下
答案是:parr1 parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针
我们来看下一行代码感受一下
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
当然这种方法比较麻烦,我们今天用函数指针数组的方式来写一段代码
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div };
while (input)
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf("输入有误\n");
printf("ret = %d\n", ret);
}
return 0;
}
给大家解释一下:函数指针数组是存放函数的地址的一个数组,我们就把这5个函数的地址放到了指针数组里面,然后后面的p[input]通过找到指针数组的下标然后再解引用找到所对应的函数名,这里再给大家解释一下函数指针数组
int(p[5])(int x, int y)—首先p[5]表示这是一个数组,然后再和结合说明数组里面放的是指针(变量)(或者说是地址),然后传递参数的类型说明了这个指针数组里面存放的地址实际上是函数的地址,然后返回类型为int和函数调用的返回类型一定要一,希望大家能够理解
14.指向函数指针数组的指针
指向函数指针数组的指针是一个指针指针指向一个数组 ,数组的元素都是函数指针
我们看代码
根据定义
//int(*(*ppArr)[5])(int, int);
看这个代码,*先和ppArr结合说明ppArr是一个指针,然后再和【5】结合说明这是一个数组指针,我们把(*paArr)【5】去掉来看效果
//int(*)(int, int);
这个代码的意思是*先和()结合说明他是一个指针,再和(int,int)结合说明他是一个函数指针,并且这个函数的返回类型为Int型
15.回调函数
接下来我们来看回调函数
那什么是回调函数呢?
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当 这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调 用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应
那我们来看下面代码
#include<stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
//趟数
for (i = 0; i < sz - 1; i++)
{
//一趟冒泡排序
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
struct Stu
{
char name[20];
int age;
};
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",10} };
bubble_sort(arr, sz);//这个冒泡排序类型只能排序整型,不能排序其他类型
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
那我们如何优化这种代码呢???这里我们需要了解一下qsort库函数
qsort----quick sort快速排序
qsort库函数有4个参数,这里重点说一下最后一个参数,这里的先和compar结合,说明他是一个桌指针,然后再和(const void,const void*e2)结合说明
这是一个函数,这就是一个函数指,且函数的返回类型为int型
给大家解释一下这四个参数分别代表的意思
base:目标数组的起始位置
num:数组的大小(单位为元素),也就是数组有几个元素
size:元素的大小(单位为字节),也就是一个元素的大小(单位为字节)
compar:比较
现在我们来看代码
#include<stdio.h>
#include<stdlib.h>
void qsort(void* base, size_t num, size_t size, int (*compar)(const void* e1, const void* e2));
int cmp_int(const void* e1, const void* e2)
{
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
//struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",10} };//结构体数组
//bubble_sort(arr, sz);//这个冒泡排序类型只能排序整型,不能排序其他类型
qsort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
我们把cmp_int函数放到qsort这个函数中,,函数名和参数值以及返回类型和qsort的第四个参数的函数参数以及函数的返回值是一致的,所以我们就可以通过(compar)找到cmp_int这个函数名(也可以叫做是函数的地址)大家这里可能有个疑问,我们见过int,char*,double*,float等等,但是没有见过void,我这里给大家解释一下,我们来看代码
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
void* p = &a;
p = &pc;
//void*类型的指针,可以接收任意类型的地址
}
指针,可以接收任意类型的地址
当我们要排序其他的类型(int*,char*等等),这是void就可以接收这些类型,就不用我们多次去改变变量类型
我们来看一下void*类型需要注意的事项,我们看代码
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
void* p = &a;
*p = 0;
p++;
//void*类型的指针,可以接收任意类型的地址
//void*类型的指针,不能进行解引用操作
//void*类型的指针,不能进行+-整数的操作
}
我们接着来看刚才我们写到的qsort函数,这里的void指针变量的值不能被改变(不能解引用),我们需要强制类型转换,强制类型转换为int类型的指针就可以进行*解引用了
打印出来是从小到大的升序数组
#include<stdio.h>
#include<stdlib.h>
//void qsort(void* base, size_t num, size_t size, int (*compar)(const void* e1, const void* e2));
int cmp_int(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 };
int sz = sizeof(arr) / sizeof(arr[0]);
//struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",10} };//结构体数组
//bubble_sort(arr, sz);//这个冒泡排序类型只能排序整型,不能排序其他类型
qsort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
结构体数组的快速排序和float类型的快速排序我将在下一个目录开头给大家做详细的讲解!
这里把void强制类型转化为struct Stu,用->操作符找到age这个元素,然后对年龄进行快速排序
//void qsort(void* base, size_t num, size_t size,int (*compar)(const void*e1, const void*e2));
#include<stdio.h>
#include<stdlib.h>
struct Stu
{
char name[20];
int age;
};
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
void test()
{
struct Stu s[3] = { { "zhangsan", 20 }, { "lisi", 30 }, { "wangwu", 10 } };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
int main()
{
test();
return 0;
}
那我们如果要将字符串进行快速排序,我们还是按照age的排序方式来排序嘛?答案是不是,这里我们要用到字符串比较函数strcmp,现在我们来看下列代码,F11走到函数内部进行排序,我们看到的结果就是lisi wangwu zhangsan
//void qsort(void* base, size_t num, size_t size,int (*compar)(const void*e1, const void*e2));
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
char name[20];
int age;
};
int cmp_stu_by_name(const void* e1, const void* e2)
{
//比较名字就是比较字符串
//字符串比较不能直接<>=来比较,需要用到strcmp函数
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
//return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
void test()
{
struct Stu s[3] = { { "zhangsan", 20 }, { "lisi", 30 }, { "wangwu", 10 } };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
test();
return 0;
}
这里再次给大家把qsort函数强调一下
void qsort (void* base, size_t num, size_t size,int (compar)(const void,const void*));
第一个参数:待排序数组的首元素的地址
第二个参数:待排序数组的元素个数
第三个参数:待排序数组的每个元素的大小,单位为字节
第四个参数:是函数指针,比较两个元素的所用函数的地址,这个函数使用者自己实现,函数指针的两个参数是待比较的两个元素的地址
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{
int i = 0;
for (i = 0; i < size; i++)
{
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{
int i = 0;
int j = 0;
for (i = 0; i < count - 1; i++)
{
for (j = 0; j < count - i - 1; j++)
{
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
这里给大家详细解释一下这个代码
为什么我们要写cmp((char*)base + j * size, (char*)base + (j + 1) * size)呢???
因为当j=0的时候,这个函数的参数就代表第一个元素和第二个元素的地址,强制类型转化为char而不是Int的原因是强制类型转化为Int然后+1会让地址向后移动四个字节,对于如果我们给你传过来的是一个结构体类型,double型,float型就不合适,所以Int不考虑,对于为什么要强制类型转换为char指针,char指针加1跳过一个字节,当我+size的时候就跳过size个字节,这是我们就把我们相邻两个元素的地址求出来了,然后把地址带到cmp这个函数中去了,但他返回cmp函数的值大于0,我们就进行交换,那我们应该怎么交换呢???这里我们就设计一个_swap函数,传的参数是(char*)base + j * size, (char*)base + (j + 1) * size, size,我们为什么还要传一个size呢?因为你应该告诉我你这个两个字符各是几个字节,让我交换相应的对数,所以我们应该把这个元素的大小传过去,我们进到_swap函数进行交换,交换完了过后再*p1++,*p2++指向下一个值 ,再进行下一组交换,接着我们再写出这行代码,看下面代码,进行正负数的返回,返回给cmp函数判断正负数,是整数就进行交换,希望大家能够理解
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);
}
这是一个一般的冒泡排序的模型,上面的代码就只是这个一般的冒泡排序的一个升华,本质是没变的,里面的排序还是正常的将两个元素进行交换,直到交换成升序为止
这里再给大家做一下说明
使用bubble_sort的程序员一定要知道自己排序的是什么数据,他就应该知道如何比较待排序数组中的元素,实现bubble_sort函数的程序员,他不知道未来排序的数据类型,那程序员也不知道待比较的两个元素的类型
16. 指针和数组面试题的解析
指针和数组笔试题解析
#include<stdio.h>
int main()
{
//一维数组
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));
return 0;
}
大家可以试着先分析一下结果是多少,分析错了不要紧,我逐个逐个给大家解释
首先这是一个数组,里面放的都是整型数据
数组元素的总大小是16个字节
sizeof(数组名)–数组名表示整个数组
&数组名—数组名表示整个数组
对于第一个sizeof(数组名),计算的是数组的总大小–单位是字节—16
对于第二个sizeof(a+0)结果是4,数组名在这里表示数组首元素的地址,a+0还是首元素的地址,地址的大小就是4/8个字节
对于第三个sizeof(*a)—数组名表示首元素的地址,*a就是首元素,sizeof(a)就是4
对于第四个sizeof(a+1)–数组名在这里表示首元素的地址,a+1是第二个元素的地址,地址的大小就是4/8个字节
对于第五个sizeof(a【1】)–第二个元素的大小–4
对于第六个sizeof(&a)–&a取出的是数组的地址,但是数组的地址那也是地址,地址的大小就是4/8个字节
对于第七个sizeof(&a)–16—&a数组的地址,数组的地址解引用访问的数组,sizeof计算的就是数组的大小–单位是字节
对于第八个sizeof(&a+1)–&a是数组的地址,&a+1虽然地址跳过整个数组,但还是地址,所以是4/8个字节
对于第九个sizeof(&a【0】)–&a【0】是第一个元素的地址
对于最后一个sizeof(&a【0】+1)–&a【0】+1是第二个元素的地址
我们把代码结果运行给大家看一下
X86平台演示的结果
X64平台演示的结果
下面我们来看字符数组
#include<stdio.h>
int main()
{
//字符数组
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));
return 0;
}
我们还是一个一个来分析
对于第一个sizeof(arr)–计算的是数组的大小,6*1=6个字节
对于第二个sizeof(arr+0)–arr是首元素的地址,arr+0还是首元素的地址,地址的大小是4/8个字节
对于第三个sizeof(*arr)–arr是首元素的地址,*arr就是首元素,首元素是字符大小是一个字节
对于第四个sizeof(arr【1】)–1
对于第五个sizeof(&arr)–&arr虽然是数组的地址,但还是地址,地址大小为4/8个字节
对于第六个sizeof(&arr+1)–&arr+1是跳过整个数组后的地址,地址的大小是4/8个字节
对于最后一个sizeof(&arr【0】+1)–第二个元素的地址—4/8个字节
希望大家能够理解,我们把两个代码运行起来给大家看一下
X86平台演示的结果
X64平台演示的结果
我们再来看一组代码
#include<stdio.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
return 0;
}
我们还是给大家讲解一下
对于第一个strlen(求字符串长度)—找不到\0的位置,\0的位置不确定,所以结果打印随机值
对于第二个strlen—1–数组名—首元素的地址,和第一种情况相同—随机值
对于第三个strlen—这里传了个’a’–97过去,他就把97当作一个地址,大家有听说过吗?所以这叫非法访问,是有问题的
对于第四个strlen—同第三个,非法访问,是有问题的
对于第五个strlen----取地址arr—虽然是取出来的是数组的地址,但是数组的地址也是从’a’开始的,所以结果也是随机值
对于第六个strlen—也是随机值,但是随机值跟前面的随机值差6
对于第六个strlen—也是随机值,但是比前面的随机值差1
我们还是把代码运行起来给大家看一下
X86平台演示的结果
X64平台演示的结果
我们接着来看代码
#include<stdio.h>
int main()
{
char arr[] = "abcdef";
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));
return 0;
}
我们还是来给大家讲解一下
第一个:计算的是数组的大小,单位是字节—7
第二个:计算的是地址的大小,arr+0是首元素的地址
第三个:*arr是首元素,计算的是首元素的大小–1
第四个:arr【1】是第二个元素,计算的是第二个元素的大小
第五个:&arr虽然是数组的地址,但也是地址,所以是4/8个字节
第六个:&arr+1是跳过整个数组后的地址,但也是地址—4/8个字节
第七个:&arr【0】+1是第二个元素的地址–4/8个字节
我们来把代码演示结果给大家展示一下
X86平台演示的结果
X64平台演示的结果
我们来看下面的代码
#include<stdio.h>
int main()
{
char arr[] = "abcdef";
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));
return 0;
}
还是给大家一个一个讲解一下
第一个:6,字符串的长度
第二个:6,和第一个相同
第三个:非法访问内存,访问97的这个地址,所以是有问题的
第四个:和第三个是一个道理,访问的是’b’–98的地址,属于非法访问,是有问题的
第五个:&arr—数组的地址–strlen的返回类型为const char*,我们正常使用应该使用数组指针—char(*p)【7】==&arr
第六个:&arr+1把\0也跳过去了,你也不知道后面的字符串有多长,所以结果是随机值
第七个:跳过了a这个字符从b开始往后数,所以长度为5
我们运行代码看一下结果
X86平台演示的结果
X64平台演示的结果
我们继续来看下面的代码
#include<stdio.h>
int main()
{
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));
return 0;
}
我们还是来解释一下
第一个:计算指针变量p的大小—4/8个字节
第二个:p+1得到的是字符b的地址,地址的大小为4/8个字节
第三个:*p就是字符串的第一个字符,也就是字符a,a为字符–1个字节
第四个:—可以等价等int arr[10]----arr[0]*(arr+0) p[0]*(p+0)==‘a’,所以还是字符a–大小为1个字节
第五个:计算的地址,地址是4/8个字节
第六个:也是地址,大小为4/8个字节
第七个:b的地址—大小为4/8个字节
我们运行起来看一下效果
X86平台演示的结果
X64平台演示的结果
我们再来看下一个代码
#include<stdio.h>
int main()
{
char* p = "abcdef";
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));
return 0;
}
第一个:6-从a开始的字符串的长度到\0结束
第二个:5-从b开始的字符串的长度到\0结束
第三个:error–将a的值传过去了
第四个:error–同第三个代码
第五个:地址如果内存分配不同,结果也会不同(涉及大小端知识)–随机值
第六个:也是随机值—取地址加1对于后面的内存,地址的大小我们也不知道,所以是随机值
第七个:5–从第二个元素开始到\0结束
给大家运行代码看一下结果
X86平台演示的结果
X64平台演示的结果
我们来看最后一个代码
#include<stdio.h>
int main()
{
//二维数组
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]));
return 0;
}
我们还是来给大家分析一下
第一个:48
第二个:4
第三个:16–a【0】相当于第一行作为一维数组的数组名,这里计算的是一行的大小
第四个:4—a【0】是第一行的数组名,数组名此时是首元素的地址,a【0】其实是第一行第一个元素的地址,a【0】+1就是第一行第二个元素的地址,地址大小为4/8个字节
第五个:4–*(a【0】+1)是第一行第二个元素,大小是4个字节
第六个:4–a是二维数组的数组名,所以a是首元素的地址,二维数组的首元素是他的第一行,a就是第一行(首元素)的地址,a+1就是第二行的地址
第七个:16—计算的是第二行的大小,单位是字节
第八个:第二行的地址–4
第九个:计算的是第二行的大小,单位是字节
第十个:a是首元素的地址–第一行的地址,*a就是第一行,计算的是第一行的大小
第十一个:16
给大家把代码运行起来看结果
X86平台演示的结果
X64平台演示的结果
全章终!看完知识点赶快去联系把!