4.2 函数调用与参数传递选讲
本节的函数将更加复杂有趣,如返回多个值,或者拥有交换两个变量的功能
错误的代码示范:(笔者认为这边的理解应该在学完指针后理解会更加了解为什么会产生这个错误)
点击查看代码
#include<stdio.h>
void swap(int a, int b)
{
int t;
t = a;
a = b;
b = t;
}
int main()
{
int a, b;
scanf("%d%d", &a, &b);
swap(a, b);
printf("%d %d\n", a, b);
return 0;
}
这个程序一看是三变量交换算法,但是这里的程序测试后并不会交换a,b的值,在这里原作的作者是这样解释的
a = a + 1;这边先计算赋值符号右边的a+1然后把它装入变量a,覆盖原来的值
1.先计算参数的值因为a = 3, b = 4,所以swap(a, b)等价于swap(3, 4)这里的3和4被称为实际参数(简称为实参)
2.把实参赋值给函数声明中的a和b。注意,这里的a和b与调用时的a和b是完全不同的。前面已经说过,实参最后将算出具体的值,swap函数知道调用他的参数是3和4(也就是说实参会先将前面的计算的式子全部计算出来后,然后将计算出来的值传送给调用的函数,此时该函数接收到的参数被称为形式参数)实参最后算出具体的值,swap函数知道调用它的参数是3和4,却不知道是怎么算出来的。函数声明中的a和b称为形式参数(简称形参)
3.这样一来,程序里有个变量a,一个在main函数里面定义,一个是swap的形参,二者不会混淆,因为函数(包括main函数)的形参和在该函数里定义的变量都被称为该函数的局部变量。不同函数的局部变量相互独立,即无法访问其他函数的局部变量。需要注意的是局部变量的存储空间是临时分配的,函数执行完毕时,局部变量的空间将被释放,其中的值无法保留到下次使用。与此对应的是全局变量,此变量在函数外声明,可以在任何时候,由任何函数访问。需要注意的是,应该谨慎使用全局变量。也就是在函数中定义的变量是局部变量,不在函数中定义的变量是全局变量
4.函数的形参和在函数内声明的变量都是该函数的局部变量。无法访问其他函数的局部变量(应该是无法直接访问该局部变量,通过指针的传递还是可以访问该变量),局部变量的存储空间是临时分配的,在函数执行完毕时,局部变量的空间将被释放,其中的值无法保留到下次使用。在函数外声明的变量是全局变量,可以被任何函数使用。操作全局变量有风险,应谨慎使用。
5.执行完毕后,函数会将返回值返回给调用他的函数,然后再次修改当前代码行,恢复到调用他的地方继续执行,那么系统是如何实现这样子的操作呢?下面会给出解释
学术讨论环节
调用栈描述的是函数之间的调用关系,它有多个栈帧组成,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。注意不同栈的局部变量是不互通的。在gdb中可以用bt命令打印所有栈帧信息,若要用p命令打印一个非当前站真的局部变量,可以用frame命令选择另一个栈帧,当然如果栈帧比较少的时候,可以直接通过up,down来实现栈帧的切换
栈帧中保存了该函数的返回地址和局部变量,因而不仅能在执行完毕后找到正确的返回地址,还很自然的保证了不同函数见的局部变量互不相干--因为不同函数对应着不同的栈帧
通过gdb调试后,会发现在swap函数调用后还没有返回main函数前对应的a和b的值,swap中的a和b确实实现了三变量法交换,但是main函数中的a和b并没有实现交换
注意gcc编译器中的编译选项-g告诉编译器生成调试信息,编译选项-std=c99告诉编译器按照C99标准编译代码
栈数据结构符合先进后出的原则,先进的称为上一个栈帧,后进的称为下一个栈帧,这样也可以方便到时候使用up,down命令
原因:在函数调用的时候,a,b只起到了计算实参的作用,但实参被赋值到形参之后,main函数中的a和b也完成了他们的使命,swap函数甚至无法知道main函数中也有着和形参同名的a和b变量,当然也就无法对其进行修改
调用栈由栈帧组成
能够读懂以上程序的关键就是理解栈这种特殊的数据结构,以及不通过栈中的局部变量互不相通
解决方法
用指针做参数
代码如下:
点击查看代码
#include<stdio.h>
void swap(int* a, int* b)
{
int t = *a; *a = *b; *b = t;
}
int main()
{
int a = 3, b = 4;
swap(&a, &b);
printf("%d %d\n", a, b);
return 0;
}
C语言的变量都是存放在内存中的,而内存中的每个字节都有一个称为地址的编号。每个变量都占有一定数目的字节(可以使用sizeof运算符获得),其中第一个字节的地址称为变量的地址
运算符&的作用是获取该变量的地址
注意此时Swap函数中的a和b两个变量存储的是int型变量的地址,同时以0x开头的整数以十六进制表示
用int* a声明的变量a是指向int型变量的指针。赋值a = &b的含义是把变量b的地址存放在指针a中,表达式a代表a指向的变量,即可以放在赋值符号的左边,也可以放在右边
注意:a是指a指向的变量,而不仅是a指向的变量所拥有的值。理解这一点相当重要。例如a = a + 1就是让a指向的变量自增1,甚至可以把它写成(a)++。注意不要写成a++,因为++运算符的优先级高于取内容运算符,实际上会被解释成(a++)
指针的出现会使得c语言变得复杂了许多,char * const ( next)()中的next是什么类型,暂时不清楚,网上说法,是指向函数的指针,其返回值是字符型指针,其中的字符不能被更改
当然算法竞赛的核心是算法,指针只是工具,了解底层的细节是有益的,没有必要纠缠如此复杂的语言特性。在编程时尽量避开,遵守一些注意事项就可以了。
千万不要滥用指针,这不仅会把自己搞糊涂,还会让程序产生各种奇怪的错误。
在swap程序中,a和b都是局部变量,在函数执行完毕以后就不复存在了,但是a和b里保存的地址却依然有效,他们是main函数中的局部变量a和b的地址,在main函数执行完毕之前,这两个地址将始终有效,并且分别指向main函数的局部变量a和b,程序交换的是a和b,也就是main函数中的局部变量a和b。(mian函数执行完毕之后,a和b两个main函数中的局部变量就会因为main函数的终止而被释放,此时ab的地址就失去作用了)
初学者易犯的错误:
点击查看代码
void swap(int* a, int* b)
{
int *t = a; a = b; b = t;
}
上述的代码段看似是一个标准的三元交换,但是它的本质只是交换了ab中的地址,也就是说ab指向的目标交换了,但是它们指向的内容并没有被交换,因此main函数中的a和b并不会改变
接下来还有一个隐蔽的错误代码段
点击查看代码
void swap(int* a, int* b)
{
int *t;
*t = *a; *a = *b; *b = *t;
}
该swap函数中的t是一个指向int型的指针,因此t是一个整数,但是问题是t指向的内容是什么也就是t中的地址是什么。我们并没有初始化t,也就是t中的地址是不确定的,如果它指向的内存单元恰好是可以能写入的,那么程序就能正常工作,如果指向的内存是只读的,那么程序可能会崩溃。
指针内容可以作为一个拓展内容,在某些时候可以起到简化优化程序的作用。
数组作为参数和返回值
错误示范:
点击查看代码
int sum(int a[])
{
int ans = 0;
for(int i = 0; i < sizeof(a); i++)
ans += a[i];
return ans;
}
注意该函数中的sizeof并不能得到数组的大小,因为把数组作为参数传递给函数时,实际上只有数组的首地址作为指针传递给了函数,也就是说在参数列表中定义的int a[]等价于int* a。在只有地址信息的情况下,是无法知道数组里有多少个元素的。因此在传递参数的时候,应该还要传递一下数组长度。
正确做法如下:
点击查看代码
int sum(int* a, int n)
{
int ans = 0;
for(int i = 0; i < n; i++)
ans += a[i];
return ans;
}
以数组为参数调用函数时,实际上只有数组首地址传递给了函数,需要另加一个参数表示元素个数。除了把数组首地址本身作为实参外,还可以利用指针加减法把其他元素的首地址传递给函数
指针a+1指向a[1],即2个元素(数组元素从0开始编号),一般的,若p是指针,k是正整数,则p+k就是指针p后面第k个元素,p-k就是p前面的第k个元素,如果p1和p2是类型相同的指针,则p2-p1是从p1到p2的元素个数(不包括p2)
sum的另外两种写法
点击查看代码
int sum(int* begin, int* end)
{
int n = end - begin;
int ans = 0;
for(int i = 0; i < n; i++)
ans += begin[i];
return ans;
}
点击查看代码
int sum(int* begin, int* end)
{
int *p = begin;
int ans = 0;
for(int *p = begin; p != end; p++)//这边的int* p去掉后并不会影响结果,不知道是否是原先的笔误
ans += *p;
return ans;
}
将数组作为指针传递给函数时,数组内容是可以修改的,不过在算法竞赛中经常采取其他的操作方法
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)