【C++】《C++ Primer 5th》笔记-Chapter6-函数

笔记:
一、函数基础
1、在C++语言中允许重载函数,也就是几个不同的函数可以使用同一个名字。
2、实参是形参的初始值。
尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。
实参的类型必须与对应的形参类型匹配,但有时存在实参隐式转换,例如实参是double、形参是int。
3、任意两个形参不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。
4、函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
5、在C++语言中,名字有作用域,对象有生命周期。名字的作用域是程序文本的一部分,名字在其中可见。对象的生命周期是程序执行过程中该对象存在的一段时间。
在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结果才会销毁。局部变量的生命周期依赖于定义的方式。
6、我们把只存在于块执行期间的对象称为自动对象。
对于局部变量对应的自动对象来说,则分为两种情况:如果变量定义本身含有初始值,就用这个初始值进行初始化;否则,如果变量定义本身不含初始值,执行默认初始化。这意味着内置类型的未初始化局部变量将产生未定义的值。
7、某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的局部静态对象。该对象所在的函数在第一次执行时该静态对象被初始化,后面再次执行该函数,该静态对象不会被再次初始化。
8、如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0.
9、和其他名字一样,函数的名字也必须在使用之前声明。函数只能定义一次,但可以声明多次。唯一的例外是,如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。
函数声明也称作函数原型。
和变量类似,函数也应该在头文件中声明而在源文件中定义。定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
10、为了允许编写程序时按照逻辑关系将其划分开来,C++语言支持所谓的分离式编译。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。如果我们修改了其中一个源文件,那么只需重新编译那个改动了的文件。
大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(Windows)或.o(UNIX)的文件,后缀名的含义是该文件包含对象代码。接下来编译器负责把对象文件链接在一起形成可执行文件。

二、参数传递
1、每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
形参初始化的机理与变量初始化一样。
2、当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用。
3、当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。
4、熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参替代指针。
5、拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
注意:如果函数无须改变引用形参的值,最好将其声明为常量引用。
6、一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。
7、当形参是const时,必须要注意是否是顶层const。顶层const作用于对象本身。
和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
8、在C++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。
void fcn(const int i){}    // fcn能读取i,但是不能向i写值
void fcn(int i){}        // 错误:重复定义了fcn(int)

9、形参的初始化方式和变量的初始化方式是一样的。我们可以使用非常量初始化一个底层const对象,凡是反过来不行;同时一个普通的引用必须用同类型的对象初始化。
C++不允许用字面值初始化一个非常量引用,允许我们用字面值初始化常量引用。

10、尽量使用常量引用。把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这样做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。例如,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

11、因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

12、尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:
// 下面3个print函数是等价的,都有一个const int*类型的形参
void print(const int*);
void print(const int[]);    // 可以看出来,函数的意图是作用于一个数组
void print(const int[10]);    // 这里的维度表示我们期望数组含有多少元素,实际不一定

13、和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。

14、因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。
①管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串。C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止。这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,但是那些像int这样所有取值都是合法值的数据就不太有效了。
②管理数组实参的第二种方法是传递指向数组首元素和尾后元素的指针。
③管理数组实参的第三种方法是专门定义一个表示数组大小的形参。

15、当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。

16、&arr两端的括号必不可少:
f(int &arr[10]);    // 错误:将arr声明成了引用的数组
f(int (&arr)[10]);    // 正确:arr是具有10个整数的整型数组的引用
但这一用法限制了函数的可用性,我们只能将函数作用于大小为10的数组。

17、下面两个语句等价:
void print(int (*matrix)[10], int rowSize) {}
void print(int matrix[][10], int rowSize) {}
matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针。
*matrix两端的括号必不可少:
int *matrix[10];    // 10个指针构成的数组
int (*matrix)[10];    // 指向含有10个整数的数组的指针

18、当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。
int main(int argc, char *argv[]) { ... } // 第一个形参argc表示数组中字符串的数量,第二个形参argv是一个数组,它的元素是指向C风格字符串的指针。当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素一依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。

19、C++有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。不过需要注意的是,省略符形参这种功能一般只用于与C函数交互的接口程序。
省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:
void foo(parm_list, ...);
void foo(...);
省略符形参所对应的实参无须类型检查。

20、如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中。
拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素。
initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。

三、返回类型和return语句
1、注意:在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。很多编译器都无法发现此类错误。
2、不要返回局部对象的引用或指针。函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域。
3、函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。
特别是,我们能为返回类型是非常量引用的函数的结果赋值:
char &get_val(string &str, string::size_type ix){    return str[ix];    }
get_val(s, 0) = 'A';    // 将s[0]的值改为A

4、C++11新标准规定,函数可以返回花括号包围的值的列表。

5、我们允许main函数没有return语句直接结束。如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。
main函数不能调用自己。

6、逐层理解该声明的含义:int (*func(int i))[10];
①func(int i)表示调用func函数时需要一个int类型的实参。
②(*func(int i))意味着我们可以对函数调用的结果执行解引用操作。
③(*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组。
④int (*func(int i))[10]表示数组中的元素是int类型。

7、在C++11新标准中海油一种可以简化上述func声明的方法,就是使用尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个->符号开头。
auto func(int i) -> int(*)[10];    // func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
因为我们把函数的返回类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针,并且该指针指向了含有10个整数的数组。

8、如果我们知道函数返回的指针将指向哪个数组,就可以使用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;    // 返回一个指向数组的指针
}
arrPtr使用关键字decltype表示它的返回类型是个指针,并且该指针所指的对象与odd的类型一致。因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。有个地方需要注意:decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想表示arrPtr返回指针还必须在函数声明时加一个*符号。

四、函数重载
1、如果同一个作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
main函数不能重载。
2、顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来,所以没法实现函数重载。
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的。因为const不能转换成其他类型,所以我们只能把const对象(或指向const的指针)传递给const形参。相反的,非常量却可以转换成const。不过,当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。

3、尽管函数重载能在一定程度上减轻我们为函数起名字、记名字的负担,但是最好只重载那些确实非常相似的操作。

4、const_cast在重载函数的情景下最有用。
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);
}
在下面版本的函数中,首先将它的实参强制转换成对const的引用,然后调用了shorterString函数的const版本。const版本返回对const string的引用,这个引用事实上绑定了某个初始的非常量实参上。因此,我们可以再将其转换回一个普通的string&,这显然是安全的。

5、函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定。编译器首先将调用的实参与重载集合中的每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。
当调用重载函数时有三种可能的结果:
①编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
②找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息。
③有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用。

6、重载对作用域的一般性质并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中的声明的同名实体。在不同的作用域中无法重载函数名。
一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
在C++语言中,名字查找发生在类型检查之前。

五、特殊用途语言特性
1、调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。
当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。

2、对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一函数也是合法的。不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。

3、局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:
// wd、def和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen();    // 调用screen(ht(), 80, ' ')
void f2()
{
    def = '*';            // 改变默认实参的值
    sz wd = 100;        // 隐藏了外层定义的wd,但是没有改变默认值
    window = screen();    // 调用screen(ht(), 80, '*')
}

4、把一些规模较小的操作定义成函数有很多好处,主要包括:
①阅读和理解上面的shorterString函数的调用要比读懂等价的条件表达式容易得多。
②使用函数可以确保行为的统一,每次相关操作都能保证按照同样的方式进行。
③如果我们需要修改计算过程,显然修改函数要比先找到等价表达式所有出现的地方再逐一修改更容易。
④函数可以被其他应该重复利用,省去了程序员重新编写的代价。
然而,使用上面的shorterString函数也存在一个潜在的缺点:调用函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

5、内联函数可避免函数调用的开销。将函数指定为内联函数,通常就是将它在每个调用点上"内联地"展开(在编译过程中)。
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联地展开。

6、constexpr函数是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();    // 正确:foo是一个常量表达式
执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。

7、constexpr函数函数不一定返回常量表达式。

8、和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器想要展开函数,仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。

9、C++程序员有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。基本思想是:程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG。

10、assert是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:assert(expr);
首先对expr求值,如果表达式为假(即0),assert函数输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。
assert宏定义在cassert头文件中。预处理名字由预处理器而非编译器管理。

11、assert宏常用于检查"不能发生"的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阀值。此时,程序可以包含一条如下所示的语句:
assert(word.size() > threshold);

12、assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。

13、assert应该仅用于验证那些确实不可能发生的事情。我们可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。

14、除了用于assert外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef和#endif之间的代码;如果定义了NDEBUG,这些代码将被忽略掉:
#ifndef NDEBUG
...
#endif

15、C++编译器为每个函数都定义了_ _func_ _,局部静态变量_ _func_ _输出当前调试函数的名字,它是const char的一个静态数组,用于存放函数的名字。

16、除了C++编译器定义的_ _func_ _之外,预处理器还定义了另外4个对于程序调试很有用的名字:
①_ _FILE_ _:存放文件名的字符串字面值。
②_ _LINE_ _:存放当前行号的整型字面值。
③_ _TIME_ _:存放文件编译时间的字符串字面值。
④_ _DATE_ _:存放文件编译日期的字符串字面值。

六、函数匹配
1、调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
2、为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
(1)精确匹配,包括以下情况:
    ①实参类型和形参类型相同。
    ②实参从数组类型或函数类型转换成对应的指针类型。
    ③向实参添加顶层const或者从实参中删除顶层const。
(2)通过const转换实现的匹配。
(3)通过类型提升实现的匹配。
(4)通过算术类型转换实现的匹配。
(5)通过类类型转换实现的匹配。

3、内置类型的提升和转换可能在函数匹配时产生意想不到的结果。

4、如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。

七、函数指针
1、函数指针指向的是函数而非对象。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
2、使用函数指针时要注意加括号。
bool (*pf)(const string &, const string &);    // pf指向一个函数
bool *pf(const string &, const string &);    // 声明一个名为pf的函数,该函数返回bool*

3、当我们把函数名作为一个值使用时,该函数自动地转换成指针。
pf = lengthCompare;        // pf指向名为lengthCompare的函数
pf = &lengthCompare;    // 等价的赋值语句:取地址符是可选的
此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针:
bool b1 = pf("hello", "goodbye");            // 调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye");        // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye");// 另一个等价的调用

4、在指向不同函数类型的指针间不存在转换规则。

5、当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。

6、直接使用函数指针类型显得冗长而烦琐。类型别名和decltype能让我们简化使用了函数指针的代码。
需要注意的是,decltype返回函数类型,此时不会将函数类型自动转换成指针类型。因此,我们需要显式地加上*以表明我们需要返回指针,而非函数本身。
// Func和Func2是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;        // 等价的类型
// FuncP和FuncP2是指向函数的指针
typedef bool (*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2;    // 等价的类型

7、编译器不会自动地将函数返回类型当成对应的指针类型处理,所以我们必须把返回的类型写成指针形式。

一些术语:
1、二义性调用是一种编译时发生的错误,造成二义性调用的原因是在函数匹配时两个或多个函数提供的匹配一样好,编译器找不到唯一的最佳匹配。
2、最佳匹配:从一组重载函数中为调用选出的一个函数。如果存在最佳匹配,则选出的函数与其他所有可行函数相比,至少在一个实参上是更优的匹配,同时在其他实参的匹配上不会更差。
3、候选函数:解析某次函数调用时考虑的一组函数。候选函数的名字应该与函数调用使用的名字一致,并且在调用点候选函数的声明在作用域之内。
4、constexpr可以返回常量表达式的函数,一个constexpr函数被隐式地声明成内联函数。
5、函数原型:函数的声明,包含函数名字、返回类型和形参类型。要想调用某函数,在调用点之前必须声明该函数的原型。
6、隐藏名字:某个作用域内声明的名字会隐藏掉外层作用域中声明的同名实体。
7、内联函数:请求编译器在可能的情况下载调用点展开函数。内联函数可以避免常见的函数调用开销。
8、链接是一个编译过程,负责把若干对象文件链接起来形成可执行程序。
9、局部静态对象:它的值在函数调用结束后仍然存在。在第一次使用局部静态对象前创建并初始化它,当程序结束时局部静态对象才被销毁。
10、对象代码:编译器将我们的源代码转换成对象代码格式。
11、对象文件:编译器根据给定的源文件生成的保存对象代码的文件。一个或多个对象文件经过链接生成可执行文件。
12、预处理宏:类似于内联函数的一种预处理功能。除了assert之外,现代C++程序很少再使用预处理宏了。
13、递归循环:描述某个递归函数没有终止条件,因为不断调用自身直至耗尽程序栈空间的过程。
14、可行函数是候选函数的子集。可行函数能匹配本次调用,它的形参数量与调用提供的实参数量相等,并且每个实参类型都能转换成相应的形参类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值