Part I: The Basics
Chapter 6. Functions
6.1 函数基础
函数定义的组成部分:返回类型、函数名字、由 0 个或多个形参(parameter)组成的列表以及函数体。
通过调用运算符(call operator,()
)来执行函数。调用运算符作用于一个表达式,该表达式是一个函数或指向一个函数。括号内是逗号分隔的实参(argument)列表。实参用于初始化函数的形参。调用表达式的类型是函数的返回类型。
函数调用完成两项工作:用相应的实参初始化函数的形参,并将控制权转移给该函数。
调用函数(calling function)的执行被挂起,被调用函数(called function)的执行开始。
void f1(){ /* ... */ } // implicit void parameter list
void f2(void){ /* ... */ } // explicit void parameter list
int f3(int v1, v2) { /* ... */ } // error
int f4(int v1, int v2) { /* ... */ } // ok
因为无法使用未命名的形参,所以形参一般都有名字。偶尔,函数会有不被使用的形参,此类形参通常不命名,以表示该形参不会在函数体内使用。
函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
局部对象
在 C++ 中,名字有作用域,对象有生命周期(lifetime)。
自动对象(automatic object):只存在于块执行期间的对象。
形参是自动对象。
局部静态对象(local static object):在第一次执行经过对象定义前进行了初始化。函数结束时不会被销毁;在程序终止时被销毁。
通过将局部变量定义为 static 来获得此类对象。
size_t count_calls()
{
static size_t ctr = 0; // value will persist across calls
return ++ctr;
}
int main()
{
for (size_t i = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
这段程序输出从 1 到 10 的数字。
如果局部静态变量没有显式的初始化程序,则会对其进行值初始化,这意味着内置类型的局部静态变量将初始化为零。
函数声明
// parameter names chosen to indicate that the iterators denote a range of values to print
void print(vector<int>::const_iterator beg, vector<int>::const_iterator end);
三要素——返回类型、函数名、形参类型——描述函数的接口,制定了调用函数所需的全部信息。
函数声明也称作函数原型(function prototype)。
在头文件中进行函数声明。含有函数声明的头文件应该被包含到定义函数的源文件中。
分离式编译
假设在 factMain.cc 文件中创建 main 函数,在 main 函数中调用 fact 函数,fact 函数定义在 fact.cc 文件中。编译过程如下:
$ CC factMain.cc fact.cc # generates factMain.exe or a.out
$ CC factMain.cc fact.cc -o main # generates main or main.exe
其中,$ 是命令提示符,CC 是编译器的名字,# 后面是注释语句。
如果修改了一个源文件内容,只需重新编译改动的文件。大多数编译器提供了分离式编译每个文件的方式。这个过程会生成一个包含对象代码(object code)的文件,文件后缀名为 .obj(Windows)或 .o(UNIX)。
编译器把对象文件链接(link)在一起形成可执行文件(executable file)。
$ CC -c factMain.cc # generates factMain.o
$ CC -c fact.cc # generates fact.o
$ CC factMain.o fact.o # generates factMain.exe or a.out
$ CC factMain.o fact.o -o main # generates main or main.exe
6.2 参数传递
传值参数(Passing Arguments by Value)
// function that takes a pointer and sets the pointed-to value to zero
void reset(int *ip)
{
*ip = 0; // changes the value of the object to which ip points
ip = 0; // changes only the local copy of ip; the argument is unchanged
}
int i = 42;
reset(&i); // changes i but not the address of i
cout << "i = " << i << endl; // prints i = 0
熟悉 C 的程序员常常使用指针类型访问函数外部的对象。在 C++ 语言中,建议使用引用类型的形参替代指针。
传引用参数(Passing Arguments by Reference)
// function that takes a reference to an int and sets the given object to zero
void reset(int &i) // i is just another name for the object passed to reset
{
i = 0; // changes the value of the object to which i refers
}
int j = 42;
reset(j); // j is passed by reference; the value in j is changed
cout << "j = " << j << endl; // prints j = 0
- 使用引用避免拷贝
如果函数无需改变引用形参的值,建议将其声明为常量引用。 - 使用引用形参返回额外信息
引用参数为函数返回多个结果提供了有效途径。
const 形参和实参
尽量使用常量引用
把函数中不会改变的形参定义为普通的引用,这是错误的,会误导函数调用者,即函数可能修改其实参的值。
使用引用而不是 const 引用,会极大地限制函数所能接受的实参类型。
// bad design: the first parameter should be a const string&
string::size_type find_char(string &s, char c, string::size_type &occurs);
类似 find_char("Hello World", 'o', ctr);
这样的调用将在编译时发生错误。
数组形参
数组的两个特殊性质:
①不允许拷贝数组,所以,无法以值传递的方式使用数组形参;
②使用数组时通常会被转换成指针,所以,当向函数传递一个数组时,实际上传递的是指向数组第一个元素的指针。
// each function has a single parameter of type const int*
void print(const int*);
void print(const int[]); // shows the intent that the function takes an array
void print(const int[10]); // dimension for documentation purposes (at best)
上面 3 个声明是等价的:每条语句声明了一个包含一个形参的函数,其形参类型为 const int*
。
int i = 0, j[2] = {0, 1};
print(&i); // ok: &i is int*
print(j); // ok: j is converted to an int* that points to j[0]
管理指针形参的 3 种常用技术:
1. 使用标记指定数组长度
该方法要求数组本身包含一个结束标记。示例:C风格字符串,它存储在字符数组中,最后的字符后面跟着一个空字符。
void print(const char *cp) {
if (cp) // if cp is not a null pointer
while (*cp) // so long as the character it points to is not a null character
cout << *cp++; // print the character and advance the pointer
}
2. 使用标准库规范
传递指向数组首元素和尾后元素的指针。
void print(const int *beg, const int *end) {
// print every element starting at beg up to but not including end
while (beg != end)
cout << *beg++ << endl; // print the current element, and advance the pointer
}
int j[2] = {0, 1};
// j is converted to a pointer to the first element in j
// the second argument is a pointer to one past the end of j
print(begin(j), end(j)); // begin and end functions, see § 3.5.3 (p. 118)
3. 显式传递一个表示数组大小的形参
// const int ia[] is equivalent to const int* ia
// size is passed explicitly and used to control access to elements of ia
void print(const int ia[], size_t size) {
for (size_t i = 0; i != size; ++i)
cout << ia[i] << endl;
}
int j[] = { 0, 1 }; // int array of size 2
print(j, end(j) - begin(j));
数组引用形参
// ok: parameter is a reference to an array; the dimension is part of the type
void print(int (&arr)[10]) {
for (auto elem : arr)
cout << elem << endl;
}
int i = 0, j[2] = {0, 1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
print(&i); // error: argument is not an array of ten ints
print(j); // error: argument is not an array of ten ints
print(k); // ok: argument is an array of ten ints
因为数组的大小是构成数组类型的一部分,这限制了此版本 print
的可用性,该函数只能作用于大小为 10 的数组。
注意:&arr
两端的括号必不可少。
f(int &arr[10]) // error: declares arr as an array of references
f(int (&arr)[10]) // ok: arr is a reference to an array of ten ints
传递多维数组
多维数组是数组的数组。所以,多维数组的首元素是数组,传递的指针是一个指向数组的指针。
数组的第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略。
// matrix points to the first element in an array whose elements are arrays of ten ints
void print(int (*matrix)[10], int rowSize) { /* . . . */ }
// equivalent definition
void print(int matrix[][10], int rowSize) { /* . . . */ }
注意:第一种定义中,*matrix
两端的括号必不可少。
int *matrix[10]; // array of ten pointers
int (*matrix)[10]; // pointer to an array of ten ints
main: 处理命令行选项
有时需要将实参传递给 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) { ... }
当参数传递给 main
时,argv
中的第一个元素要么指向程序名称,要么指向空字符串。后续元素传递命令行上提供的参数。最后一个指针之后的元素保证为 0
。
给定上面的命令行,argc
将为 5
,而 argv
将包含以下 C样式字符串:
argv[0] = "prog"; // or argv[0] might point to an empty string
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
注意:在使用 argv
中的实参时,记住可选参数以 argv[1]
开始;argv[0]
包含程序的名称,而不是用户输入。
含有可变形参的函数
initializer_list 形参(C++11标准)
使用情况:函数实参数量未知,但全部实参的类型都相同。
initializer_list
是一种标准库类型,表示某种特定类型的值的数组;定义在 initializer_list
头文件中。
表6.1 initializer_list
上的操作
操作 | 含义 |
---|---|
initializer_list<T> lst; | 默认初始化;T 类型元素的空列表 |
initializer_list<T> lst{a, b, c ...}; | lst 中的元素数量与初始值一样多;其元素是对应初始值的副本;列表中的元素是 const |
lst2(lst) | 复制或赋值一个 initializer_list 对象,不会复制列表中的元素;复制后,原列表和副本共享元素 |
lst2 = lst | 同上 |
lst.size() | 列表中的元素数量 |
lst.begin() | 返回指向 lst 中首元素的指针 |
lst.end() | 返回指向 lst 中尾后元素的指针 |
initializer_list
是一个模板类型。定义对象时,必须指定列表包含的元素类型。
initializer_list<string> ls; // initializer_list of strings
initializer_list<int> li; // initializer_list of ints
initializer_list
中的元素总是 const
值;没有方式改变 initializer_list
中元素的值。
例,使用如下形式编写输出错误信息的函数,使其可作用于可变数量的形参:
void error_msg(ErrCode e, initializer_list<string> il) {
cout << e.msg() << ": ";
for (const auto &elem : il)
cout << elem << " " ;
//也可以使用迭代器访问元素。
//for (auto beg = il.begin(); beg != il.end(); ++beg)
// cout << *beg << " " ;
cout << endl;
}
// expected, actual are strings
if (expected != actual)
error_msg(ErrCode(42), {"functionX", expected, actual});
else
error_msg(ErrCode(0), {"functionX", "okay"});
如果想要传递一个值的序列给 initializer_list
形参,那么必须把序列放在一对花括号内。
省略符形参
C++中的省略符形参,允许程序连接到,使用名为 varargs
的 C库工具的 C代码。通常,省略符形参不应该用于其他目的。C编译器文档中描述了如何使用 varargs
。
省略符形参应该仅用于C和C++通用的类型。特别是,大多数类类型的对象在传递给省略符形参时均无法正确复制。
省略符形参只能出现在形参列表中的最后一个位置,有两种形式:
void foo(parm_list, ...);
void foo(...);
第一种形式指定一些 foo
形参的类型,对与指定形参对应的实参进行类型检查,但不会对与省略符形参相对应的实参进行类型检查。在第一种形式中,形参声明后面的逗号是可选的。
类型 va_list
:保存有关实参信息。
该类型用作 <cstdarg>
中定义的宏的形参,以检索函数的其他实参。
va_start
初始化该类型的对象,使用方式:随后调用 va_arg
按顺序检索传递给该函数的其他实参。
在用 va_start
初始化 va_list
对象的函数返回之前,必须调用 va_end
宏。
宏 | 操作 | 含义 |
---|---|---|
va_start | void va_start (va_list ap, paramN); | 初始化一个变量实参列表。初始化 ap 以获取形参 paramN 之后的其他实参。 |
va_end | void va_end (va_list ap); | 结束使用变量实参列表。保证使用对象 ap 检索其他实参的函数正常返回。 |
va_arg | type va_arg (va_list ap, type) | 检索下一个实参。该宏扩展为类型 type 的表达式,其具有由 ap 标识的变量实参列表中当前实参的值。 |
va_copy | void va_copy (va_list dest, va_list src); | 复制变量实参列表。将 dest 初始化为 src 的副本(处于其当前状态)。 |
/* va_arg example */
#include <stdio.h> /* printf */
#include <stdarg.h> /* va_list, va_start, va_arg, va_end */
int FindMax(int n, ...) {
int i, val, largest;
va_list vl;
va_start(vl, n);
largest = va_arg(vl, int);
for (i=1; i<n; i++) {
val = va_arg(vl, int);
largest = (largest > val) ? largest : val;
}
va_end(vl);
return largest;
}
int main() {
int m = FindMax(7, 702, 422, 631, 834, 892, 104, 772);
printf("The largest value is: %d\n", m);
return 0;
}
输出:
The largest value is: 892
va_list
类型的对象只能用作 va_start
,va_arg
,va_end
和 va_copy
宏或使用它们的函数的实参,例如 <cstdio>
中的变量实参函数(vprintf
,vscanf
,vsnprintf
,vsprintf
和 vsscanf
)。
/* vsprintf example */
#include <cstdio>
#include <cstdarg>
void PrintFError(const char *format, ...) {
char buffer[256];
va_list args;
va_start(args, format);
vsprintf(buffer, format, args);
perror(buffer);
va_end(args);
}
int main() {
FILE * pFile;
char szFileName[] = "myfile.txt";
pFile = fopen(szFileName,"r");
if (pFile == NULL)
PrintFError("Error opening '%s'", szFileName);
else {
// file successfully open
fclose(pFile);
}
return 0;
}
如果文件 myfile.txt 不存在,输出错误信息如下:
Error opening file 'myfile.txt': No such file or directory
6.3 返回类型和 return 语句
return
语句有两者形式:
return;
return expression;
值是如何被返回的
返回值的方式与初始化变量和形参的方式完全相同:返回值用于在调用点上初始化一个临时变量,该临时变量是函数调用的结果。
不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域,局部指针将指向不存在的对象:
// disaster: this function returns a reference to a local object
const string &manip() {
string ret; // transform ret in some way
if (!ret.empty())
return ret; // WRONG: returning a reference to a local object!
else
return "Empty"; // WRONG: "Empty" is a local temporary string
}
引用返回左值
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到的是左值,其他返回类型得到右值。
char &get_val(string &str, string::size_type ix) {
return str[ix]; // get_val assumes the given index is valid
}
int main() {
string s("a value");
cout << s << endl; // prints a value
get_val(s, 0) = 'A'; // changes s[0] to A
cout << s << endl; // prints A value
return 0;
}
若返回类型是 const
引用,则不能给调用的结果赋值。
列表初始化返回值
C++11标准规定,函数可以返回花括号包围的值的列表。
若列表为空,则对函数返回的临时变量进行值初始化。否则,返回值取决于函数的返回类型。
vector<string> process() {
// . . .
// expected and actual are strings
if (expected.empty())
return {}; // return an empty vector
else if (expected == actual)
return {"functionX", "okay"}; // return list-initialized vector
else
return {"functionX", expected, actual};
}
如果函数返回一个内置类型,花括号列表中最多包含一个值,并且该值一定不需要进行缩小转换。
如果函数返回一个类类型,由类本身定义初始值如何使用。
主函数 main 的返回值
如果函数的返回类型不是 void
,那么它必须返回一个值。
这条规则有个例外:main
函数允许没有 return
语句直接结束。
如果控制到达 main
函数的结尾处而没有 return
语句,编译器隐式插入 return 0;
。
main
函数不能调用它自己。
返回数组指针
因为数组不能被复制,所以函数不能返回数组。但是,函数可以返回一个指向数组的指针或引用。
要想定义一个返回数组的指针或引用的函数,有 4 种方法。
1. 最直接的方法是使用类型别名
typedef int arrT[10]; // arrT is a synonym for the type array of ten ints
using arrtT = int[10]; // equivalent declaration of arrT; see § 2.5.1 (p. 68)
arrT* func(int i); // func returns a pointer to an array of five ints
2. 声明一个返回数组指针的函数
int arr[10]; // arr is an array of ten ints
int *p1[10]; // p1 is an array of ten pointers
int (*p2)[10] = &arr; // p2 points to an array of ten ints
如果想定义一个返回指针数组的函数,数组的维度必须跟在函数的名字之后。
返回数组指针的函数的形式如下:
Type (*function(parameter_list))[dimension]
具体实例:
int (*func(int i))[10];
3. 使用尾置返回类型(C++11)
任何函数定义都可以使用尾随返回,但这种方式对于具有复杂返回类型的函数最有用,比如指向数组的指针或引用。
尾随返回类型(trailing return type)在形参列表之后,并以 ->
开头。为了表明返回类型在参数列表的后面,在返回值类型通常出现的地方使用 auto
:
// fcn takes an int argument and returns a pointer to an array of ten ints
auto func(int i) -> int(*)[10];
4. 使用 decltype
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
// returns a pointer to an array of five int elements
decltype(odd) *arrPtr(int i) {
return (i % 2) ? &odd : &even; // returns a pointer to the array
}
注意:必须记住 decltype
不会自动将数组转换为其相应的指针类型。decltype
返回的类型是数组类型,必须在其中添加 *
以表明 arrPtr
返回一个指针。