目录
目录
一、前言
二、字符指针
三、指针数组
四、数组指针
1.数组指针的定义
2.&数组名VS数组名
3.数组指针的使用
五、函数指针
1.函数指针的使用
2.实例使用
六、函数指针数组
七、回调函数
八、指针笔试题
九、总结
指针-C语言的灵魂-C语言的一把利器
今天让我们来深入探究其用法
在指针的类型中我们知道有一种指针类型为字符指针 char*
一般使用:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有一种使用方式如下:
int main()
{
const char* p = "abcd";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", p);
return 0;
}
而当你看到第二种使用方法的时候,会不会误认为指针p中存放了字符串“abcd”呢❓
那就大错特错啦!
我们知道,指针变量用于存放地址所以字符指针,就是用来存放字符的地址。咱不能理解为p里面放了一个字符串呀。再说了,这放得下嘛?p是指针变量,4个字节,怎么可能放得下辣么多字符
所以第二种写法,实际上仍然是存入了地址,当我们使用时就提供了一个内存地址然后就开始打印输出,直至遇到‘\0’才结束
在这给大家一点小普及✔️✔️
关于”常量字符串“
像上面代码中的字符串“abcd”也称为常量字符串,是不允许修改的,当你创建的时候是存入到一个叫做“只读虚拟区”当中。而如果像我们平时使用指针一样,直接将“abcd”就赋给指针变量的话,系统会有警告,因为这样子就会有修改常量字符串的可能。所以我们应该在指针变量左边加上 const 关键字。
这时有调皮的兄弟问了:那我非要改呢?!
你也改不了,程序会挂掉的(就什么都做不了了)
而了解完我们可以看看这样一道题
#include
int main() { char str1[] = "hello bit."; char str2[] = "hello bit."; const char *str3 = "hello bit."; const 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; } ❓程序的输出结果会是什么呢?
解释:
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
什么是指针数组?是指针?还是数组?
是数组!
数组我们已经知道有整型数组:用于存放整型;有字符数组:用于存放字符
int arr1 [5];
char arr2 [ 6 ];那指针数组就是用于存放指针的数组咯
int* arr3 [ 5 ];arr3是一个数组,有五个元素,每个元素是一个整形指针
那指针数组可以怎么使用?我们实践出真知
如何利用指针数组模拟实现二维数组?
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[3] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", *(parr[i] + j));
}
printf("\n");
}
}
我们知道,数组名即是数组首元素的地址,所以可以存入指针变量,也即意味着指针数组中的每个元素就是我们定义的每一个一维数组的首元素地址✏️✏️然后我们利用循环,达到每次输出完之后指针向后移动即可遍历数组。
//我们还可以这样写 //因为 *(p+i) ---> p[i] //printf("%d",*(parr[i]+j)) printf("%d",parr[i][j]);
❓❓数组指针是指针?还是数组?
答案是:指针。
✔️✔️我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
下面代码哪个是数组指针?
int * p1 [ 10 ];int ( * p2 ) [ 10 ];//p1, p2 分别是什么?
首先我们应该注意:[ ]的优先级是高于 * 号的
所以p1先和方块结合,就变成数组,然后再是和 int* 结合,表示存放整型指针的指针数组p2先和*结合,说明p2是一个指针变量然后指着指向的是一个大小为10个整型的数组。所以p2是一个指针,指向一个数组,叫数组指针。
而数组指针怎么用呢?我们需要先理解一下 &数组名 和 数组名 的区别
对于下面的数组:
int arr[10];
arr 和 &arr 分别是什么?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们看一段代码:
#include
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
代码运行结果:
✔️可见数组名和&数组名打印的地址是一样的。
❓难道两个是一样的吗?
我们再看一段代码:
#include
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1= %p\n", &arr+1);
return 0;
}
代码运行结果:
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
✏️实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
arr是首元素地址,所以当arr加1的时候,指针就向后移动一单位,指向第二个元素而&arr是表示一整个数组的地址,所以当加1时,指针会跳过整个数组
✏️总结:
数组名通常表示的都是数组首元素的地址
但是有2个例外:
1.sizeof(数组名),这里数组名表示整个数组,计算的是整个数组的大小
2.&数组名,这里的数组名表示的依然是整个数组,所以&数组名取出的是整个数组的地址
- 整型指针是用来存放整型的地址
- 字符指针是用来存放字符的地址
- 数组指针是用来存放数组的地址
所以数组指针怎么用呢?我们可以类比另外两种指针
int arr[10] = {0};
int* p = arr;
//那么怎么定义数组指针用于存放一整个数组的地址呢?
int (*p2)[10] = &arr;
//定义一个指针变量p2,然后p2指向一个有着10个元素的整型数组int[10](&arr)
数组指针的使用——模拟打印二维数组
void print_arr(int (*p)[5], int row, int col) {
int i = 0;
for(i=0; i|
一开始我们用这个例子加深了对指针数组的理解,现在同样也能用来加深对数组指针的理解✔️
有小伙伴浅看了一下就有了疑问:刚刚还在那热火朝天地对比arr和&arr 现在一使用数组指针,怎么传入函数的参数还是arr啊?
✏️ 这里我们需知道的是,数组名arr,表示的是首元素的地址。但是二维数组的首元素是二维数组的第一行。所以这里传递的arr,其实相当于第一行的地址,也就是一整个一维数组的地址,所以可以用数组指针来接收参数。
而到这可能有老铁看着 *(*(p+i)+j)觉得眼花缭乱的,没事我们来慢慢理解✨✨
而 *(*(p+i)+j)也等同于 p[ i ][ j ]
✏️总结:
1.二维数组的首元素是代表二维数组的第一行
(二维数组可以看成由多个一维数组组合而成)
2.数据的类型是什么决定了它加1减1跳过的单位是多少
学习完了指针数组和数组指针,那么留给大家一个思考题❓❓:
以下代码分别代表了什么?
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
首先看一段代码
#include
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出的结果:
输出的是两个地址✔️因此我们知道,函数名和取地址函数名都是表示函数的地址
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:
void test ( ){printf ( "hehe\n" );}// 下面 pfun1 和 pfun2 哪个有能力存放 test 函数的地址?void ( * pfun1 )( );void * pfun2 ( );
首先,能存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
了解了函数指针之后,我们应该如何去使用呢
int Add(int x, int y)
{
return x + y;
}
int main()
{
int arr[5] = { 0 };
//&数组名 - 取出数组的地址
int(*p)[5] = &arr;//数组指针
//&函数名 - 取出函数的地址
int (*pf)(int, int) = &Add;//函数指针
//函数指针的调用
int ret = (*pf)(2, 3);
printf("%d", ret);
return 0;
}
和定义数组指针同理,我们先让变量名pf和*结合,结合完便是指针,那指针指向什么?再向外看即是与括号结合。方括号是数组,圆括号便是函数了再与函数的返回值类型和形参类型结合便是函数指针了(同理与数组指针的定义)
这里有一个小tip✔️:
当我们遇到看上去较为复杂的数据想确定其类型时,我们可以先将变量名忽略,即为此数据的数据类型。
例如上面的函数指针,我们将变量名pf先忽略 ---> int ( * )(int, int)
其含义是:指向形参为int,int 返回值为int类型的函数的指针 ---> 函数指针
接下来我们来阅读两段有趣的代码来加深理解
// 代码 1( * ( void ( * )( )) 0 )( );// 代码 2void ( * signal ( int , void ( * )( int )))( int );
以上代码是出自一本书《C陷阱和缺陷》那么这两段代码是什么意义呢?
代码1:(*(void (*)( ))0)( );
这左左右右的括号一整个晕了,所以我们要找到突破口: 0
我们知道 0 是一个数字,是一个整型,然后0的前面加一个括号,在一种数据前加个括号我们会想到是不是要强制类型转换?
0前面的括号内又刚好就是我们刚学的函数指针类型,所以即是转换为函数指针,然后再调用
✏️综上所述
1.代码1是一次函数调用,调用的是0作为地址处的函数
2.把0强制类型转换为:无参、返回类型是void的函数的地址(指针即是地址)
3.最后再调用0地址处的这个函数
代码2:void (*signal(int , void(*)(int)))(int);
这个代码看上去更复杂!那突破口呢?
我们可以看到中间那一部分我们很熟悉呀,就是一个函数声明 -
声明的signal函数的第一个参数类型是int,第二参数的类型是函数指针,该函数指针指向的函数参数是int,返回类型是void -
那我们函数声明时,不仅要定义参数,也要定义返回类型呀,那signal函数的返回类型是什么?那即是剩下的那部分 void( * )(int) 这不就是函数指针嘛 -
所以signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数为int,返回类型是void -
✏️综上所述
代码2的意义是一次函数声明
看完函数指针是不是觉得十分多此一举?明明可以直接调用干嘛要费劲?
接下来我们用一个实例来感受一下函数指针的好处。
实现一个对整数加减乘除的简易版计算器
void Menu()
{
printf("****************************\n");
printf("***** 1.Add 2.Sub ******\n");
printf("***** 3.Mul 4.Div ******\n");
printf("***** 0.exit ******\n");
printf("****************************\n");
}
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;
}
int main()
{
int x = 0;
int y = 0;
int ret = 0;
int input = 0;
do
{
Menu();
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入2个操作数:>\n");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n",ret);
break;
case 2:
printf("请输入2个操作数:>\n");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入2个操作数:>\n");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入2个操作数:>\n");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
看完这个代码是不是感觉十分冗杂?有很多重复的代码应该如何优化呢?
我们发现代码中只有调用加减乘除函数的代码是不一样的,那我们能不能将其抽离出来然后封装成一个函数?
我们可以封装一个计算器函数 calc( ) ,然后借助这个函数来帮我们完成运算,我们把加减乘除的函数传进去,然后给我们执行相应的功能。
✨而这个calc( )函数接受的也是函数,即是函数的地址,那么我们需要定义其参数为指针函数
优化版计算器代码:
void Menu()
{
printf("****************************\n");
printf("***** 1.Add 2.Sub ******\n");
printf("***** 3.Mul 4.Div ******\n");
printf("***** 0.exit ******\n");
printf("****************************\n");
}
void calc(int (*pf)(int,int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入2个操作数:>\n");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("%d\n", ret);
}
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;
}
int main()
{
int input = 0;
do
{
Menu();
scanf("%d", &input);
switch (input)
{
case 1:
calc(Add);
break;
case 2:
calc(Sub);
break;
case 3:
calc(Mul);
break;
case 4:
calc(Div);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,
比如:
int * arr [ 10 ];// 数组的每个元素是 int*
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢❓❓
int ( * parr1 [ 10 ])( );int * parr2 [ 10 ]( );int ( * )( ) parr3 [ 10 ];
答案是:parr1
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。
函数指针数组的用途:转移表
接下来我们再借助简易版计算器来加深理解
优化版计算器代码:
void Menu()
{
printf("****************************\n");
printf("***** 1.Add 2.Sub ******\n");
printf("***** 3.Mul 4.Div ******\n");
printf("***** 0.exit ******\n");
printf("****************************\n");
}
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;
}
int main()
{
int x = 0;
int y = 0;
int ret = 0;
int input = 0;
int(*pfArr[])(int, int) = { 0,Add,Sub,Mul,Div };
do
{
Menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
printf("退出计算器\n");
else if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("%d\n", ret);
}
else
printf("选择错误\n");
} while (input);
return 0;
}
✨对比我们上面计算器的初始版本,这个优化版显然代码不会那么冗余
代码中的函数指针数组其实就被称为“转移表”当我们输入不同的选择时就会跳转到不同的功能执行
✨其次,使用函数指针数组还能够降低程序的耦合性。假如现在我们想拓展计算器的功能:按位与、按位或...要是按照我们初始版本,那么就需要对程序添加很长的代码,也要对main函数进行大量修改。而如果用我们这个优化版本,程序的拓展力比较强,极大简化了以后修改代码的步骤。
接下来我们来了解一种需要运用到函数指针的很厉害的函数 --- 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
其实在上面我们利用函数指针实现的简单版计算器,就是运用了回调函数✔️
我们自己实现的加减乘除函数,到最后并不是我们自己(函数实现方)直接调用,而是把函数的指针作为参数传递给calc函数,然后calc函数在某个特定的时机,便会去调用加减乘除函数。
‼️‼️接下来我们通过介绍C语言一个运用了回调函数思想的库函数:qsort( ),来加深对回调函数的理解
❓❓在开始进入之前,我们先解决一个问题:假如我现在要将一个整型数组调整为升序排列那该如何实现?
其实对于排序算法有很多:快速排序法、冒泡排序法...
这些算法十分地经典也十分常用,我这里就直接上代码使用冒泡排序法实现了
//冒泡排序 void bubble(int arr[], int sz) { for (int i = 0; i < sz - 1; i++) { int flag = 1; for (int j = 0; j < sz - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; flag = 0; } } if (flag == 1) break; } } int main() { int arr[] = { 9,8,7,6,5,4,3,2,1,0 }; int sz = sizeof(arr) / sizeof(arr[0]); bubble(arr, sz); for (int i = 0; i < sz; i++) printf("%d ", arr[i]); return 0; }
但是假如我们不仅仅想要对整型进行排序呢?我想对字符串排序,我想对结构体排序。假如现在结构体里面有很多元素,比如现在要对两个人的信息(名字、年龄...)进行比较,那该如何比较?
所以就需要运用到一个库函数:qsort()
以上是帮助文档对qsort函数的解释,我相信肯定有英语大佬看得懂,看不懂也没事,我来简单地介绍一下。
✨qsort函数是一个使用快速排序的思想实现的一个排序函数,默认是升序使用此函数能够将任何数据(整型、结构体、字符串...)按照升序进行重新排序并返回
✨我们知道,有很多排序算法:快速排序、冒泡排序...但是这些我们实现之后只能用来排序特地的数据类型
✨而qsort函数则是使用快速排序的算法,再进行优化而封装出来的一个能用于排序任何数据类型的库函数
那我们要怎样使用它呢?
我们知道,qsort函数能够对任何类型的数据进行排序,那怎么这么厉害啊?我们每次使用一种类型的变量不都得定义类型之后才能使用吗?所以让我们来进一步研究一下qsort函数设计的巧妙之处
❓当我们要使用时看到需要传入的第一个参数类型是 void* 这可是我们第一次遇到,那又是什么?
关于 void*
int main() { int a = 10; char* a = &a; }
首先我们看到这样一段代码,这段代码在编译的时候系统会报出警告:“从“int *”到“char *”的类型不兼容”,因为对a取地址之后本应该赋给其对应的指针类型 int* 但是如果我们使用void* 便不会弹出警告了
int main() { int a = 10; void* pv = &a; }
✏️void* 是一种无具体类型的指针,可以接受任意类型的地址,但是不能直接对void* 进行解引用操作,也不能对其加减任意整数
也就是说我们无法对void*所指向的数据进行具体的操作,若需要进行操作则需将其强制类型转换为基本数据类型指针
为什么要使用void* 呢?
这便是qsort函数能够对任意类型数据进行排序的关键设计之一✨因为我们可能会使用这个函数排序各种各样类型的数据,所以参数设计为void*便能让我们直接传入数据,等要进一步具体操作时我们再将其强制类型转换为对应的基本数据类型
而qsort函数第四个参数是一个函数指针 int(*compar)(const void* e1,const void* e2)
参数为一个函数指针,也就意味着需要我们传入一个函数,那要传什么函数呢?这与我们需要排序的数据类型有关。
compar便是比较的意思,也就是说qsort函数要求我们要自己实现一个比较函数并传入,并且需要注意的是,还要求我们在实现这个比较函数的时候,当第一个元素e1>第二个元素e2时返回大于0的数,当e1 = e2时返回0,当e1 < e2时返回小于0的数
因为不同类型的数据在比较大小的时候方式肯定是不同的,那么就将比较的这部分模块单独提取出来,供使用者去根据想要排序的数据类型去实现后再传入。
纸上得来终觉浅,下面我们来举几个例子
1.利用qsort函数排序整型
//自己实现对整型的比较函数
int cmp_int(const void* e1, const void* e2)
{
if (*(int*)e1 > *(int*)e2)
return 1;
else if (*(int*)e1 == *(int*)e2)
return 0;
else
return -1;
}
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]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
结果展示:
既然我们自己实现的比较函数要求大于就返回大于0的数,等于就返回0,小于就返回小于0的数,那么是否能对代码进行优化呢
优化版:
int cmp_int(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2);
}
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]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
❓那假如想要降序排序呢?
降序排序的逻辑与升序排序相反(e1>e2:返回小于0的数、e1=e2:返回0、e1
那只需将比较函数中的e1和e2互换位置即可✔️
2.利用qsort函数对结构体进行排序
假设结构体中有元素:名字、年龄
对结构体中的名字进行排序:
struct Stu
{
char name[20];
int age;
};
int cmp_stu_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
int main()
{
struct Stu s[3] = { {"zhangsan",15},{"lisi",30},{"wangwu",25} };
int sz = sizeof(s) / sizeof(s[0]);
printf("排序前:\n");
for (int i = 0; i < 3; i++)
printf("名字:%s 年龄:%d\n", s[i].name, s[i].age);
qsort(s, sz, sizeof(s[0]), cmp_stu_name);
printf("排序后:\n");
for (int i = 0; i < 3; i++)
printf("名字:%s 年龄:%d\n", s[i].name, s[i].age);
return 0;
}
代码结果:
结构体中的姓名是字符串类型,而strcmp库函数正是用于比较字符串的库函数,并且正好其返回值为:当字符串1 > 字符串2 --返回大于0的数、当字符串1 = 字符串2 --返回0、当字符串1 < 字符串2 -- 返回小于0的数这正好与qsort要求的比较函数返回值相吻合
对结构体中的年龄进行排序:
struct Stu
{
char name[20];
int age;
};
int cmp_stu_age(const void* e1, const void* e2)
{
return (((struct Stu*)e1)->age - ((struct Stu*)e2)->age);
}
int main()
{
struct Stu s[3] = { {"zhangsan",15},{"lisi",30},{"wangwu",25} };
int sz = sizeof(s) / sizeof(s[0]);
printf("排序前:\n");
for (int i = 0; i < 3; i++)
printf("名字:%s 年龄:%d\n", s[i].name, s[i].age);
qsort(s, sz, sizeof(s[0]), cmp_stu_age);
printf("排序后:\n");
for (int i = 0; i < 3; i++)
printf("名字:%s 年龄:%d\n", s[i].name, s[i].age);
return 0;
}
代码结果:
看到以上排序的实现,是否觉得可以用于通讯录嘞?
✏️qsort函数是一个扩展力很强的库函数,利用了void*这一指针,即是“泛型”的思想,使得其函数能接收任何类型的数据
✏️由于不同类型的数据比较的方式不同,所以将此模块抽取出来供使用者自行实现并传入,当在特定的事件或条件发生时由qsort函数去调用我们实现的比较函数(回调函数)
✏️由于qsort里面封装了快速排序的算法,所以只要我们按照其参数要求传入参数,便可得到一段按升序(降序)排序完的代码
注:以下题目默认在x86环境下运行
(x86环境下指针的单位为4个字节,x64环境下指针单位为8个字节)
✏️笔试题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:
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和第一题同理,即是指向数组后面一位的地址,而这里有个方块表达式我们需要知道其意义:ptr1[-1] = *(ptr1-1) 所以即是输出最后一个元素:4
而第二个输出就有点意思了
我们需要时刻记住:数据的类型决定了数据加减的跨度(加减的单位)
举个:
假设题中的整型数组的首元素地址 a = 0x0012ff40
那么请问:a+1 和 (int)a+1 分别等于多少?
a是整型数组的地址,而整型数组中每个整型元素是4个字节
所以整型指针+1就跳过4个字节即是:a+1 = 0x0012ff44
而 (int)a+1 这里先将a强制类型转换为一个整型,就像我们日常整数加减运算一样,那整数+1不就是+1吗
所以最后 (int)a+1 = 0x0012ff41
以上例子也是为了这道题而做铺垫,那么我们来看题
因为(int)a+1只是跳过1个字节,所以在这道题我们的数组画图就要画的更严谨些,将每个字节都表示出来。而在小端存储模式中(我们的系统)数据存储时会将低位存在低地址处,所以在内存存储中,我们的数组就如图所示,每个方块表示一个数组元
所以(int)a+1就指向了第一个元素的第二个字节处:00 而我们又将这个运算结果再强制类型转换为 int* 又把它当成指针了再赋给ptr2
我们最终要输出 *ptr2 ptr2是个整型指针呀 而*ptr2 就应该从ptr2指向的位置向后访问一个整型(整型指针解引用--访问一个整型)
所以站在整型指针的角度,他认为他向后访问了一个整型,也就是图中我们用蓝线圈出的。而这个整型是在内存中存放的,而内存是小端存储的,所以当我们要拿出这个整型的时候应该要倒着拿 所以我们拿出来的值*ptr2 = 02 00 00 00 打印结果即是:2000000
✏️笔试题3:
#include int main (){int a [ 3 ][ 2 ] = { ( 0 , 1 ), ( 2 , 3 ), ( 4 , 5 ) };int * p ;p = a [ 0 ];printf ( "%d" , p [ 0 ]);return 0 ;}// 程序的结果是什么?
我们知道,二维数组其实可以理解为是多个一维数组组成的。所以二维数组的首元素其实就代表着第一行数组。那么这道题的输出结果是什么呢?有的兄弟可能一下子就看出来,结果是:0!真的吗?
结果:1
不少友友应该也和我一样,看到数组的赋值以为是下面这种情况
所以 a代表第一行数组,a[0]就代表第一行的第一个元素:0
但是我们应该注意,如果是以上这种赋值形式的话应该这样写: int a[3][2] = { {0, 1}, {2, 3}, {4, 5} }; 大括号里面每个元素的赋值也应该写大括号
所以题目中的这种形式其实称为:逗号表达式 也就是逗号前和逗号后都会参与到实际运算,但是最终取值是以逗号后的数值为准。所以实际上数组是以下这种情况
所以正确输出结果为:1
若能认真看到最后,我相信你对指针的理解及使用会有不少长进✔️✔️
博主认为指针是c语言的一把利器,十分地锋利有可能误伤自己,但若能掌握好它,那便能大杀四方⚔️⚔️
码字不易,若觉得内容有趣,请关注点赞评论走一波
我们一起讨论,一起学习,一起进步