11.3 指针和函数
本节必须掌握的知识点:
指针作为函数的参数
数组作为函数的参数
指针作为函数的返回值
在C语言中,指针的一个重要作用就是作为函数参数使用,本节将介绍这一重要作用。
11.3.1 指针作为函数的参数
实验一百一十三:通过指针间接地修改身高
在VS中新建项目11-3-1.c:
/*
通过指针间接地修改身高
*/
#include <stdio.h>
#include <stdlib.h>
void cmp(int* height)
{
if (*height < 178)
{
*height = 178;
}
}
int main(void)
{
int n_XiaoMing = 176;
//如果小明身高小于178,则该为178
cmp(&n_XiaoMing);
printf("小明的身高:%d\n", n_XiaoMing);
system("pause");
return 0;
}
●运行结果:
小明的身高:178
请按任意键继续. . .
●代码分析:
首先分析main函数:定义了一个变量n_XiaoMing;并给n_XiaoMing赋初始值176;紧接着执行cmp函数,cmp(&n_XiaoMing);传递给cmp函数的实参是n_XiaoMing 的地址,形参int* height接收,此时指针*height指向了n_XiaoMing。这里*height可以看作是n_XiaoMing的别名。
若*height的值小于178,就执行*height = 178;这个操作,也就是把178赋值给*height,也相当于对n_XiaoMing进行赋值。如图11-4所示:
图11-4 函数调用中指针的传递
我们可以通过分析反汇编代码验证:
void cmp(int* height)
{
00401820 push ebp
00401821 mov ebp,esp
…
if (*height < 178)
00401848 mov eax,dword ptr [height] ;取出身高
0040184B cmp dword ptr [eax],0B2h
00401851 jge cmp+3Ch (040185Ch)
{
*height = 178;
00401853 mov eax,dword ptr [height] ;取出身高
00401856 mov dword ptr [eax],0B2h ;修改身高为178
}
}
0040185C pop edi
0040185D pop esi
0040185E pop ebx
0040185F add esp,0C0h
00401865 cmp ebp,esp
00401867 call __RTC_CheckEsp (0401221h)
}
}
0040186C mov esp,ebp
0040186E pop ebp
0040186F ret
int main(void)
{
…
int n_XiaoMing = 176;
004018C2 mov dword ptr [n_XiaoMing],0B0h
//如果小明身高小于178,则该为178
cmp(&n_XiaoMing);
004018C9 lea eax,[n_XiaoMing] 堆栈传递实参- n_XiaoMing变量地址
004018CC push eax
004018CD call _cmp (040132Ah) 函数调用
004018D2 add esp,4
printf("小明的身高:%d\n", n_XiaoMing);
004018D5 mov eax,dword ptr [n_XiaoMing]
004018D8 push eax
004018D9 push offset string "\xd0\xa1\xc3\xf7\xb5\xc4\xc9\xed\xb8\xdf:%d\n" (0407B30h)
004018DE call _printf (040104Bh)
004018E3 add esp,8
system("pause");
004018E6 mov esi,esp
system("pause");
004018E8 push offset string "pause" (0407B44h)
004018ED call dword ptr [__imp__system (040B168h)]
004018F3 add esp,4
004018F6 cmp esi,esp
004018F8 call __RTC_CheckEsp (0401221h)
return 0;
004018FD xor eax,eax
}
cmp(int* height)函数调用时传递的实参为0x0054fcf4为变量n_XiaoMing的地址,即int* height形参0x0054fc20地址处存储的值0x0054fcf4。
名称 | 值 | 类型 | ||
▶ | &n_XiaoMing | 0x0054fcf4 {176} | int * | |
▶ | &height | 0x0054fc20 {0x0054fcf4 {176}} | int * * |
00401853 mov eax,dword ptr [height] ;取出身高
00401856 mov dword ptr [eax],0B2h ;修改身高为178
注意这两条语句,第一条语句先从height地址(0x0054fc20)处存储的值0x0054fcf4存入eax。第二条语句再将修改后的值178存入eax(0x0054fcf4)地址处。
总结
如果指针作为函数的参数,修改参数就相当于对传进来的实参进行修改。传入的是指针,但操作的是指针指向的对象进行操作,比如代码中:int* height指向的对象是&n_XiaoMing,间接的对n_XiaoMin进行操作。我们将函数的地址传参称为引用调用,被调函数可以直接修改主调函数的原值。
实验一百一十四:将用户输入的整数进行交换
在VS中新建项目11-3-2.c:
/*
将用户输入的整数进行交换
*/
#include <stdio.h>
#include <stdlib.h>
void swap(int* px, int* py)
{
int temp = *px;
*px = *py;
*py = temp;
}
int main(void)
{
int n_x;
int n_y;
printf("请输入两个整数:\n");
printf("第一个整数n_x:\t");
scanf_s("%d", &n_x);
printf("第二个整数n_y:\t");
scanf_s("%d", &n_y);
swap(&n_x, &n_y);
printf("交换后的结果:\n");
printf("n_x:%d\n", n_x);
printf("n_y:%d\n", n_y);
system("pause");
return 0;
}
●运行结果:
请输入两个整数:
第一个整数n_x: 1
第二个整数n_y: 2
交换后的结果:
n_x:2
n_y:1
请按任意键继续. . .
●代码分析:
swap(&n_x,&n_y);
传入实参&n_x,&n_y;
实现两个数进行交换的动作在swap函数里处理的,
void swap(int* px,int* py)
{
int temp = *px; //定义一个变量temp,存放*px的数据
*px = *py; //把*py的数据赋值给*px,此时*px 的数据与*py的数据相同
*py = temp; //temp存放的是*px的数据,把temp的数据给*py,此时完成交换。
}
11.3.2 数组作为函数的参数
在前面我们已经介绍了,指针与数组之间的互相转换,这里将介绍把数组的内容传递给函数,那么该如何传递呢?我们知道数组名就是地址,因此,只要把数组名及数组的长度传给函数就可以了,我们来做一个实验。
实验一百一十五:函数使用数组名作为形参
在VS中新建项目11-3-3.c:
/*
函数使用数组名作为形参
*/
#include <stdio.h>
#include <stdlib.h>
void Array(int* pArr, int len)
{
for (int i = 0; i < len; i++)
{
printf("arr[%d] = %d\n", i, *(pArr + i));
}
}
int main(void)
{
int arr[5] = { 1,2,3,4,5 };
Array(arr, 5);
system("pause");
return 0;
}
●运行结果:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
请按任意键继续. . .
●代码分析:
Array(arr,5);//这是传递的arr数组名,也就是数组起始地址,5是数组的长度。
void Array(int* pArr,int len) //使用int* pArr接收数组地址,int len = 5
{
for( int i = 0;i < len; i++) //遍历数组
{
//通过数组首地址+偏移的方式获取到数组中每一个元素。
printf("arr[%d] = %d\n",i,*(pArr+i));
}
}
实验一百一十六:函数使用指针作为形参
在VS中新建项目11-3-4.c:
/*
函数使用指针作为形参
*/
#include <stdio.h>
#include <stdlib.h>
void sortArr(int* pArr, int len)
{//控制循环轮数(数组长度-1)轮
for (int i = 0; i < len - 1; i++)
{//控制每轮次数//(数组长度-1)-当前轮数
for (int j = 0; j < len - i - 1; j++)
{
if (*(pArr + j) > *(pArr + j + 1))
{
//使用冒泡排序算法
int temp = *(pArr + j);
*(pArr + j) = *(pArr + j + 1);
*(pArr + j + 1) = temp;
}
}
}
}
void printArr(int* pArr, int len)
{
for (int i = 0; i < len; i++)
{
printf("arr[%d]=%d\n", i, *(pArr + i));
}
}
int main(void)
{
int arr[10] = { 4,2,8,11,3,6,9,33,23,15 };
sortArr(arr, 10);
printArr(arr, 10);
system("pause");
return 0;
}
●运行结果:
arr[0]=2
arr[1]=3
arr[2]=4
arr[3]=6
arr[4]=8
arr[5]=9
arr[6]=11
arr[7]=15
arr[8]=23
arr[9]=33
请按任意键继续. . .
●代码解析:
sortArr函数是对数组进行从小到大的顺序排序;
printfArr函数是对数组进行遍历输出的操作;
sortArr函数使用了冒泡排序算法,此处冒泡排序的算法请读者尝试理解,我们将在第十四章详细讲解。
思考
请读者思考sortArr函数和printfArr函数中的形参int* pArr是否与数组名pArr[]形式的形参是否完全等价呢?
数组名pArr[]表示的是数组的起始地址,即第0个数组元素pArr[0]的地址。
而int* pArr指针存储的是数组起始地址,使用前需要使用解运算符‘*’取出数组元素地址处的值。二者的区别如图11-5所示:
图11-5 指针形参与数组名形参
11.3.3 指针作为函数的返回值
在C语言里,允许函数的返回值是一个指针,我们称这样的函数为指针函数。
指针函数: 当一个函数声明其返回值为一个指针时,实际上就是返回一个地址给调用函数,以用于需要指针或地址的表达式中。
格式:
类型说明符* 函数名(参数)
实验一百一十七:指针作为函数的返回值
在VS中新建项目11-3-5.c:定义一个函数pFun,用来比较两个数的大小,并返回比较的结果。
/*
指针作为函数的返回值
*/
#include <stdio.h>
#include <stdlib.h>
int* pFun(int* px, int* py)//定义了一个指针函数,接收传入的实参
{
if (*px >= *py)//比较两个数的大小
return px;
else
return py;
}
int main()
{
int x, y, *p;
printf("请输入两个整数:\n");
scanf_s("%d", &x);
scanf_s("%d", &y);
p = pFun(&x, &y);//传入x,y的地址
//p是接收pFun函数的返回结果,此时返回的是一个地址。
printf("%d\n", *p);//使用*p是打印该地址里指向的值。
system("pause");
return 0;
}
●运行结果:
请输入两个整数:
1
2
2
请按任意键继续. . .
●代码分析:
p = pFun(&x, &y);//传入x,y的地址
int* pFun(int* px, int* py)//定义了一个指针函数,接收传入的实参,函数返回值类型int*
return px;或return py;//返回一个指针。
提示
【scanf函数遗留的问题:
在刚接触scanf函数时,我们已经在使用“&”取地址符,在接触时,由于没有介绍指针这个概念,此处我们来理解scanf函数是怎么运作的。
scanf函数是为主调函数中定义的对象保存的值,假如它只接收到变量的值,是无法进行保存的,scanf函数接收的是指针(指向对象的地址),由该指针指向的对象保存我们从键盘上键入的值。
scanf("%d",&x);
004010B5 lea eax,[ebp-4]
首先读取[ebp-4]里的内容给eax,也就是ebp-4的地址
004010B8 push eax
将地址压入堆栈
004010B9 push offset string "%d" (00420020)
%d压入堆栈
004010BE call scanf (0040da90)
调用scanf函数
004010C3 add esp,8
最后以把 存放该地址指向的内容以%d的形式输出。这就是scanf函数。】
上述代码中,我们使用的函数是scanf_s,这是scanf函数的安全形式,scanf_s函数只有当参数是字符串时,需要再增加一个存放字符串缓冲区长度的参数,防止缓冲区大小不足时发生溢出。如果参数是其他数据类型,则二者参数不变。下面是两个函数的原型:
int scanf(const char * restrict format,...); 函数调用scanf("%d %d",&a,&b);
int scanf_s(const char * restrict format, . . . ); 函数调用scanf_s("%c", &c, 1);
总结
指针是灵活多变的,但只要我们知道它每一步都去做了什么,哪里改变了,为什么改变,围绕这三个问题展开分析,只要想明白这些,那么指针对你来说就不是什么难事了。每当我们对于指针认识模糊的时候,请将“指针”两个字改为“地址”,再次尝试怎样理解。
练习
1、下面代码实现:调用_A111改变main里的x的值。
int _A111(int* x){
}
void main(){
int x = 0;
//调用_A111
_A111();
}
- 把数组 arr[5]={1,2,3,4,5};作为函数的参数传递,并逆序输出数组元素。
- 输入10个数,存入一个数组,并找出数组中的最大值。(请用指针相关的知识)。
- 判断某个值是否在该数组中(数组已经排好序):{2,3,6,8,11,13,16,17,20,22}( 要求:请使用指针相关知识,尽可能高效,提示:二分查找)。