C++ Primer第六章②
C++ Primer
第六章 函数
参数传递
前面我们学过,每次调用函数的时候,都会重新创建它的形参,并用传入的实参去初始化它。记住这句话:形参的类型决定了形参和实参交互的方式:
- 形参为引用:形参绑定到实参上,形参就是实参的绰号,是同一个人。
- 形参为值类型(不加引用),形参和实参是两个独立的对象,只不过是实参的值拷贝给形参了。
传值参数
我们来看一下传值的时候发生了什么,我随手写一段程序,只是为了说明问题哈:
main函数中对a调用了一个减1的函数f,但是最后打印出来的并没有减1,还是2,其实等价于如下程序:void f(int val) { val = val - 1; } int main() { int a = 2; f(a); cout << a << endl; return 0; }
这样看就很清楚了吧?那我们用传值的函数就不能得到减1的功能了吗?当然不是,我们可以借助返回值啊:int main() { int a = 2; int val = a; val = val - 1; cout << a << endl; return 0; }
不过它是经过了两次拷贝。int f(int val) { val = val - 1; return val; } int main() { int a = 2; a = f(a); cout << a << endl; return 0; }
指针形参
指针形参其实没什么特别的,跟上面那些值传递基本一样,只不过它是指针,指来指去容易搞混,所以我单独拿出来说一下:
先给你看输出: 0x9ffe4c 0 0x9ffe4c 自己想想吧~void reset(int *ip) { *ip = 0; //改变ip所指的对象的值为0 ip = 0; //改变指针本身的值,其实就是改变了地址, //但是没有改变i的地址,只是改变了拷贝后的ip } int main() { int i = 1; cout << &i << endl; reset(&i); cout << i << endl; cout << &i << endl; return 0; }
传引用参数
记住一点:引用和对象本身是一样的,所以对引用的操作就是对对象本身的操作。 我们来实现一个志玲函数,置零函数,不需要通过返回值哦:void reset(int &i) //引用形参 { i = 0; } int main() { int i = 1; reset(i); cout << i << endl; //打印输出0 return 0; }
使用引用避免拷贝
对吧?前面减1那个函数要拷贝,我们这个置零函数就不用了,是不是省了很多啊。更重要的是,有些类型是不支持拷贝的,比如前面学的数组,那我们只能用引用传参了。 举个栗子:我们要写个函数比较两个string的长度,string可能会很长,所以我们不要拷贝,用引用传参:bool isShorter(const string &s1, const string &s2) //为毛用const,尽量都得用它 { return s1.size() < s2.size(); }
使用引用形参返回额外信息
什么意思呢?比如说,我想写这么个函数:它实现一个阶乘功能,除了返回给我阶乘的结果外,我还要知道它乘了几次,你肯定会说这不是没事找事吗,它是几就乘几次呗,我不过是为了说明问题嘛,看代码吧:
其实我就是借助了引用传参会改变对象本身的值,所以我在求阶乘的时候多传进去一个b来记录乘法的次数,为什么它能记录呢,因为在被调函数中改变的cnt就是改变了b啊。int fact(int &i, int &cnt) //简单些,只考虑正整数 { int sum = 1; while(i>1) { sum *= i--; ++cnt; } return sum; } int main() { int a = 5; int b = 0; cout << fact(a, b) << endl; cout << b << endl; return 0; }
const形参和实参
来看一个很有意思的函数定义:
我们当然可以给它传一个const int的类型,那么我们可以给它传一个int型的吗?要回答这个问题,我们要知道函数调用的时候参数是怎么过去的呢,其实值传递的话就是一个拷贝对吧,我把int的拷贝给const int的有什么不可以,这个你如果忘了的话,请回顾之前第二章的内容。再联系我们说过的一个概念,顶层const,即对象本身是常量,我们在拷贝时会忽略顶层const:void f(const int i){}
所以啊,如果我们在程序中再定义一个函数:const int a = 1; int b = a; //你看,忽略了吧
就会报错了,因为它跟之前带const的f函数重复了。void f(int i){}
指针或引用形参与const
这里要注意的跟以前变量初始化的时候类似,我先写两个函数,然后我们来看看下列调用方式是否合法(就是看参数传递是否合法):
如果看着有困难的话,你就看实参去初始化形参的时候,是否符合以前的规矩,就是第二章学的那些。void reset(int *p){} //我就写个壳子 void reset(int &p){} //重载函数 int main() { int i = 42; const int ci = i; string::size_type ctr = 0; reset(&i); //正确:调用1(上面那个) reset(&ci); //错误:传入的是底层const,不能初始化int * reset(i); //正确:调用2 reset(ci); //错误:不能把普通引用绑定到常量const上 reset(42); //错误:理由基本同上 reset(ctr); // 错误:类型不匹配 }
尽量使用常量引用
只要不需要改变形参的值,都尽量用常量,理由有两点: - 该用常量而不用常量,会给函数的调用者一种误导,即函数可以改变它的实参的值
- 使用引用而非常量引用会限制函数所能接受的实参类型。刚刚就学过,我们不能把const给非const的,但可以把非const的给const。
数组形参
为什么要单独把数组的形参拿出来说呢,有两个原因: - 数组不允许被拷贝:所以无法使用值传递
- 使用数组名时会将其转换为指针。
编译器在处理f函数调用时,只会检查传入的参数是不是const int*类型。 其实我们还漏了一点,因为数组是以指针的形式传给函数的,所以我们的被调函数不知道数组有多长,这个责任是调用者的,所以调用者应该提供一些额外信息来说明://以下三个函数声明等价(还记得函数声明吧) void f(const int*); void f(const int[]); //数组名转换为数组首元素指针 void f(const int[5]); //我们期望它有五个元素,其实都行
使用标记指定数组长度
C就是这样的,以前老师在教C的时候一定会说,不要忘了反斜杠零,它是结束标志,就是这个意思。有些像字符串什么的还是好用的,但是对于int这种所有取值都有效的就不适用了。使用标准库规范
就是用begin()和end():void f(const int *beg, const int *end) { while(beg != end) { cout << *beg++ << endl; } } //这样就把数组首和尾后信息都传进去了 //调用如下 int j[3] = {1, 2, 3}; f(begin(j), end(j));
显式传递一个表示数组大小的形参
就是多一个传入参数表示数组长度,easy老套。数组形参和const
一句金玉良言:当函数不需要对数组元素执行写操作时,数组的形参应该是指向const的指针;只有当函数确实要改变元素值得时候,才把形参定义成指向非常量的指针
数组引用形参
数组当然也可以引用绑定:
//这里的10形参,是数组类型的一部分,所以只接受10个元素的数组
void f(int (&arr)[10])
{
for(auto elem : arr)
{
cout << elem << endl;
}
}
传递***数组
***数组就是数组的数组。和所有数组一样,在将***数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针,要完全匹配才能调用。
void print(int (*matrix)[10], int rowsize){}
调用这个函数,第一个参数虽然是指针,但是是指向数组的指针,所以数组的维度也是数组类型的一部分,所以再调用的时候那个数字10也要完全匹配。
含有可变参数的函数
有时候我们不清楚应该向函数传递几个实参,为了能处理不同数量的实参,C++提供了两种方法:
- 如果所有的实参类型相同,则可以传递一个名为initializer_list的标准库类型。
- 编写可变参数模板(以后再介绍) 比如我们要写一个输出错误的程序,我们又不知道具体有多少错误,但是这些错误的类型输出都是string,于是:
我们可以用不同的参数数量去调用:void errorMsg(initializer_list<string> il) //这个是类模板嘛 { for(auto beg = il.begin(); beg != il.end(); ++beg) { cout << *beg << endl; } }
除了initializer_list表示的数量不定的同种类型的参数,当然还可以加其他类型的参数,另外加上就行。errorMsg({"aaa", a, b}); //向initializer_list传递值序列要用花括号 errorMsg({"a", "b"}); //不同数量的实参
void errorMsg(int a, initializer_list<string> il)
省略符形参
void f(int a, ...){} int main() { int a = 5; f(a,6); f(a, "a"); //懂了吧~ return 0; }