3.1 参数传递
引用传递和值传递
1. 值传递
当形参是非引用类型时,实参的值会被拷贝给形参,实参和形参是两个完全不同的对象,函数对形参做的所有操作都不会影响实参。
Tips:当形参是指针类型时,形参和实参也是两个完全不同的指针,只不过他们指向同个对象。因为指针使我们可以间接地访问它所指向的对象,因此通过指针可以修改它所指对象的值。
熟悉C语言的程序员常常使用指针类型的形参访问函数外部的对象,在C++语言中,建议使用引用类型的形参替代指针。
2. 引用传递
Tips:如果函数无须改变引用形参的值,那么最好将其声明为常量引用。
当形参是引用类型时,我们说它对应的实参被引用传递,使用引用传递的原因在于:
- 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型和数组等)根本就不支持拷贝操作,因此只能通过引用形参来访问该类型的对象
- 引用形参可以实现多返回值(当然指针形参也可以实现这个功能)
const形参和实参
1. 忽略形参的顶层const
Tips:常量引用和非常量引用参数是可以重载的,因为这个时候是底层const而非顶层const。
当用实参初始化形参时会忽略形参的顶层const,即当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:
// 既可以给fcn传入const int, 也可以传入int
void fcn(const int i);
// 错误: fcn(const int i)忽略了顶层const, 相当于重复定义了fcn(int)
void fcn(int i);
2. 指针或引用形参与const
前面提到顶层const是不可以实现重载的,因为实参初始化形参时会忽略掉顶层const。由于我们可以用非常量初始化一个底层const对象,但是反过来不行,因此常量引用和非常量引用是可以重载的。
3. 形参尽量使用常量引用
Tips:一个普通的引用必须用同类型的对象初始化,我们不能将需要类型转换的对象传递给普通的引用形参。
把函数不会改变的形参定义成普通的引用是一种比较常见的错误,这么做给函数的调用者一种误导,即函数可以修改它的实参的值。另外使用引用而非常量引用也会极大地限制函数所能接受的实参类型(普通引用形参无法接受const对象、字面值或者需要类型转换的对象)。
数组形参
1. 传递数组形参
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响:
- 不允许拷贝数组:无法以值传递的方式使用数组参数
- 使用数组时会将其转换成指针:当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针
尽管不能以值传递的方式传递数组,但是我们把形参写成类似数组的形式:
// 下面三个函数等价, 都接受const int*类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10]); // 这里的维度表示我们期望数组含有多少个元素, 实际上不一定
2. 传递数组形参大小
由于数组是以指针的形式传递给函数的,所以函数并不知道数组的确切尺寸,调用者一般需要提供一些额外的信息。管理数组形参通常有三种技术:
2.1 数组中包含结束标记(一般只有C风格字符串)
第一种方法要求数组本身包含一个结束标记,最典型的例子是C风格字符串,接受C风格字符串的函数在遇到空字符时就会停止:
void print(const char *cp) {
if (cp) { // cp不是空指针
while (*cp) { // 指针所指字符不是空字符
cout << *cp++; // 输出当前字符并将指针向前移动一个位置
}
}
}
2.2 使用标准库规范
Tips:标准库begin和end函数可以返回数组的首元素指针和尾后元素指针。
第二种方式是传递指向数组首元素和尾后元素的指针:
void print(const int *beg, const int *end) {
while (beg != end) {
cout << *beg++ << endl;
}
}
int j[2] = {0, 1};
print(begin(j), end(j));
2.3 显式传递一个表示数组大小的形参
第三种方法是专门定义一个表示数组大小的形参:
// const int ia[]等价于const int *ia
// size表示数组的大小
void print(const int ia[], size_t size) {
for (size_t i = 0; i != size; ++i) {
cout << ia[i] << endl;
}
}
int j[] = {0, 1};
print(j, end(j) - begin(j));
3. 数组形参与const
当函数不需要对数组元素执行写操作时,数组形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向常量的指针。
4. 数组引用形参
Tips:当形参是数组的引用时,维度也是类型的一部分。
C++语言允许将变量定义为数组的引用:
// 形参是数组的引用, 维度是类型的一部分
void print(int (&arr)[10]) {
for (auto elem : arr) {
cout << elem << endl;
}
}
注意arr两边的括号是必不可少的:
f(int &arr[10]); // 错误: 将arr声明成了引用的数组
f(int (&arr)[10]); // 正确: arr是具有10个整数的整型数组的引用
由于数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内我们可以放心地使用数组。但是这一用法也无形中限制了print函数的可用性,我们只能将函数作用于维度为10的数组。
5. 传递多维数组
前面我们提到过C++中并没有真正的多维数组,所谓的数组其实是数组的数组。和所有的数组一样,当我们把多维数组传递给函数时,实际上传递的是指向数组首元素的指针,即一个指向数组的指针。
Tips:由于数组第二维以及后面的维度的大小都是数组类型的一部分,因此传递多维数组时不能省略。
// matrix是指向含有10个整数的数组的指针
void print(int (*matrix)[10], int rowSize);
// 等价定义
// 由于编译器会忽略掉第一个维度, 因此最好不要把它包含在形参列表内
void print(int matrix[][10], int rowSize);
main函数处理命令行选项
假设我们的可执行文件名为prog,我们可以向程序传递如下选项:
prog -d -o ofile data0
这些选项会通过两个可选的形参传递给main函数:
int main(int argc, char *argv[]);
// 等价
int main(int argc, char** argv);
其中第二个形参argv是一个数组,它的元素是指向C风格字符串的指针,第一个形参argc表示数组中字符串的数量。
当实参传递给main函数之后,argv第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。
在前面的例子中,argc等于5,argv指向的类型如下:
argv[0] = "prog"; // 或者一个空字符串
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
可变形参
1. 支持可变形参的三种方法
有时候我们无法知道应该向函数提供几个实参,为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:
- 如果所有的实参类型相同,传递名为
initializer_list
的标准库类型 - 如果实参的类型不同,可以编写可变参数模板
C++还提供了一种特殊的形参类型(即省略符),可以用于传递可变数量的实参,不过这种功能一般只用于与C函数交互的接口程序。
2. initializer_list形参
Tips:
initializer_list
对象中的元素永远都是常量。
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list
类型的形参。和vector一样,initializer_list
也是一种模板类型,但是initializer_list
对象中的元素永远都是常量值,我们是无法改变的。
#include <initializer_list>
#include <string>
#include <iostream>
void print(std::initializer_list<std::string> list) {
for (auto it = list.begin(); it != list.end(); ++it) {
std::cout << *it << std::endl;
}
}
int main() {
print({"tomo", "cat", "tomocat"});
}
3. 省略符形参
Tips:省略符形参只能出现在形参列表的最后一个位置,并且仅仅用于C和C++通用的类型。
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。它包含如下几个宏:
va_list
:定义了一个指针arg_ptr,
用于指示可选的参数va_start(arg_ptr, argN)
:使参数列表指针arg_ptr
指向函数参数列表中的第一个可选参数,argN
是位于第一个可选参数之前的固定参数, 或者说最后一个固定参数va_arg(arg_ptr, type)
:返回参数列表中指针arg_ptr
所指的参数, 返回类型为type
. 并使指针arg_ptr
指向参数列表中下一个参数.返回的是可选参数, 不包括固定参数va_end(arg_ptr)
:清空参数列表, 并置参数指针arg_ptr
无效
#include <stdarg.h>
#include <string>
std::string format_string(const char* fmt, ... ) {
const int SIZE = 500;
char res[SIZE + 1];
res[SIZE] = NULL;
va_list va;
va_start(va, fmt);
vsnprintf(res, SIZE, fmt, va);
va_end(va);
return res;
}
int main() {
printf("%s\n", format_string("str:%s int:%d float:%.2f", "cat", 102, 3.1415).c_str());
return 0;
}
4. 可变参数函数模板
4.1 参数包
可变参数函数模板指的是接收可变数目参数的模板函数。可变数目的参数被称为参数包,包括两种参数包:
- 模板参数包:表示零个或多个模板参数
- 函数参数包:表示零个或多个函数参数
// Args: 模板参数包
// rest: 函数参数包
template <typename T, typename... Args>
void foo(const T &t, const Args&... rest);
4.2 sizeof…运算符
我们可以使用sizeof...
运算符来获取参数包中元素个数:
template <typename... Args> void bar(Args... args) {
cout << sizeof...(Args) << endl; // 类型参数的数目
cout << sizeof...(args) << endl; // 函数参数的数目
}
4.3 例子
Tips:可变参数函数模板通常是递归的。当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中,否则可变参数版本会无限递归。
#include <iostream>
// 用来终止递归并打印最后一个元素的函数, 必须在可变参数版本的print定义之前声明
template <typename T>
void print(const T &t) {
std::cout << t << std::endl;
}
// 参数包中除最后一个元素之外的其他元素都会调用这个版本的print
template <typename T, typename... Args>
void print(const T &t, const Args&... rest) {
std::cout << t << std::endl;
print(rest...);
}
int main() {
print("tomo", "cat", "tomocat");
}
3.2 默认实参
简介
Tips:一旦某个形参被赋予了默认值,那么它后面的所有形参都必须有默认值。
某些函数包含一类形参,它们在函数的很多次调用中都被赋予了一个相同的值,我们将这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参:
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
string window;
window = screen(); // screen(24, 80, ' ')
window = screen(66); // screen(66, 80, ' ')
window = screen(66, 256); // screen(66, 256, ' ')
window = screen(66, 256, '#'); // screen(66, 256, '#')
编码规范:只允许在非虚函数中使用默认实参,且必须保证默认实参的值始终一致。不建议使用默认实参,一般情况下建议使用函数重载(默认实参实际上就是函数重载语义的另一种实现方式),因为默认实参会干扰函数指针,导致函数签名与调用点的签名不一致,而函数重载不会导致这样的问题。
Tips:可以在构造函数中使用默认实参,毕竟不可能取得它们的地址。
声明
在给定的作用域内,一个形参只能被赋予一次默认实参(因为两个函数都是最佳匹配,会导致二义性调用的错误),但是可以给其他的形参添加默认实参:
typedef string::size_type sz;
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // 错误: 重复声明
string screen(sz = 24, sz = 80, char); // 正确: 添加默认实参
表达式作为默认实参的初始值
局部变量不能作为默认实参,但是只要表达式的类型能转换成形参所需的类型,那么该表达式就能作为默认实参初始值:
sz wd = 80;
char def = ' ';
sz ht();
// 函数声明
string screen(sz = ht(), sz = wd, char = def);
// 函数调用: 等价于screen(ht(), 80, ' ')
string window = screen();
// 函数调用
void foo() {
def = '*'; // 改变默认实参def的值
sz wd = 100; // 隐藏了外层定义的wd, 但是没有改变默认值
window = screen(); // 等价于screen(ht(), 80, '*')
}