-
通过调用运算符
()
调用函数 -
函数的调用完成两项工作:
- 用实参初始化函数对应的形参
- 将控制权转移给被调用函数:主调函数的执行被暂时中断,被调函数开始执行
-
尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值
-
任意两个形参都不能同名,形式参数名是可选的,但是由于我们无法使用未命名的形式参数,所以形式参数一般都应该有一个名字
-
函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针
-
函数体必须是大括号包围的!
-
普通局部变量对应的对象是自动对象:当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。
-
局部静态对象:在程序的执行路径第一次经过对象定义时对它初始化,并且直到程序终止时才被销毁,在此期间即使对象所在的函数执行结束也不会对他有影响。如果局部静态变量没有显式的初始值,则执行值初始化(内置类型会初始化为0)
-
函数只能定义一次,但是可以声名多次,唯一的区别是函数的声明不需要函数体,用一个分号替代即可(因此经常省略形式参数的名字,但是写上名字也有利于理解函数的功能),函数声明也称作函数原型
-
形式参数初始化的机理和变量初始化一样
-
拷贝大的类类型对象或者容器对象比较低效,甚至有的类型(包括IO类型在内)根本不支持拷贝操作。因此函数只能通过引用形式参数访问该类型的对象,如果函数无须改变引用形式参数的值,最好将其声明为常量引用
-
熟悉C的程序员常常使用指针类型的形参访问函数外部的对象,在C++中最好还是使用引用类型的形式参数代替指针
-
对于有可能是临时参数的形式参数,我们不应该使用引用(因为无法引用到常量上)
-
函数重载要求同名函数的形式参数列表应该有明显的区别,因此如果仅仅是
const
的不同则不能进行重载(应该要求形式参数类型不同) -
形式参数的初始化方式和变量的初始化方式是一样的:我们可以使用非常量初始化一个底层
const
对象,但是反过来不行,同时一个普通的引用必须使用同类型的对象初始化(详细同第二章指针和引用部分) -
尽量使用常量引用
- 给函数调用者传递正确的信息
- 使用非常量引用会极大地限制函数所能接受的实际参数类型:我们不能把
const
对象、字面值对象或者需要类型转换的对象传递给普通的引用参数
-
数组形式参数
-
数组的两个特点:
- 不允许拷贝数组
- 使用数组时通常会将其转换成指针
-
以下三种声明方式是等价的:
void print(const int*); void print(const int[]); void print(const int[10]);
-
因为我们不清楚数组的实际大小,因此在使用过程中必须通过一定的方式判断是否越界
-
使用标记指定数组长度:例如C风格的字符串,最后一个一定是一个
\0
,我们可以判断是否为\0
来判断是否到达末尾 -
使用标准库规范:
void print(const int *beg, const int *end) {while(beg != end) {cout << *beg++ << endl;} } int arr[] = {0, 1, 2}; print(begin(arr), end(arr)); //#include<iterator>
-
显式传递一个表示数组大小的形式参数
-
同常量引用,当函数不需要对数组元素执行读写操作的时候,数组形式参数应该是指向
const
的指针
-
-
数组引用参数
void print(int (&arr)[10]) {for (auto item : arr) {cout << item << endl;} }
对于数组的引用详细可以看第三章关于数组部分的笔记
-
传递多维数组:C++语言中实际上没有真正的多维数组,所谓的多维数组其实是数组的数组。数组第二维(以及后面所有的维度)的大小都是数组类型的一部分,不能省略
void print(int (*matrix)[10], int rowSize); void print(int matrix[][10], int rowSize);
上面两种声明是完全等价的
-
-
命令行选项可以通过两个(可选的)形式参数传递给
main
函数:int main(int argc, char *argv[]); int main(int argc, char **argv);
- 第二个形式参数是一个数组,它的元素指向C风格字符串的指针,第一个形式参数
argc
表示数组中字符串的数量 argv
的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的参数,最后一个指针的元素值保证为0
- 第二个形式参数是一个数组,它的元素指向C风格字符串的指针,第一个形式参数
-
为了编写能处理不同数量实际参数的函数,C++11标准提供了几种方法:
-
如果函数的实际参数数量未知但是类型相同,我们可以使用
initializer_list
类型的形式参数(需要#include<initializer_list
)initializer_list<T> lst; //默认初始化,T类型元素的空列表 initializer_list<T> lst{a,b,c...}; //lst的元素是对应初始值的副本,列表中的元素是const lst2(lst); //等价与lst2 = lst ,赋值,不会拷贝列表中的元素,原始列表和副本共享元素 lst.size() //列表中的元素数量 lst.begin() lst.end()
initialzer_list
对象中的元素永远是常量值void err_msg(initializer_list<string> il) {for (auto beg = il.begin(); beg != il.end(); ++beg)cout << *beg << " "; //也可以通过范围for循环访问65cout << endl; } err_msg({"A", "B", "C"}); err_msg({"A", "B"});
-
使用可变参数模板
-
-
返回
void
的函数不要求非得有return
语句,因为在这类函数的最后一句会隐式地执行return
,一个返回类型是void
的函数也能使用return expression
,不过此时return
语句的expression
必须是另一个返回void
的函数,强行令void
函数返回其他类型将产生编译错误 -
有返回值函数
bool str_subrange(const string &str1, const string &str2) {auto size = (str1.size() < str2.size()) ? str1.size() : str2.size();for (decltype(size) i = 0; i < size; ++i) {if (str1[i] != str2[i])return false;}return true; }
-
返回一个值的方式和初始化一个变量或形式参数的方式完全一样:返回的值用于初始化调用点的一个临时量,这个临时量就是函数调用的结果
-
不要返回局部对象的引用或者指针:函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用(或指针)将指向不再有效的内存区域
-
调用一个返回引用的函数得到左值,其他返回类型得到右值
-
C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化
-
我们允许
main
函数没有return
语句直接结束,如果控制到达了main
函数的结尾处而且没有return
语句,编译器将隐式地插入一条返回0的return
语句。在cstdlib
头文件中定义了两个预处理变量,我们用这两个变量分别表示成功与失败int main() {if (some_failure) {return EXIT_FAILURE; } else {return EXIT_SUCCESS;} }
-
从语法上来说,想要定义一个返回数组的指针或引用的函数比较繁琐,但是使用类型别名可以简化这一任务
typedef int arrT[10]; //using arrT = int[10]; arrT* func(int i); //返回一个指向含有10个整数的数组的指针 int (*func(int i))[10]; //等价于上面的声明
我们还可以使用尾置返回类型使得上面的声明变得清晰:
auto func(int i) -> int(*)[10];
如果我们知道函数返回的指针指向哪个(类别)的数组,我们还可以使用
decltype
关键字声明返回类型int arr[] = {0, 1, 2, 3, 4}; decltype(arr) *arrPtr(int i) {return &arr; }
-
如果同一作用域内的几个函数名字相同但是形式参数列表(形式参数数量或形式参数类型)不同,我们称之为重载函数。
main
函数不能重载。需要注意的是,函数的重载和返回类型关系不大 -
顶层
const
不影响传入函数的对象,一个拥有顶层const
的形式参数无法和另一个没有顶层const
的形式参数区分开来。但是底层const
是会影响函数的重载的,当传入的对象是常量时,会选择带有底层cosnt
的函数版本,如果传递一个非常量对象,编译器会优先选用非常量版本的函数 -
最好只重载那些确实非常相似的操作
-
我们也可以使用
const_cast
实现const
到非const
的转换:const string &func(const string &s1, const string &s2) {return s1.size() < s2.size() ? s1 : s2; } string &func(string &s1, string &s2) {return const_cast<string&>(func(const_cast<const string&>(s1), const_cast<const string&)(s2)); }
-
当调用重载函数时的结果:
- 编译器找到一个与实际参数最佳匹配的函数
- 找不到任何一个函数匹配,发出无匹配的错误
- 有多于一个函数可以匹配,但是没一个都不是明显的最佳选择,此时也将发生错误,称为二义性调用
-
如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名
-
一旦某个形式参数被赋予了默认值,它后面所有形式参数都必须有默认值。当设计含有默认实际参数的函数时,其中一项任务是合理设置形式参数顺序,尽量让不怎么使用默认值的形式参数出现在前面
-
通常,应该在函数声明中指定默认实际参数,并将声明放在合适的头文件中。局部变量不能作为默认实际参数
-
用作默认实际参数的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时。比如函数
A
某个默认实际参数的值是一个函数B
调用的返回值,则该函数调用B
会在A
被调用的时候调用#include <iostream>using namespace std;string A = "global A"; string B = "global B";const string &func() {return const_cast<const string&>(B); }int main() {ios::sync_with_stdio(false);void test(const string &a = A, const string &b = func());string A = "local A"; //local varibale cannot be default value::A += " has been changed";B = "local B";test();return 0; }void test(const string &a, const string &b) {cout << a << endl;cout << b << endl; }
运行结果:
global A has been changed local B
-
将一些简单但需要多次重复的函数定义为内联函数的好处:
- 有利于阅读理解
- 可以被重复利用,使得代码简洁
- 需要修改时只用修改一个地方
-
在函数前面加上
inline
便可以将函数生命为内联函数。内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数 -
constexpr
函数是指能够用于常量表达式的函数- 函数的返回类型以及所有形式参数的类型都必须是字面值类型,而且函数体中必须有且只有一条
return
语句 constexpr
函数被隐式地指定为内联函数- 允许
constexpr
函数的返回值不是一个常量,如果参数非常量表达式导致最后返回值不是常量表达式则在需要常量的地方调用会报错
- 函数的返回类型以及所有形式参数的类型都必须是字面值类型,而且函数体中必须有且只有一条
-
内联(
inline
)函数和constexpr
函数可以在程序中多次定义,但是多个定义必须一致。基于这个原因,内联函数和constexpr
函数通常定义在头文件中 -
程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码
assert (expr)
预处理宏:首先对expr
求值,如果表达式为假(0),assert
输出信息并终止程序的执行,如果为真,则什么也不做- 需要头文件
cassert
,因为是供预处理器处理,所以无需提供using
声明 assert
宏常用于检查“不能发生”的条件,即程序的运行是建立在assert
的条件成立的情况下
- 需要头文件
assert
的行为依赖于一个名为NDEBUG
的预处理变量的状态,如果定义了NDEBUG
,则assert
什么也不做。默认情况下没有定义NDEBUG
,此时assert
将执行检查。如果想要关闭assert
检查:- 在程序开头加上
#define NDEBUG
- 或在编译的时候加上
-D NDEBUG
参数
- 在程序开头加上
assert(word.size() >= threshold); //等价写法: #ifndef NDEBUG 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; #endif
-
函数匹配
- 选定候选函数:
- 与被调用函数同名
- 其声明在调用点可见
- 选定可行函数:
- 形式参数和实际参数数量一直
- 类型符合(相同或可以进行转换)
- 寻找最佳匹配
- 该函数的每个实际参数的匹配不劣于其他可行函数需要的匹配
- 至少有一个实际参数的匹配优于其他可行函数提供的匹配
- 如果最终确定了一个函数,则匹配成功,如果最后匹配出多个函数,则匹配失败,报告二义性错误
- 选定候选函数:
-
为了确定最佳匹配,编译器将实际参数类型到形式参数类型的转换分成了几个等级:
- 精确匹配
- 实际参数类型和形式参数类型相同
- 实际参数从数组类型或函数类型转换成相应的指针类型
- 向实际参数添加或者删除顶层
const
- 通过
const
转换实现的匹配 - 通过类型提升实现的匹配(小整数类型会自动变成
int
,如果放不下再变成unsigned int
) - 通过算数类型转换或指针转换实现的匹配
- 通过类类型转换实现的匹配
- 精确匹配
-
想要声明一个指向函数的指针,只需要用指针替换函数名即可:
bool func(const string &, const string &); bool (*pf)(const string &, const string &);
当我们把函数名作为一个值使用时,该函数自动地转换成指针
pf = func; //等价于 pf = &func; //&是可选的
我们可以直接使用指向函数的指针调用该函数,无需提前解引用指针
//等价的三种调用方法 bool b1 = pf("A", "B"); bool b2 = (*pf)("A", "B"); bool b3 = func("A", "B");
-
在指向不同函数类型的指针之间不存在转换规则,但是我们可以为函数指针赋一个
nullptr
或者0 -
当我们使用重载函数为指针赋值时,上下文必须清晰地界定到底应该选用哪个函数
-
我们可以定义函数指针作为形式参数
void work(bool pf(const string &, const string &)); //看起来是函数类型,实际上会自动转换成指针 //等价于 void work(bool (*pf)(const string &, const string &));
我们同样可以使用
typedef
和decltype
简化操作typedef bool funcT(const string &, const string &); //funcT是函数类型 typedef decltype(func) funcT2; //同上 typedef bool (*funcTP)(const string &, const string &); //funcTP是函数指针 typedef decltype(func) *funcTP2; //同上 void work(funcT); //同之前定义,函数类型会自动转换成指针类型 void work(funcTP); //同之前定义
-
编译器不会自动地将函数返回类型当成对应的指针类型进行处理
using F = int(int *, int); using FP = int(*)(int *, int); //以下四种方式是等价的 FP f1(int); F *f1(int); int (*f1(int))(int *, int); auto f1(int) -> int (*)(int *, int);
如果使用
decltype
指定返回函数指针类型记得decltype(func)
如果func
是一个函数则得到的是函数类型,还需要加上*
decltype(func) *getFunc(const string &);