6.1函数基础
函数可以理解成是可以循环使用的代码块.函数的定义通常包含以下部分:返回类型,函数名称,参数列表,函数体.
//以下是一个返回值为int类型,有两个参数的函数的定义
int add(int a,int b){
return a+b;
}
我们用调用运算符( )来执行函数,在运算符内传入参数.调用时会打断主调函数(正在执行的函数),被调函数(我们调用的函数)开始执行.
上面例子的 int a 和 int b 是函数的形式参数(形参),下面例子的20和30是实际参数(实参)
int a = add(20,30); //调用add函数,并传入20和30
std::cout << a << std::endl; //输出50
执行函数的第一步是隐式地定义并初始化函数的形参(将实参的值传给形参).如果形参不能接收实参的值(类型不一致或是类型不能进行隐式转换即类型不关联),那么函数执行失败.
若函数的参数列表为空(不需要参数),那么有以下两种写法:
int add(){} //隐式定义空形参列表
int add(void){} //显示定义空形参列表
形参名是可选的,即我们可以选择不写实参名,意味着我们在函数中无法使用该参数(在后面的函数重载中用于区分),但是调用时必须要写,例如:
//函数定义
void test(int){
std::cout<<"hello world"<<std::endl;
}
//调用函数
test(10);
6.1.1局部对象
C++中名字有作用域,对象有生命周期.
名字的作用域是程序文本的一部分,名字在其中可见(有效);
对象的生命周期是程序执行过程中该对象存在的一段时间.
形参与函数体内定义的变量称为局部变量,仅在函数体中生效,同时与局部变量同名的全局变量会在函数体内失效.函数结束后局部变量会自动销毁.
局部静态变量可以在函数结束后不销毁,而下次调用函数时保持上次的值.在变量前加上static关键字可以使其变成局部变量;
void test(){
static int a=0; //无论函数执行多少次,这条语句只执行一次
a++;
std::cout<<a<<std::endl;
}
test();
test();
//控制台输出
// 1
// 2
6.1.2函数声明
函数声明也被称为函数原型.函数声明可以多次,但函数定义只能一次.若是分文件编写,函数声明写在头文件中,并且有函数声明的头文件应该被包含到定义函数的源文件中.
6.1.3分离式编译
C++支持分离式编译,允许我们把文件分割到多个文件中,每个文件独立编译.(即分文件编写)
如果我们修改了某个源文件,只需要重新编译改动的文件,而不用把全部文件都重新编译(g++).
6.2参数传递
形参初始化的机理与变量初始化一样.形参和实参是两个相互独立的对象,可以说是实参被值传递或者函数被传值调用.
6.2.1传值参数
初始化形参后,对形参的改变不会影响到实参,如果需要形参影响实参,那么将形参类型改成指针或是引用即可.因为本质上是传递了地址,改变了地址上的值,自然会影响到原来的值.在C++中推荐用引用来代替指针.
6.2.2传引用参数
前面说了,执行函数的第一步是隐式地定义并初始化函数的形参,而拷贝大的类型对象或者容器对象比较低效,使用引用类型的形参可以避免拷贝.(有时候刷力扣,暴力解法超时,但是把函数形参改成引用类型就有可能能通过,正是因为使用普通类型的形参直接拷贝实参比较低效).
如果函数无需改变形参的值,那么保险起见最好声明成常量引用.
6.2.3const形参和实参
当用const修饰形参后,实参不必是const,而在函数中,形参无法被改变.而const修饰实参,形参也不必是const.
但若形参为引用类型,那么用const修饰实参时同时也要用const修饰形参.而反过来却不必.
6.2.4数组形参
因为数组会转换成指针,所以我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针.函数体使用形参数组时需要保证数组不越界,但是仅凭一个指针没法确定数组的大小.所以管理指针形参有以下三种常用办法:
使用标记指定数组长度:在数组结尾处包含一个结束标记(例如一个空字符),当函数处理数组时碰到了结束标记则停止.
使用标准库规范:传入指向元素首元素的指针和尾元素的指针,当两个指针相等时停止.
显示传递一个表示数组大小的形参:直接把数组的大小传给函数
传递多维数组时需要牢记C++中没有真正意义上的多维数组,而是数组的数组.
6.2.5 main处理命令行选项
主函数main也是可以传入参数的,主函数可以接收两个参数:(int argc,cahr *argv[])形参名字可以自己随便定义,但是大家都是用的argc和argv.
argc指的是argv中字符串的数量,argv是数组.
6.2.6含有可变形参的函数
如果函数的实参数量未知但是全部实参的类型都一样,可以使用initializer_list类型的形参,用于表示某种特定类型的值的数组(然后我个人通常会直接使用vector,但书中有讲initializer_list类型那就简单介绍一下),initializer_list类型定义在同名的头文件.
和vetcor一样,initializer_list也是一种模板类型,需要说明列表中所含元素的类型:
例如: initializer_list<int> test;
和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法修改里面元素的值.
6.3返回类型和return语句
return语句终止当前正在执行的函数,并将控制权返回到调用该函数的地方.
6.3.1无返回值函数
函数声明中返回值类型为void的函数即为无返回值函数,函数定义中,函数体不一定需要return语句,因为在这类函数的最后一句后面会隐式执行return;
6.3.2有返回值函数
返回值类型不为void的函数即为有返回值函数,在函数体中的最后应当有return语句,即使它永远不会执行.
//这样的函数定义是错误的,即使我们知道函数一定会有返回值,但是在编译器眼中是没有的
int test(){
for(int i=0;i<10;i++){
if(i>=5){
return 1;
}
}
}
//正确的写法是这样的
int test(){
for(int i=0;i<10;i++){
if(i>=5){
return 1;
}
}
return 1;//加上这句,函数定义才是正确的,即使这条语句永远不会执行到
}
不要返回局部对象的引用或指针,因为函数执行结束后局部变量就会被销毁,而返回值所返回的地址所指的数据也会被清理.
如果一个函数直接或是间接调用了自身,那么该函数为递归函数.main不能进行递归.递归函数需要定义好结束循环的条件,否则永远无法结束递归直至程序的栈空间消耗殆尽.
6.3.3返回数组指针
因为数组不能被拷贝,所以函数不能返回数组,但是可以返回数组的指针或是引用(C++中推荐引用).
也可以使用尾置返回类型.形如:
auto fun() -> int{
//函数体
}
在原返回值类型处写上auto,然后在参数列表后接箭头->以及真正返回的类型,上例以int为例.
6.4函数重载
如果同一作用域内的几个函数名字相同但是形参列表不同,那么称之为重载函数.main不能被重载.
返回值类型不能作为函数重载的条件.
调用重载函数时,会将名字一样的函数进行匹配,找出最佳匹配的函数然后执行,如果没有可以匹配的形参列表或是有两个以上的形参列表都可以通过隐式转换来匹配时会报错.
如下面的例子.调用test时传入int类型参数则调用第一个,传入两个int类型参数则调用第二个,传入long类型参数则调用第三,传入double类型则调用第三个.
如果把第一个函数定义注释掉,此时传入int类型参数则会报错,因为出现二义性,因为int既可以隐式转换成long类型调用第三个函数,也可以隐式转换成double类型调用第四个函数.
void test(int a) {
cout << 1 << endl;
}
void test(int a,int b) {
cout << 2 << endl;
}
void test(long a) {
cout << 3 << endl;
}
void test(double a) {
cout << 4 << endl;
}
6.4.1重载与作用域
一般来说,我们不讲函数声明置于局部作用域.
6.5特殊用途语言特性
6.5.1默认实参
//此函数定义中,b为默认实参,其值默认为10,调用函数可以传入,则将b的默认值10给代替.
//也可以不传入b的值,则b默认为10
int add(int a,int b=10){
return a+b;
}
//以下两种调用方式都是正确的,第一个不传入形参b的值,则b使用默认值10
//第二个调用则传入形参b的值,则20将代替b的默认值10
add(10);
add(10,20);
一旦某个形参被赋予了默认值,那么后面的形参都必须有默认值,这个稍微想想就很好理解了,如果有多个形参,其中在中间的某些形参有默认值,那么将无法判断哪些实参的值应该赋给哪些形参,而哪些形参又应该使用默认值.
通常应该在函数声明中指定默认实参,并将该声明放在合适的头文件中.
6.5.2内联函数和constexpr函数
在函数声明的开头(返回值类型的前面)加上关键字inline可以将函数定义为内联函数,内联函数可以避免函数调用的开销.适用于优化规模较小,流程直接,频繁调用的函数.然而内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求.
constexpr函数指能用于常量表达式的函数,定义constexpr函数的方法与其他函数类似,不过要遵循以下规定:函数返回值类型以及所有形参的类型都是字面量类型,并且函数体中有且只有一条return语句(只有一条语句,那就是return语句).
内联函数和constexpr函数通常定义在头文件中.
6.5.3调试帮助
当程序编写完成准备发布时,需要屏蔽掉调试代码,可以用到两项预处理功能:assert和NDEBUG.
assert是一种预处理宏,就是一个预处理变量,其行为类似于内联函数.使用时如下:
assert(expr) 如果expr为假(0),则assert输出信息并终止程序执行,反正什么也不做
assert定义在cassert头文件中,预处理名字由预处理器而非编译器管理,因此使用assert时无序使用using,也就不用在assert前面加上std::
assert的行为依赖与NDEBUG的状态,如果定义了NDEBUG,那么assert失效,反之assert工作.
因此我们可以使用定义NDEBUG来关闭调试状态.
预处理器定义了几个名字方便程序调试:
__func__:当前调用的函数的名字
__FILE__:文件名的字符串字面量
__LINE__:当前行号的整型字面量
__TIME__:文件编译时间的字符串字面量
__DATE__:文件编译日期的字符串字面量
6.6函数匹配
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数.
第二步是从候选函数中挑出能被调用的函数,这些被挑出的函数被称为可行函数,如果没找到可行函数,那么编译器报错.
第三步是从可行函数中选出最匹配函数,实参类型与形参类型越接近,它们匹配得越好.
调用重载函数时应当避免强制类型转换.
6.6.1实参类型转换
6.7函数指针
一个指针指向函数时,我们称这个指针为函数指针.
使用时如下:
bool test(int a,int b){……}
bool *pf(int a,int b); //声明函数指针
pf=test; //定义函数指针
pf(10,20) //调用
(*pf)(10,20) //调用