6.1 函数基础
- 函数定义:包括返回类型、函数名字和0个或者多个形参组成的列表和函数体。
- 调用运算符:调用运算符的形式是一对圆括号(),作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号内是用逗号隔开的实参列表。
函数的调用完成两项工作:
- 用实参初始化函数对应的形参
- 将控制权转移给被调用函数。(此时,主调函数的执行被暂时中断,被调函数开始执行)
注:
- 形参和实参:形参和实参的个数和类型必须匹配上。尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。
- 返回类型: void表示函数不返回任何值。函数的返回类型不能是 数组类型 或者 函数类型,但可以是指向数组或者函数的指针。
- 生命周期:对象的生命周期是指 程序执行过程中该对象存在的一段时间。
- 局部变量:形参和函数体内部定义的变量统称为局部变量。(它对函数而言是局部的,对函数外部而言是隐藏的)
- 自动对象:只存在于 块执行期间 的对象。当块的执行结束后,它的值就变成未定义的了。
- 局部静态对象:static类型的局部变量,在 程序的执行路径第一次经过对象定义语句时 初始化,直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
size_t count_calls() { static size_t ctr = 0; // 调用结束后,这个值仍然有效(str为局部静态对象) return ++ctr; } int main() { for (size_t i = 0; i != 10; ++i) //(i为局部自动对象) cout << count_calls() << endl; return 0; }
注:
- 局部自动对象未初始化,将产生未定义的值。
- 局部静态对象未初始化,将默认初始化为0。
- 函数声明:函数的声明和定义唯一的区别是声明无需函数体,用一个分号替代。函数声明主要用于描述函数的接口,也称函数原型。(建议函数在头文件中声明,在源文件中定义)
- 分离编译: CC a.cc b.cc直接编译生成可执行文件;CC -c a.cc b.cc编译生成对象代码a.o b.o; CC a.o b.o编译生成可执行文件。
6.2 参数传递
值传递和引用传递:
- 值传递:指实参的值是通过拷贝传递给形参。(函数对形参做的所有操作 都不会影响实参的值)
- 引用传递:形参是引用类型,引用形参是它对应的实参的别名;引用形参直接绑定实参对象,而非对象的副本。(对引用形参的操作 可改变实参的值)
使用引用形参可以用于函数返回额外的信息:一个函数只能返回一个值, 然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。
// 返回s中c第一次出现的位置索引 // 引用形参occurs负责统计c出现的总次数(虽然occur没有return返回,但是因为是引用类型,所以被引用的实参对象会随occurs改变) 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; // 记录c第一次出现的位置 } ++occurs; // 将出现的次数加1 } } return ret; // 出现次数通过occurs隐式地返回 }
建议:
- 使用引用类型的形参代替指针。
- 经常用引用形参来避免不必要的复制。
- 如果无需改变引用形参的值,最好将其声明为常量引用。
const形参和实参:
- 形参的顶层const被忽略。
- 我们可以使用非常量初始化一个底层const对象,但是反过来不行。
- 在函数中,不能改变实参的局部副本。(如果传的是指针,也只是能改指针指向对象,但是指针本身还是改变不了的,除非指针的指针)
- 不要改变对象的情况下,尽量使用常量引用。(既然不要改,也肯定不想之后被意外改,所以设为const;传引用则可直接对引用所指对象操作,而不需要生成一个对象副本)
void fun1(int i) {} void fun2(const int i) {} void fun3(int* i) {} void fun4(int* const i){} void fun5(const int* i) {} int main() { int i1 = 1; const int i2 = 2; fun1(i1); // 顶层const可以忽略(顶层const只是对象本身,不同于对象的引用或者指针,就算对象相互赋值也不会影响对方对象值) fun1(i2); fun2(i1); fun2(i2); fun3(&i1); fun3(&i2); // 实参&i2为 const int*,形参i为 int*(底层const不可忽略。实参i1对象本身不可以改,如果能赋值给i,则i1希望i也不能改i1所指对象,但是这里i是能改的,所以越权了,因此不能传递) fun4(&i1); fun4(&i2); // 实参&i2为 const int*,形参i为 int* const(i是个顶层const,&i2是底层const。要是可以,岂不是指针i可以改变常量i2) fun5(&i1); // 形参i为底层const,说明指向常量对象,i1为普通指针当然可以传递(实参i1本身可以改,说明i1允许形参i可以有改或者不改的权力,这里形参i不能改的,所以没有越权,因此可以传递) fun5(&i2); // 实参&i2为 const int*,形参i也是const int *y(形参i没有改所指对象的权力,实参i2有没有,所以没有越权,因此可以传递) }
数组形参:
当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
(1) 一维数组作为形参时,不需要指定维度的长度(除非是 数组引用形参)
// 尽管形式不同,但这三个print函数是"等价的" (形参都是const int* 类型) void fun(int*); void fun(int[]); void fun(int[5]); // 5表示我们"期望"数组含有5个元素,实际不一定(5是给人看的,不是必须指定的) //形参是数组的引用,此时维度是类型的一部分 (编译器不会把数组实参转换为指针,而是传递数组本身,引用形参绑定到实参数组上; 并且编译器检查实参维度的大小与形参维度的大小是否匹配 ) void fun1(int (&a)[5]); // 维度的长度需指定(数组名可要可不要) int main() { int i = 0; int a[2] = { 0,1 }; int b[5] = { 0,1,2,3,4 }; fun(&i); // 正确: &i的类型是int* fun(a); // 正确: a转换成int*并指向a[0](这里可看出传递一维数组,并不需数组大小相同) fun1(&i); // 错误:实参不是含有5个整數的数组 fun1(a); // 错误:实参不是含有5个整数的数组 fun1(b); // 正确:实参是含有5个整数的数组 }
void fun(int a[][5]); // 一定要给出第二个维度 (并且编译器检查 实参第二个维度的大小与形参第二个维度的大小 是否匹配) void fun1(int(*a)[5]); // "一维数组指针"作为形参 int main() { int a[2][3] = { {0,1,2},{3,4,5} }; int b[2][5] = { {0,1,2,3,4} ,{5,6,7,8,9} }; fun(a); // 错误:第二个维度不匹配 fun(b); // 正确 fun1(a); // 错误:第二个维度不匹配 fun1(b); // 正确 }
二维数组与一维数组之间的联系:
- 传递一维数组可看成传递数组名,即第一个元素指针(类型为 int*)
- 二维数组是数组的数组,每个元素都是一维数组。传递二维数组也可看成传递二维数组名,即第一个元素(一维数组)的指针,也就是指向一维数组的指针(如 int(*)[5])。
main处理命令行选项:
格式:int main(int argc, char *argv[]){...}解释:第一个形参代表参数的个数;第二个形参是参数C风格字符串数组。
注:当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;因为argv[0]保存程序的名字,而非用户输入。
含有可变形参的函数:
如果函数的 实参数量未知 但是全部实参的类型相同,可以使用initializer_ list 类型的形参。(C++11)
initializer_list提供的操作:
操作 | 解释 |
initializer_listlst; | 默认初始化;T类型元素的空列表 |
initializer_listlst{a,b,c...}; | lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const |
lst2(lst) | 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素 |
lst2 = lst | 同上 |
lst.size() | 列表中的元素数量 |
lst.begin() | 返回指向lst中首元素的指针 |
lst.end() | 返回指向lst中尾元素下一位置的指针 |
使用示例:
void fun(std::initializer_list<string> il) { for (auto i = il.begin(); i != il.end(); i++) { std::cout << *i << std::endl; } } int main() { std::initializer_list<string> il{ "123", "456" }; fun(il); // 正确 fun({ "abc","efd","hij" }); // 正确 fun({ "abc",1,2 }); // 错误:类型不一致 }
省略符形参:是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。
省略符形参只能出现在形参列表的最后一个位置,它有两种形式:
void foo(parm_1ist, ...); // 指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查, // 而省略符形参所对应的实参 无须类型检查。(形参声明后面的逗号是可选的) void foo(...);
建议:省略符形参应该仅仅用于C和C++通用的类型。(特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝)
6.3 返回类型和return语句
无返回值函数:
没有返回值的 return语句只能用在返回类型是 void的函数中,返回 void的函数不要求非得有 return语句。有返回值函数:
- 主函数main的返回值:如果结尾没有return,编译器将隐式地插入一条返回0的return语句。返回0代表执行成功。
- 值的返回:返回的值用于 初始化调用点的一个临时量,该临时量就是 函数调用的结果。
- 引用返回左值:函数的返回类型 决定函数调用是否是左值。调用一个返回引用的函数得到左值;其他返回类型得到右值。
- 列表初始化返回值:函数可以返回花括号包围的值的列表。(C++11)
vectorfun() { return { 1,2,3 }; } string fun1() { return { 'a','b','c' }; } int main() { vectorvec = fun(); // vec={1,2,3} string str = fun1(); // str="abc" }
- 不要返回局部对象的引用或指针。(当函数结束时局部对象占用的空间也就随之释放掉了,所以return语句指向了不再可用的内存空间)
- return语句的返回值的类型必须和函数的返回类型相同,或者能够隐式地转换成函数的返回类型。
int* fun() { int a = 1; return &a; } int& fun1() { int a = 1; int& t = a; return t; } int fun2() { double a = 3.14; return a; } int main() { int* p = fun(); // 错误:*p=1374389535 (函数内局部变量a被释放,p指向一个不可用的空间) int& b = fun1(); // 错误:b=1374389535 (同理,b引用的对象a被释放掉了) int c = fun2(); // 正确:c=3 (double可转换为int) std::cout << *p << " " << b << " " << c; }
返回数组指针:
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。
typedef int arrT[10]; // arrT 是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int[10]; // arrT 的等价声明
arrT* fun(); // fun()返回一个 指向含有10个整数的数组 的指针
int arr[10]; int(*p)[10] = &arr; // p是一个指向 含有10个整数的数组 的指针 int(*fun())[10]; // fun()函数返回一个 指向含有10个整数的数组 的指针(对照上面一句比较理解)
(3) 使用尾置返回类型
auto fun()->int(*)[10]; // auto 开头, 形参列表之后是->,紧接着就是函数返回类型(即指向 含有10个整数的数组 的指针)
(4)使用decltype
int arr[10]; decltype(a) *fun(); // 返回一个指向 含有10个整数的数组 的指针(因为是返回的是指针,别忘了(*)!decltype返回一个 含有10个整数的数组类型,所以还要加上(*))
6.4 函数重载
- 重载:如果同一作用域内几个函数名字相同但形参列表不同,我们称之为重载(overload)函数。(main函数不能重载)
- 重载和const形参:一个有顶层const的形参和没有顶层const的形参无法区分。相反,是否有某个底层const形参可以区分。
- 重载和作用域:若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。
//重载 int fun(int a); int fun(double a); // 正确: 参数类型不同 int fun(int a, int b); // 正确: 参数个数不同 float fun(int a); // 错误: 和第一个函数相比,只有返回值类型不同,不构成重载 //重载和const形参 int fun(const int a); int fun(int a); // 错误: 和上面函数相比,没有const的形参和有 顶层const 的形参无法区分,不构成重载 int fun(const int* a); int fun(int* a); // 正确: 和上面函数相比,没有const的形参和有 底层const 的形参可以区分 //重载和作用域 string read(); void print(const string&); void print(double); // 重载print函数 void fooBar(int ival) { bool read = false; // 新作用域:隐藏了外层的read(一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体) string s = read(); // 错误: read 是一个布尔值,而非函数 void print(int); // 新作用城:隐藏了外层的print(一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体) print("Value: "); // 错误: print(const string &)被隐藏掉了 print(ival); // 正确:当前print(int)可见 print(3.14); // 正确:但是print(double)被隐藏掉了,调用的是print(int)。(因为double可以隐式转为int类型,所以可以调用) }
注:在C++语言中,名字查找发生在类型检查之前。
6.5 特殊用途语言类型
默认实参:
默认实参作为形参的初始值出现在形参列表中,我们可以为一个或多个形参定义默认值。(注意:一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值)
使用默认实参调用函数:
void fun(int a = 1, int b = 2, string c = "c") { std::cout << a << " " << b << " " << c << std::endl;
}
int main() {
fun(); // 正确: a=1,b=2,c="c"(abc都为默认值)
fun(4); // 正确: a=4,b=2,c="c"(bc为默认值)
fun(4, 4); // 正确: a=4,b=4,c="c"(c为默认值)
fun(4, 4, "d"); // 正确: a=4,b=4,c="d"
fun("d"); // 错误: 只能省略尾部的实参(实参必须按形参顺序从左到右给,因此可以省略尾部的实参)
}
默认实参声明:
void fun(int a, int b, int c = 3) { std::cout << a << " " << b << " " << c << std::endl;
}
void fun(int a, int b, int c = 4); // 错误: 重复定义默认参数(在"给定的作用域"中一个形参只能被赋予一次默认实参。如果fun函数定义到另一个源文件中则可以生效,因为此时没有重复定义)
void fun(int a , int b = 2, int c); // 正确:添加默认实参(c本来就是默认实参,这是同一个函数,所以没有违反 默认实参都在右边的法则)
void fun(int a = 4, int b , int c); // 正确:添加默认实参
int main() {
fun(); // a=4,b=2,c=3
}
int i = 1, j = 2;
double k = 3;
void fun(int a = i, int b = j, int c = k) { // 注:(1)局部变量不能作为默认实参!(比如fun定义在main后面,前面的声明在main里面,这时绝对不能用main中定义的局部变量来作为默认实参的初始值) std::cout << a << " " << b << " " << c << std::endl;
}
int main() {
fun(); // a=1,b=2,c=3
i = 10; // 改变默认实参的值(a=10)
int j = 10; // 内层定义的j 隐藏了外层定义的j,但是没有改变外层定义的的j,也就没有改变默认值(这个两个j定义作用域不同,属于俩个不同的变量)
fun(); // a=10,b=2,c=3
}
注:用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时。(所以要注意默认实参的变化,如果改变之后,可能会产生错误的结果)
内联函数和constexpr函数:
内联函数:
- 内联函数:函数返回类型前 加上关键字inline。
- 普通函数的优点:行为同一,重复利用,易于维护。
- 普通函数的缺点:调用函数 比求解等价表达式要慢得多。
- inline函数既有普通函数的优点,又没有函数调用开销的缺点。(可以避免函数调用的开销,可以让编译器在编译时内联地展开该函数)
注:内联说明 只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
constexpr函数:
- constexpr函数:指能用于常量表达式的函数。(函数的返回类型及所有形参类型 都要是 字面值类型,而且函数体中 必须有且只有一条 retrun语句)
constexpr int fun() { return 2; } constexpr int m = fun(); // 执行该初始化任务时,编译器把对constexpr函数的调用 替换成其结果值。为了能在编译过程中随时展开,constexpr函数被"隐式地指定为内联函数"。 constexpr int fun1(int a) { // 如果a是常量表达式则,则fun1(a)也是常量表达式 return fun() * a; } int main() { int arr[fun()]; // 正确:fun()是常量表达式 int i = 1; int arr1[fun1(i)]; // 错误:i不是常量表达式,所以fun1(i)返回值 不是常量表达式,(constexpr函数不一定返回常量表达式) }
注:我们要把内联函数和constexpr函数定义在头文件中。(因为内联函数是内部链接的,如果你在b.cpp中定义这个函数,那么在a.cpp中即使有这个函数声明,但由于内联函数是内部链接的,所以b.cpp不会提供其定义。所以在链接时a.obj无法找到这个函数的定义,便会出现无法解析的外部符号的错误)
constexpr与const的本质区别:
- const并不能代表“常量”,它仅仅是对变量的一个修饰,告诉编译器这个变量只能被初始化,且不能被”直接修改“。而且这个变量的值,可以在运行时也可以在编译时指定。
- constexpr可以用来修饰变量、函数、构造函数。一旦以上任何元素被constexpr修饰,那么等于说是告诉编译器 “请大胆地将我看成编译时就能得出常量值的表达式去优化我”。
- 与const相比,被constexpr修饰的对象则强制要求其 初始化表达式能够在 编译期 完成计算。之后所有引用该常量对象的地方,若非必要,一律用计算出来的常量值替换。
调试帮助:
assert预处理宏:assert(expr);作用:首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。
开/关 调试状态:CC -D NDEBUG main.c 可以变量NDEBUG。(如果定义NDEBUG,则assert什么也不做)
注:定义NDEBUG能避免检查各种条件所需的运行时开销。(除了用于assert外,也可以使用NDEBUG编写自己的条件调试代码)
建议:因此, assert应该仅用于验证那些确实不可能发生的事情。我们可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程.序本身应该包含的错误检查。
编译器为每个函数定义了:
- _ _func_ _ :用于存放函数名字的字符串字面值。
预处理器为程序定了:
- _ _FILE_ _ :存放文件名的字符串字面值。
- _ _LINE_ _ :存放当前行号的整型字面值。
- _ _TIME_ _ :存放文件编译时间的字符串字面值。
- _ _DATE_ _ :存放文件编译日期的字符串字面值。
注:这些常量用来 在错误消息中 提供更多信息。
6.6 函数匹配
- 候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数。(与被调用函数同名,且其声明在调用点可见)
- 可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数。(参数数量相同,参数类型 相同或者可以转换)
- 寻找最佳匹配:基本思想:实参类型和形参类型越接近,它们匹配地越好。(参数类型相同最好,或者找出匹配结果最好的)
为了确定最佳匹配,编译器将 实参类型到形参类型的转换 划分成几个等级,具体排序如下所示:
注:如果有且只有一个函数满足下列条件,则匹配成功。(否则编译器会报 二义性调用 的错误提示)- 精确匹配:(1)实参类型和形参类型相同。(2)实参从数组类型或函数类型转换成对应的指针类型。(3)向实参添加项层const或者从实参中删除顶层const。
- 通过const转换实现的匹配。
- 通过类型提升实现的匹配。
- 通过算术类型转换或指针转换实现的匹配。
- 通过类类型转换实现的匹配。
- 该函数 每个实参的匹配都不劣于 其他可行函数需要的匹配。
- 至少有一个实参的匹配优于 其他可行函数提供的匹配。
经验:调用重载函数时应 尽量避免强制类型转换。如果在实际应用中 确实需要强制类型转换,则说明我们设计的 形参集合 不合理。
6.7 函数指针
函数指针:是指向函数的指针。int fun(int a, int b) { return a + b; } int fun(double a, double b) { return a + b; } double fun1(double a, double b) { return a + b; } int main() { int (*pf)(int, int); // pf是一个指向 有两个int参数且返回值为int类型的函数 的指针(注: pf两端括号必不可少,不然pf变成一个返回值为int*类型的函数) pf = fun; // pf指向fun函数(当我们把函数名作为一个值使用时,该函数自动地转换成指针) pf = &fun; // 与上面赋值语句等价: 取地址符是可选的 int a = pf(1, 1); // 调用fun函数,a=2(同理,当我们把函数名作为一个值使用时,该函数自动地转换成指针) int b = (*pf)(1, 1); // 等价调用,a=2 pf = 0; // 正确,pf不指向任何函数 pf = fun1; // 错误: 返回类型不匹配(在指向不同函数类型的指针间不存在转换规则,必须精确匹配!) pf = fun; // 正确: 和fun(int,int)精确匹配(形参类型,形参个数,返回值类型 都要精确匹配) }
注:
- 当我们把函数名作为一个值使用时,该函数自动地转换成指针
- 在指向不同函数类型的指针间不存在转换规则,必须精确匹配!
函数指针形参:形参为函数指针。(虽然不能定义函数类型形参,但是形参可以是指向函数的指针)
int fun(int a, int b); int fun1(int a, int b, int pf(int, int)); // 第三个形参是函数类型,但它会自动转换成指向函数的指针 int fun1(int a, int b, int(*pf)(int, int)); // 等价声明: 显式地将形参定义成 指向函数的指针 // 使用类型别名或者decltype typedef int func(int a, int b); typedef decltype(fun) func1; // func和fun1是相同的 函数类型 typedef int (*func_p)(int a, int b); typedef decltype(fun)* func1_p; // func_p和func1_p是相同的 函数指针类型 int main() { fun1(1, 1, fun); // 自动将函数fun转换成指向该函数的指针(可以直接把函数作为实参使用,此时它会自动转换成指针) func f; // f为 fun函数类型 func_p f_p; // f_p为 fun函数指针类型 fun1(1, 1, f); // 正确 fun1(1, 1, f_p); // 正确 }
返回指向函数的指针:函数返回值为函数类型指针。(虽然不能返回一个函数,但是能返回指向函数类型的指针)
int fun1(int, int) {} int (*fun(int))(int, int) { } // 定义 using func = int(int, int); // func为 函数类型 using func_p = int(*)(int, int); // func_p为 函数指针类型 // 使用 类型别名、直接定义、尾置返回类型、decltype 四种方式声明返回函数指针的函数 func_p fun(int); int (*fun(int))(int, int); auto fun(int) ->int (*)(int, int); decltype(fun1)* fun(int); // (*)别忘记,decltype作用于函数时返回的是函数类型 func fun(int); // 错误: func是函数类型,fun不能返回一个函数(必须把返回值写成函数指针形式,编译器不会自动地将 函数返回类型当成对应指针类型处理) func_p fun(int); // 正确: func_p是指向函数的指针,fun返回指向函数的指针,并且类型匹配 func* fun(int); // 正确: 显示地 指定返回类型 是指向函数的指针