函数
函数基础
- 函数是一个命名了的代码块,我们通过调用函数执行相应的代码,函数可以有0个活多个参数,通常会产生一个结果,可以重载函数,也就是说,同一个名字可以对应多个不同的函数
- 一个典型的函数(function)定义包括以下部分
- [x] 返回类型(return type)
- [x] 函数名字
- [x] 0个或多个参数(parameter)组成的列表
- [x] 函数体
- [x] 用实参初始化函数相对应的参数
- [x] 将控制权转移给被调用函数
- 主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行
- return语句完成两项工作:
- [x] 返回return语句中的值
- [x] 将控制权从被调函数转移回主调函数
void f1(){/*......*/} //隐式地定义空形参列表
void f2(void){/*......*/} //显式地定义空形参列表
- 形参列表中的每一个形参都是含有一个声明符的声明,必须写出类型
- 任意两个形参不能同名
局部对象
- [x] 名字的作用域是程序文本的一部分,名字在其中可见
- [x] 对象 的生命周期是程序执行过程中该对象存在的一段时间
- [x] 对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它,只存在于块执行期间的对象被称为`自动对象`
- [x] 形参是一种自动对象,函数开始时为形参申请存储控件,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁
- [x] 当有必要令局部变量的生命周期贯穿函数调用之后的时间时,可以将局部变量定义成`static`类型从而获得这样的对象
- [x] 如果局部静态变量没有显式的初始值,它将执行初始化,内置类型的局部静态变量初始化为0
- [x] 局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响
size_t count_calls(){
static size_t ctr = 0;
return ++ctr;
}
int main(){
for(size_t i = 0 ; i != 10 ; ++i)
{
cout<
函数声明
- 函数可以只声明不定义
- 函数的声明和函数的定义类似,唯一的区别是函数声明无需函数体,用一个分号替代
- 函数的三要素:
返回类型
函数名
形参类型
- 函数声明也称作
函数原型
- 函数的声明以及变量的声明都放在头文件
- 含有函数声明的头文件应该被包含到定义函数的源文件中
参数传递
- 每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化
- 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象,我们这样的实参被
值传递
或者函数被传值调用
传值参数
- 当初始化一个非引用类型的变量时,初始值被拷贝给变量,此时,对变量的改动不会影响初始值
- 熟悉C的程序猿常常使用指针类型的形参访问函数外部的对象,在C++语言中,建议使用引用类型的形参替代指针
- 使用引用形参避免拷贝
- [x] 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作,当某种类型不支持拷贝操作时,函数只能使用引用形参访问该类型的对象
- 如果函数无须改变引用形参的值,最好将其声明为常量引用
const形参和实参
- 当形参有顶层const时,传给它常量对象或者非常量对象都是可以的
数组形参
- 和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界
- 数组以指针的形式传递给函数,所以一开始函数并不知道数组的确切尺寸,调用这应该为此提供一些额外的信息以管理,如以下三种:
- [x] 使用标记指定数组的长度
- 数组本身包含一个结束标记,使用这个方法的典型示例是C风格字符串,
- C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符,函数在处理C风格字符串时遇到空字符停止
void print(const char * cp)
{
if(cp)
{
while(*cp)
cout<<*cp++;
}
}
- [x] 使用标准库规范
- 传递数组首元素和尾后元素的指针
- 通过`begin()` && `end()`函数可以获取数组的首地址以及尾后地址
- [x] 显式传递一个表示数组大小的形参
- 专门定义一个表示数组大小的形参,在C程序和过去的C++程序中经常使用这样的方法
void print(const char * cp , size_t size)
{
for(size_t i = 0 ; i < size ; ++i)
{
...
}
}
- [x] 当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针,只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针
- [x] 形参可以是数组的引用,引用形参绑定到对应的实参上,也就是绑定在数组上
void print(int (&arr)[10])
{
....
}
- [x] C++语言中实际上没有真正的多维数组,所谓多维数组实际上都是数组的数组
int * matrix[10]; \\10个int指针元素构成的数组
int (*matrix)[10]; \\指向10个int元素的数组的指针
main:处理命令行选项
int main(int argc, char * argv[])
{
......
}
- 通过上述main函数的声明方式可以给main函数传递参数
- 第一个形参argc表示数组中字符串的数量,第二个形参argv是一个数组,它的元素是C风格的字符串的指针
int main(int argc, char ** argv)
{
......
}
- 当实参传给main函数之后,
argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参,最后一个指针之后的元素值保证为0
当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户的输入
含有可变形参的函数
- 有时我们无法提前预知应该向函数传递几个实参,例如,我们想要编写代码输出程序产生的错误信息,此时最好用同一个函数实现该功能
- 为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要方法
- [x] 如果所有实参类型相同,可以传递一个名为`initializer_list`的标准库类型
- [x] 如果实参类型不同,可以编写一种特殊的函数,也就是所谓的可变参模板函数
- [x] C++有一种特殊符号`省略符`,可以用它传递可变数量的实参
initializer_list形参
- 如果函数的实参数量未知但是全部实参类型都相同,可以使用initializer_list类型形参
- 与vector不同,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中的元素值
void error_msg(initializer_list il)
{
for(auto beg = il.begin() ; beg != il.end() ; ++beg)
{
......
}
}
- 如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一堆花括号内
error_msg({"functionx","expected","actual"});
省略符形参
- 省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能,通常,省略符形参不应有于其他目的
- 省略符形参应该仅仅用于C和C++通用的类型,特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝
- 省略符形参只能出现在形参列表的最后一个位置
void foo(int n,...);
void foo(...);
返回类型和return语句
无返回值函数
- 无返回值的return语句只能用在返回类型是void的函数中
- 返回void的函数不要求非得有return语句,因为这类函数的最后一句后面会隐式的执行return
有返回值的函数
- 只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值,return语句的返回值的 类型必须与函数的返回类型相同,或者能隐式的转换成函数的返回类型
- 在含有return语句的循环后面也应该有一条return语句,如果没有的话程序就是错误的,而编译器无法发现此类错误
- 不要返回局部对象的引用或指针
- [x] 函数完成后,它所占用的空间也随之被释放掉,因此,函数终止意味着局部变量的引用将指向不再有效的内存区域
auto sz = shorterString(s1,s2).size();
- [x] C++11新标准规定,函数可以返回花括号包围的值的列表
vector process()
{
....
....
return {"aaaa","bbbb","cccc"};
}
- [x] 我们允许main函数没有return语句直接结束,编译器会隐式的插入一条return语句
- [x] main函数的返回值可以看作是状态指示器,`返回0表示执行成功,返回其他值表示执行失败`,其中非0值的具体含义依机器而定
- [x] 为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量
int main()
{
if(some_failure)
return EXIT_FAILURE;
else
return EXIT_SUCCESS;
}
- [x] 如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都成该函数为递归函数
int factorial(int val)
{
if(val > 1)
{
return factorial(val-1)*val
}
return 1;
}
- [x] 在递归函数中,一定有某条路径是不包含递归调用的
返回数组指针
- 因为数组不能被拷贝,所以函数不能返回数组,不过,函数可以返回数组的指针或引用
- 使用尾置返回类型
auto func(int i)->int(*)[10]{......}
- 将函数返回类型放在形参列表后,可以清楚的看到func函数返回的是一个指针,指向了含有10个int元素的数组
- 任何函数的定义都能使用尾置返回
函数重载
- 如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为
重载函数
- main函数不能重载
重载与作用域
- 不要在局部作用域中声明函数(即不要在函数中声明函数)
- 在C++语言中,名字查找发生在类型检查之前
特殊用途语言特性
默认实参
- 某些函数有这样一种形参,在函数的很多次调用中被赋予一个相同的值,我们将这个反复出现的值成为函数的默认实参
void screen(int = 10 , string s = "hehe");
通常应该在函数声明中指定默认实参,并将该声明放在合适的头文件中
- 默认变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需要的类型,该表达式就能作为默认实参
内联函数和constexpr函数
- 内联函数可以避免函数调用的开销
- 内联函数将在编译过程中展开,以消除函数运行时的开销
- 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
- 一般来说,内敛机制用于优化规模较小,频繁调用的函数,很多编译器都不支持内联递归的函数,而且一个75行的函数也不可能在调用点内联的展开
调试帮助
assert(expr);
- 首先expr求值,如果表达式为假(0),assert输出信息并终止程序的执行,如果为真(非0),assert什么也不做
- assert宏定义在cassert头文件中
- 含有cassert头文件的程序不能再定义名为assert的变量、函数或其他实体,实际编程中,即使我们没有包含cassert头文件,最好也不要为了其他目的使用assert
- FUNC存放当前调试函数的名字,是const_char的一个静态数组
- FILE存放文件名的字符串字面值
- LINE存放当前行号的整型字面值
- TIME存放文件编译时间的字符串字面值
- DATE存放文件编译日期的字符串字面值
函数匹配
- 调用重载函数时应尽量避免强制类型转换,如果在实际应用中确实需要强制类型转换,则说明我们设计的形参不合理
函数指针
- 函数指针指向的是函数而非对象
- 函数指针指向某种特定类型,
函数的类型由它的返回类型和形参类型共同决定
,与函数名无关
- 如下为例:
bool lengthCompare(const string & , const string &);
bool (*ptr)(const string & , const string &); //ptr指向函数,该函数的参数是两个const string &的引用,返回值是bool类型
ptr = lengthCompare; //ptr指向名为lengthCompare的函数
ptr = &lengthCompare; //等价赋值语句:取地址符可选
//可以直接使用指向函数的指针调用该函数,无需提前解引用指针
bool b1 = ptr("hello","goodbye");
bool b2 = (*ptr)("hello","goodbye"); //等价调用
bool b1 = lengthCompare("hello","goodbye"); //等价调用
- 在指向不同函数类型的指针之间不存在转换贵组
- 可以为函数指针赋一个
nullptr
或者值为0的整型常量表达式