【C语言】形参实参以及参数传递


一、形参与实参

1、定义

==形参:==形参全称形式参数,就是定义函数时圆括号中的参数。
==实参:==实参全称实际参数,就是调用函数时圆括号中的参数。

如下所示:

#include <stdio.h>
//计算从m加到n的值
int sum(int m, int n) {
    int i;
    for (i = m+1; i <= n; ++i) {
        m += i;
    }
    return m;
}
int main() {
    int a, b, total;
    printf("Input two numbers: ");
    scanf("%d %d", &a, &b);
    total = sum(a, b);
    printf("a=%d, b=%d\n", a, b);
    printf("total=%d\n", total);
    return 0;
}

函数定义处的 m、n 是形参,函数调用处的 a、b 是实参。

2、区别

(1)形参本质是一个占位符,是一个名字,是虚拟变量,不占用内存;实参本质是一个变量,它包含实实在在的数据,占用内存
(2)实参在函数外部有效,而形参在函数内部有效。

3、联系

(1)实参和形参在数量上、数据类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。

(2)函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参

(3)形参和实参虽然可以同名,但它们之间是相互独立的,互不影响

二、参数传递

1、定义

发生函数调用时,实参的值会传递给形参,这一过程叫做参数传递。

2、分类

(1)值传递

什么是值传递?看下面这个例子

#include <stdio.h>
 
void swap (int x, int y)
{
	int temp;
	temp = x;
	x = y;
	y = temp;
	printf ("x = %d, y = %d\n", x, y);
}
 
int main (void)
{
	int a = 4, b = 9;
	swap (a, b);//这里调用swap函数
	printf ("a = %d, b = %d\n", a, b);
	return 0;
}
输出结果:
x = 9, y = 4
a = 4, b = 9

调用swap时实际完成的操作如下:

int x=a;  //←
int y=b;  //←注意这里,头两行是调用函数时的隐含操作
int tmp;
tmp=x;
x=y;
y=tmp;
printf ("x = %d, y = %d\n", x, y);

其实函数在调用时是隐含地把实参a,b 的值分别赋值给了x,y(这就是所谓的值传递),之后在你写的 swap 函数体内再也没有对a,b进行任何的操作了。交换的只是x,y变量。并不是a,b。因此a,b的值并没有改变。

(2)址传递

址传递本质上还是值传递,它是值传递的一种特殊情况。

看下面的例子:

#include <stdio.h>
void swap (int *px, int *py)
{
	int temp=*px;
	*px=*py;
	*py=temp;
	printf("*px = %d, *py = %d\n", *px, *py);
}
 
int main(void)
{
	int a=4;
	int b=6;
	swap (&a,&b);
	printf("a = %d,b = %d\n", a, b);
	return 0;
}
输出结果:
*px = 6, *py = 4
a = 6,b = 4

对于void swap (int *px, int *py),请注意参数px,py都是指针。

调用时:swap (&a, &b); 它将 a 的地址 &a 代入到 px,b 的地址 &b 代入到 py。

调用 swap 函数时实际执行过程如下:

px=&a;   
py=&b;  //请注意这两行,它是调用 swap 的隐含动作。
int temp=*px;
*px=*py;
*py=temp;
printf("*px=%d,*py=%d\n",*px, *py);

指针 px,py 的值已经分别是 a,b 变量的地址值了。接下来,对 *px,*py 的操作当然也就是对 a,b 变量本身的操作了。所以函数里头的交换就是对 a,b 值的交换了,因此 a,b 的值改变了。这就是所谓的址传递(将 a,b 的地址传递给 px,py)。

现在可以理解为什么址传递是值传递的一种了。他们都是做的变量赋值。即

值传递和址传递都是在调用函数时将实参的值赋值给形参,但址传递时实参和形参都是地址。

注意:这里不要理解为 “对于址传递,调用函数是将实参的地址赋值给形参。”这是错误的,因为这里实参为&a,&b本身就是地址,因此就是将实参的值赋值给形参。

对于值传递,调用函数并不能改变变量a,b的值;对于址传递,调用函数并不能改变&a, &b的值。

他们本质上是一样的。

(3)引用传递

引用传递效果和址传递一样,但比址传递简单。

如下所示:

#include <stdio.h>
void swap (int &x, int &y)
{
	int temp = x;
	x = y;
	y = temp;
	printf ("x = %d, y = %d\n", x, y);
}
 
int main (void)
{
	int a = 4;
	int b = 6;
	swap (a, b);
	printf ("a = %d, b = %d\n", a, b);
	return 0;
}
 
输出结果:
x = 6, y = 4
a = 6, b = 4

引用传递中形参改变会使实参也改变。

为什么?

因为变量和变量的引用操作的是同一块内存。


三种传递方式的实参形参区别如下:

项目值传递址传递引用传递
实参普通变量地址普通变量
形参普通变量地址引用
形参的改变会否影响实参不会

三、一个看起来是址传递其实是值传递的例子

1、问题描述

这里我新建一个链表,使用尾插法插入4个节点,存储数据1,2,3,4。遍历链表后删除整个链表。

节点定义如下:

typedef struct node {
	data_t data;
	struct node* next;
}listnode, * linklist;

创建链表函数如下:

linklist list_create()//创建链表
{
	//第一步:申请内存
	linklist H;
	H = (linklist)malloc(sizeof(listnode));//这里用llistnode和用linklist到底有什么区别?
	//答:申请内存大小不一样,若实际使用的内存超过申请的内存,free时就会报错
	if (H == NULL)
	{
		printf("malloc failed\n");
		return H;
	}
	//第二步:赋初值
	H->data = 0;//头结点数据域没有用到,默认放0
	H->next = NULL;

	//返回头结点
	return H;
}

尾部插入节点函数如下:

int list_tail_insert(linklist H, data_t value)
{
	linklist p;
	linklist temp;//为找到尾节点而定义的临时节点指针

	if (H == NULL)
	{
		printf("H is NULL\n");
		return -1;//若链表还没创建就不能有后续操作,提升程序健壮性
	}
	//第一步:建立一个新节点
	if ((p = (linklist)malloc(sizeof(listnode))) == NULL)
	{
		printf("malloc failed\n");
		return -1;
	}

	p->data = value;
	p->next = NULL;//因为是尾节点,所以指针域为NULL

	//第二步:找到尾节点,尾节点的指针域为NULL
	temp = H;//从头开始找
	while (temp->next)
	{
		temp = temp->next;
	}
	//temp从循环出来已经是指向尾节点了

	//第三步:建立新节点与链表尾部的连接
	temp->next = p;
	return 0;
}

链表删除函数如下:

int list_free(linklist H)
{
	linklist p;
	if (H == NULL)
	{
		printf("H is NULL\n");
		return -1;
	}
	while (H != NULL)
	{
		p = H;
		H = H->next;
		printf("释放元素:%d\n", p->data);
		free(p);
		p = NULL;
	}
	puts("");//换行???
	return 0;
}

链表遍历函数如下:

int list_show(linklist H)
{
	linklist p;
	if (H == NULL)
	{
		printf("H is NULL\n");
		return -1;
	}
	p = H;
	while (p->next != NULL)
	{
		printf("%d ", p->next->data);
		p = p->next;
	}
	puts("\n");
	return 0;
}

执行结果如下:

第一步:创建链表完成
input a number:1
input a number:2
input a number:3
input a number:4
input a number:-1
第二步:输入数据完成
第三步:遍历链表:
1 2 3 4

第四步:删除整个链表
before free:00866A58
释放元素:0
释放元素:1
释放元素:2
释放元素:3
释放元素:4

after free:00866A58

首先看int list_tail_insert(linklist H, data_t value)函数,只看第一个形参,应该是址传递,因为linklist H是结构体指针,它也可以写成linklist *H,这就跟我们常见的int fun(int *a)是一样一样的了,传入的是一个指针。从结果上看也是如此,即函数内H的变化也会引起函数外H的变化,这符合址传递的特点。

因此int list_tail_insert(linklist H, data_t value)函数是址传递无疑。

再看另一个int list_free(linklist H)函数,除了函数名其他的跟上面的函数简直是一模一样,那它也是址传递咯。

真的吗?

list_free中有这样一个操作:

	while (H != NULL)
	{
		p = H;
		H = H->next;
		printf("释放元素:%d\n", p->data);
		free(p);
		p = NULL;
	}

要跳出该循环,H只能为NULL。

那好我们看看list_free函数执行前后,H是不是变成NULL了。

在main中我们做如下操作:

printf("第四步:删除整个链表\n");
	printf("before free:%p\n", H);
	list_free(H);
	printf("after free:%p\n", H);

执行结果:

第四步:删除整个链表
before free:00866A58
释放元素:0
释放元素:1
释放元素:2
释放元素:3
释放元素:4
after free:00866A58

执行完,H不是NULL!,并且还保持原样!

函数内部的变化并没有影响函数外部的H,那这又是值传递咯?

为什么对于如下两个函数,形参都是linklist H,一个是址传递,一个却是值传递?

int list_tail_insert(linklist H, data_t value);//址传递
int list_free(linklist H);//值传递

2、推理过程

(1)事实——址传递本质上是值传递

还是看之前址传递中提及的例子:

#include <stdio.h>
void swap (int *px, int *py)
{
	int temp=*px;
	*px=*py;
	*py=temp;
	printf("*px = %d, *py = %d\n", *px, *py);
}
 
int main(void)
{
	int a=4;
	int b=6;
	swap (&a,&b);
	printf("a = %d,b = %d\n", a, b);
	return 0;
}
输出结果:
*px = 6, *py = 4
a = 6,b = 4

说址传递本质还是值传递,是因为调用函数只能改变a,b的值,并不能改变&a, &b的值。即只能改变指针指向的值的大小,不能改变指针本身。
也就是说:

void swap (int *px, int *py)
{
	*px=3;//可以改变px指向的值的大小,属于址传递
	px=NULL;//不可以改变px的指向,属于值传递
	int temp=*px;
	*px=*py;
	*py=temp;
	printf("*px = %d, *py = %d\n", *px, *py);
}

你看,对于一个典型的址传递的例子,内部也同时出现了址传递和值传递的情况。

(2)类比

这两个函数传递参数都是linklist H,或者说是linklist *H,两者等价。

int list_tail_insert(linklist H, data_t value);//址传递
int list_free(linklist H);//值传递

一个是址传递,对应的是上述中的*px=3; 想改变指针指向的值,这样做能影响函数外部参数。
一个是值传递,对应的是px=NULL; 想改变指针本身,这样做不能影响函数外部参数。

也就是说list_tail_insert函数中发生的是类似于px=3;的操作,即H=something,那我们找找该函数哪里发生了这种操作:

int list_tail_insert(linklist* H, data_t value)
{
	linklist p;
	linklist temp;
	if (H == NULL)//不是这里
	{
		printf("H is NULL\n");
		return -1;
	}

	if ((p = (linklist)malloc(sizeof(listnode))) == NULL)
	{
		printf("malloc failed\n");
		return -1;
	}
	p->data = value;
	p->next = NULL;

	temp = H;//不是这里
	while (temp->next)
	{
		temp = temp->next;//不是这里
	}
	temp->next = p;//不是这里
	return 0;
}

该函数就没有改变H指向的值的代码,因为H作为头指针不需要改变!

然后我们再看list_free,说它是值传递,那函数中发生的应该就是类似于px=NULL;这样 的操作,我们找一找:

int list_free(linklist H)
{
	linklist p;
	if (H == NULL)
	{
		printf("H is NULL\n");
		return -1;
	}
	while (H != NULL)
	{
		p = H;
		H = H->next;//就是这里了,想改变指针H本身的指向,
		            //这样做不能影响函数外部参数
		printf("释放元素:%d\n", p->data);
		free(p);
		p = NULL;
	}
	puts("");
	return 0;
}

(3)结论

list_tail_insert函数中因为没有对H进行操作,所以不管它是址传递还是值传递都不会对H产生影响,变动的是链表节点,函数中对节点的操作,在函数外也会同步。

list_free就是值传递了,因为它想改变H,想使H==NULL,但因为是值传递,所以不能如愿,函数内的操作不能影响函数外参数。

3、解决办法

(1)方法1——返回指针

改成这样

linklist list_free(linklist H)
{
	linklist p;
	if (H == NULL)
	{
		printf("H is NULL\n");
		return NULL;
	}
	while (H != NULL)
	{
		p = H;
		H = H->next;
		printf("释放元素:%d\n", p->data);
		free(p);
		p = NULL;
	}
	puts("");//换行???
	return NULL;
}

再执行此操作

printf("第四步:删除整个链表\n");
	printf("before free:%p\n", H);
	H = list_free(H);
	printf("after free:%p\n", H);
	list_show(H);
	
第四步:删除整个链表
before free:00647DD8
释放元素:0
释放元素:1
释放元素:2
释放元素:3
释放元素:4

after free:00000000
H is NULL

结果H就为NULL了。

(2)方法2——址传递

int list_free(listnode** H)
{
	linklist p;
	if (*H == NULL)
	{
		printf("H is NULL\n");
		return -1;
	}
	while (*H != NULL)
	{
		p = *H;
		*H = (*H)->next;
		printf("释放元素:%d\n", p->data);
		free(p);
		p = NULL;
	}
	puts("");
	return 0;
}

执行操作:

	printf("第四步:删除整个链表\n");
	printf("before free:%p\n", H);
	list_free(&H);
	printf("after free:%p\n", H);
	list_show(H);
	
第四步:删除整个链表
before free:00FB7DD8
释放元素:0
释放元素:1
释放元素:2
释放元素:3
释放元素:4

after free:00000000
H is NULL

结果正常!

参考资料

1、http://c.biancheng.net/view/1853.html
2、https://blog.csdn.net/qq_29350001/article/details/53740305
3、https://baike.baidu.com/item/引用传递/2880658?fr=aladdin
4、http://www.makeru.com.cn/course/details/12749

  • 59
    点赞
  • 295
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
图书目录编辑 第一章:程序设计和C语言   1.1什么是计算机程序   1.2什么是计算机语言   1.3C语言的发展及其特点   1.4最简单的C语言程序   1.4.1最简单的C语言程序举例   1.4.2C语言程序的结构   1.5运行C程序的步骤与方法   1.6程序设计的任务   习题 第2章:算法——程序的灵魂 2.1什么是算法 2.2简单的算法举例 2.3算法的特性   2.4怎样表示一个算法   2.4.1;用自然语言表示算法 2.4.2用流程图表示算法 2.4.3三种基本结构和改进的流程图   2.4.4用N-S流程图表示算法   2.4.5用伪代码表示算法   2.4.6用计算机语言表示算法   2.5结构化程序设计方法   习题 第3章:简单的C程序设计——顺序程序设计 3.1顺序程序设计举例   3.2数据的表现形式及其运算   3.2.1常量和变量   3.2.2数据类型   3.2.3整型数据   3.2.4字符型数据   3.2.5浮点型数据   3.2.6怎样确定常量的类型   3.2.7运算符和表达式   3.3C语句   3.3.1C语句的作用和分类   3.3.2最基本的语句——赋值语句   3.4数据的输入输出   3.4.1输入输出举例   3.4.2有关数据输入输出的概念   3.4.3用printf函数输出数据   3.4.4用scanf函数输入数据   3.4.5字符数据的输入输出   习题 第4章:选择结构程序设计 4.1选择结构和条件判断   4.2用if语句实现选择结构   4.2.1用if语句处理选择结构举例   4.2.2if语句的一般形式   4.3关系运算符和关系表达式   4.3.1关系运算符及其优先次序   4.3.2关系表达式   4.4逻辑运算符和逻辑表达式   4.4.1逻辑运算符及其优先次序   4.4.2逻辑表达式   4.4.3逻辑型变量   4.5条件运算符和条件表达式   4.6选择结构的嵌套   4.7用switch语句实现多分支选择结构   4.8选择结构程序综合举例   习题 第5章;循环结构程序设计 5.1为什么需要循环控制 5.2用while语句实现循环   5.3用do…while语句实现循环   5.4用for 语句实现循环   5.5循环的嵌套   5.6几种循环的比较   5.7改变循环执行的状态   5.7.1用break语句提前终止循环   5.7.2用continue语句提前结束本次循环   5.7.3break语句和continue语句的区别   5.8循环程序举例   习题 第6章;利用数组处理批量数据 6.1.怎样定义和引用一维数组   6.1.1怎样定义一维数组   6.1.2怎样引用一维数组元素   6.1.3一维数组的初始化   6.1.4一维数组程序举例   6.2.怎样定义和引用二维数组   6.2.1怎样定义二维数组   6.2.2怎样引用二维数组的元素   6.2.3二维数组的初始化   6.2.4二维数组程序举例   6.3.字符数组 6.3.1怎样定义字符数组   6.3.2字符数组的初始化   6.3.3怎样引用字符数组中的元素   6.3.4字符串和字符串结束标志   6.3.5字符数组的输入输出   6.3.6使用字符串处理函数   6.3.7字符数组应用举例   习题 第7章:用函数实现模块化程序设计 7.1为什么要用函数   7.2怎样定义函数   7.2.1为什么要定义函数   7.2.2定义函数的方法   7.3调用函数174   7.3.1函数调用的形式   7.3.2函数调用时的数据传递   7.3.3函数调用的过程   7.3.4函数的返回值   7.4对被调用函数的声明和函数原型   7.5函数的嵌套调用   7.6函数的递归调用   7.7数组作为函数参数   7.7.1数组元素作函数实参   7.7.2数组名作函数参数   7.7.3多维数组名作函数参数   7.8局部变量和全局变量   7.8.1局部变量   7.8.2全局变量   7.9变量的存储方式和生存期   7.9.1动态存储方式与静态存储方式   7.9.2局部变量的存储类别   7.9.3全局变量的存储类别   7.9.4存储类别小结   7.10关于变量的声明和定义   7.11内部函数和外部函数   7.11.1内部函数   7.11.2外部函数   习题 第8章;善于利用指针 8.1指针是什么   8.2指针变量 8.2.1使用指针变量的例子   8.2.2怎样定义指针变量   8.2.3怎样引用指针变量   8.2.4指针变量作为函数参数   8.3通过指针引用数组   8.3.1数组元素的指针   8.3.2在引用数组元素时指针的运算   8.3.3通过指针引用数组元素   8.3.4用数组名作函数参数   8.3.5通过指针引用多维数组   8.4通过指针引用字符串   8.4.1字符串的引用方式   8.4.2字符指针作函数参数   8.4.3使用字符指针变量和字符数组的比较   8.5指向函数的指针   8.5.1什么是函数指针   8.5.2用函数指针变量调用函数   8.5.3怎样定义和使用指向函数的指针变量   8.5.4用指向函数的指针作函数参数   8.6返回指针值的函数   8.7指针数组和多重指针   8.7.1什么是指针数组   8.7.2指向指针数据的指针   8.7.3指针数组作main函数的形参   8.8动态内存分配与指向它的指针变量   8.8.1什么是内存的动态分配   8.8.2怎样建立内存的动态分配   8.8.3void指针类型   8.9有关指针的小结   习题 第9章;用户自己建立数据类型 9.1定义和使用结构体变量   9.1.1自己建立结构体类型   9.1.2定义结构体类型变量   9.1.3结构体变量的初始化和引用   9.2使用结构体数组   9.2.1定义结构体数组   9.2.2结构体数组的应用举例   9.3结构体指针   9.3.1指向结构体变量的指针   9.3.2指向结构体数组的指针   9.3.3用结构体变量和结构体变量的指针作函数参数   9.4用指针处理链表   9.4.1什么是链表   9.4.2建立简单的静态链表   9.4.3建立动态链表   9.4.4输出链表   9.5共用体类型   9.5.1什么是共用体类型   9.5.2引用共用体变量的方式   9.5.3共用体类型数据的特点   9.6使用枚举类型   9.7用typedef声明新类型名   习题 第10章;对文件的输入输出 10.1C文件的有关基本知识   10.1.1什么是文件   10.1.2文件名   10.1.3文件的分类   10.1.4文件缓冲区   10.1.5文件类型指针   10.2打开与关闭文件   10.2.1用fopen函数打开数据文件   10.2.2用fclose函数关闭数据文件   10.3顺序读写数据文件   10.3.1怎样向文件读写字符   10.3.2怎样向文件读写一个字符串   10.3.3用格式化的方式读写文件   10.3.4用二进制方式向文件读写一组数据   10.4随机读写数据文件   10.4.1文件位置标记及其定位   10.4.2随机读写   10.5文件读写的出错检测   习题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值