1.函数基础
一个函数只能返回一个值!!!
编写函数:返回类型 函数名 (形参) {
函数体;
}
调用函数:类型 变量 = 函数名(实参);
调用函数时,主调函数的执行被暂时中断,被调函数开始执行。当遇到一条return语句时函数结束执行过程。
-
形参和实参
实参是形参的初始值。实参的类型必须与对应的形参类型匹配;函数有几个形参,我们就必须提供相同数量的实参。 -
函数的形参列表
①空形参列表
②一个形参
③多个形参
用逗号隔开,每个形参都含有一个声明符的声明。即使两个形参的类型一样,也必须把两个类型都写出来。
-
函数返回类型
void:不返回任何值;
一般类型:int,string等;
函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
(1)局部对象
名字有作用域:名字的作用域是程序文本的一部分,名字在其中可见;
对象有生命周期:对象的生命周期是程序执行过程中该对象存在的一段时间。
形参和函数体内部定义的变量统称为局部变量。
自动对象:只存在于块执行期间的对象。形参是一种自动对象。
局部静态对象:将局部变量定义成static类型即得到局部静态对象,可使局部变量的生命周期贯穿函数调用及之后的时间。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁。
(2)函数声明
作用: 告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。
和其他名字一样,函数的名字也必须在使用之前声明。函数只能定义一次,但是可以声明多次。
函数声明无须函数体,用一个分号替代即可。基本形式:
返回类型 函数名 (形参); 或
返回类型 函数名 ();
函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型。
建议函数在头文件中声明而在源文件中定义。如果把函数声明放在头文件中,就能确保同一函数的所有声明保持一致。含有函数声明的头文件应该被包含到定义函数的源文件中。
(3)分离式编译
分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。如果我们修改了其中一个源文件,只需重新编译那个改动了的文件。
2.参数传递
形参的类型决定了形参和实参的交互方式。当形参是引用类型时,它对应的实参被引用传递,函数被传引用调用。当实参的值被拷贝给形参时,实参被值传递,函数被传值调用。
(1)传值参数
传值参数情况下,函数对形参做的所有操作不会影响实参,除非使用指针。
指针形参:拷贝之后,两个指针是不同的指针。
在C++语言中,建议使用引用类型的形参访问函数外部的对象。
(2)传引用参数
交换两整数的值:
#include<iostream>
using namespace std;
void aswapb(int &a, int& b) { //函数的声明及定义
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 1, b = 2;
aswapb(a, b); //直接传入对象而无需传入对象的地址
cout << a << ' ' << b << endl;
system("pause");
return 0;
}
- 使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型不支持拷贝操作。把形参定义成对常量的引用,可以避免拷贝操作。
如果函数无需改变引用形参的值,最好将其声明为常量引用。 - 使用引用形参返回额外信息
一个函数只能返回一个值,可以给函数传入一个额外的引用实参,令其保存想要返回的多的值。
例:下面find_char函数返回指定字符第一次出现的位置,额外的引用occurs保存字符出现的次数。
//定义find_char
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;
}
++occurs;
}
}
return ret;
}
int main()
{
string s = "aobocodo";
string::size_type ctr;
auto index = find_char(s, 'o', ctr);
cout << index << ' ' << ctr << endl;
system("pause");
return 0;
}
(3)const形参和实参
注意顶层const:顶层const作用于对象本身,用实参初始化形参时会忽略掉顶层const,当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
尽量使用常量引用:当函数是非常量引用时,函数不能接受const参数,非常量引用会极大地限制可接受的实参类型。
(4)数组形参
①参数类型
数组的两个特殊性质:不允许拷贝数组(无法使用值传递);
使用数组时通常会将其转换成指针(传递数组实际上传递的是指向数组首元素的指针)。
所以,数组形参的本质是 int * 。
②获取数组长度
和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,管理指针形参有三种常用的技术:
-
使用标记指定数组长度
-
使用标准库规范
管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,类似标准库begin和end。
-
显式传递一个表示数组大小的形参
③数组形参和const
当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针(const int *);只有当函数确实要改变元素值的时候,才把参数定义成指向非常量的指针( int *)。
④数组引用形参
当形参是数组的引用时,引用形参绑定到对应的实参上,也就是绑定到数组上。
注意:上述函数只能作用于大小为10的数组,不能作用于int arr[2]、int arr[20]等类型的数组。
后续我们会做一个改进,使其可以给引用类型的形参传递任意大小的数组。
⑤传递多维数组
(5)main:处理命令行选项
int main(int argc, char** argv)//实参列表
{
string str;
for (int i = 1; i != argc; ++i) {
str += argv[i];
str += " ";
}
cout << str <<endl;
return 0;
}
使用argv中的实参时,一定要记得可选参数从argv[1]开始,argv[0]保存的是程序的名字。
(6)含有可变形参的函数
有时我们无法提前预知应该向函数传递几个实参。解决方法:
①如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;
initializer_list和vector:都是一种模板类型,定义其对象时,都必须说明列表中所含元素类型;不同的是,initializer_list对象中的元素永远是常量值。
例:
函数定义:
函数调用:
含有initializer_list形参的函数也可以同时拥有其他形参。
②如果实参的类型不同,可通过编写可变参数模板实现;
③无须类型检查,使用省略符形参… ;
省略符形参仅仅用于C和C++通用的类型。形式:
3.返回类型和return语句
return语句的两种形式:
return;
retrun expression;
(1)无返回值函数
声明 void
返回 return; ,return;可省略;也可返回return expression;形式,但要求expression是另一个返回void的函数。
(2)有返回值函数
return语句返回值的类型必须与函数的返回类型相同,或者能隐式转换成函数的返回类型。
-
值是如何被返回的
返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。对于一般的返回,返回值被拷贝到调用点;若函数返回引用,则该引用仅是它所引对象的一个别名,不会真正拷贝string对象。 -
不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域。 -
返回类类型的函数和调用运算符
调用运算符的优先级:与点运算符和箭头运算符相同
调用运算符的结合律:符合左结合律 -
引用返回左值
调用一个返回引用的函数返回左值,其他返回类型得到右值。如我们可以为返回类型是非常量引用的函数的结果赋值。
-
列表初始化返回值
函数可以返回花括号包围的值的列表。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。 -
主函数main的返回值
**允许main函数没有return语句直接结束。**此时编译器隐式地插入一条返回0的return语句。
cstdlib头文件定义了两个预处理变量,分别表示成功与失败。 -
递归
如果一个函数调用了它自身,不论是直接调用还是间接调用,都称该函数为递归函数。
在递归函数中,一定有某条路径是不包含递归调用的,否则,函数会陷入死循环。
main函数不能调用它自己。
(3)返回数组指针
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上说,定义一个返回数组的指针或引用的函数比较繁琐,但可以通过使用类型别名(typedef或using)等方法简化这一任务。
-
声明一个返回数组指针的函数
不使用类型别名:
返回数组指针的函数形式为:Type ( *function(parameter_list) ) [dimension]。例如:
-
使用尾置返回类型
对于返回类型比较复杂的函数,使用尾置返回类型是一个有效的方法。基本形式:auto function(parameter_list) -> Type。如:auto func(int i) -> int (*) [10]; -
使用decltype
如果我们知道函数返回的指针指向哪个数组,就可以使用decltype关键字声明返回类型。
4.函数重载
定义:同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
函数的名字仅仅让编译器知道它调用的是哪个函数,而函数重载可以在一定程度上减轻程序员起名字、记名字的负担。
main函数不能重载!
(1)定义重载函数
对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。如果两个函数除了返回类型外其他所有要素都相同,是不允许的。
(2)判断两个形参的类型是否相异
(3)重载和const形参
如前所述,用实参初始化形参时会忽略掉顶层const,即顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
另一方面,如果形参是某种类型的指针或引用,则可通过底层const区分其指向的是常量对象还是非常量对象实现函数重载。
(4)const_cast和重载
const_cast:将常量对象转换成非常量对象,只能改变运算对象的底层const。
(5)调用重载的函数
函数匹配(重载确定):一个过程,在此过程中把函数调用与一组重载函数中的某一个关联起来。
调用重载函数时有3种可能的结果:
- 最佳匹配
- 无匹配
- 二义性调用
(6)重载与作用域
如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的共同实体。在不同的作用域中无法重载函数名。
在C++中,名字查找发生在类型检查之前。一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
一般来说,将函数声明置于局部作用域内不是一个明智的选择。
5.特殊用途语言特性
(1)默认实参
在定义函数时给形参赋初值,这个值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参(新的指定值),也可以省略该实参(默认值)。
注:一旦某个形参被赋予了默认值,他后面的所有形参都必须有默认值。不能出现前面省略实参,后面使用指定值的情况。所以要合理设置形参顺序。
-
使用默认实参调用函数
使用默认实参,只需在调用函数时省略该实参就可以了。 -
默认实参声明
-
默认实参初始值
局部变量不能作为默认实参。
(2)内联函数和constexpr函数
把一些规模较小的操作定义成函数的好处:
①容易阅读和理解;
②使用函数可以确保行为的统一,每次相关操作都能按照同样的方式进行;
③修改方便;
④可以被其他应用重新利用。
但是,使用函数的缺点:调用函数包含着一系列工作(调用前保存寄存器并在返回时恢复,拷贝实参,程序转向新的位置继续执行),调用函数所需时间要长一些。
-
内联函数可避免函数调用的开销
在函数的返回类型前面加上关键字 inline,就将其声明为内联函数了:
内联机制用于优化规模较小、流程直接、频繁调用的函数,建议不大于75行。一般不支持内联递归函数。 -
constexpr函数
constexpr变量:声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。否则语句报错。
constexpr函数:能用于常量表达式的函数。constexpr函数的返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句。
constexpr函数被隐式地指定为内联函数。调用 constexpr函数时,编译器直接将对 constexpr函数地调用替换成其结果值。 constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作,如空语句、类型别名及using声明。
-
将内联函数和constexpr函数放在头文件内
一般将函数的声明放在头文件,函数的定义放在源文件。但是,对于某个给定的内联函数或constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。
(3)调试帮助
有选择地执行调试代码:程序可以包含一些用于调试地代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。
-
assert预处理宏
基本形式:assert (expr);
预处理宏是一个预处理变量,如果表达式为假,assert输出信息并终止程序的执行。如果表达式为真,assert什么也不做。
assert宏定义在cassert头文件中。宏名字在程序内必须唯一。含有cassert头文件的程序不能再定义名为assert的变量、函数或者其他实体。
-
NDEBUG预处理变量
assert的行为依赖于NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
NDEBUG关闭调试状态:
① #define NDEBUG;
② 命令行:$ CC -D NDEBUG main.c # use /D with the Microsoft compiler,等价于在main.c文件的一开始写#define NDEBUG。
除使用assert外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,则执行#ifndef和#endif之间的代码;否则这些代码将被忽略掉。
5个对程序调试很有用的名字:
_ _ func _ _ 输出当前调试的函数的名字
_ _ FILE _ _ 存放文件名的字符串字面值
_ _ LINE _ _ 存放当前行号的整型字面值
_ _ TIME _ _ 存放文件编译时间的字符串字面值
_ _ DATE _ _ 存放文件编译日期的字符串字面值
(4)函数占位参数
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
语法: 返回值类型 函数名 (数据类型){}
在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术
示例:
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
cout << "this is func" << endl;
}
int main() {
func(10,10); //占位参数必须填补
system("pause");
return 0;
}
6.函数匹配
为重载函数选择最佳函数匹配。当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换的来时,最佳匹配就变复杂起来。
(1)确定候选函数和可行函数
候选函数:本次调用对应的重载函数集。特征:一是与被调用的函数同名,二是其声明在调用点可见。
可行函数:从候选函数中选出能被这组实参调用读的函数。特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同或者能转换成形参的类型。
如果没有找到可行函数,编译器将报告无匹配函数的错误。
(2)寻找最佳匹配(如果有的话)
最匹配:实参类型与形参类型越接近,它们匹配得越好。
(3)含有多个形参的函数匹配
最佳匹配:
该函数每个实参的匹配都不劣于其他可行函数需要的匹配。
至少有一个实参的匹配优于其他可行函数提供的匹配。
注:编译器最终可能因为调用具有二义性而报错。
(4)实参类型转换
实参类型转换到形参类型的几个等级:
①精确匹配
实参类型和形参类型相同
实参从数组类型或函数类型转换成对应的指针类型
向实参添加顶层const或从实参删除顶层const
②通过const转换实现的匹配
③通过类型提升(小整型转化为大整型)实现的匹配
④通过算术类型转换或指针转换实现的匹配
⑤通过类类型转换实现的而匹配
7.函数指针
函数指针指向的是函数而非对象。
声明一个指向函数的指针:
(1)使用函数指针
(2)重载函数的指针
(3)函数指针形参
(4)返回指向函数的指针
8.函数的分文件编写
**作用:**让代码结构更加清晰
函数分文件编写一般有4个步骤:
(1)创建后缀名为.h的头文件
(2) 创建后缀名为.cpp的源文件
(3) 在头文件中写函数的声明
aswapb.h:
#include<iostream>
using namespace std;
void aswapb(int a, int b); //函数的声明
(4) 在源文件中写函数的定义
aswapb.cpp:
#include"aswapb.h"
void aswapb(int a, int b) { //函数的定义
int temp = a;
a = b;
b = temp;
cout << a << ' ' << b << endl;
}
(5) 函数调用
源.cpp:
#include<iostream>
#include"aswapb.h"
using namespace std;
//void aswapb(int a, int b); //函数的声明
//void aswapb(int a, int b) { //函数的定义
// int temp = a;
// a = b;
// b = temp;
// cout << a << ' ' << b << endl;
//}
int main() {
int num1 = 1, num2 = 2;
aswapb(num1, num2); //直接传入对象而无需传入对象的地址
cout << num1 << ' ' << num2 << endl;
system("pause");
return 0;
}