形参的初始化与变量的初始化一样:如果形参具有非引用类型,则复制实参的值,如果形参为引用类型,则它只是实参的别名。
- 非引用形参:
普通的非引用类型的参数通过复制对应的实参实现初始化。当用实参副本初始化形参时,函数并没有访问调用所传递的实参本身,因此不会修改实参的值。
while 循环体虽然修改了 v1 与 v2 的值,但这些变化仅限于局部参数,而对调用 gcd 函数使用的实参没有任何影响。于是,如果有函数调用gcd(i, j),i 与 j 的值不受 gcd
#include <iostream>
#include <string>
using namespace std;
int gcd(int v1, int v2)
{
while (v2) {
int temp = v2;
v2 = v1 % v2;
v1 = temp;
}
cout << "v1:" << v1 << endl;
cout << "v2:" << v2 << endl;
return v1;
}
int main()
{
int i = 100;
int j = 3;
gcd(i, j);
cout << "i:" << i << endl;
cout << "j:" << j << endl;
system("pause");
return 0;
}
输出结果:
所以,非引用形参表示对应实参的局部副本。对这类形参的修改仅仅改变了局部副本的值。一旦函数执行结束,这些局部变量的值也就没有了。
- 指针形参:
函数的形参可以是指针,此时将复制实参指针。与其他非引用类型的形参一样,该类形参的任何改变也仅作用于局部副本。如果函数将新指针赋给形参,主调函数使用的实参指针的值没有改变。但如果对局部副本的指针所指向的对象的值进行修改,则实参指针所指对象的值也会相应的被修改。
如果函数形参是非 const
void reset(int *ip)
{
*ip = 0; // changes the value of the object to which ip points
ip = 0; // changes only the local value of ip; the argument is unchanged
cout << "local ip: " << ip << endl;
}
int main()
{
int i = 42;
int *p = &i;
cout << "i: " << *p << '\n'; // prints i: 42
cout << "p: " << p << endl;
reset(p); // changes *p but not p
cout << "i: " << *p << endl; // ok: prints i: 0
cout << "p: " << p << endl;//并没有改动
system("pause");
return 0;
}
如果保护指针指向的值,则形参需定义为指向 const
指针形参是指向 const 类型还是非 const 类型,将影响函数调用所使用的实参。我们既可以用 int* 也可以用 const int* 类型的实参调用 use_ptr 函数;但仅能将 int* 类型的实参传递给 reset 函数。这个差别来源于指针的初始化规则——可以将指向 const 对象的指针初始化为指向非 const 对象的指针,但不可以让指向非 const 对象的指针初始化为指向 const。
- const形参:
在调用函数时,如果该函数使用非引用的非 const 形参,则既可给该函数传递 const 实参也可传递非 const。例如,可以传递两个 int 型 const 对象调用 gcd:
int gcd(int v1, int v2)
{
while (v2) {
int temp = v2;
v2 = v1 % v2;
v1 = temp;
}
return v1;
}
int main()
{
const int i = 3, j = 6;
int k = gcd(3, 6);
system("pause");
return 0;
}
这种行为源于 const 对象的标准初始化规则。因为初始化复制了初始化时的对象,仅仅是要用那个值,即便修改也是对拷贝后的对象进行修改,而不对原对象进行修改,所以可用 const 对象初始化非 const
如果将形参定义为非引用的 const
void fcn(const int i) {
}
则在函数中,不可以改变实参的局部副本。由于实参仍然是以副本的形式传递,因此传递给 fcn 的既可以是 const 对象也可以是非 const
尽管函数的形参是 const,但是编译器却将 fcn 的定义视为其形码被声明为普通的 int:
void fcn(const int i) { }
void fcn(int i) { }
这种用法是为了支持对 C 语言的兼容,因为在 C 语言中,具有 const 形参或非 const
- 复制实参的局限性:
当需要在函数中修改实参的值时。
当需要以大型对象作为实参传递时。对实际的应用而言,复制对象所付出的时间和存储空间代价往往过大。
当没有办法实现对象的复制时。
- 引用形参
考虑下面不适宜复制实参的例子,该函数希望交换两个实参的值:
void swap(int v1, int v2)
{
int tmp = v2;
v2 = v1;
v1 = tmp;
}
这个例子期望改变实参本身的值。但对于上述的函数定义,swap 无法影响实参本身。执行 swap 时,只交换了其实参的局部副本,而传递 swap
int main()
{
int i = 10;
int j = 20;
cout << "Before swap():\ti: "
<< i << "\tj: " << j << endl;
swap(i, j);
cout << "After swap():\ti: "
<< i << "\tj: " << j << endl;
system("pause");
return 0;
}
输出结果:
为了使 swap
void swap(int &v1, int &v2)
{
int tmp = v2;
v2 = v1;
v1 = tmp;
}
int main()
{
int i = 10;
int j = 20;
cout << "Before swap():\ti: "
<< i << "\tj: " << j << endl;
swap(i, j);
cout << "After swap():\ti: "
<< i << "\tj: " << j << endl;
system("pause");
return 0;
}
输出结果:
与所有引用一样,引用形参直接关联到其所绑定的实参,而并非这些对象的副本。定义引用时,必须用与该引用绑定的对象初始化该引用。引用形参完全以相同的方式工作。每次调用函数,引用形参被创建并与相应实参关联。此时,当调用swap(i, j); 形参 v1 只是对象 i 的另一个名字,而 v2 则是对象 j 的另一个名字。对 v1 的任何修改实际上也是对 i 的修改。同样地,v2 上的任何修改实际上也是对 j
从 C 语言背景转到 C++ 的程序员习惯通过传递指针来实现对实参的访问。在 C++ 中,使用引用形参则更安全和更自然。
- 使用引用形参返回额外的信息
引用形参的另一种用法是向主调函数返回额外的结果。函数只能返回单个值,但有些时候,函数有不止一个的内容需要返回。
如何定义既返回一个迭代器又返回出现次数的函数?我们可以定义一种包含一个迭代器和一个计数器的新类型。而更简便的解决方案是给 find_val
vector<int>::const_iterator find_val(
vector<int>::const_iterator beg,
vector<int>::const_iterator end,
int value,
vector<int>::size_type &occurs)
{
vector<int>::const_iterator res_iter = end;
occurs = 0;
for (; beg != end; ++beg)
if (*beg == value) {
if (res_iter == end)
res_iter = beg;
++occurs;
}
return res_iter;
}
int main()
{
vector<int> ivec = { 1, 42, 3, 42, 5, 6, 7, 8 };
vector<int>::size_type ctr = 0;
vector<int>::const_iterator it = find_val(ivec.begin(), ivec.end(), 42, ctr);
cout << ctr << endl;
system("pause");
return 0;
}
输出结果:
- 利用const引用避免复制
在向函数传递大型对象时,需要使用引用形参,这是引用形参适用的另一种情况。虽然复制实参对于内置数据类型的对象或者规模较小的类类型对象来说没有什么问题,但是对于大部分的类类型或者大型数组,它的效率(通常)太低了;此外,某些类类型是无法复制的。使用引用形参,函数可以直接访问实参对象,而无须复制它。编写一个比较两个 string 对象长度的函数作为例子。这个函数需要访问每个 string 对象的 size,但不必修改这些对象。由于 string 对象可能相当长,所以我们希望避免复制操作。使用 const
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
其每一个形参都是 const string 类型的引用。因为形参是引用,所以不复制实参。又因为形参是 const 引用,所以 isShorter 函数不能使用该引用来修改实参。如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为 const。
- 更灵活的指向const的引用
如果函数具有普通的非 const 引用形参,则显然不能通过 const。毕竟,此时函数可以修改传递进来的对象,这样就违背了实参的 const
int incr(int &val)
{
return ++val;
}
int main()
{
const int v2 = 42;
int v3 = incr(v2); // error: v1 is not an int
}
会报错:
但比较容易忽略的是,调用这样的函数时,传递一个右值或具有需要转换的类型的对象同样是不允许的:
int main()
{
short v1 = 0;
const int v2 = 42;
int v3 = incr(v1); // error: v1 is not an int
v3 = incr(0); // error: literals are not lvalues
v3 = incr(v1 + v2); // error: addition doesn't yield an lvalue
int v4 = incr(v3); // ok: v3 is a non const object type int
system("pause");
return 0;
}
会提示错误:
问题的关键是非 const 引用形参只能与完全同类型的非 const。
应该将不修改相应实参的形参定义为 const 引用。如果将这样的形参定义为非 const。例如,可编写下面的程序在一个 string
string::size_type find_char(string &s, char c)
{
string::size_type i = 0;
while (i != s.size() && s[i] != c)
++i; // not found, look at next character
return i;
}
这个函数将其 string 类型的实参当作普通(非 const)的引用,尽管函数并没有修改这个形参的值。这样的定义带来的问题是不能通过字符串字面值来调用这个函数:
if (find_char("Hello World", 'o'))
虽然字符串字面值可以转换为 string
继续将这个问题延伸下去会发现,即使程序本身没有 const 对象,而且只使用 string 对象(而并非字符串字面值或产生 string 对象的表达式)调用 find_char 函数,编译阶段的问题依然会出现。例如,可能有另一个函数 is_sentence 调用 find_char 来判断一个 string
bool is_sentence (const string &s)
{
return (find_char(s, '.') == s.size() - 1);
}
如上代码,函数 is_sentence 中 find_char 的调用是一个编译错误。传递进 is_sentence 的形参是指向 const string 对象的引用,不能将这种类型的参数传递给 find_char,因为后者期待得到一个指向非 const string
应该将不需要修改的引用形参定义为 const 引用。普通的非 const 引用形参在使用时不太灵活。这样的形参既不能用 const。
- 传递指向指针的引用:
假设我们想编写一个与前面交换两个整数的 swap 类似的函数,实现两个指针的交换。已知需用 * 定义指针,用 &
void ptrswap(int *&v1, int *&v2)
{
int *tmp = v2;
v2 = v1;
v1 = tmp;
}
形参int *&v1的定义应从右至左理解:v1 是一个引用,与指向 int 型对象的指针相关联。也就是说,v1 只是传递进 ptrswap 函数的任意指针的别名。调用 ptrswap
int main()
{
int i = 10;
int j = 20;
int *pi = &i; // pi points to i
int *pj = &j; // pj points to j
cout << "Before ptrswap():\t*pi: "
<< *pi << "\t*pj: " << *pj << endl;
ptrswap(pi, pj); // now pi points to j; pj points to i
cout << "After ptrswap():\t*pi: "
<< *pi << "\t*pj: " << *pj << endl;
system("pause");
return 0;
}
输出结果:
即指针的值被交换了。在调用 ptrswap 时,pi 指向 i,而 pj 则指向 j。在 ptrswap 函数中,指针被交换,使得调用 ptrswap 结束后,pi 指向了原来 pj 所指向的对象。换句话说,现在 pi 指向 j,而 pj 则指向了 i。
- vector和其它类型容器的形参
通常,函数不应该有 vector 或其他标准库容器类型的形参。调用含有普通的非引用 vector 形参的函数将会复制 vector 的每一个元素。从避免复制 vector 的角度出发,应考虑将形参声明为引用类型。然而,事实上,C++ 程序员倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器:
void print(vector<int>::const_iterator beg,
vector<int>::const_iterator end)
{
while (beg != end) {
cout << *beg++;
if (beg != end) cout << " "; // no space after last element
}
cout << endl;
}
这个函数将输出从 beg 指向的元素开始到 end
- 数组形参
数组有两个特殊的性质,影响我们定义和使用作用在数组上的函数:一是不能复制数组;二是使用数组名字时,数组名会自动转化为指向其第一个元素的指针。因为数组不能复制,所以无法编写使用数组类型形参的函数。因为数组会被自动转化为指针,所以处理数组的函数通常通过操纵指向数组指向数组中的元素的指针来处理数组。
如果要编写一个函数,输出 int
void printValues(int*) { /* ... */ }
void printValues(int[]) { /* ... */ }
void printValues(int[10]) { /* ... */ }
虽然不能直接传递数组,但是函数的形参可以写成数组的形式。虽然形参表示方式不同,但可将使用数组语法定义的形参看作指向数组元素类型的指针。上面的三种定义是等价的,形参类型都是 int*。
形参的长度会引起误会。编译器忽略为任何数组形参指定的长度。根据数组长度(权且这样说),可将函数 printValues
void printValues(const int ia[10])
{
for (size_t i = 0; i != 10; ++i)
{
cout << ia[i] << endl;
}
}
尽管上述代码假定所传递的数组至少含有 10 个元素,但 C++ 语言没有任何机制强制实现这个假设。下面的调用都是合法的:
int main()
{
int i = 0, j[2] = {0, 1};
printValues(&i); // ok: &i is int*; probable run-time error
printValues(j); // ok: j is converted to pointer to 0th
// element; argument has type int*;
// probable run-time error
return 0;
}
虽然编译没有问题,但是这两个调用都是错误的,可能导致运行失败。在这两个调用中,由于函数 printValues
当编译器检查数组形参关联的实参时,它只会检查实参是不是指针、指针的类型和数组元素的类型时是否匹配,而不会检查数组的长度。
和其他类型一样,数组形参可定义为引用或非引用类型。大部分情况下,数组以普通的非引用类型传递,此时数组会悄悄地转换为指针。一般来说,非引用类型的形参会初始化为其相应实参的副本。而在传递数组时,实参是指向数组第一个元素的指针,形参复制的是这个指针的值,而不是数组元素本身。函数操纵的是指针的副本,因此不会修改实参指针的值。然而,函数可通过该指针改变它所指向的数组元素的值。通过指针形参做的任何改变都在修改数组元素本身。不需要修改数组形参的元素时,函数应该将形参定义为指向 const
void f(const int*) { /* ... */ }
- 通过引用传递数组
和其他类型一样,数组形参可声明为数组的引用。如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况下,数组大小成为形参和实参类型的一部分。编译器检查数组的实参的大小与形参的大小是否匹配:
void printValues(int(&arr)[10])
{
}
int main()
{
int i = 0, j[2] = { 0, 1 };
int k[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
printValues(&i); // error: argument is not an array of 10 ints
printValues(j); // error: argument is not an array of 10 ints
printValues(k); // ok: argument is an array of 10 ints
system("pause");
return 0;
}
报错:
这个版本的 printValues 函数只严格地接受含有 10 个 int
- 多维数组的传递
所谓多维数组实际是指数组的数组。和其他数组一样,多维数组以指向 0 号元素的指针方式传递。多维数组的元素本身就是数组。除了第一维以外的所有维的长度都是元素类型的一部分,必须明确指定:
void printValues(int (matrix*)[10], int rowSize);
上面的语句将 matrix 声明为指向含有 10 个 int。//如果传递的数组时int a[10],那么形参应该是void printValues(int locala[10], int rowSize);,locala就是指向int类型的指针。传递数组,形参类型就是数组元素的指针类型。因为int a[10]是整型数组,所以形参是整形指针,因为这里要传递的是数组的数组,所以形参应该是数组的指针。
我们也可以用数组语法定义多维数组。与一维数组一样,编译器忽略第一维的长度,所以最好不要把它包括在形参表内:
void printValues(int matrix[][10], int rowSize);
这条语句把 matrix 声明为二维数组的形式。实际上,形参是一个指针,指向数组的数组中的元素。数组中的每个元素本身就是含有 10 个 int
举例:
int matrix[3][10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
11, 12, 13, 14, 15, 16, 17, 18, 18, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30 };
其实二维数组matrix中的数据是现行排放的,而matrix则是首元素的地址:
非引用数组形参的类型检查只是确保实参是和数组元素具有同样类型的指针,而不会检查实参实际上是否指向指定大小的数组。任何处理数组的程序都要确保程序停留在数组的边界内。
有三种常见的编程技巧确保函数的操作不超出数组实参的边界。第一种方法是在数组本身放置一个标记来检测数组的结束。C 风格字符串就是采用这种方法的一个例子,它是一种字符数组,并且以空字符 null 作为结束的标记。处理 C 风格字符串的程序就是使用这个标记停止数组元素的处理。
第二种方法是传递指向数组第一个和最后一个元素的下一个位置的指针。如:
void printValues(const int *beg, const int *end)
{
while (beg != end) {
cout << *beg++ << endl;
}
}
int main()
{
int j[2] = {0, 1};
// ok: j is converted to pointer to 0th element in j
// j + 2 refers one past the end of j
printValues(j, j + 2);
return 0;
}
第三种方法是将第二个形参定义为表示数组的大小,这种用法在 C 程序和标准化之前的 C++ 程序中十分普遍。
- main处理命令行选项
传统上,主函数的实参是可选的,用来确定程序要执行的操作。比如,假设我们的主函数 main 位于名为 prog
prog -d -o ofile data0
这种用法的处理方法实际上是在主函数 main
int main(int argc, char *argv[]) { ... }
第二个形参 argv 是一个 C 风格字符串数组。第一个形参 argc 则用于传递该数组中字符串的个数。由于第二个参数是一个数组,主函数 main
int main(int argc, char **argv) { ... }
表示 argv 是指向 char* 的指针。//形参是char*,其原型可以是char类型的数组,argv是char类型数组的指针,那么原型可以是char类型数组的数组。简化思考,可以发现,参数中有几个“*”,传递的就可以是几维数组。
以前面的命令行为例,argc 应设为 5,argv
argv[0] = "prog"; argv[1] = "-d"; argv[2] = "-o"; argv[3] = "ofile"; argv[4] = "data0";
- 含有可变形参的函数
在无法列举出传递给函数的所有实参的类型和数目时,可以使用省略符形参。省略符暂停了类型检查机制。它们的出现告知编译器,当调用函数时,可以有 0 或多个实参,而实参的类型未知。省略符形参有下列两种形式:
void foo(parm_list, ...);
void foo(...);
第一种形式为特定数目的形参提供了声明。在这种情况下,当函数被调用时,对于与显示声明的形参相对应的实参进行类型检查,而对于与省略符对应的实参则暂停类型检查。在第一种形式中,形参声明后面的逗号是可选的。大部分带有省略符形参的函数都利用显式声明的参数中的一些信息,来获取函数调用中提供的其他可选实参的类型和数目。因此带有省略符的第一种形式的函数声明是最常用的。