友友们:
今天我们将开启对不同指针的深度解析!!
Are you ready?<( ̄︶ ̄)↗[GO!]
本章重点
- 字符指针
- 数组指针
- 指针数组
- 数组传参和指针传参
- 函数指针
- 函数指针数组
- 指向函数指针数组的指针
- 回调函数
- 指针和数组面试题的解析
指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:
1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2.指针的大小是固定的4/8个字节(32位平台/64位平台)。
3.指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
4.指针的运算。
这个章节,我们继续探讨指针的高级主题。
1.字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一般使用:
int main()
{
char ch = 'w';
char *pc = &ch;//将ch的地址存放在pc中
*pc = 'w';//解引用pc得到的是ch所对应的值
return 0;
}
还有一种使用方式如下:
int main()
{
char* pstr = "hello bit.";
printf("%c\n", *pstr);
return 0;
}
问题: 这里是把一个字符串放到pstr指针变量里了吗?
根据打印结果我们可以知道代码 char* pstr = "hello bit."本质是把字符串 hello bit. 首字符
的地址
放到了pstr中。
解析: 根据最上面的常规写法我们可以知道char *pstr存的是地址,而这里相当于把char ch="hello bit."和char *pstr=&ch结合起来,所以根据本质可以知道存的是首元素地址。
注意:上述代码中,使用%s
输出时,会从arr
所指向的首元素地址
开始,依次输出字符,直到遇到 '\0'
,最终输出整个字符串
hello bit 。
补充:
- %c 用于输出
单个字符型
数据 - %s 用于输出以 ‘\0’ 结尾的
字符串
,即找到\0之前的所有字符
有这样的一道面试题:
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
char *str3 = "hello bit.";
char *str4 = "hello bit.";
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和str2开辟的是两块不同的空间,这里不是比较两个字符串的内容,str1指向一块空间首元素的地址,str2指向另一块空间首元素的地址,两块空间不同,那自然地址也就不相同了;str3放的是h的地址,str4放的也是h的地址,既然都是h的地址,那么自然也就相同了(str3和str4里面的hello bit为
常量字符串
,不能更改
,既然不能被改的字符串一样,所以在内存中没有必要存两份,大家一起共用就行了)
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。但是用
相同的常量字符串
去初始化不同的数组
的时候就会开辟
出不同的内存块
。所以str1和str2不同,str3和str4不同。
2.指针数组
在《指针》章节我们也学了指针数组,指针数组
是一个存放指针
(也就是地址)的数组。
这里我们再复习一下,下面指针数组是什么意思?
int* arr1[10]; //存放整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的!数组
画图解释:
解析: a b c 为数组名,指的是数组首元素的地址,数组arr存放的是地址,也就是a中1,b中2,c中3的地址;他们是int * 类型;由于a b c一共有三个数组所以定义变量i来确定外面大循环的趟数为3趟(也就是3行),所以arr[i]指的是i=0时为第一行数组的地址,i=1时为第二行数组的地址…;然后确定里面小循环的次数为5次,通过解引用 *arr[0]可以找到其所对应的值,因为每个数组里面有5个元素,这里arr指向首元素地址,所以再定义变量j,通过+j来移动指针依次找到后面的值,例如第一行中j=0时指向1,j=1时指向2…(这里i和j指的是下标)然后对(arr[i]+j)进行解引用即可遍历出整个数组。
3.数组指针
3.1数组指针的定义
数组指针是指向数组的指针
。(是一个指针,而不是数组)
这里举个例子来理解上面标注的部分,也就是为什么int(*parr)[10]不能写成int *parr[10],为什么这里要外加一个(),原因如下:
int (*p)[10];
解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。
这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
请区分一下p1和p2
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
- p1 是一个
数组
,数组名为 p1,它包含 10 个元素
,每个元素的类型是int*
,即指向整型的指针,因此 p1 是一个指针数组
- p2 是一个
指针
,它指向一个包含 10 个整型元素的数组
,所以 p2 是一个数组指针
- 注意二者写法上的问题就是有无()
举个例子怎么写数组指针:
double* d[5];
double* (*pd)[5] = &d;//pd就是一个数组指针
这里定义了一个double* d[5];要我们写&d前面的怎么写?
首先写pd前面加上* 表示pd是一个指针,然后写[5]表明这个指针指向的是一个数组,里面的元素类型为double * 类型
3.2&数组名VS数组名
我们看一段代码:
可见数组名和&数组名打印的地址是一样的,但是他们表达的意义不一样!
原因如下:
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。实际上:
&arr
表示的是数组的地址
,而不是
数组首元素
的地址。(细细体会一下)数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.
补充:
1.整形指针+1跳过四个字节大小
2.数组指针+1跳过整个数组的大小(这里int类型4个字节,有10个元素,所以占40个字节)
注意: 数组名是数组首元素的地址
但是有两个例外
- sizeof(数组名)-数组名表示整个数组,计算的是整个数组大小,单位是字节
- &数组名-数组名表示整个数组,取出的是整个数组的地址
3.3数组指针的使用
那数组指针
是怎么使用的呢?
既然数组指针指向
的是数组
,那数组指针中存放
的应该是数组的地址
。
看代码:
注意:这样写不好,首先这里我们拿到的是整个数组的地址,而for循环中的*p相当于拿到数组名,也就是数组首元素的地址,然后通过+(下标)i来依次拿到后面的元素。
一个数组指针的使用:
法一:
法二:注意:二维数组首元素的地址指的是第一行的地址! 由于这里第一行有五个元素,相当于一维数组int a[5],存放的是&a,也就是int*p=&a(
指针数组
);所以这里传过去拿数组指针
接收为int(*p)[5];然后for循环中的打印为什么是这样的呢?因为二维数组p指向的是第一行
的地址,p+i(下标)指向第几行第几行,然后通过 解引用
* (p+i)找到某一行的数组名
,由于我们知道第一行的数组名为arr[0],第二行数组名为arr[1],第三行数组名为arr[2];拿到数组名就相当于拿到数组首元素的地址,也就是拿到某一行里面第一个数字的地址,再通过+j来找到某一行里面第几个数字的地址,然后通过解引用 *( *(p+i))+j)找到里面地址对应的具体的值
上述整体代码如下:
void print1(int arr[3][5],int r,int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
//这里p是一个数组指针
void print2(int(*p)[5],int r,int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; 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);
print2(arr, 3, 5);//arr数组名表示数组首元素的地址
return 0;
}
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
分析如下图:
4.数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1一维数组传参
#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);/arr2是一个存放int*类型的数组
}
4.2二维数组传参
void test(int arr[3][5])ok?✔
{}
void test(int arr[][])ok?×(二维数组传参行可以省略,列不能)
{}
void test(int arr[][5])ok?✔
{}
总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
这样才方便运算。
void test(int *arr)ok?×(第一行是5个整型的地址,不能拿一级指针接收)
{}
void test(int* arr[5])ok?×(这里写成数组,而不是指针,那更不行了)
{}
void test(int (*arr)[5])ok?✔(指针指向5个整型元素的数组)
{}
void test(int **arr)ok?×(不能写成二级指针,因为传过去的不是二级指针,是第一行的地址,所以只能写成一维数组的指针)
{}
int main()
{
int arr[3][5] = {0};
test(arr);二维数组首元素地址指的是第一行的地址,传过去的也就是第一行的地址
}
4.3一级指针传参
void print(int* ptr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(ptr + i));//这里的i为下标
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//p是一级指针
print(p, sz);
return 0;
}
思考:
当一个函数的参数部分为
一级指针
的时候,函数能接收什么参数?
比如:
void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?
test1接收的参数如上图:
test2接收的参数如下图:
4.4二级指针传参
注意:标红部分第一个解引用找到的是pa的地址,再进行一层解引用找到的是a的值,然后调用test函数时,对a进行了修改。
思考:
当函数的参数为二级指针的时候,可以接收什么参数?
void test(int** p2)
{
**p2 = 20;
}
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;//二级指针是存放一级指针的地址
//把二级指针进行传参
test(ppa);传二级指针变量
test(&pa);传一级指针变量的地址
int* arr[10] = { 0 };
test(arr);传存放一级指针的数组
printf("%d", a);
return 0;
}
5.函数指针
函数指针是指向函数
的指针,存放函数地址
的指针。
上面两个地址一样我们可以知道:
- 数组名 != &数组名
- 函数名 == &函数名
那我们的函数的地址要想保存起来,怎么保存?
int Add(int x, int y)
{
return x + y;
}
int main()
{
//函数指针-存放函数地址的指针
//&数组名-取到的就是函数的地址
//pf就是一个函数指针变量
int (*pf)(int, int) = &Add;
}
pf与*结合,表示它是一个指针,后面这个小括号表示指向一个函数,()里面指的是传过去参数的类型,说明这是一个函数指针,然后这个函数的返回值类型为int
下面我们做一个练习:
void test(char* str)
{
}
int main()
{
pt = &test;
return 0;
}
如何定义这个pt?
根据上面的解析就能够很快正确的写出!!!
观察下面一段代码,分析其解引用:
这里划红线的( * pt )表示的是对指针变量pt进行解引用;这里说明用函数指针去调用这个Add函数,那如何通过pt去调用这个函数呢,那就是对pt进行解引用
根据这个观察可以发现上面红线的那个*是摆设
int Add(int x, int y)
{
return x + y;
}
int main()
{
//int (*pt)(int,int) = &Add;//由于&函数名=函数名,所以可以写成
int (*pt)(int, int) = Add;//这里是将函数名的地址存放到pt里面
int ret = (*pt)(3, 5);//1 这里就是解引用这个函数然后传参
int ret = Add(3, 5);//2
int ret = pt(3, 5);//3
printf("%d", ret);
return 0;
}
上面1 2 3三种写法都可以!!!是等价的
5.1阅读两段有趣的代码:
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
代码1:
从0开始分析,0是一个数字(也是一个变量名),这里要将0强制转换
为函数指针类型(所以要一个括号强转),那么0就被看作是一个函数地址;所以转换后为(void(*)())0;这里 ( * )指的是指针,然后后面这个()表示的是函数,结合起来就是指针指向函数,函数的返回类型为void,总的()里面的就是函数指针类型;然后这里是调用0地址处的函数,所以对其进行解引用,并用()括起来,因为函数是void无返回类型,也就是无参,所以后面传参不用写值,所以直接一个();
总结:
void(*) () -函数指针类型
( void(*) () )0 -对0进行强制类型转换,被解释为一个函数地址
*( void(*) () )0 -对0地址进行了解引用操作
(*( void(*) () )0 )()-调用0地址处的函数
代码2:
根据上面分析我们可以总结道:
1.signal和()先结合,说明signal是函数名
2.signal函数的第一个参数类型是int;第二个参数的类型是函数指针,该函数指针,指向一个参数为int,返回类型是void的函数
3.signal函数的返回类型也是一个函数指针,为void(*)(int),该函数指针指向一个参数为int,返回类型是void的函数
signal是一个函数的声明
为了便于理解代码2,我们可以这样写:
void (*)(int) signal(int , void(*)(int))
注意:上面这样写语法是错误的!!!
那么我们怎么改写呢?简化如下:
typedef-对类型进行重定义
typedef void(*pfun_t)(int);//对void(*)(int)的函数指针类型重命名为pfun_t
所以就能写成:
pfun_t signal(int, pfun_t);//这就是对刚刚那个错误的进行修改
6.函数指针数组
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组, 比如:
整型指针:int*
整型指针数组:int *arr[10];
//数组的每个元素是int*
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
如上图所示:pfArr先和 [] 结合,说明pfArr是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。这里也就是说数组里面有2个函数指针 ,这个()里面的int,int指的是函数的2个参数类型为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 input = 0;
//计算器-计算整型变量的加、减、乘、除
do {
menu();
int x = 0;
int y = 0;
int ret = 0;
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;
}
我们发现这样写代码过于繁长,过于冗长
使用函数指针数组
的实现:
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 input = 0;
//计算器-计算整型变量的加、减、乘、除
do {
menu();
//pfArr就是函数指针数组
//转移表
int (*pfArr[5])(int, int) = { NULL,add,sub,mul,div };//通过下标去访问
int x = 0;
int y = 0;
int ret = 0;
printf("请选择:>");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数>:");
scanf("%d %d", &x, &y);
ret = (pfArr[input])(x, y);
printf("ret=%d\n", ret);
}
else if (input == 0)
{
printf("退出程序!\n");
break;
}
else
{
printf("选择错误!\n");
}
} while (input);
return 0;
}
注意:这段代码每次循环开始时先调用 menu 函数展示操作菜单,接着定义了一个函数指针数组 pfArr
,它可以存放指向具有两个整型参数且返回整型值的函数的指针。这里初始化数组的元素,将 add、sub、mul、div 这几个函数的地址依次放入数组对应位置(索引为 1 到 4 的位置,索引 0 位置设为 NULL),以便后续通过下标
方便地调用对应的函数(也就是说当用户根据提示的菜单输入1时,说明用户是要进行add的加法运算,然后这样对应数组中下标1,也就是add)。
- input是我们要输入的东西,也就是一个下标,我们根据下标找到某个数组名的元素,这个元素恰好是某个数组名的地址,然后去调用这个地址所对应的函数
- input 是一个用户输入的整数,用于选择要执行的操作。通过 pfArr[input],可以根据用户的输入从函数指针数组中获取相应的函数指针。例如,如果 input 的值为 1,则 pfArr[input] 等价于 pfArr[1],即获取到数组中的第二个元素,也就是指向 add 函数的指针
- 函数调用 (pfArr[input])(x, y)
在获取到相应的函数指针后,使用 (pfArr[input])(x, y) 的形式来调用该函数指针所指向的函数,并传递参数 x 和 y。例如,如果 pfArr[input] 指向 add 函数,那么 (pfArr[input])(x, y) 就相当于调用 add(x, y),执行加法运算并返回结果
7.指向函数指针数组的指针
指向函数指针数组的指针是一个指针
指针指向一个数组
,数组的元素都是 函数指针
;
如何定义?
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);//这里就是加了一个Arr[5]
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[10])(const char*) = &pfunArr;
return 0;
}
补充一个小知识:
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");
}
int calc(int (*pf)(int,int))
{
int x = 0;
int y = 0;
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
return pf(x, y);
}
int main()
{
int input = 0;
//计算器-计算整型变量的加、减、乘、除
do {
menu();
int ret = 0;
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
ret=calc(add);//函数名就是函数的地址,所以直接将函数传进去
printf("ret=%d\n", ret);
break;
case 2:
ret = calc(sub);
printf("ret=%d\n", ret);
break;
case 3:
ret = calc(mul);
printf("ret=%d\n", ret);
break;
case 4:
ret = calc(div);
printf("ret=%d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
} while (input);
return 0;
}
8.1演示一下qsort函数的使用:
qsort指的是快速排序的意思,下面解释一下qsort的使用
- 这里使用的都是升序
排序整型数据(实现冒泡排序)
void qsort (void* base,//base中存放的是待排序数据中第一个元素(对象)的地址
size_t num,//排序数据元素的个数
size_t size,//排序数据中一个元素的大小,单位为字节(也就是传过去的元素是什么类型,传int型就是4)
int (*compar)(const void*,const void*));//compar是用来比较待排序数据中的两个元素的函数
返回大于0的数字,则说明第一个元素大于第二个元素;返回0,表示第一个元素等于第二个元素;返回的
如果是小于0的数字,则说明第一个元素小于第二个元素
代码如下:
int compar_int(const void*e1, const void*e2)//注意返回类型一定是int
{
return *(int*)e1 - *(int*)e2;//这里先强转为int*型,然后再解引用找到所对应的数字,再做减法
}
void print(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
//排序
qsort(arr, sz, sizeof(arr[0]), compar_int);
//打印
print(arr, sz);
return 0;
}
使用qsort函数排序结构体数据
这里是根据年龄来排序
这里是根据名字来排序
补充一个知识:strcmp的比较是按照从左到右的顺序,依次比较对应字母的ASCLL码值(比较的是对应位置的字符,不要误以为比较的是长度)
- 那么如何实现降序呢
只需要改变compar_int函数里面的逻辑,原先前一个数比后一个数大的话,返回的是一个比0大的数,现在返回一个比0小的数,那么就可以实现反向的排序
补充e1-e2
:
对于 qsort 函数的比较函数 compar,它接收两个 const void * 类型的指针,这两个指针指向要比较的元素。它的返回值
决定了元素的排序顺序
:
- 如果 compar 返回
小于 0
的值,第一个元素会被排在第二个元素之前
。(第一个元素<第二个元素,实现降序
) - 如果 compar 返回 0,两个元素被认为相等。(第一个元素=第二个元素)
- 如果 compar 返回
大于 0
的值,第一个元素会被排在第二个元素之后
。(第一个元素>第二个元素,实现升序
)
补充e2-e1
:
当 *pb 大于 *pa 时,*pb - *pa 为正,qsort 会将 b 指向的元素排在 a 指向的元素之前,实现降序排序。
当 *pb 小于 *pa 时,*pb - *pa 为负,qsort 会将 b 指向的元素排在 a 指向的元素之后。
当 *pb 等于 *pa 时,结果为 0,qsort 会认为它们相等,不改变它们的顺序。
总结来说就是:
第一个元素与第二个元素相减实现的是升序
第二个元素与第一个元素相减实现的是降序
模仿qsort函数来实现冒泡排序的通用算法
void swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
//一个字节一个字节交换
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2))
{
int i = 0;
for (i = 0; i < sz-1; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
//两个元素比较
if (cmp((char*)base+j*width,(char*)base+(j+1)*width ) > 0)
//强制转换为char*类型是因为我们不知道这个元素是什么类型,base是首元素的地址,然后+j*width是因为可以根据下标和宽
//度来跳过几个字节,实现对哪两个数的比较
{
//交换
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
int compar_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void print(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 1,3,5,7,9,2,4,6,8,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
//排序
bubble_sort(arr, sz, sizeof(arr[0]), compar_int);
//打印
print(arr, sz);
return 0;
}
运行结果:
9.指针和数组笔试题解析
sizeof(数组名)
-计算的是整个数组的大小
&数组名
-数组名表示的是整个数组,取出的是整个数组的地址
除此之外,所有的数组名都是数组首元素的地址
sizeof求所占空间大小
注意
:
sizeof计算指针(地址)的大小,而不是它所指向的数据的大小。在大多数系统中,指针的大小
取决于系统的地址空间
,通常是 4 字节(32 位系统)或 8 字节(64 位系统)
- 一维数组
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//16
//因为数组a有4个元素,每个元素4个字节 4*4=16
printf("%d\n",sizeof(a+0));//4/8
//因为a+0是第一个元素的地址,sizeof(a+0)计算的是地址的大小,地址也就是指针大小要看平台是32/64位平台
printf("%d\n",sizeof(*a));//4
//这里a不满足sizeof和&两种情况,所以这里表示的是首元素的地址,然后解引用找到第一个元素1,1是整数(整型)所以为4
printf("%d\n",sizeof(a+1));//4/8
//这里a+1是第二个元素的地址,sizeof(a+1)计算的是地址的大小,地址也就是指针大小要看平台是32/64位平台
printf("%d\n",sizeof(a[1]));//4
//a[1]通过下标找到的是第二个元素2,计算的是第二个元素的大小
printf("%d\n",sizeof(&a));//4/8
//这里&a虽然是整个数组的地址,但也是地址,sizeof(&a)计算的是一个地址的大小,也得看平台是32/64
printf("%d\n",sizeof(*&a));//16
//&a--int(*p)[4]=&a,数组指针解引用找到的是这个数组,sizeof计算的是整个数组的大小为16(简单来说&和*可以抵消)
printf("%d\n",sizeof(&a+1));//4/8
//&a取出整个数组的地址,但开始时它指向的是首元素的位置上的地址,+1表示跳过整个数组指向另一块空间的起始位置的地址,也就是4后面这个空间的地址
printf("%d\n",sizeof(&a[0]));//4/8
//a[0]找到的是第一个元素1,然后&a[0]取出的是1的地址,所以sizeof计算的是地址的大小,看平台是32/64
printf("%d\n",sizeof(&a[0]+1));//4/8
//同理这里取出的是第二个元素2的地址,计算的是地址的大小
- 字符数组
//字符数组
char arr[] = {'a','b','c','d','e','f'};//放了6个字符[]中为6
printf("%d\n", sizeof(arr));//6
//sizeof(数组名)计算的是整个数组的大小,所以6*1=6(注意这里没有\0噢,""里面才有一个默认\0)
printf("%d\n", sizeof(arr+0));//4/8
//这里arr是首元素的地址,+0相当于没加,还是首元素‘a’的地址,所以要看平台是32/64
printf("%d\n", sizeof(*arr));//1
printf("%d\n", sizeof(arr[1]));//1
//这两个计算的都是单个元素的大小,char类型所占空间为1
printf("%d\n", sizeof(&arr));//4/8
printf("%d\n", sizeof(&arr+1));//4/8
printf("%d\n", sizeof(&arr[0]+1));//4/8
//上面三个计算的都是地址
补充:
strlen是根据地址一直往后数,知道找到\0才停止,求字符串长度
printf("%d\n", strlen(arr));//随机值
//这里数组名指的是首元素的地址,所以从第一个元素开始往下找,直到找到\0为止,由于后面空间不知道什么时候有\0,所以为随机值
printf("%d\n", strlen(arr+0));//随机值
//这里和上面同理,因为arr表示首元素的地址,+0还是表示首元素的地址...
printf("%d\n", strlen(*arr));//×
//这里传过去的地址是有问题的,因为strlen传的是地址,这里*arr找到的是第一个元素'a',它所对应的值是97,这里相当于把97当作一个地址传过去是会出问题的
printf("%d\n", strlen(arr[1]));//×
//这里和上面同理,传的是第二个元素'b',也就是将98当作一个地址传过去,这个地址是不合法的
printf("%d\n", strlen(&arr));//随机值
//这里&arr取的是整个数组的地址,但指针指向的位置任然是首元素那,因此和最上面两个一样,从第一个元素出发,一直找到\0为止
printf("%d\n", strlen(&arr+1));//随机值/随机值-6
//这里和上面一样,跳过一个数组,从'f'后面的那个位置地址出发,一直找到\0,所以也是一个随机值,那至于为什么另一个答案-6呢,是因为和arr里面六个字符差6(和上面一个随机值相比)
printf("%d\n", strlen(&arr[0]+1));//随机值/随机值-1
//这里是从'b'这个地址开始往后面找
上面可见\0
非常重要!!!
char arr[] = "abcdef";
//这里面放了 a b c d e f \0 7个元素
printf("%d\n", sizeof(arr));//7
//sizeof(数组名)求的是整个数组的大小,七个元素都是char类型,各占1个字节,所以7*1=7
printf("%d\n", sizeof(arr+0));//4/8
//arr数组名表示首元素的地址,sizeof求地址要看平台是32/64位
printf("%d\n", sizeof(*arr));//1
//arr表示首元素的地址,也就是a的地址,对其解引用找到a,类型为char,占1个字节
printf("%d\n", sizeof(arr[1]));//1
//这里计算的是第二个元素的大小
printf("%d\n", sizeof(&arr));//4/8
printf("%d\n", sizeof(&arr+1));//4/8
printf("%d\n", sizeof(&arr[0]+1));//4/8
//上面三个求的都是地址,要根据平台来判断
printf("%d\n", strlen(arr));//6
printf("%d\n", strlen(arr+0));//6
//arr数组名表示首元素的地址,也就是从第一个元素开始数,一直到找到\0为止
printf("%d\n", strlen(*arr));//×
printf("%d\n", strlen(arr[1]));//×
//这两个传过去的不是地址,只能将传过去的97和98看作地址,是非法的
printf("%d\n", strlen(&arr));//6
//&arr取出的仍然是数组起始位置的地址,也就是从起始位置开始数
printf("%d\n", strlen(&arr+1));//随机值
//这里&arr刚开始指向的是起始位置的地址,+1表示跳过整个数组指向\0之后位置的地址,由于后面是未知的,所以是一个随机值
printf("%d\n", strlen(&arr[0]+1));//5
//&arr[0]取出的是第一个元素的地址,+1指向的是第二个元素的地址,也就是从第二个元素开始数
char *p = "abcdef";
学了指针我们可以知道,这里表示将首字符'a'的地址存放在指针变量p里面
//这里空间里面放的是a b c d e f \0 p里面存放的是a的地址
printf("%d\n", sizeof(p));//4/8
//sizeof计算的是指针变量的大小
printf("%d\n", sizeof(p+1));//4/8
//p本来是a的地址,p+1变成了b的地址,计算的仍然是指针的大小
printf("%d\n", sizeof(*p));//1
//p指向a,*p指向的就是a这个值,所占空间为1
printf("%d\n", sizeof(p[0]));//1
//求的是字符a所占的空间大小,这里p[0]和有解引用*(p+0)的写法是等价的
printf("%d\n", sizeof(&p));//4/8
printf("%d\n", sizeof(&p+1));//4/8
printf("%d\n", sizeof(&p[0]+1));//4/8
//&p取出的是P的地址,p[0]表示的是第一个元素,&p[0]取第一个元素的地址,sizeof求的是指针的大小
printf("%d\n", strlen(p));//6
printf("%d\n", strlen(p+1));//5
//p 指向字符串 "abcdef" 的首字符 'a',p存放的是a的地址,第一个从a开始数,第二个从b开始数
printf("%d\n", strlen(*p));//×
printf("%d\n", strlen(p[0]));//×
//p[0]和*p是一个道理,指的是第一个元素,不是地址,是不合法的
printf("%d\n", strlen(&p));//随机值
printf("%d\n", strlen(&p+1));//随机值
//这里的p本来就是一个指针变量,取出p的地址,我们压根就不知道p它自己的地址是什么,里面有什么
printf("%d\n", strlen(&p[0]+1));//5
//取出第一个元素的地址,+1指向第二个元素,也就是从b开始数
//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48
//十二个元素,每个元素都是int整型,所以12*4=48
printf("%d\n",sizeof(a[0][0]));//4
//a[0][0]第一行第一个元素所占空间的大小
printf("%d\n",sizeof(a[0]));//16
//sizeof(a[0])表示数组名a[0]单独放在sizeof内部,a[0]表示整个第一行,sizeof(a[0])计算的就是第一行的大小,
//第一行有4个元素,一个元素四个字节
printf("%d\n",sizeof(a[0]+1));//4/8
//这里a[0]不是单独放在sizeof内部,也没有&数组名,所以表示的是首元素的地址,指向的是第一行第一个元素的地址,
//a[0]+1就是一维数组的第二个元素的地址,即 a[0][1] 的地址,sizeof计算的是指针的大小,所以要看是32/64平台
printf("%d\n",sizeof(*(a[0]+1)));//4
//a[0]+1就是一维数组的第二个元素的地址,即 a[0][1] 的地址,对其解引用找到第一行第二个元素,是一个整型,大小为4个字节
printf("%d\n",sizeof(a+1));//4/8
//二维数组名a没有单独放在sizeof内部,也没有&数组名,表示的是二维数组首元素的地址,二维数组首元素的地址表示第
//一行一维数组的地址,+1表示跳过一行,指向第二行的地址(a + 1 指向的是数组 a 的第二行的起始地址,也就是 &a[1])
printf("%d\n",sizeof(*(a+1)));//16
//由上可知a+1表示第二行的地址,对其解引用找到第二行,求的是第二行的大小,第二行4个元素,每个元素占4个字节;*(a+1)其实就等价于a[1]
printf("%d\n",sizeof(&a[0]+1));//4/8
//a[0]表示第一行数组名,&a[0]取出的是第一行整个数组的地址,+1表示指向第一行之后的空间,也就是第二行的地址
printf("%d\n",sizeof(*(&a[0]+1)));//16
//由上可知&a[0]+1表示第二行的地址,对其解引用,找到的是第二行,计算的是第二行的大小,第二行4个元素,每个元素占4个字节,4*4=16
printf("%d\n",sizeof(*a));//16
//这里二维数组数组名表示的是首元素的地址,因为a没有单独写在sizeof里面,也没有&,二维数组首元素的地址表示第一
//行一维数组的地址,解引用求的是第一行的大小
printf("%d\n",sizeof(a[3]));//16
//根据a[0]表示第一行;a[1]表示第二行...可以推测出a[3]表示第四行的数组名,这里虽然没有第四行,但是我们可以根
//据类型推测出a[3]-->int [4],这里不会去访问第四行,但是我们可以根据其类型去算出其大小
注意sizeof()内部的表达式是不算的
注意:二维数组的数组名 a
指向该二维数组的首行
,也就是第一个一维数组
的起始地址
。
因为我们常见的int arr[5]一维数组,访问里面的元素是arr[i],i的取值范围是0~4
所以这里我们可以理解 数组名[0]表示的是第一行的数组名
补充
:
注意sizeof()内部的表达式是不算的
- s = a + 6 是一个赋值表达式,但由于 sizeof 操作符的特性,这个表达式不会被执行。sizeof 只
关心
表达式结果的类型
,而不关心
表达式的结果值
。 - a + 6 的结果类型是 int,因为 a 是 int 类型,与 6 相加后结果
还是 int 类型
。 - 将 int 类型的值赋给
short
类型的s
,结果仍然是short
类型,因为s
是short
类型,所以 sizeof(s = a + 6) 实际上是计算 short 类型的大小
。所以结果为2
下面printf结果仍然为5,是因为sizeof里面的表达式不参与运算
总结: 数组名的意义:
- sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
- &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
- 除此之外所有的数组名都表示首元素的地址。
9.1指针笔试题
笔试题1:
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(*)[5]指针数组,强转为一个整型指针,然后-1表示减去4个字节,才能向左移动一位
笔试题2:
//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;//这里结构体+*表示结构体指针,P是指针变量
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
int main()
{
printf("%p\n", p + 0x1);//0X10014
//P是一个指针变量,指针加1表示跳过这个结构体,上述可知结构体大小为20个字节,所以这里+1表示+20,0X表示16进制,所以0X100000+0x1=0x100014(这里14表示十六进制,1*16^1+4*16^0=14;
printf("%p\n", (unsigned long)p + 0x1);//0X10001
//p原本是指针类型,这里强转为整型类型(无符号长整型),整型+1加的就是1,结果为0X100001
printf("%p\n", (unsigned int*)p + 0x1);//0X10004
//强转为无符号整型指针,指针+1表示跳过一个无符号整型也就是4个字节,结果为0X10004
return 0;
}
考察的是:指针类型决定了指针的运算!指针+1取决于指针类型,也就是加多少个字节。(int 整型指针+4个字节;结构体指针+20个字节…)
笔试题3:
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
//%x打印的没有0X,而是直接打印有效数字(也就是0X和数字之前的0都会省略)
- 注意:整型+1是直接加1,假设原先a的地址为0X0012ff44,先强转为int
整型
变成了整型数字
0X0012ff44,再+1变成数字0X0012ff45
,然后再强转
为整型指针
(也就是地址0X0012ff45
),由于地址加为0X0012ff45之后正好指向的就是内存地址里面00那个位置的地址,所以如图所示 - 学了数据的存储我们可以知道:
在大多数现代计算机系统中,内存是按字节编址的,这意味着每个字节都有一个唯一的地址,并且相邻字节
的地址
相差 1
。
笔试题4:
#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
笔试题5:
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;
}
结果为fffffffc和-4
笔试题6
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
笔试题7:
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
答案at
笔试题8:
int main()
{
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
printf("%s\n", **++cpp);//POINT
printf("%s\n", *--*++cpp+3);//ER
printf("%s\n", *cpp[-2]+3);//ST
printf("%s\n", cpp[-1][-1]+1);//EW
return 0;
}
终于结束了指针…学习不易,仍需努力!( ゚д゚)つBye