目录
第 6 章 函数
6.1 函数基础
6.2 参数传递
6.3 返回类型和return语句
6.4 函数重载
6.5 特殊用途语言特性
6.6 函数匹配
6.7 函数指针
第 6 章 函数
6.1 函数基础
① 一个典型的函数包括:返回类型、函数名字、形参列表以及函数体,执行函数使用调用运算符,即一对圆括号。函数的调用完成两项工作:一是实参初始化函数对应的形参,二是将控制权转移给被调用的函数。
注意:调用运算符(())的优先级与点运算符和箭头运算符相同,并且也符合左结合律。例如:
auto sz = shorterString(s1, s2).size();
// shorterString的结果是点运算符的左侧运算对象,点运算符可以得到该string对象的size成员,size又是第二个调用运算符的左侧运算对象。
#include <iostream>
#include <vector>
#include <list>
#include <string>
using namespace std;
int main(int argc,char** argv)
{
cout<<"+*a[2]:"<<+*a[2]<<endl; // *(解引用)运算符优先级与一元正号+运算符相同,皆为右结合律
int c[3]= {1,3,5};
vector<int> b(c,c+3);
auto p = b.begin(); // *(解引用)运算符的优先级低于后置递增运算符(++),但是结果为1,后置++返回的是原对象的副本,并不会将其递增的结果返回
cout<<"*p++:"<<*p++<<endl;
cin.get();
return 0;
}
② 实参的求值顺序没有规定,编译器可以以任何可行的顺序对实参求值。
③ 注意:即使两个形参类型一样,也必须把两个类型都写出来,并且任意两个形参都不能重名。
④ 函数的返回值类型不能是数组类型或函数类型,但是可以是指向数组或函数的指针。
⑤ 局部静态对象:在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。例如:
size_t count_calls()
{
static size_t ctr = 0; // 函数调用结束后,ctr仍然有效,直到main函数执行完毕
return ++ctr;
}
int main()
{
for (size_t ctr = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
⑥ 函数只能定义一次,但是可以声明多次。建议在头文件中对函数进行声明,在源文件中进行定义。?含有函数声明的头文件应该包含到定义函数的源文件中。
6.2 参数传递
① C++语言中,建议使用引用类型的形参替代指针。拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本就不支持拷贝操作。如果函数无需改变引用形参的值,最好将其声明为常量引用。
② 使用引用形参返回额外信息:一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效途径。例如:
/*
* 功能描述:
* 返回s中c第一次出现的位置索引
* 引用形参occurs负责统计c出现的总次数
*/
// 只读不写,用const
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; // 出现的次数通过occurs隐式地返回
}
// 调用:
auto index = find_char(s, 'o', ctr);
*③ const 形参和实参:用实参初始化形参时会忽略掉顶层const,即形参的顶层const被忽略掉了,当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
void fcn(const int i) { } // fcn能够读取i,但是不能向i写值
void fcn(int i) { } // 错误,重复定义fcn
?第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。
*?④ 指针或引用形参与const
?形参的初始化和变量的初始化是一样的,可以用非常量初始化一个底层const,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。例如:
void reset(int *ip) { }
void reset(int &i) { }
string::size_type find_char(const string &s, char c, string::size_type &occurs)
int main()
{
int i;
const int ci;
string::size_type ctr = 0;
//*****************************************************************
reset(&i); // 调用形参类型是int*的reset函数
reset(&ci); // 错误!不能将一个指向const对象的指针传递给一个普通指针。这里这样初始化,存在用ip修改const变量ci的值的风险。
//*****************************************************************
reset(i); // 调用形参类型是int&的reset函数
reset(ci); // 错误!不能将一个指向const对象的引用传递给一个普通引用。这里这样初始化,存在用i修改const变量ci的值的风险。
reset(42); // 错误!存在用i修改42的值的风险,但实际上42是个常量,除非函数改为void reset(const int &i) { }
//*****************************************************************
reset(ctr); // 错误!类型不匹配,ctr是一个无符号类型
find_char("Hello World!", 'o',ctr);
// 正确:find_char的第一个形参是对常量的引用
}
⑤ 数组形参:数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外信息。管理指针形参有三种常见的技术:使用标记指定数组的长度、
void print(const char *cp)
{
if (cp)
while (*cp)
cout << *cp++ <<endl;
}
使用标准库规范、
void print(const int *beg, const int *end)
{
while(beg != end)
cout << *beg++ << endl;
}
显式传递一个表示数组大小的形参
void print(const int ia[], size_t size)
{
for (size_t i = 0; i != size; ++i) {
cout << ia[i] << endl;
}
}
⑥ main:处理命令行选项:有时我们需要给main函数传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。
int main(int argc, char *argv[]) { }
// 或
int main(int argc, char **argv) { }
// 第一个形参argc表示数组中的字符串数量,第二个形参argv是一个数组
// 若传递下面的选项:prog -d -o ofile data0
/*
* argc = 5,
* argv[0] = "prog",
* argv[1] = "-d",
* argv[2] = "-o",
* argv[3] = "ofile",
* argv[4] = * "data0",
* argv[5] = "0"。
* 注意:可选实参从argv[1]开始,argv[0]保存程序的名字,而非用户输入。
*/
⑦ 含有可变形参的函数:C++新标准提供了两种主要方法:1)如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型。如果实参的类型不同,可以编写一种特殊的函数——可变参数模板。
initializer_list<string> ls;
initializer_list<int> li;
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " ";
cout << endl;
}
注意:如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内。含有initializer_list形参的函数也可以同时含有其他形参。
error_msg({"functionx","okay"});
?省略符形参:为了方便C++程序访问某些特殊的C代码而设置的,省略符形参不应用于其他目的,且只能出现在形参列表的最后一个位置,例如:
void foo(parm_list, ...);
void foo(...);
6.3 返回类型和return语句
① 有返回值函数:含有return语句的循环后面应该也有一条return语句,如果没有的话程序就是错误的,并且很多编译器都无法发现此类错误。
② 不要返回局部对象的引用或指针:
const string &manip()
{
string ret;
if (!ret.empty())
return ret; // 错误!返回局部对象的引用
else
return "Empty"; // 错误!"Empty"是一个局部临时值
}
?③?调用一个返回引用的函数得到左值,其他返回类型得到右值。特别地,可以为返回类型是非常量引用的函数的结果赋值,如:
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;
}
④ 列表初始化返回值:C++11新标准规定,函数可以返回花括号包围的值的列表。?
vector<string> process()
{
// expected、actual是string对象
if (expected.empty())
return {};
else if (expected == actual)
return {"functionx", "okay"}
else
return {"functionx", expected, actual}
}
⑤ 主函数main的返回值:允许main函数没有return语句直接结束,编译器会在执行到结尾处隐式地插入一条返回0的return语句。main函数返回0表示执行成功,其他值表示失败,非0值具体含义依机器而定。?
⑥ 函数递归:函数直接或者间接调用它自身。如:
int factorial(int val)
{
if (val > 1)
return factorial(val - 1) * val;
return 1;
}
⑦ 返回数组指针:定义一个返回数组的指针或者引用的函数比较麻烦,但是可以使用类型别名来简化这一任务。
// 使用类型别名
using arrT = int[10]; // 等价于typedef int arrt[10]
arrT* func (int i); // func返回一个指向含有10个整数的数组的指针
// 不使用类型别名
int (*func(int i)) [10];
/*
* 对于上述表达式的理解:从内到外按顺序理解
* func(int i)表示调用该函数时,需要一个int类型的实参。
* (*func(int i))表示对调用结果解引用
* (*func(int i)) [10]表示解引用后得到一个大小是10的数组
* int (*func(int i)) [10]表示数组中的元素类型是int类型
*/
// 使用尾置返回类型
auto func(int i) -> int(*) [10] // 把函数返回类型放在形参列表之后,可以知道函数返回的是一个指针,该指针指向含有10个整数的数组
// 使用decltype
int odd[] = {1, 3, 5, 7, 9};
int even[] = {2, 4, 6, 8};
decltype(odd) *arrPtr(int i) // 返回一个指针,该指针指向含有5个整数的数组
{
return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
}
6.4 函数重载
① 函数重载:同一作用域内的几个函数名字相同但是形参列表不同,注意main函数只能有一个,不能重载。
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*); // 新函数
② 不允许两个函数除了返回类型外其他所有的要素都相同。如:
Record lookup(const Account&)
bool lookup(const Account&) // 错误!与上一个函数只有返回类型不同
?③ const_cast和重载。例如:
/*
* 解读:函数Ⅰ的参数和返回类型都是const string的引用,当我们用两个非
* 常量string实参调用这个函数时,返回的结果将还是const string的引用;
* 当我们用两个非常量string实参调用,返回的结果却要是一个普通的引用时,
* 我们可以定义新的函数:函数Ⅱ。
*/
// 函数Ⅰ
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
// 函数Ⅱ
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string>(s1), const_cast<const string>(s2));
return const_cast<string&>(r);
}
④ 重载与作用域:在内层作用域声明名字,将会隐藏外层作用域中声明的同名实体。?
6.5 特殊用途语言特性
① 默认实参:默认实参作为形参的初始值出现在形参列表中。
注意:1)?一旦某个形参被赋予了初始值,那么它后面的所有的形参都必须有默认值。2)?尽量让不怎么使用默认值的形参出现在前面,让经常使用默认值的形参出现在后面。3)?不能修改一个已经存在的默认值,但是可以添加默认值。
② 内联函数:内联函数可以避免函数调用的开销。内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。
③ constexpr函数:函数的返回类型和所有的形参类型都必须是字面值类型,函数体中必须有且只有一条return语句,constexpr函数被隐式地指定为内联函数。constexpr函数不一定返回常量表达式。
注意:内联函数和constexpr函数通常定义在头文件中。
④ 调试帮助:开发过程中使用的代码,发布时可屏蔽。
assert预处理宏常用于检查“不能发生”的条件。
// 一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阈值。
assert(word.size() > threshold);
可以用NDEBUG使得assert无效,如:
#define NDEBUG // 关闭调试状态,必须在cassert头文件上面。
#include <cassert>
int main(void)
{
int x = 0;
assert(x);
}
6.6 函数匹配
? ? ? ? ① 确定候选函数和可行函数。
????????② 寻找最佳匹配。?
6.7 函数指针
bool (*pf) (const string &, const string &);
?pf指向了一个函数,该函数的参数是两个const string的引用,返回值是bool类型。
注意:*pf两端的括号必不可少,如果不写,则表示pf是一个返回值为bool指针的函数。
① 和数组类似,当我们把函数名作为一个值使用时,该函数自动地转换成指针。
② 函数和指针地类型必须精确匹配。
③?和函数类型的形参不一样,返回类型不会自动转换成指针,必须显式地将返回类型指定为指针。
④ 如果明确知道返回的函数是哪一个,就可以使用decltype简化书写函数指针返回类型的过程。
|