一个典型的函数定义包含以下部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。

函数基础

编写函数

例如:5的阶乘

1
2
3
4
5
6
7
int fact(int val)
{
int ret = 1;
while (val > 1)
ret *= val--;
return ret;
}

调用函数

函数调用完成两项工作:

  1. 用实参初始化函数对应的形参
  2. 将控制权转移给被调用函数(此时主调函数的执行被暂时中断,被调函数开始执行)
1
2
3
4
5
6
int main()
{
int j = fact(5);
cout << "5! is " << j << endl;
return 0;
}

形参与实参

实参是形参的初始值,且实参的类型必须与对应的形参类型匹配。

函数的形参列表

形参列表为空的表示方法:

1
2
void f1() {}         // 隐式
void f1(void) {} // 显式

即使两个形参的类型一样,也必须把两个类型都写出来

1
2
int f3(int v1, v2) {}       // 错误
int f4(int v1, int v2) {} // 正确

函数返回类型

函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。

局部对象

在C++语言中,名字有作用域,对象有生命周期。

  • 名字的作用域是程序文本的一部分,名字在其中可见
  • 对象的生命周期是程序执行过程中该对象存在的一段时间

函数体是-一个语句块。块构成- -个 新的作用域,我们可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量( local variable)。它们对函数而言是“局部"的,仅在函数的作用域内可见,同时局部变量还会隐藏(hide)在外层作用域中同名的其他所有声明中。

自动对象

只存在于块执行期间的对象称为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。

形参是一种自动对象

局部静态对象

有些时候,我们需要令局部变量的生命周期贯彻函数调用及以后的时间,这时候便需要定义为static类型。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,直到程序终止才会被销毁。

When a variable is declared as static, space for it gets allocated for the lifetime of the program.

例:

1
2
3
4
5
size_t count_calls()
{
static size_t ctr = 0;
return ++ctr;
}

函数声明

函数的名字必须在使用之前先声明。

函数的声明和函数的定义类似,唯一的区别是函数声明无需函数体,用一个分号替代即可。

例:

1
void print(vector<int>::const_iterator beg, vector<int>::const_iterator end);

函数的三要素(返回类型,函数名,形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型

参数传递

如果形参是引用类型,它将绑定到对应的形参上,否则,将实参的值拷贝后赋给形参。

指针形参

1
2
3
4
5
6
7
8
9
void reset(int *ip)
{
*ip = 0; // 改变指针ip所指对象的值
ip = 0; // 只改变了ip的局部拷贝,实参并未改变
}

int i = 42;
reset(&i);
cout << "i = " << i << endl;

在C++中建议使用引用类型代替指针。

传引用参数

例:该函数接受一个int对象的引用,然后将对象的值置为0

1
2
3
4
5
6
7
8
void reset(int &i)
{
i = 0; // 改变了i所引对象的值
}

int j = 42;
reset(j);
cout << "j = " << j << endl;

调用这一版本的reset函数时,我们直接传入对象而无须传递对象的地址。

使用引用避免拷贝,且如果函数无需改变引用形参的值,最好将其声明为常量引用。

const形参与实参

当用实参初始化形参时会忽略掉顶层的const。

1
void fcn(const int i) {/* */}

调用该函数时,既可以传入const int 也可以传入int。

把函数不会改变的形参定义成引用是一种比较常见的错误,这么做带给函数的调用者一张误导,即函数可以修改它的实参的值,

所以尽量使用常量引用

数组形参

数组两个性质为:

  • 不允许拷贝
  • 使用数组时会将其转换为指针

所以我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

管理指针形参有三种常用的技术

  1. 使用标记指定数组长度
  2. 使用标准库规范
  3. 显式传递一个表示数组大小的形参

main:处理命令行选项

假设main函数位于可执行文件prog之内,我们可以向程序传递下面的选项

1
prog -d -o ofile data0

这些命令行选项通过两个可选的形参传递给main函数

1
int main(int argc, char *argv[]) {...}

其中第一个形参argc表示数组中字符串的数量,第二个形参argv是一个数组

当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存进程的名字,而非用户输入。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>
using namespace std;

int main(int argc, char **argv)
{
string str;
for (auto i = 1; i < argc; ++i)
{
str += argv[i];
str += " ";
}

cout << str << endl;
return 0;
}

含有可变形参的函数

为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:

  1. 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型
  2. 如果类型不同,可变参数模板

initializer_list形参

如果函数的实参数量未知但是知道全部实参的类型都相同,我们可以使用initializer_list类型的形参。

例如:

1
2
3
4
5
6
7
8
9
10
11
void error_msg(initializer_list<strign> 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", "okey"});

含有initializer_list形参的函数也可以同时拥有其他形参。

1
2
3
4
5
6
7
8
9
10
11
12
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", "okey"});

返回类型和return语句

无返回值的函数

也就是void

有返回值函数

return语句返回值的类型必须与函数的返回类型相同

值是如何被返回的

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时变量,该临时量就是函数调用的结果。

不要返回局部对象的引用或指针。

引用返回左值

函数的返回类型绝对函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。

1
2
3
4
5
6
7
8
9
10
11
12
13
char &get_val(string &str, string::size_type ix)
{
return str[ix];
}

int main()
{
string s("a value");
cout << s << endl; // 输出 a value
get_val(s, 0) = "A";
cout << s << endl; // 输出 A value
return 0;
}

列表初始化返回值

c++11中,函数可以返回花括号包围的值的列表

1
2
3
4
5
6
7
8
9
vector<string> process()
{
if (expected.empty())
return {};
else if (expected == actual)
return {"functionX", "okey"};
else
return {"functionX", expected, actual};
}

主函数main的返回值

我们允许main函数没有return语句直接结束。

main函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。

为了使返回值与机器无关, 可以这样做

1
2
3
4
5
6
7
int main()
{
if (some_failure)
return EXIT_FAILURE;
else
return EXIT_SUCCESS;
}

递归

如果一个函数调用了它本身,不管这种调用是直接的还是间接的,都称该函数为递归函数

例如 阶乘

1
2
3
4
5
6
int factorial(int val)
{
if (val > 1)
return factorial(val-1) * val;
return 1;
}

返回数组指针

因为数组不能被拷贝,所以函数不能返回数组,但是函数可以返回数组的指针或者引用。

使用类型别名,我们可以定义一个返回数组的指针或引用的函数

1
2
3
typedef int arrT[10];
using arrT = int[10];
arrT* func(int i);

声明一个返回数组指针的函数

1
2
3
int arr[10];            // arr是一个含有10个整数的数组
int *p1[10]; // p1是一个含有10个指针的 数组
int (*p2)[10] = &arr; // p2是一个指针,它指向含有10个整数的数组

所以返回数组指针的函数形式如下所示:

1
type (*function(parameter_list))[dimension]

使用尾置返回类型

1
2
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];

使用decltype

1
2
3
4
5
6
7
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even;
}

函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,称之为重载函数

1
2
3
void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);

这些函数接受的形参类型不一样,但是执行的操作非常相似,当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数。

1
2
3
4
int j[2] = {0, 1};
print("hello world");
print(j, end(j) - begin(j));
print(begin(j), end(j));

main函数不能重载

调用重载的函数

函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的一个关联起来,函数匹配也叫做重载确定

当调用重载函数时有三种可能的结果

调用重载函数时有三种可能的结果:

  1. 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码
  2. 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息
  3. 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用

特殊用途语言特性

默认实参

1
2
typedef string::size_type sz;
string screen(sz ht = 42, sz wid = 80, char backgrnd = ' ');

如果我们想使用默认实参,只要在调用函数的时候省略该实参就可以。

需要注意的是函数调用时实参按其位置解析

1
2
window = screen(, , '?');   // 错误:只能省略尾部的实参
window = screen('?'); // 调用screen('?', 80, ' ')

所以当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序。

内联函数

内联函数可以避免函数调用的开销

将函数指定为内联函数,通常就是将它在每个调用点上"内联”展开。假设我们将shorterString函数定义为内联函数,则如下调用

1
cout << shorterString(s1, s2) << endl;

将在编译过程中展开成类似于下面的形式

1
cout << (s1.size() < s2.size() ? s1 : s2) << endl;

从而消除了shorterString函数的运行开销。

内联函数的声明

1
2
3
4
inline const string & shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}

内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求

函数指针

函数指针指向的是函数而非对象。

1
bool lengthCompare(const string &, const string &);

声明一个可以指向该函数的指针,只需要用指针替换函数名即可

1
bool (*pf)(const string &, const string &); // 未初始化

pf前面有个*,因此pf是指针;右侧是形参列表,表示pf指向的函数;在观察左侧,发现函数的返回类型是布尔值,因此pf就是一个指向函数的指针。

待续……