文章目录
第六章 函数
- 函数是一个命名了的代码块,我们通过调用函数执行和相应的代码。
- 函数可以有0个或多个参数,而且(通常)会产生一个结果。
- 可以重载函数,同一个名字对应几个不同的函数。
6.1 函数基础
- 一个典型的函数定义包括以下部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。
int fact(int val)
{
int ret = 1;
while (val > 1)
{
ret *= val--;
}
return ret;
}
int main()
{
int j = fact(5);
cout << "5! is " << j << endl;
return 0;
}
5! is 120
- 函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。
- 此时,主调函数的执行被暂时中断,被调函数开始执行。
- 实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。
- 尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。
fact("hello");
fact();
fact(42, 10, 0);
fact(3.14);
void f1(){}
void f2(void){}
- 形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明,即使两个形参的类型一样,也必须把两个类型都写出来。
int f3(int v1,v2){}
int f4(int v1,int v2){}
- 大多数类型都能用作函数的返回类型。
- 一种特殊的返回类型是void, 它表示函数不返回任何值。
6.1.1 局部对象
- 在C++语言中,名字有作用域,对象有生命周期。
- 名字的作用域是程序文本的一部分,名字在其中可见。
- 对象的生命周期是程序执行过程中该对象存在的一段时间。
- 同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。
- 局部静态对象在程序的执行路径上第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。
#include <iostream>
using namespace std;
size_t count_calls()
{
static size_t ctr = 0;
return ++ctr;
}
int main()
{
for (size_t i = 0; i != 10; ++i)
{
cout << count_calls() << endl;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
6.1.2 函数声明
void print(vector<int>::const_iterator beg, vector<int>::const_iterator end);
6.1.3 分离式编译
- 随着程序越来越复杂,需要使用分离式编译。
- 声明写在
.h文件中,定义在.cpp文件中。
6.2 参数传递
- 每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
- 当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。
- 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象,我们说这样的实参被值传递或者函数被传值调用。
6.2.1 传值参数
- 当初始化一个非引用类型的变量时,初始值被拷贝给变量。
- 传值参数的机理完全一样,函数对形参做的所有操作都不会影响实参。
int n = 0;
int i = n;
i = 42;
- 指针的行为和其他非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值,但是我们可以间接地访问它所指的对象。
void reset(int *ip)
{
*ip = 0;
ip = 0;
}
int i = 42;
reset(&i);
cout << "i=" << i << endl;
6.2.2 传引用参数
void reset(int &i)
{
i = 0;
}
int j = 42;
reset(j);
cout << "j=" << j << endl;
- 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本就不支持拷贝操作,函数只能通过引用形参访问该类型的对象。
bool isShorter(const string &s1,const string &s2)
{
return s1.size()<s2.size();
}
#include <iostream>
using namespace std;
string::size_type find_char(const string &s, char c, string::size_type &occurs)
{
auto ret = s.size();
occurs = 0;
for (decltype(ret) i = 0; i != s.size(); ++i)
{
if (s[i] == c)
{
if (ret == s.size())
{
ret = i;
}
++occurs;
}
}
return ret;
}
int main()
{
string s = "openopen";
string::size_type ctr = 0;
auto index = find_char(s, 'o', ctr);
cout << s << endl;
cout << ctr << endl;
}
openopen
2
6.2.3 const形参和实参
- 因为当用实参初始化形参时会忽略掉顶层const,换句话说,形参如果有顶层const,实参既可以传入const int也可以传入int,如果重载具有相同名字相同类型的变量形参,就会出现不确定性,以上为个人理解,书上写的不太懂。
const int ci = 42;
int i = ci;
int *const p = &i;
*p = 0;
void fcn(const int i){ }
void fcn(int i){ }
- 指针或引用形参与const,我们可以使用非常量初始化一个底层const对象,但是反过来不行,一个普通的引用必须用同类型的对象初始化。
int i = 42;
const int *cp = &i;
const int &r = i;
const int &r2 = 42;
int *p = cp;
int &r3 = r;
int &r4 = 42;
void reset(int &i)
{
i = 0;
}
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i) {}
reset(&ci);
reset(i);
reset(ci);
reset(42);
reset(str);
6.2.4 数组形参
- 数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响。
- 不允许拷贝数组。
- 使用数组时通常会将其转换成指针。
void print(const int *);
void print(const int[]);
void print(const int[10]);
int i = 0, j[2] = {0, 1};
print(&i);
print(j);
void print(const char *cp)
{
if (cp)
{
while (*cp)
{
cout << *cp++;
}
}
}
void print(const int *beg, const int *end)
{
while (beg != end)
{
cout << *beg++ << endl;
}
}
int j[2] = {0, 1};
print(begin(j), end(j));
void print(const int ia[], size_t size)
{
for (size_t i = 0; i != size; ++i)
{
cout << ia[i] << endl;
}
}
- 数组形参和const,当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。
- 数组引用形参,形参可以是数组的引用。
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);
print(j);
print(k);
- 当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。
void print(int (*matrix)[10], int rowSize)
{
}
int *matrix[10];
int (*matrix)[10];
void print(int matrix[][10], int rowSize)
{
}
6.2.5 main:处理命令行选项
- main函数平时定义的时候只有空形参列表
int main(){}。 - main函数可以接受传递实参
int main(int argc, char *argv []) { ... }等价于int main(int argc, char **argv) { ... }。
6.2.6 含有可变形参的函数
- 有时我们无法提前预知应该向函数传递几个实参。
- 为了编写能处理不同数噩实参的函数,C++11新标准提供了两种主要的方法:
- 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型。
- 如果实参的类型不同,可以编写一种特殊可变参数模板。
- initializer_list类型形参是一种标准库类型。
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
{
cout << *beg << " ";
}
cout << endl;
}
if (expected!=actual)
{
error_msg({"functionX";expected,actual});
}
else
{
error_msg({"functionX","okay"});
}
- 含有initializer_list类型形参的函数也可以同时拥有其他形参。
void error_msg(ErrCode e, initializer_list<string> il)
{
cout << e.msg() << ": ";
for (const auto &elem : il)
{
cout << elem << " ";
}
cout << endl;
}
if (expected != actual)
{
error_msg(ErrCode(42),{"functionX";expected,actual });
}
else
{
error_msg(ErrCode(0),{"functionX", "okay"});
}
- 省略符形参是为了便于C++程序访问某些特殊的C代码而设置的。
void foo(parm_list,...);
void foo(...);
6.3 返回类型和return语句
- return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。
- 有两种形式:
return;return expression;
6.3.1 无返回值函数
- 没有返回值的return语句只能用在返回类型是void的函数中。
void swap(int &v1, int &v2)
{
if (v1 == v2)
{
return;
}
int tmp = v2;
v2 = v1;
v1 = tmp;
}
6.3.2 有返回值函数
- return语句的第二种形式提供了函数的结果,只要函数的返回类型不是void,则该函数内的每条return语句必须返回个值。
bool str_subrange(const string &str1, const string &str2)
{
if (str1.size() == str2.size())
{
return str1 == str2;
}
auto size = (str1.size() < str2.size()) ? str1.size() : str2.size();
for (decltype(size) i = 0; i != count; ++i)
{
if (str1[i] != str2[i])
{
return;
}
}
}
- 返回一个值的方式和初始化一个变量或形参的方式完全一样。
string make_plural(size_t ctr, const string &word, const string &ending)
{
return (str > 1) ? word + ending : word;
}
* 同其他引用类型一样,如果函数返回引用,则该引用仅是它所引对象的一个别名。
const string &shorterString(const string &s1, cosnt string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
const string &manip()
{
string ret;
if (!ret.empty())
{
return ret;
}
else
{
return "Empty";
}
}
- 和其他运算符一样,调用运算符也有优先级和结合律。
- 调用运符符的优先级与点运符符和箭头运算符相同,并且也符合左结合律。
auto sz = shorterString(sl, s2).size();
- 引用返回左值,调用一个返回引用的函数得到左值,其他返回类型得到右值。
char &get_val(string &str, string::size_type ix)
{
return str[ix];
}
int main()
{
string s("a value");
cout << s << endl;
get_val(s, 0) = 'A';
cout << s << endl;
return 0;
}
- 如果返同类型是常量引用,我们不能给调用的结果赋值。
const string &shorterString(const string &s1, cosnt string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
shorterString("hi", "bye") = "X";
- C++11新标准规定,列表初始化返回值可以返回花括号包围的值的列表。
vector<string> process()
{
if (expected.empty())
{
return {};
}
else if (expected == actual)
{
return {"functionX", "okay"};
}
else
{
return {"functionX", expected, actual};
}
}
- 允许主函数main没有return语句直接结束。控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。
- 如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数。
int factorial(int val)
{
if (val > 1)
{
return factorial(val - 1) * val;
}
return 1;
}
6.3.3 返回数组指针
- 因为数组不能被拷贝,所以函数不能返回数组,不过,函数可以返回数组的指针或引用。
typedef int arrT[10];
using arrT = int[10];
arrT *func(int i);
int arr[10];
int *p1[10];
int (*p2)[10] = &arr;
- 返回数组指针的函数形式如下所示:
- Type表示元素的类型。
- dimension表示数组的大小。
- (*function(parameter_list))两端的括号必须存在,就像我们定义p2时两端必须有括号一样。
func(int i)表示调用func函数时需要一个int类型的实参。(*func(int i))意味着我们可以对函数调用的结果执行解引用操作。(*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组。int (*func(int i))[10]表示数组中的元素是int类型。
Type (*function(parameter_list))[dimension]
int (*func(int i))[10];
- 使用队尾表示法,任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。
auto func(int i) -> int (*)[10];
- 使用decltype,如果找们知道函数返回的指针将指向哪个数组,就可以使用关键字声明返回类型。
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even;
}
6.4 函数重载
- 如果同一个作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);
int j[2] = {0, 1};
print("Hello World");
print(j, end(j) - begin(j));
print(begin(j), end(j));
- main函数不能重载。
- 重载函数使得可以定义一组函数,更据不同的调用方式调用不同的函数。
Record lookup(const Account &);
Record lookup(const Phone &);
Record lookup(const Name &);
Account acct;
Phone phone;
Record r1 = lookup(acct);
Record r2 = lookup(phone);
- 对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。
- 假设有两个函数,它们的形参列表一样但是返回类型不同,那么它是错误的。
Record lookup(const Account &);
bool lookup(const Account &);
Record lookup(const Account &acct);
Record lookup(const Account &);
typedef Phone Telno;
Record lookup(const Phone &);
Record lookup(const Telno &);
- 重载和const形参,顶层const不影响传入函数的对象。
Record lookup(Phone);
Record lookup(const Phone);
Record lookup(Phone *);
Record lookup(Phone *const);
Record lookup(Account &);
Record lookup(const Account &);
Record lookup(Account *);
Record lookup(const Account *);
- 尽管函数重载能在一定程度上减轻我们为函数起名字、记名字的负担,但是最好只重载那些确实非常相似的操作。
- const_cast和重载。
const string &shorterString(const string &s1, cosnt string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string &>(sl), const_cast<const string &>(s2));
return const_cast<string &>(r);
}
string &shorterString(string &s1, string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
- 函数匹配是指定义了一组重载函数后,我们需要以合理的实参调用它们。
- 在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定。
- 当调用重载函数时有三种可能的结果:
- 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息。
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时报错称二义性调用。
6.4.1 重载与作用域
- 如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。
string read();
void print(const string &);
void print(double);
void fooBar(int ival)
{
bool read = false;
string s = read();
void print(int);
print("Value:");
print(ival);
print(3.14);
}
void print(const string &);
void print(double);
void print(int);
void fooBar2(int ival)
{
print("Value:");
print(ival);
print(3.14);
}
6.5 特殊用途语言特性
- 三种函数相关的语言特性:
- 默认实参。
- 内联函数和constexpr函数。
- 程序调试过程中常用的一些功能。
6.5.1 默认实参
- 在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参。
- 调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrand = ' ');
string window;
window = screen();
window = screen(66);
window = screen(66, 256);
window = screen(66, 256, '#');
window = screen(, , '?');
window = screen('?')
- 对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。
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);
string window = screen();
void f2()
{
def = '*';
sz wd = 100;
windown = screen();
}
6.5.2 内联函数和constexpr函数
- 使用函数一般比求等价表达式的值要慢一点,所以可以将规模较小的操作定义为内联函数。
- 将函数指定为内联函数,通常就是将它在每个调用点上“内联地”展开。
const string &shorterString(const string &s1, cosnt string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
cout << shorerString(s1, s2) << endl;
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
- 在函数返回类型前加上关键字inline,这样可以声明成内联函数。
inline const string &shorterString(const string &s1, cosnt string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
6.5.3 调试帮助
- C++有时会用到一种类似头文件保护的技术,以便有选择地执行调试代码。
- assert是一种预处理宏,所谓预处理宏其实是一个预处理变量,它的行为有点类似内联函数。
- assert由预处理器管理而非编译器管理,所以使用时都不用命名空间声明。
assert(expr);
- NDEBUG预处理变量,assert的行为依赖于NDEBUG的状态。
- 如果定义了NDEBUG,则assert什么也不做。
- 默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
- 很多编译器都提供了一个命令行选项使我们可以定义预处理变量。
gcc -D NDEBUG main.c
#define NDEBUG
- 也可以使用NDEBUG编写自己的条件调试代码,条件编译。
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
cerr << __func__ << ": array size is" << size << endl;
#endif
}
__fun__
__FILE__
__LINE__
__TIME__
__DATE__
if (word.size() < threshold)
{
cerr << "Error: " << __FILE__ << ": in function " << __func__ << " at line " << __LINE__ << endl
<< " Compiled on " << __DATE__ << " at " << __TIME__ << endl
<< " Word read was \"" << word << "\": Length too short" << endl;
}
6.6 函数匹配
- 当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时要进行函数匹配。
- 第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。
- 第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6);
- 第三步是从可行函数中选择与本次调用最匹配的函数,实参类型与形参类型越接近,它们匹配得越好,所以精确匹配比需要类型转换的匹配更好。
- 当实参的数量有两个或更多时,函数匹配就比较复杂了,如下,因为每个可行函数各自在个实参上实现了更好的匹配,编译器最终将因为这个调用具有二义性而拒绝其请求。
f(42,2.56)
6.6.1 实参类型转换
- 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
- 精确匹配,包括以下情况:
- 实参类型和形参类型相同。
- 实参从数组类型或函数类型转换成对应的指针类型。
- 向实参添加顶层const或者从实参中删除顶层const。
- 通过const转换实现的匹配。
- 通过类型提升实现的匹配。
- 通过算术类型转换或指针转换实现的匹配。
- 通过类类型转换实现的匹配。
- 小整型一般都会提升到int类型或更大的整数类型,即使实参是一个很小的整数值,也会直接将它提升成int类型,此时使用short版本反而会导致类型转换。
void ff(int);
void ff(short);
ff('a');
- 字面值3.14的类型是double,它既能转换成long也能转换成float,因为存在两种可能的算数类型转换,所以该调用具有二义性。
void manip(long);
void manip(float);
manip(3.14);
- 如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。
Record lookup(Account &);
Record lookup(const Account &);
const Account a;
Account b;
lookup(a);
lookup(b);
6.7 函数指针
- 函数指针指向的是函数而非对象。
- 和其他指针一样,函数指针指向某种特定类型。
- 函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
bool lengthCompare(const string &, const string &);
bool (*pf)(const string &, const string &);
bool *pf(const string &, const string &);
- 当我们把函数名作为一个值使用时,该函数自动地转换为指针。
bool (*pf)(const string &, const string &);
pf = lengthCompare;
pf = &lengthCompare;
bool b1 = pf("hello", "goodbye");
bool b2 = (*pf)("hello", "goodbye");
bool b3 = lengthCompare("hello", "goodbye");
string::size_type sumLength(const string &, const string &);
bool cstringCompare(const char *, const char *);
pf = 0;
pf = sumLength;
pf = cstringCompare;
pf = lengthCompare;
- 重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
void ff(int *);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff;
void (*pf2)(int) = ff;
double (*pf3)(int *) = ff;
bool lengthCompare(const string &, const string &);
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
userBigger(s1, s2, lengthCompare);
typedef bool Func(const string &, const string &);
typedef decltype(lengthCompare) Func2;
typedef bool (*FuncP)(const string &, const string &);
typedef decltype(lengthCompare) *Func2;
void useBigger(const string &, const string &, Func);
void useBigger(const string &, const string &, FuncP2);
using F = int(int *, int);
using PF = int (*)(int *, int);
PF f1(int);
F f1(int);
F *f1(int);
int (*f1(int))(int *, int);