一、指针与函数传参:
1、普通变量作为函数形参:
(1)函数传参时,普通变量作为参数时,形参和实参名字可以相同也可以不 同,实际上都是用实参来替代相对应的形参的。
(2)在子函数内部,形参的值等于实参。原因是函数调用时把实参的值赋值给了形参。
(3)这种传值方式我们一般叫“传值调用”:相当于实参做右值,形参做左值),下面我们来看一个示例:
#include <stdio.h>
// &a和&b不同,说明a和b不是同一个变量(在内存中a和b是独立的2个内存空间)
// 但是a和b是有关联的,实际上b是a赋值得到的。
void func1(int b)
{
// 在函数内部,形参b的值等于实参a
printf("b = %d.\n", b);
printf("in func1, &b = %p.\n", &b);
}
int main()
{
int a = 4;
printf("&a = %p.\n", &a);
func1(a);
return 0;
}
输出结果如下图所示,所以我们在子函数内去修改形参被赋予实参的值,其实是不会改变实参原来的值,因为他们的内存空间是独立的,下面会有例子来讲解这个的用法:
2、数组作为函数形参:
(1)数组名作为形参传参时,实际传递是不是整个数组,而是数组的首元素的首地址(也就是整个数组的首地址。因为传参时是传值,所以这两个没区别)。所以在子函数内部,传进来的数组名就等于是一个指向数组首元素首地址的指针。
(2)在子函数内传参得到的数组首元素首地址,和外面得到的数组首元素首地址的值是相同的。很多人把这种特性叫做“传址调用”(所谓的传址调用就是调用子函数时传了地址(也就是指针),此时可以通过传进去的地址来访问实参。)
(3)数组作为函数形参时,[]里的下标数字是可有可无的。为什么呢?因为数组名做形参传递的实际只是个指针,根本没有数组长度这个信息。下面我们来看示例:
#include <stdio.h>
void func2(int a[])
{
printf("sizeof(a) = %d.\n", sizeof(a));
printf("in func2, a = %p.\n", a);
}
int main()
{
int a[5];
printf("a = %p.\n", a);
func2(a);
return 0 ;
}
#include <stdio.h>
void func2(int a[5])
{
printf("sizeof(a) = %d.\n", sizeof(a));
printf("in func2, a = %p.\n", a);
}
int main()
{
int a[5];
printf("a = %p.\n", a);
func2(a);
return 0 ;
}
输出结果,这两种写法都正确,数组做函数形参时,它的访问下标可以写,也可以不写,都不会报错:
3、指针作为函数形参:
(1)和数组作为函数形参是一样的.这就好像指针方式访问数组元素和数组方式访问数组元素的结果一样是一样的。我们来看示例:
#include <stdio.h>
void func3(int *a)
{
printf("sizeof(a) = %d.\n", sizeof(a));
printf("in func2, a = %p.\n", a);
}
int main()
{
int a[5];
printf("a = %p.\n", a);
func3(a);
return 0;
}
输出结果:
4、结构体变量作为函数形参
(1)结构体变量作为函数形参的时候,实际上和普通变量(类似于int之类的)传参时表现是一模一样的。所以说结构体变量其实也是普通变量而已。
(2)因为结构体一般都很大,所以如果直接用结构体变量进行传参,那么函数调用效率就会很低。(因为在函数传参的时候需要将实参赋值给形参,所以当传参的变量越大调用效率就会越低)。怎么解决?思路只有一个那就是不要传变量了,改传变量的指针(地址)进去。
(3)结构体因为自身太大,所以传参应该用指针来传(但是程序员可以自己决定,你非要传结构体变量过去C语言也是允许的,只是效率低了)。下面我们来看示例:
#include <stdio.h>
struct A
{
char a; // 结构体变量对齐问题
int b; // 因为要对齐存放,所以大小是8
};
void func4(struct A a1)
{
printf("sizeof(a1) = %d.\n", sizeof(a1));
printf("&a1 = %p.\n", &a1);
printf("a1.b = %d.\n", a1.b);
}
void func5(struct A *a2)
{
printf("sizeof(a2) = %d.\n", sizeof(a2)); // 4
printf("sizeof(*a2) = %d.\n", sizeof(*a2)); // 8
printf("&a2 = %p.\n", &a2); // 二重指针
printf("a2= %p.\n", a2);
printf("a2->b = %d.\n", a2->b);
}
int main()
{
struct A a =
{
.a = 4,
.b = 5555,//gcc编译器里面的初始化写法
};
printf("sizeof(a) = %d.\n", sizeof(a));
printf("&a = %p.\n", &a);
printf("a.b = %d.\n", a.b);
func4(a);
struct A a2=
{
.a = 4,
.b = 5555,
};
printf("sizeof(a2) = %d.\n", sizeof(a2)); // 4
//printf("sizeof(*a2) = %d.\n", sizeof(*a2)); // 8
printf("&a 2= %p.\n", &a2);
//printf("a 2= %p.\n", a2);
printf("a2.b = %d.\n", a2.b);
func5(&a1);
return 0;
}
输出结果:
5、传值调用与传址调用:
#include <stdio.h>
void swap1(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
printf("in swap1, a = %d, b = %d.\n", a, b);
}
int main()
{
int x = 3, y = 5;
swap1(x, y);
printf("x = %d, y = %d.\n", x, y);
//x=3,y=5,交换失败
return 0;
}
输出结果:
说明:
传值调用描述的是这样一种现象:x和y作为实参,自己并没有真身进入swap1函数内部,而只是拷贝了一份自己的副本(副本具有和自己一样的值,但是是不同的变量)进入子函数swap1,然后我们在子函数swap1中交换的实际是副本而不是x、y真身。所以在swap1内部确实是交换了,但是到外部的x和y根本没有受影响。
#include <stdio.h>
void swap2(int *a, int *b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
printf("in swap1, *a = %d, *b = %d.\n", *a, *b);
}
int main()
{
int x = 3, y = 5;
swap2(&x, &y);
printf("x = %d, y = %d.\n", x, y); // 交换成功
return 0;
}
输出结果:
说明:
在swap2中x和y真的被改变了(但是x和y真身还是没有进入swap2函数内,而是swap2函数内部跑出来把外面的x和y真身改了)。实际上实参x和y永远无法真身进入子函数内部(进去的只能是一份拷贝),但是在swap2我们把x和y的地址传进去给子函数了,于是乎在子函数内可以通过指针解引用方式从函数内部访问到外部的x和y真身,从而改变x和y。
6、小结:
通过上面的学习,我们可以看出,如果要在一个子函数里面来改变传进来的实参赋给形参的值(也就是主函数里面的变量值),采用指针的方式就能解决这个问题,这也是我们在看许多源代码都会看到这样的写法,你要明白这样的写法的好处(效率高)。
二、输入型参数和输出型参数:
1、函数为什么需要形参与返回值:
(1)函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针常量,所以在程序中使用到函数名时都是当地址用的,用来调用这个函数的。示例如下,函数名它表示一个地址:
(2)函数体是函数的关键,由一对{}括起来,包含很多句代码,函数体就是函数实际做的工作。
(3)形参列表和返回值。形参是函数的输入部分,返回值是函数的输出部分。对函数最好的理解就是把函数看成是一个加工机器(程序其实就是数据加工器),形参列表就是这个机器的原材料输入端;而返回值就是机器的成品输出端。示例如下:
#include <stdio.h>
int multip5(int a)
{
return a*5;
}
int main(void)
{
// 程序要完成功能是:对一个数乘以5
// 第一种方法:函数传参
int a = 3;
int b;
b = multip5(a);
printf("result = %d.\n", b);
return 0;
}
输出结果:
result = 15.
(4)其实如果没有形参列表和返回值,函数也能对数据进行加工,用全局变量即可。用全局变量来传参和用函数参数列表返回值来传参各有特点,在实践中都有使用。总的来说,函数参数传参用的比较多,因为这样可以实现模块化编程,而C语言中也是尽量减少使用全局变量。
示例:
#include <stdio.h>
int x,y;
void multip5_2(void)
{
y = 5 * x;
}
int main(void)
{
// 第二种方法:用全局变量来传参
x = 2;
multip5_2();
printf("y = %d.\n", y);
return 0;
}
输出结果:
y = 10.
(5)全局变量传参最大的好处就是省略了函数传参的开销,所以效率要高一些;但是实战中用的最多的还是传参,如果参数很多传参开销非常大,通常的做法是把很多参数打包成一个结构体,然后传结构体变量指针进去。
2、函数传参中使用const指针:
(1)const一般用在函数参数列表中,用法是const int *p;(意义是指针变量p本身可变的,而p所指向的变量是不可变的)。示例如下:
#include <stdio.h>
void func1(int *p)
{
*p = 5;
}
void func2(const int *p)
{
*p = 5;
}
int main()
{
int a = 1;
func1(&a);
func2(&a);
return 0;
}
输出结果:
const用来修饰指针做函数传参,作用就在于声明在函数内部不会改变这个指针所指向的内容。
2、函数需要向外部返回多个值时怎么办?
(1)一般来说,函数的收入部分就是函数参数,输出部分就是返回值。问题是函数的参数可以有很多个,而返回值只能有1个。这就造成我们无法让一个函数返回多个值。
(2)现实编程中,一个函数需要返回多个值是非常普遍的,因此完全依赖于返回值是不靠谱的,通常的做法是用参数来做返回(在典型的linux风格函数中,返回值是不用来返回结果的,而是用来返回0或者负数用来表示程序执行结果是对还是错,是成功还是失败)。
(3)普遍做法,编程中函数的输入和输出都是靠函数参数的,返回值只是用来表示函数执行的结果是对(成功)还是错(失败)。如果这个参数是用来做输入的,就叫输入型参数;如果这个参数的目的是用来做输出的,就叫输出型参数。
#include <stdio.h>
int multip5_3(int a, int *p)
{
int tmp;
tmp = 5 * a;
if (tmp > 100)
{
return -1;
}
else
{
*p = tmp;
return 0;
}
}
int main(void)
{
int a, b = 0, ret = -1;
a = 30;
ret = multip5_3(a, &b);//a 为输入型参数,b为输出型参数
if (ret == -1)
{
printf("出错了\n");
}
else
{
printf("result = %d.\n", b);
}
return 0;
}
输出结果:
出错了
(4)输出型参数就是用来让函数内部把数据输出到函数外部的。
3、小结:
(1)看到一个函数的原型后,怎么样一眼看出来哪个参数做输入哪个做输出?函数传参如果传的是普通变量(不是指针)那肯定是输入型参数;如果传指针就有2种可能性了,为了区别,经常的做法是:如果这个参数是做输入的(通常做输入的在函数内部只需要读取这个参数而不会需要更改它)就在指针前面加const来修饰;如果函数形参是指针变量并且还没加const,那么就表示这个参数是用来做输出型参数的。譬如C库函数中strcpy函数:
char *strcpy(char *dest, const char *src);