这篇介绍一下C/C++中的函数,主要是函数指针和传参(在文章的后半部分)相关的讨论
如有侵权,请联系删除,如有错误,欢迎大家指正,谢谢
函数的作用主要是为了方便代码复用
0. 函数调用
- 函数调用的本质:函数地址+(参数列表)
- 函数名与数组名的作用类似,一个函数为 void fun(int a); 通过fun(a); 或者&fun(a); 都可以成功调用函数,也就是说函数名或者是对函数名取地址意义是一样的
- 在C中,函数如不接受任何参数,必须在函数的参数列表处写void,函数列表啥也不写表示参数个数不能确定
- 在C++中,参数列表啥也不写默认是void,不接受任何参数,函数的调用跟C是一样的
1. 函数声明
- 函数可以不声明,直接定义在主函数前面,这样可以被主函数正常调用,但是如果定义的函数去调用其他的自定义函数,就要注意函数的位置,函数只能调用定义在它前面的函数,不能调用定义在它后面的函数,这样的话,定义函数还要考虑它们定义的位置就比较麻烦了,所以声明函数就可以解决这个问题,声明可以写在所有函数的前面,也可以写在其他位置,但是如果声明在其它位置,那么这个函数还是不能被自由的调用,声明也就失去了它的意义,所以最好是将函数的声明写在所有函数的前面
2. 函数返回指针
- 函数的返回类型如果是指针变量,这个指针变量指向的空间一般应该是堆区的地址空间,因为如果返回的是指向栈区空间的指针,在函数执行结束后,这个栈区空间(局部变量)有可能生命周期已经结束,被系统释放掉了,虽然有时候可以正确访问到数据,但是很容易造成访问野指针的错误,因此,最好是返回指向堆区地址的指针,需要注意的一点是,返回堆区地址的指针用完之后,记得要释放掉,避免造成内存泄漏
3. 函数类型
- 函数的类型是由返回值类型,参数类型和参数数量确定的
- 如函数原型是void fun(int a, double b){ ; } 这样的,它的声明可以写成void fun(int, double); 这条语句表明了函数的返回值类型,参数类型和参数数量可以确定一个函数的类型
int fun(int a, double b) {
cout<< a << " " << b <<endl;
return 0;
}
int main() {
int a = 1;
double b = 1.2;
int (*p)(int, double) = fun; // 定义一个函数指针,满足三点要求返回值类型,参数类型和参数数量
int (*q)(int, double) = &fun; // 还可以这样写,与上面一条语句意义相同,因为fun和&fun都指向函数的首地址,这与数组名的含义类似
// 以下 6 种方式均可成功调用函数
fun(a, b);
&fun(a, b);
p(a, b);
(*p)(a, b);
q(a, b);
(*q)(a, b);
}
- p指向的是函数fun的地址,p(a, b); 满足函数调用地址+参数列表的规则,(*p)(a, b); p指向函数地址,( *p)就是函数本身, ( *p)(a, b); 也就相当于fun(a, b);也符合函数的调用规则
4. 函数参数不确定的情况
- 形式:void fun(int n, …);
- 这样声明的函数可以实现参数个数不确定时的函数声明或定义,其中n表示参数的个数,后面三个点是可能的参数个数
void fun(int n,...) {
va_list ap; // 定义一个保存参数的数组
__va_start(&ap, n); // 将参数装入数组中,ap是保存参数的数组,n是参数个数
cout << __crt_va_arg(ap, int) << endl; // 从参数数组中取数据,是按照参数类型往取的,且需要按照输入的顺序取
cout << __crt_va_arg(ap, double) << endl;
cout << __crt_va_arg(ap, int) << endl;
}
int main() {
int a = 2, b = 3;
double c = 3.4;
fun(3, a, c, b);
system("pause");
return 0;
}
注意
- 在取参数的时候是按照参数的类型取的,并且要与出入的参数顺序相同,比如第一个传int,第二个传double,取的时候也必须是第一个取int,第二个取double,在去参数的时候类似于出队列的操作,出一个就从队列中删除一个,但具体底层是如何实现的还不是很清楚,只知道与出队列的操作类似,FIFO原则
5. 传值 & 传址 & 传引用
传值
- 传值时,形参是实参的拷贝,改变形参的值不会影响外部实参的值,从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出
- 当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递
- 值传递对于较小的数据量比较方便,但是如传递结构体或类等数据量比较大,进行拷贝会造成比较大时间和空间的开销
- 还有一点也是最重要的,在类的值传递时,会进行拷贝构造,这里的拷贝构造是浅拷贝,对于需要深拷贝的类还需要重写深拷贝,有一点复杂
传址
- 形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作
- 当传递的数据量比较大时,采用传址的方式会节省时间和空间的开销,为防止对实参的修改,可在形参前加const,构成底层const保护实参数据
- 数组作参数时,会退化成指针,传递二维数组,(*p)[3]第二维不能省略(在上一篇文章最后有介绍C/C++的指针)
- 指针的一个很大的优势是使用灵活方便,但是这也是它的一个缺点,容易造成内存的越界访问,造成一些意想不到的危险,接下来的传引用可以避免这个问题
传引用
- 形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作
- 在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址,被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量,正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量(这篇文章篇幅已经很长了,引用的更详细的介绍在后面会专门出一片文章)
参考文章:
[1] C++ 值传递、指针传递、引用传递详解
下一篇:C/C++内存申请和释放(一)