1. 编写+调用函数
a) 一个典型的函数定义包括以下部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。
b) 我们通过调用运算符执行函数,它作用于一个表达式,该表达式是函数或者指向函数的指针。调用表达式的类型就是函数的返回类型
c) 函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数
d) return语句也完成两项工作:一是返回return语句中的值,二是将控制权从被调函数转移回主调函数
int fact(int val){
int ret=1;
while(val>1)
ret*=val--;
return ret;
}
2. 形参和实参
a) 实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推
b) 实参的类型必须与对应的形参类型匹配;且函数有几个形参,我们必须提供相同数量的实参
c) void返回类型表示函数不返回任何值
d) 函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针
3. 局部对象
a) 形参和函数体内部定义的变量统称为局部变量,仅在函数的作用域内可见
b) 对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块的末尾时销毁它
c) 自动对象是指只存在于块执行期间的对象,形参是一种自动对象:函数开始时为形参申请存储空间,一旦函数终止,形参被销毁
d) 局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。可以将局部变量定义成static类型获得这样的对象
#include <iostream>
using namespace std;
size_t count_calls(){
static size_t ctr=0;
return ++ctr;
}
int main(){
for(size_t i=0;i!=10;++i)
cout<<count_calls()<<endl;
return 0;
}
输出结果是1~10
4. 函数声明
a) 类似于变量,函数名字也必须在使用之前声明。
b) 函数只能定义一次,但可以声明多次
c) 函数的返回类型、函数名、形参类型描述了函数的借口
5. 参数传递
a) 如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参
b) 当形参是引用类型时,引用形参是它绑定的对象的别名;引用形参是它对应的实参的别名(传引用参数)
c) 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象(传值参数)
#include <iostream>
using namespace std;
void reset(int *p){
*p=0;
p=0;
}
int main(){
int i=42;
reset(&i);
cout<<i<<endl;
return 0;
}
输出0
6. 传值参数
a) 当初始化一个非引用类型的变量时,初始值被拷贝给变量。传值函数对形参做的所有操作都不会影响实参
b) 指针的行为和其他非引用类型:当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针
#include <iostream>
using namespace std;
void swap(int *a,int *b){
int c=*a;
*a=*b;
*b=c;
}
int main(){
int i=42,n=56;
swap(&i,&n);
cout<<i<<" "<<n;
return 0;
}
输出56 42
7. 传引用参数
a) 如果函数无须改变引用形参的值,最好将其声明为常量引用
b) 拷贝大的类类型对象或者容器对象比较低效,最好通过引用形参访问该类型的对象
c) 一个函数只能返回一个值,而引用形参为一次返回多个结果提供了有效的途径
下面的栗子是一个名为find_char的函数,它返回在string对象中某个制定自负第一次出现的位置及字符出现的总次数
#include <iostream>
using namespace std;
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隐式地返回
}
//给函数传入一个额外的引用实参保存字符出现的次数
8. 数组形参
a) 不允许拷贝数组(3.5.1节,102页),且使用数组时(通常)会将其转换成指针(3.5.3,105页)
b) 因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针
void print(const int*);
void print(const int[]);
void print(const int[10]);
//这三个函数是等价的,每个函数的唯一形参都是const int*类型
c) 管理数组实参有三种常用的方法:
1) 使用标记指定数组长度:要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串。C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止
2) 使用标准库规范:传递指向数组首元素和尾后元素的指针
void print(const int *beg, const int *end){
//输出beg到end之间的所有元素
while(beg!=end)
cout<<*beg++<<endl; //输出当前元素并将指针向前移动一个位置
}
3) 显式传递一个表示数组大小的形参:专门定义一个表示数组大小的形参
void print(const int ia[], size_t size){
//const int ia[]等价于const int *ia
//size表示数组的大小,将它显式地传给函数用于控制对ia元素的访问
for(size_t i=0;i!=size;++i)
cout<<ia[i]<<endl;
}
d) 数组引用形参:形参可以是数组的引用,引用形参绑定到对应的实参上,也就是绑定到数组上
void print(int (&arr)[10]){
for(auto elem:arr)
cout<<elem<<endl;
}
&arr两端的括号必不可少
f(int&arr[10]) //错误:将arr声明成了引用的数组
f(int (&arr)[10]) //正确:arr是具有10个整数的整形数组的引用
e) 数组的大小对函数的调用没有影响
9. return语句(终止当前正在执行的函数并将控制权返回到调用该函数的地方)
return;
return expression;
a) 无返回值函数
1) 只能用在返回类型是void的函数中
2) void函数如果想在它的中间位置提前退出,可以使用return语句。这种用法类似于用break语句退出循环
void swap(int &v1, int &v2){
if(v1==v2)
return; //如果两个值是相等的,不需要交换,直接退出
int tmp=v2;
v2=v1;
v1=tmp; //不需要显式的return语句
}
3) 一个返回类型是void的函数也能使用return语句的第二种形式,但语句的expression必须是另一个返回void的函数
b) 有返回值函数
1) 只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值
2) return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型
c) 不要返回局部对象的引用或指针,因为函数终止意味着局部变量的引用将指向不再有效的内存区域
10. 函数重载
a) 如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数
b) 这些函数接受的形参类型不一样,但执行的操作非常类似。调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数
c) 对于重载的函数来说,它们应该在形参数量或形参类型上有所不同
d) 不允许两个函数除了返回类型外其他所有的要素都相同
11. 调用重载的函数
a) 在函数匹配过程当中,我们把函数调用与一组重载函数中的某一个关联起来。编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数
b) 当调用重载函数时有三种可能的结果:
1) 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码
2) 找不到任何一个函数与调用的实参匹配,编译器发出无匹配的错误信息
3) 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择,发生错误,成为二义性调用
12. assert预处理宏
a) 可以有选择地执行调试代码
b) 预处理宏其实是一个预处理变量
c) assert宏使用一个表达式作为它的条件:assert(expr); 首先对expr求值。如果expr为假(0),assert输出信息并终止程序的执行。如果为真,assert什么也不做
d) assert宏定义在cassert头文件中
e) 含有cassert头文件的程序不能再定义名为assert的变量、函数或者其他实体