【C++基础】第三十四课:函数基础

编写函数,调用函数,形参和实参,函数返回类型,局部对象,自动对象,局部静态对象,函数声明,分离式编译

Posted by x-jeff on December 4, 2021

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.函数基础

函数可以有0个或多个参数,而且(通常)会产生一个结果。可以重载函数,也就是说,同一个名字可以对应几个不同的函数。

一个典型的函数定义包括以下部分:返回类型(return type)、函数名字、由0个或多个形参组成的列表以及函数体。其中,形参以逗号隔开,形参的列表位于一对圆括号之内。函数执行的操作在语句块中说明,该语句块称为函数体(function body)。

我们通过调用运算符(call operator)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参(argument)列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。

1.1.编写函数

编写一个求数的阶乘的程序:

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;
}

函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。return语句也完成两项工作:一是返回return语句中的值(如果有的话),二是将控制权从被调函数转移回主调函数。

1.3.形参和实参

实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。实参的类型必须与对应的形参类型匹配。函数有几个形参,我们就必须提供相同数量的实参。

在上面的例子中,fact函数只有一个int类型的形参,所以每次我们调用它的时候,都必须提供一个能转换成int的实参:

1
2
3
4
fact("hello");//错误:实参类型不正确
fact();//错误:实参数量不足
fact(42,10,0);//错误:实参数量过多
fact(3.14);//正确:该实参能转换成int类型

1.4.函数的形参列表

函数的形参列表可以为空,但是不能省略。要想定义一个不带形参的函数,最常用的办法是书写一个空的形参列表。不过为了与C语言兼容,也可以使用关键字void表示函数没有形参:

1
2
void f1() { /*...*/ } //隐式地定义空形参列表
void f2(void) { /*...*/ } //显式地定义空形参列表

形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明。即使两个形参的类型一样,也必须把两个类型都写出来:

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

任意两个形参都不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。

形参名是可选的,但是由于我们无法使用未命名的形参,所以形参一般都应该有个名字。偶尔,函数确实有个别形参不会被用到,则此类形参通常不命名以表示在函数体内不会使用它。不管怎样,是否设置未命名的形参并不影响调用时提供的实参数量。即使某个形参不被函数使用,也必须为它提供一个实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int f1(int a, double) {
    return a * 2;
}

int f2(int a, int b = 2) {
    return a * b;
}

int main() {
    int a;
    a = f1(3, 4.0);
    cout << a << endl;//a=6
    a = f2(3);
    cout << a << endl;//a=6
    a = f2(3, 3);
    cout << a << endl;//a=9
}

1.5.函数返回类型

大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值。⚠️函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。

2.局部对象

形参和函数体内部定义的变量统称为局部变量。它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。

2.1.自动对象

对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象(automatic object)。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。形参就是一种自动对象。

2.2.局部静态对象

‼️某些时候,有必要令局部变量的声明周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
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到10(包括10在内)的数字。在第二次调用函数时,局部静态变量ctr不会再次初始化,它仅在第一次的时候做唯一的一次初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
size_t count_calls() {
    static size_t ctr = 0;//调用结束后,这个值仍然有效
    return ++ctr;
}

size_t count_called() {
    static size_t ctr = 0;
    return ++ctr;
}


int main() {
    for (size_t i = 0; i != 10; ++i)
        cout << count_calls() << endl;//输出1-10
    cout << ctr << endl;//错误,访问不到ctr
    for (size_t i = 0; i != 10; ++i)
        cout << count_called() << endl;//输出1-10
    return 0;
}

局部静态变量会一直存在于局部(比如函数内),全局是访问不到的。

如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。

3.函数声明

和其他名字一样,函数的名字也必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。

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

因为函数的声明不包含函数体,所以也就无须形参的名字(可加可不加):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//以下两种声明方式都可以
int f1(int, int);//没有形参名字
//int f1(int a, int b);

int main() {
    int n = 0;
    n = f1(3, 4);
    cout << n << endl;
    return 0;
}

int f1(int a, int b) {
    return a * b;
}

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

函数通常在头文件中声明而在源文件中定义(含有函数声明的头文件应该被包含到定义函数的源文件中)。

4.分离式编译

C++语言支持所谓的分离式编译(separate compilation)。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。如果我们修改了其中一个源文件,那么只需重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(Windows)或.o(UNIX)的文件,后缀名的含义是该文件包含对象代码(object code)。接下来编译器负责把对象文件链接在一起形成可执行文件。