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;
// 还可以这样写,与上面一条语句意义相同,因为 fun 和 &fun 都指向函数的首地址
// 这与数组名的含义类似
int (*q)(int, double) = &fun;
// 以下 6 种方式均可成功调用函数
fun(a, b);
&fun(a, b);
// p 指向的是函数 fun 的地址,p(a, b); 满足函数调用地址+参数列表的规则
p(a, b);
(*p)(a, b);
q(a, b);
// (*q)(a, b); q指向函数地址,(*q)就是函数本身, (*q)(a, b); 也就相当于 fun(a, b)
// 也符合函数的调用规则
(*q)(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] 第二维不能省略
-
指针的一个很大的优势是使用灵活方便,但是这也是它的一个缺点,容易造成内存的越界访问,造成一些意想不到的危险,接下来的传引用可以避免这个问题
传引用
-
形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作
-
在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址,被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量,正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量