-
2018-04-14 09:58:07两个数的交换:#include <stdio.h>void exchange(int one, int another);
void exchange(int one, int another) {
int tmp;
printf("exchange():%p\n", &tmp);
tmp = one;
one = another;
another = tmp;
}
int main() {
int num1;
int num2;printf("请输入两个数:");
scanf("%d%d", &num1, &num2);exchange(num1 ,num2);
printf("交换结果:%d %d\n", num1, num2);
return 0;
}函数exchange()并不能实现主函数中num1和num2空间的值交换。原因:在主函数调用函数exchange()时,以栈“先进后出"的原则,先将并num1空间的值传递给one空间再将num2空间的值传递给two空间,再在exchange()函数中借助局部变量tmp,将one和two空间的值进行交换,此刻num1和num2空间的值未因此改变。调用完毕后,指针回落,tmp,two和one空间将会出栈,不在受到栈的束缚,而num1和num2空间并未发生变化。因此exchenge( )函数并不能将num1和num2中的值进行交换。
为了实现num1和num2空间值得交换。我们将采用地址传递的方法。代码如下:#include<stdio.h>void realchange(int *, int *);
void realchange(int *one, int *another)
{
int tmp;
tmp =*one;
*one = *anthoner;
*another = tmp;
}
int main()
{
int num1;
int num2;
scanf('%d %d', &num1,&num2);
realchange(&num1, &num2);
printf('交换后:%d %d', num1, num2);
return 0;
}主函数在调用realchange()是将num1和num2空间的首地址分别传递给指针变量one和two的空间,即:*one=&num1(one指向num1)*two=&num2(two指向num2) 在realchange()中,将one所指向的空间(num1)的值赋给tmp空间,再将two所指向的空间(num2)的值赋给one所指向的空间,最后将tmp空间的值赋给two所指向的空间.最终达到交换num1和num2空间的值的目的。
更多相关内容 -
为什么我的交换函数不起作用?
2021-01-28 20:34:56不少学习c语言的小白在写交换函数时都会碰到这样一个问题:明明在函数里已经进行了数值交换,但是输出的值并没有改变,还是之前的值。这是为什么呢?让我们一起来分析一下吧。 一、错误示范 我们初学者会想当然的...
前言
不少学习c语言的小白在写交换函数时都会碰到这样一个问题:明明在函数里已经进行了数值交换,但是输出的值并没有改变,还是之前的值。这是为什么呢?让我们一起来分析一下吧。
一、错误示范
我们初学者会想当然的用形参进行数值交换的操作,下面是演示代码:
void Swap1(int x, int y) { int tmp = 0; tmp = x; x = y; y = tmp; } int main() { int a = 10; int b = 20; printf("交换前: a=%d b=%d\n", a, b); Swap1(a, b); printf("Swap1交换后:a=%d b=%d\n", a, b); return 0; }
输出结果:
我们发现,a和b的值并没有进行交换,为什么呢?函数里明明已经进行交换了呀?这样做的朋友就是对函数参数的传递理解还不够透彻,我们首先来理解一下函数的实参和形参。1.实参
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
2.形参
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
int a = 10; int b = 20; Swap1(a, b);
这里的a和b是确定的值,叫做实参,把他们传递给函数进行下一步操作。
void Swap1(int x, int y)
这里的x和y就是形参,用于接收 Swap1(a, b) 传递过来的参数,然后进行相应的处理,但是处理完以后变量就会自动销毁。这跟局部变量类似,只在局部的范围内起作用,用完就销毁了。而 Swap1函数返回值类型是void,没有返回任何值,所以最终的输出还是原来的a和b的值,没有发生任何改变。
也可这么理解,形参只是实参的一份拷贝,它们都有自己的独立空间,函数内部只是对拷贝的参数进行操作而不会影响到实参的数值。下图是调试状态下的监视窗口,我们可以看到两点重要的信息:
- 分别查看了a,b和x,y的地址,它们并不相同,所以x,y的改变并不影响a,b。
- 当程序走出函数体时,x,y都变成了灰色,表示已经被销毁了,这也印证了我们之前所说的结论。
二、正确示范
分析出了错误原因,接下来既然好办了。我们要找到a和b的地址,然后再改变其对应的值,这样才能达到真正的交换目的。以下是正确的代码:
void Swap2(int* px, int* py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int a = 10; int b = 20; printf("交换前: a=%d b=%d\n", a, b); Swap1(a, b); /*printf("Swap1交换后:a=%d b=%d\n", a, b);*/ Swap2(&a, &b); printf("Swap2交换后: a=%d b=%d\n", a, b); return 0; }
输出结果:
这里涉及到了指针的用法,所谓指针就是地址的一个标记,举个通俗的例子。你去上课的时候,首先要找到教室在哪,你才能上课,而为了方便找到教室,每间教室都会进行相应的编号,如E405就表示E楼的4层第5间教室,有了这个编号你就可以找到教室,找到教室以后,你可以对教室里的布局进行改变,例如你可以改变教室里的桌子数量。同样道理,在程序里你可以通过地址找到a和b,然后可以改变其对应的值。
调试程序后,我们发现a,b的地址和px,py的地址一模一样,说明函数已经定位到了a和b的空间,接下来只需要对相应的值进行交换,程序就能正确输出了。
三、总结
我们来进行总结,通过一个小小的交换函数,我们用到了很多知识,首先是函数的调用,涉及到实参和形参的使用,如果不理解清楚,很容易会出现逻辑上的错误。其次我们还学会用指针进行传参,这样就能真正的改变参量的值。所以学习还真不能只看到表面现象,理解到其背后的意义也是很重要的。
-
C语言swap函数 解决函数不能交换值的问题
2020-12-15 17:04:04如果直接把两个int传入函数,用一个中间变量交换他们俩,那么这只是改变了这个变量在swap函数里面的值,回到main里面就复原了。 那么,如何真正交换两个int变量的值呢? 用指针,把两个int的地址传过去才可以。 如下...首先在看这一篇博客之前应该要了解到在main函数里面调用一个main函数外的函数,参数传过去只是一个拷贝的值。
如果直接把两个int传入函数,用一个中间变量交换他们俩,那么这只是改变了这个变量在swap函数里面的值,回到main里面就复原了。
那么,如何真正交换两个int变量的值呢?
用指针,把两个int的地址传过去才可以。
如下#include <stdio.h> void swap1(int a,int b) { int temp; temp = a; a=b; b=temp; } void swap2(int *a,int *b) { int temp; temp = *a; *a = *b; *b = temp; } int main() { int a=4,b=5; swap1(a,b); printf("假交换 a:%d b:%d\n",a,b); swap2(&a,&b); printf("真交换 a:%d b:%d",a,b); } //假交换 a:4 b:5 //真交换 a:5 b:4
-
函数调用栈
2022-04-03 14:19:44函数调用栈 我们在编程中写的函数,会被编译器编译为机器指令,写入可执行文件,程序执行的时候,会把这个可执行文件加载到内存,在虚拟地址空间中的代码段存放。 如果在一个函数中调用另一个函数,编译器就会对应...函数调用栈
我们在编程中写的函数,会被编译器编译为机器指令,写入可执行文件,程序执行的时候,会把这个可执行文件加载到内存,在虚拟地址空间中的
代码段
存放。如果在一个函数中调用另一个函数,编译器就会对应生成一条
call
指令,当程序执行到这条call指令时,就会跳到对应的函数入口处开始执行,而每一个函数的最后,都有一条ret
指令,负责在函数结束后跳回到调用处继续执行。栈区
函数执行的时候需要有足够的内存空间来存放局部变量,参数,返回值等数据,这些数据存在上图中的栈中。
栈就是先入后出,先入栈的在底部。
在虚拟地址空间的栈区,上面的是高地址,下面是低地址,放了一些数据,栈底通常称为
栈基
,栈顶又叫栈指针
。具体的栈帧布局是:
调用者栈基地址
(也就是谁调用了这个函数)局部变量
调用函数的返回值
参数
通过栈指针加上偏移来定位到每个参数和返回值。
比如栈指针+8字节处,就是栈指针的上一格,通过这种方式来进行偏移。
还记得我们之前说当在A函数中调用B函数时,会在A函数中插入一条
call
指令,当执行到call
指令的时候,会去B函数开始处运行。那么
call
指令做的事情就是:- 首先把A函数中下一条指令的地址入栈(栈基地址,当B函数执行完之后,可以再通过这个地址回到A函数的调用处继续执行A函数。)
- 跳转到被调用函数的入口处执行(也就是被调用函数的栈帧,而所有的函数栈帧布局都遵循统一的结构约定。)
栈具体的入栈策略
程序执行时,CPU通过特定的寄存器来存运行时的栈基和栈指针,也有指令指针寄存器用来存储下一条要执行的指令地址。
执行指令的过程有两种,第一种是逐步扩张:
- 如果要执行
入栈3
这条指令,CPU读取之后,会先把指令指针移向下一条指令,然后栈指针向下移动,入栈数字3。 - 然后再执行
入栈4
这条指令,CPU读取之后,再把指令指针移向下一条指令,然后栈指针向下移动,入栈数字4。 - 一直往复。
Go语言中的是第二种——
一次性分配
,它会直接将栈指针移动到所需最大栈空间的位置,然后通过右边这种相对寻址的方式,来把对应的值入栈。Go语言选择使用
一次性分配
的策略是有原因的,拿下图来讲,下面三个goroutine,初始分配的栈空间只有那么大,如果要逐步扩张的话,如果g2执行到最后了,但是接下来要执行的函数又要用掉很多的空间,如果函数栈是逐步扩张的,执行时就可能会发生栈访问越界。函数栈帧的大小可以在编译时期确定, 对于栈消耗大的函数,Go编译器会在函数头部插入检测代码,如果发现需要进行
栈增长
,则会另外分配一段足够大的空间,然后把原来的内容移过来,并释放原来的空间。call和ret的细节
首先我们可以看到,下面是
栈区
和代码段
。当代码段执行到对应的指令时,就会给栈中添加对应的元素,最终再把栈全部出栈。
假如说,我们是在函数A中的a1处调用函数B(函数B开始位置为b1)。
首先,在最开始的时候,寄存器在栈中的情况是这样的:
ip寄存器中存的是下一条要运行的指令,那么当我们的代码段运行到
a1
的call指令时,会做两件事:首先会入栈返回地址a2,然后栈指针sp向下一格,然后给ip寄存器b1的指令地址,接下来要去B函数的开始处运行。
call指令就结束了。
接下来就要运行四步函数都要做的事:
- 第一步是先把栈指针sp移动到足够大的位置——
s7
上。 - 第二步是存储一下之前栈基
bp
寄存器的值,这样可以在运行完之后,还能回到原来的栈基地址。 - 第三步是把
s5
存入栈基地址。 - 接下来就要做函数剩下的指令了——参数,代码等,并一一入栈。
在函数B运行到最后——
ret
指令之前,编译器还会插入两条指令:- 恢复调用者栈基。最开始我们分配了多少空间,此时就释放多少空间,修改bp寄存器为之前入栈的s1,bp继续指向s1处。
- 然后就到ret指令了,它首先会弹出call指令压栈的返回地址
a2
,sp赋值为s3。然后跳转到这个返回地址a2,把ip寄存器赋值为a2。 接下来可以从a2这里继续执行了。
简单来说,call指令会分配栈帧,ret指令又会释放栈帧,恢复到call之前的样子。通过这些指令的配合,就能实现函数的层层嵌套了。
函数传参和返回值
首先看一个例子,下面这个例子是交换两个局部变量的值,可以看到,结果并没有改变:
上面那个函数在栈中的分配如下:
- 首先分配局部变量的空间,然后把局部变量存进去。
- 然后分配被调用函数的参数,从右至左分配。先入栈第二个参数,再入栈第一个参数。传参是值拷贝,所以把两个参数的值压入栈。
- 接下来是call指令存入的返回地址。也就是fmt.Println这一行代码所对应的指令
- 再接下来就是swap函数栈帧了
当swap函数执行到
a,b=b,a
时,就会修改参数对应的值,但是调用者的局部变量a和b在上面,交换的并不是它们,所以最终结果显示没有交换成功。我们再修改一下:
还是和上一次的一样,只是我们把指针作为参数,函数参数还是值类型,所以会拷贝两个地址的值。
再swap函数中,会将对应地址的值进行交换,修改的是调用者的局部变量
a
和b
,所以最终修改成功。通常,返回值是通过寄存器传递的,但是Go语言支持多返回值,所以在栈上分配返回值更合适。
接下来我们看一个有返回值的例子:
-
一次性分配main函数栈帧,sp直接到达对应的位置。
-
把局部变量压入栈
-
压入栈函数返回值(默认为0)——因为栈是先入后出的缘故,所以一个函数的执行步骤要从后往前的压入栈。
-
把函数参数压入栈
-
保存调用者函数main的栈基地址以方便最后回到main函数。
-
接下来进入函数
incr
的函数栈帧。 首先初始化局部变量b,默认为0, -
然后执行
a++
指令,把局部变量a的值加1. -
运行到
b=a
的指令,把参数a赋给局部变量b。 -
接下来就是返回值和defer函数的问题。 在Go语言中,是先给返回值赋值,然后再执行defer函数。
- 把局部变量b的值,拷贝到返回值空间。
- 执行注册的defer函数,在defer函数中,参数a再次自增1,局部变量b也加1。但是需要注意的是它让局部变量b自增1,不代表就把返回值自增1,因为在defer之前,已经给返回值赋值过了,可以看下图,b的值是2,但是返回值还是1.
- 把返回值给局部变量b
- 输出a,b——0,1
接下来我们看这个例子,用的是命名返回值:
- main函数的栈情况还是如下图右边所示。
- 接下来会运动到
incr
函数的a++
指令,然后把参数a的值加1. return a
指令赋值a变量的值给返回值局部变量b,此时b=a=1.- 运行defer函数,a++,b++,a和b都是2.
- 此时返回值的位置为2,所以会把main函数中的局部变量b赋值为2.
- 打印0,2
当函数A中调用函数B和函数C时,栈的寻址策略
-
首先分配A函数的局部变量空间。
-
因为后面有两个函数要执行,又因为Go是一次性分配空间的,所以会分配最大的参数和返回值空间,函数B比函数C的空间要大,就以函数B所需要的空间标准来分配,如下图
r2~p1
这么大的空间。 -
接下来把函数B的参数和返回值压入栈,进入函数B的栈帧。
- 当函数B执行完毕之后,会释放这两片空间。把函数C的参数和返回值压入栈,但是此时空间还是那么大,
r1
和p1
是存在这片空间的上面,还是下面,还是中间?
最终的答案就是,会把
r1
和p1
分配到最下面,和函数C的栈指针挨着,这样虽然上面会空出来一块,但是被调用者通过栈指针相对寻址自己的参数和返回值时会比较方便。 -
C语言交换函数
2018-09-21 22:30:58函数调用分析: 调用swap1打印结果:before x = 100 y = 900 after x = 100 y = 900 分析:swap1函数进行数值交换,主函数并没有交换 调用swap2打印结果:before x = 100 y = 900 before a = 0x7fff... -
调用函数时,到底什么时候要用指针接收,看完就不难理解了(Swap交换函数为例)
2018-09-29 20:16:24在这里我通过讲解一个交换函数来讲解一下什么时候传参需要用指针接收。 这里先看一段代码,很多初学者对指针掌握不够,经常会写出这样的代码: #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h&... -
关于为什么做一个交换函数要用到指针
2018-12-26 10:40:19形参是自定义的函数所拥有的参数,在自身中可以被使用,自身外不可用,也就是他的作用域是定义它的函数本身 实参是主调函数的参数,在被调函数中不可用 例如: int swap(int a,int b){.......} int main(void... -
调用函数,实现两个数的值交换
2019-03-31 21:32:162.2、同理,我们在函数调用的时候写入的实参应该为两个数的地址;(也可以理解为调用函数实现的值交换是对数值地址的地址交换,进而实现的值交换) 3、打印结果。 具体代码如下: #define _CRT_SE... -
【C语言】交换函数(swap)
2020-03-18 10:19:01这次我们要透过一个简单的函数swap深入理解函数传参的本质以及在C中如何选择传参方式。 首先,先来看第一段程序: ...通过main函数的调用,我们发现x,y并未实现交换: int main() { int x = 1; int y = 37; ... -
Swift 函数的定义及调用
2022-03-03 18:02:491、函数的基本概念 ...形式参数能提供一个默认的值来简化函数的调用,也可以被当作输入输出形式参数被传递,它在函数执行完成时修改传递来的变 量 Swift 中的每一个函数都有类型,由函数的形式参数类型和返回 -
【C语言函数】 - 库函数、自定义函数、函数参数、函数调用、嵌套调用链式访问、递归与迭代、缓冲区
2021-07-24 15:53:57传址三、函数参数1、实际参数2、形式参数四、函数调用传值、传址五、练习1、写一个函数判断一个数是不是素数2、写一个函数判断是不是闰年3、写一个函数 实现一个整数有序数组的二分查找4、写一个函数 每调用一次这个... -
汇编:函数调用
2019-07-18 18:57:33让我们再回忆一下脑海中关于函数调用的概念,也许会是这个样子: 这里的“控制流转移”又是如何发生的呢?在解释这个之前,也许我们需要科普一点有关于汇编的知识。 2. 函数调用中的一些细节说明 2.1 函数调用中的... -
C语言-函数(一):函数的定义和调用
2022-04-15 15:18:32函数(一)----- 函数的定义、调用及一些函数的例子 -
C++的函数的传值调用、指针调用、引用调用的联系区别
2018-10-22 14:06:39当调用函数时,有两种向函数传递参数的方式: 调用类型 描述 传值调用 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。 指针调用 该方法把参数的... -
c语言 通过调用函数交换两个实参指针的指向地址
2020-07-18 00:16:38下面以交换两个数的值的问题为例来进行说明:a = 5,b = 6;交换两个数的值。 错误做法: 用指针作为形参,交换a,b的值(这里通过交换指针地址的方式)。下面给出常见的错误代码: #include<bits/stdc++.h> ... -
C语言函数(函数分类,参数,调用,声名及定义)
2022-04-24 16:57:13文章目录@[TOC](文章目录)一、C语言中函数的分类二、函数的参数及调用三、函数的嵌套调用和链式访问四、函数的声名和定义 一、C语言中函数的分类 库函数 自定义函数 库函数:在开发的过程中,每个程序员都可能用... -
自定义函数及函数调用
2019-05-10 12:17:10传来传去都不知道传的到底是什么? 接下来,我就重点围绕道友们常见的问题,说一说: 1、自定义函数声明、函数调用、函数实现 比如:交换两个数的值的自定义函数swap 函数声明方式: void swap(int &a,int &... -
交换两个数的3种方法:宏定义,直接,函数调用
2018-04-23 20:23:391.不调用函数法:建立临时变量交换直接在需要处int temp=b; b=a; a=temp;2。宏定义法:加减法、需要时时有括号 a=a+b b=a-b a=a-b#define swap(a,b) ((a)=(a)+(b);(b)=(a)-(b);(a)=(a)-(b))缺点:a+b可能会溢出3.... -
C语言--有返回值与无返回值的函数调用(函数内容一)
2022-03-23 17:51:22有返回值和无返回值之间的函数调用 C语言中创建及调用函数的简介 上期在说到Python实现阶乘相加时,对于函数的调用及递归没有进行详解,这次主要介绍清楚C语言中的函数调用。 在C语言中,不管是对数值进行交换,或者... -
c语言函数调用及应用举例
2019-04-20 16:27:29一般来说,执行源程序就是执行主函数main,其他函数只能被主函数所调用,而其他函数之间也可以相互调用。 1.标准库函数: 分为:I/O函数,字符串,字符处理函数,数学函数,接口函数,时间转换和操作函数,动态... -
C++函数调用之传值调用、指针调用和引用调用
2019-03-07 18:40:44当调用函数时,有三种向函数传递参数的方式: 调用类型 描述 传值调用 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。 指针调用 该方法把参数的地址... -
用函数实现两个数的交换
2021-11-14 16:49:37这里要注意,在用函数实现两个数交换的功能时,不能简单地用一般的形参实现。 如果用简单的形参实现的话,主函数中在调用该函数后,传入的实参的值根本不会随着形参的值而变化,而只是单纯地交换了形参的值。 如果... -
指针:调用自定义交换函数,完成三个数整从小到大排列
2018-12-02 09:30:07调用自定义交换函数swap(int *p1, int *p2),完成三个整数从小到大排列 Input 多组测试数据,每组输入三个任意整数 Output 输出从小到大排列的三个数 Sample Input 9 2 7 0 -2 12 8 3 1 Sample Output 2 7 9 -2 0 12... -
C语言中对函数指针的调用
2021-04-21 22:08:09调用函数(指针变量)对a,b值进行交换 /* ↓此处a,b地址与main函数a,b地址不同,a,b仅仅在此函数内互相交换,main函数内a,b并没有交换 */ void Swap_err1(int a,int b) { int tmp = a; a = b; b = tmp; } ... -
使用函数实现两个数的交换(C语言)
2018-12-23 23:47:40定义函数,调用函数,完成交换。 你的代码是否和下面一样呢? #include<stdio.h> #include<stdlib.h> void Swap(int a,int b) { int tmp = a; a = b; b = tmp; } ... -
C语言-调用swap函数,交换主函数中变量x和y中的数据。
2020-06-06 17:09:41#include<stdio.h> void swap(int *,int *); main() { int x=30,y=20; printf("(1)x=%d y=%d\n",x,y); swap(&x,&y); printf("(4)x=%d y=%d\n",x,y); } void swap(int *a,int *b) ... printf -
函数的声明、定义和调用
2017-08-08 15:33:13本博简单介绍一下C语言中函数声明、定义和调用的概念 -
用函数实现两个整数的交换
2019-05-14 21:18:26这里主要考察 实参和形参的用法 对形参的修改不会改变实参 函数的调用 传值调用 ...这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操作函数外部的 变量。 ... -
C语言 函数参数和调用函数
2019-04-14 08:39:07当调用函数时,有两种向函数传递参数的方式: 传值调用 默认情况下,C 语言使用传值调用方法来传递参数。一般来说,这意味着函数内的代码不会改变用于调用函数的实际参数。函数 swap() 定义如下: /* 函数定义 */ ... -
MATLAB提示不能在脚本中定义函数,是什么意思?
2021-04-26 12:30:40点击查看MATLAB提示不能在脚本中定义函数,是什么意思?具体信息答:你试图在命令窗口定义函数,这种做法是错误的。 你需要建立一个.m文件,文件名是Chebyshev.m,然后在里面输入源程序。答:定义函数必须在M文件内...