函数调用与参数传递

1.形参与实参

//程序1-1 用函数交换变量(错误)
#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", a, b); 
	return 0;
 }

这是三变量交换算法。让我们来运行一下。

3 4

出乎意料,结果并不是我们想要的。为什么会这样呢?
原来啊,main函数中的a、b与swap函数中的a、b并非同一个a、b,它们之间无法互相 访问。因为a=3,b=4,所以swap(a,b)等价于swap(3,4),这里的3、4被称为实际参数(简称实参),而函数声明中的a、b被称为形式参数(简称形参)。函数(包括main函数)的形参和在该函数里定义的变量都被称为该函数的局部变量(local variable)。不同函数的局部变量相互独立,即无法访问其他函数的局部变量。需要注意的是,局部变量的存储空间是临时分配的,函数执行完毕时,局部变量的空间将被释放,其中的值无法保留到下次使用。与此对应的是全局变量(global variable):此变量在函数外声明,可以在任何时候,由任何函数访问。需要注意的是,应该谨慎使用全局变量。
这样,我们就明白了为什么运行之后结果不对了。swap(a,b)是把实参3,4赋给了swap函数声明中的a、b,运行之后,swap中的a、b的值是交换了,但main中的a、b却没有交换。
为了让我们更好的理解函数调用,接下来我们讲讲调用栈。

2.调用栈

调用栈描述的是函数之间的调用关系。它由多个栈帧(Stack Frame)组成,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量,因而不仅能在执行完毕后找到正确的返回地址,还很自然地保证了不同函数间的局部变量互不相干——因为不同函数对应着不同的栈帧。C语言用调用栈(Call Stack )来描述函数之间的调用关系。调用栈由栈帧(Stack Frame )组成,每个栈帧对应着一个未运行完的函数。在gdb(点击跳转至gdb简介)中可以用backtrace(简称bt)命令打印所有栈帧信息。若要用p命令打印一个非当前栈帧的局部变量,可以用frame命令选择另一个栈帧。
接下来我们就通过gdb来看一下swap和main函数所对应的栈帧中a和b的值。

1步:编译程序
gcc swap.c -std=c99 -g
//生成可执行程序a.exe(在Linux下是a.out)。
//编译选项-g告诉编译器生成调试信息。
//编译选项-std=c99告诉编译器按照C99标准编译代码。2步:运行gdb
gdb a.exe
//这样,gdb在运行时会自动装入刚才生成的可执行程序。3步:查看源码
(gdb)  l
#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", a, b); 
	return 0;
//这里(gdb)是gdb 的提示符,字母 l 是输入的命令,为list(列出程序清单)的缩写。
//正如代码所示,swap函数的最后一行是第4行,当执行到这一行时,swap函数的主体已经结束,但函数还没有返回。4步:加断点并运行
(gdb) b 4
Breakpoint l at 0x401308: file swap.c, line 4.
(gdb) r
Starting program: D:\s.exe

Breakpoint l, swap (a=4, b=3) at swap.c:4
4		}
//其中b命令把断点设在了第4行,r命令运行程序,之后碰到了断点并停止。5步:查看调用栈
(gdb) bt
#0 swap (a=4, b=3) at swap.c:4
#1 0x00401356 in main () at swap.c:8
(gdb) p a
$1 = 4
(gdb) p b
$2 = 3
(gdb) up
#1 0x00401356 in main () at swap.c:8
8		swap(a, b);
(gdb) p a
$3 = 3
(gdb) p b
$4 = 4
// 根据bt命令,调用栈中包含两个栈帧:#0和#1,其中0号是当前栈帧——swap函数,1号是其“上一个”栈帧——main函数。
//使用p命令可以打印变量值。
//up命令选择上一个栈帧。
//q命令推出gddb。

首先使用p命令查看当前栈帧中a和b的值,分别等于4和3–这正是用三变量法交换后的结果。接下来用up命令选择上一个栈帧,再次使用p命令查看a和b的值,这次却得到3 和 4,为 main函数中的a和b。在函数调用时,a、b只起到了 " 计算实参 " 的作用。实参被赋值到形参之后,main函数中的a和b也完成了它们的使命。swap函数甚至无法知道main函数中也有着和形参同名的a和b变量,当然也就无法对其进行修改。
到此,第二部分差不多了,友友们若想进一步了解调用栈,推荐以下文章:
C语言函数调用栈(一)
函数调用栈

3.用指针作参数

接下来我们来揭秘,正确编写swap的方法——指针。

//程序3-1 用函数交换变量(正确)
#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", a, b); 
	return 0;
 }

不难发现,swap函数中的声明改为了 " int* a, int* b " ,main函数中的 " swap(a, b) " 改为 " swap(&a, &b) " 。用 int * a 声明的变量 a 是指向 int 型变量的指针。赋值 " a=&b " 的含义是把变量 b 的地址存放在指针a 中,表达式 * a 代表 a 指向的变量,既可以放在赋值符号的左边(左值),也可以放在右边(右值)。
(*a 是指 " a 指向的变量 " ,而不仅是 " a 指向的变量所拥有的值 " 。理解这一点相当重要。例如,*a=*a+1 就是让 a 指向的变量自增1。甚至可以把它写成 (*a)++ 。注意不要写成 *a++ ,因为" ++ " 运算符的优先级高于 " 取内容 " 运算符 " * ",实际上会被解释成 *(a++)。)变量名前加 " & " 得到的是该变量的地址。C 语言的变量都是放在内存中的,而内存中的每个字节都有一个称为地址(address)的编号。每个变量都占有一定数目的字节(可用sizeof 运算符获得),其中第一个字节的地址称为变量的地址。
通俗的讲,指针存的是地址,地址住的是值,地址是独一份的,但不同的地址可以起相同的名字,程序1-1中,传入swap函数的实参是3、4,而程序3-1传入swap函数的是地址,地理位置是独一无二的,所以在swap函数里,家里的人换了,那么在main函数里也就换了。
现在就让我们用gdb来调试上面的程序3-1,看看和1-1有什么不同吧。前4步一致,这里直接看调用栈。

(gdb) bt
#0 swap (a=0x22ff74, b=0x22f70) at swap.c:4
#1 0x0040135c in main () at swap.c:8
(gdb) p a
$1 = (int *) 0x22ff74
(gdb) p b
$2 = (int *) 0x22ff70
(gdb) p *a
$3 = 4
(gdb) p *b
$4 = 3
(gdb) up
#1 0x0040135c in main () at swap.c:8
8		swap(&a, &b);
(gdb) p a
$5 = 4
(gdb) p b
$6 = 3
(gdb) p &a
$7 = (int *) 0x22ff74
(gdb) p &b
$8 = (int *) 0x22ff70

比较草率简略,补充友友:
C语言-指针变量作为函数参数

4.数组作为参数和返回值

话不多说,先看例子

//程序4-1 计算数组元素和(错误)
int sum(int a[]) {
	int ans = 0;
	for(int i = 0; i < sizeof(a); i++)
		ans += a[i];
	return ans;
}

//想回顾一下数组的友友看这里: C语言数组详解

程序4-1不难发现,错在sizeof(a)无法得到数组的大小。为什么会这样?因为把数组作为参数传递给函数时,实际上只有数组的首地址作为指针传递给了函数。换句话说,在函数定义中的inta[ ]等价于int* a。在只有地址信息的情况下,是无法知道数组里有多少个元素的。正确的做法是加一个参数,即数组的元素个数。

//程序4-2 计算数组元素和(正确)
int sum(int* a, int n) {
	int ans = 0;
	for(int i = 0; i < n; i++)
		ans += a[i];
	return ans;
}

此代码中,直接把参数a写成了int* a,暗示a实际上是一个地址。在函数调用时a不一定非要传递一个数组,如:

int main(){
	int a[] = {1, 2, 3, 4};
	printf("%d\n", sum(a+1, 3)); 
	return 0;
 }

以数组为参数调用函数时,实际上只有数组首地址传递给了函数,需要另加一个参数表示元素个数。除了把数组首地址本身作为实参外,还可以利用指针加减法把其他元素的首地址传递给函数。
指针a+1指向 a[1],即2这个元素(数组元素从0开始编号)。因此函数sum " 看到 " {2,3,4}这个数组时,返回9。一般地,若p是指针,k是正整数,则p+k就是指针p后面第k个元素,p-k是p前面的第k个元素,而如果p1和p2是类型相同的指针,则p2-p1是从p1到p2的元素个数(不含p1含p2)。
把数组作为指针传递给函数时,数组内容是可修改的。因此如果需要写一个 " 返回数组 " 的函数,可以加一个数组参数,然后在函数内修改这个数组的内容。
*数组与指针有着千丝万缕的联系,感兴趣的友友可以下载文章顶部的题目文档稍作理解,也可以私信一起讨论分享,这里就不做过多阐释了。

5.把函数作为函数的参数

C语言函数作为函数参数

#include <stdio.h>

#define YES 1
#define NO 0

///*判断函数,进行元素大小判断,increase判断大小比较*/
int compare(int a, int b, int increase)
{
	if (increase > 0) {
		if (a > b) return YES;
		else return NO;
	}
	else
	{
		if (a < b)  return YES;
		else return NO;
	}
}
/*冒泡排序进行数组排序*/
void OrderArr(int arry[], int(*compare)(int, int, int), int length, int increase = 1)
{
	for (int i = 0; i < length - 1; i++)
	{
		for (int j = 0; j < length - i - 1; j++)
		{
			if (compare(*(arry + j), *(arry + j + 1), increase))
			{
				int temp = *(arry + j + 1);
				*(arry + j + 1) = *(arry + j);
				*(arry + j) = temp;
			}
		}
	}
}

/*输出函数*/
void Print(int a[], int length)
{
	for (int i = 0; i < length; i++)
	{
		printf("%d ", *(a + i));
	}
	printf("\n");
}

int main()
{
	int a[5] = { 1, 4, 2, 6, 3 };
	//增序排列数组
	OrderArr(a, compare, 5);
	Print(a, 5);
	//降序排列数组
	OrderArr(a, compare, 5, -1);
	Print(a, 5);
}

6.补充

C语言函数详解

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值