1.函数基础
一个典型的函数定义包括返回类型、函数名字、形参组成的列表以及函数体。
函数执行的操作在语句块中说明,该语句块称为函数体。
函数调用需要完成两个操作:用实参初始化函数对应的形参;将控制权转移给被调用函数。
在调用函数时,主调用函数的执行被暂时中断。
实参是形参的初始值,实参的类型必须与对应的形参类型匹配或能进行类型转换。
函数的调用规定实参数量应与形参数量一致,形参一定会被初始化。
函数的形参列表可以为空,但是不能省略,可以使用关键字void来显式表示函数无形参。
void func(void)
形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明。
int func(int v1,v2) //错误
int func(int v1, int v2) //正确
//即使类型相同,也要把类型写出来
任意两个形参不能同名,且与程序的全局变量不能同名,否则会出现错误。
函数的返回类型不能是数组类型或函数类型,但可以式指向数组或函数的指针。
1.1 局部对象
在C++语言中,名字有作用域,对象有生命周期。
名字的作用域是程序文本的一部分,名字在其中可见。
对象的生命周期是程序执行过程中该对象存在的一段时间。
在形参和函数体内部定义的变量统称为局部变量,它们仅在函数的作用域内可见。
自动对象:当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁
局部静态对象:执行路径经过对象定义语句时初始化,直到程序终止才被销毁,在此期间即使对象所在的函数结束也不会对其有影响。
static int str = 0; //静态变量
如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0.
1.2 函数声明
函数的名字也必须在使用前声明,函数声明无须函数体(允许没有形参名字)。
函数只能定义一次,但可以声明多次。
函数的三要素(返回类型、函数名、形参类型)说明调用该函数所需的全部信息,函数声明也称作函数原型。
函数应该在头文件中声明,在源文件中定义。
含有函数声明的头文件应该被包含到定义函数的源文件中。
2. 参数传递
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
当实参的值被拷贝给形参时(实参被值传递),形参和实参是两个相互独立的对象。
当形参类型是引用时,形参将绑定到对应的实参上(实参被引用传递)。
2.1 传值参数
当实参被值传递时,函数对形参做的所有操作都不会影响实参。
当执行指针拷贝操作时,拷贝的是指针的值(指针指向的对象的地址),两个指针是不同的指针,但是指向的对象是相同的,因此我们可以间接地访问它所指的对象。
在C++中,建议使用引用类型地形参来代替指针。
2.2 传引用参数
引用形参绑定初始化它的对象,通过使用引用形参来改变实参的值。
当某种类型(string、类、函数)不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
如果函数无须改变引用形参的值,最好将其声明为常量引用。
使用引用形参来充当统计变量,用来存放函数运行次数信息,以此返回额外信息。
2.3 const形参和实参
当使用const实参初始化形参时(值传递),会忽略顶层const,形参的顶层const被忽略掉了。
const int ci = 42; //ci有顶层const,不能改变ci值
int i = ci; //当拷贝ci时,忽略它的顶层const
当形参有顶层const时,传给它常量对象或者非常量对象都是允许行为。
void fcn(const int i) //fcn能读取i,但是不能修改i的值
在C++中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。
允许使用字面值初始化一个常量引用。
const int &r = 42;
使用非常量引用会极大地限制函数所能接受的实参类型,因此推荐使用常量引用。
2.4 数组形参
数组的两个特殊性质:不允许拷贝数组 ;使用数组时会将其转换成指针。
当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
void print(const int*);
void print(const int[]);
void print(const int[10]); //维度表示我们期望该数组含有多少元素,实际上代码并没强制要求
//上面的三个函数的形参是等价的
管理数组实参的三种常用技术:(避免函数在使用数组时出现越界问题)
1、要求数组本身包含一个结束标记(C风格字符串的空字符)
2、使用标准库中的begin和end函数,向函数传递数组首元素和尾后元素的指针。
3、专门定义一个表示数组的形参。
C++语言允许将变量定义成数组的引用,因此形参可以是数组的引用。
void print(int(&arr)[10}) //形参是数组的引用,维度是类型的一部分
//&arr两端的括号不可省略
f(int& arr[10]) //错误:将arr声明成了以引用为元素的数组
f(int (&arr)[10]) //正确:arr是具有10个整数的数组的引用
因为数组的大小是构成数组类型的一部分,因此数组的维度在形参中被确定下来。
我们在传递数组时,应确保实参数组和形参数组的维度相同。
在C++中实际上没有真正的多维数组,所谓的多维数组其实是数组的数组。
多维数组的首元素本身就是一个数组,指针就是一个指向数组的指针。
多维数组的第二维(以及后面的所有维度)的大小都是数组类型的一部分,不能省略。
void print(int matrix[][10],int rowsize)
//把matrix[]看成一个指向首元素的指针,元素类型是int[10]
//形参是指向含有10个整数的数组的指针
2.5 main:处理命令行选项
main函数通常只有空形参列表,有时我们需要给main传递实参。
int main(int argc, char *argv[])
当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,最后一个指针之后的元素值保证为0;
argv[0] = "prog";
.
.
.
.
argv[5] = 0;
2.6 含有可变形参的函数
如果函数的实参数量未知但是全部实参类型相同,可以使用initializer_list类型的形参。
initializer_list类型定义在同名的头文件中,用于表示某种特定类型的值的数组。
initializer_list<T> lst; | 默认初始化:T类型的空列表 |
initializer_list<T> lst {a,b,c,...}; | lst的元素数量和初始值数量一致,lst的元素是 对应初始值的副本 |
lst2(lst) | 拷贝后,原始列表和副本共享元素 |
lst2 = lst | 拷贝后,原始列表和副本共享元素 |
lst.size() | 列表中的元素数量 |
lst.begin() | 返回指向lst中的首元素的指针 |
lst.end() | 返回指向lst中尾元素下一个位置的指针 |
和vector一样,initializer_list也是一种模板类型,定义对象时,必须说明列表中的元素类型。
initializer_list对象中的元素永远是常量值,我们无法改变其元素的值。
向initializer_list形参中传递一个值的序列,必须把序列放在一对花括号内。
void msg(initializer_list<string>i1)
{...}
int main()
{
msg({"function","okay"});
}
含有initializer_list形参的函数也可以同时拥有其他形参。例如,调试系统的ErrCode用来表示是不同类型的错误。
void msg(ErrCode e, initializer_list<string> i1)
省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,代码形式如下:
void foo(parm_list, ...);
void foo(...);
//省略符形参所对应的实参无需类型检查
3. 返回类型和return语句
return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。
3.1 无返回值函数
没有返回值的return语句只能用在返回类型是void的函数中。
返回类型是void的函数不强制要求return语句,该类函数会在最后一句后面隐式地执行return。
3.2 有返回值的函数
有返回值的return语句提供了函数的结果。
return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。
在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。
int findaim(const int arr[], const int sz) //寻找目标值的下标
{
for(int i = 0;i < sz; i++)
{
if(arr[i] == aim)
return i;
}
return -1; //找不到目标值,返回-1
}
函数所返回的值,创建一个临时对象,把返回值拷贝进这个临时对象内。
如果函数返回引用,该引用仅是它所引对象的一个别名,不会发生真正的拷贝。
函数终止意味着局部变量的引用将指向无效的内存区域,因此不要返回局部对象的指针或引用。
调用运算符的优先级与点运算符符和箭头运算符相同,并且符合左结合律。
string s1 = "hello"; string s2 = "world";
auto sz = shorter(s1,s2).size();
//先调用shorter函数,再调用string对象的size函数
函数的返回类型决定函数调用是否是左值。
调用一个返回引用的函数得到左值,其他返回类型得到右值。
我们可以像使用左值那样为返回类型是非常量引用的函数的结果赋值。
char &getval(string &str,string::size_typr ix) //返回引用类型
{
return str[ix];
}
int main()
{
string s("hello world");
get_val(s,0) = 'A'; //函数调用放在赋值语句左侧
return 0;
}
C++新标准规定,函数可以返回花括号包围的值的列表;若列表为空,执行值初始化。
vector<string> process()
{
return {"function","okay"};
}
如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不得大于目标类型的空间。
我们允许main函数没有return语句直接结束,因为编译器会隐式地插入一条返回0的return语句。
main函数的返回值可以看做是状态指示器,返回0为成功,返回其他值表示失败,其他值含义由机器决定,为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,用来表示成功与失败.
int main()
{
if(exp)
return EXIT_FAILURE; //失败
else
return EXIT_SUCCESS; //成功
}
由于它们是预处理变量,所以既不能在前面加上std::,也不能在using声明中出现。
在递归函数中,一定有可实现的终止条件;否则,函数将不断地调用它自身直到程序栈空间耗尽为止。
main函数不能调用它自己。
3.3 返回数组指针
由于数组不能被拷贝,所以函数不能返回数组,但是函数可以返回数组的指针或引用。
我们可以通过类型别名来简化定义返回数组的指针或引用的函数的任务。
typedef int arr[10]; //arr是类型别名,表示含有10个整数的数组的类型
using arr = int[10]; //等价于typedef
arr* func(int i); //返回arr的指针
如果想在声明func时不使用类型别名,我们要记住以下要点:
1、数组的维度必须跟在函数名字之后
2、函数的形参列表跟在函数名字后面,而且形参列表先于数组的维度。
int(*func(int i))[10];
//func(int i)表示调用函数的形参是int
//(*func(int i))表示该函数返回一个指针类型
//int(*func(int i))[10] 表示该函数返回一个指向含有10个整数的数组的指针
C++新标准中可以使用尾置返回类型来简化func声明,任何函数的定义都能使用尾置返回。
auto func(int i) -> int(*)[10];
如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字来声明返回类型。
int odd[] = {1,2,3,4,5};
decltype(odd) *arr(int i)
{...}
//由于arr要返回一个指针类型,但decltype并不会把数组类型转换成指针,所以得加个*
4. 函数重载
同一作用域内的几个函数名字相同但形参列表(数量或类型)不同,我们称之为重载函数。
函数的返回类型不同,形参列表相同,不是重载函数;main函数不能重载。
void print(const int *beg, const int *end);
void print(const char *cp);
顶层const不影响传入函数的对象,无法和没有顶层const对象区分开来。
如果形参是某种类型的指针或引用,则通过区分其指向的是常量还是非常量可以实现函数重载。
record lookup(account&);
record lookup(const account&);
虽然非常量对象或指向非常量对象的指针可以传递给const形参,但是编译器会优先选用非常量版本的函数。
const_cast可以改变对象的常量属性,在重载函数的情景中最有用。
string s1 = "hello";
string &s3 = s1;
auto &r = const_cast<const string&>(s3); //把s3转换成常量引用,赋给r
r = const_cast<string&>(r); //将r转换成对非常量引用
在调用重载函数时,编译器会把实参与函数的形参进行比较,这个过程称为函数匹配。
二义性调用:有多于一个函数可以调用,每个都不是最佳选择,此时会发生错误。
4.1 重载与作用域
如果我们在内层作用域中声明名字,它将隐藏外层作用域的同名实体。
当我们调用函数时,编译器会先在内层进行匹配,逐渐向外层寻找。
在C++语言中,名字查找发生在类型检查之前。
5.特殊用途语言特性
5.1 默认实参
某些函数有一种形参,在函数的很多次调用中它们都被赋予相同的值,该值被称为默认实参。
默认实参作为形参的初始值出现在形参列表中,我们可以为它们定义默认值。
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char back = " "); //ht、wid、back都有默认实参
注意:一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
如果我们想使用默认实参调用函数,只要在调用函数时省略该实参即可。
函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参。
当设计含有默认实参的函数时,尽量让少使用默认值的形参出现在前面,多使用的形参放在后面
在给定的作用域中一个形参只能被赋予一次默认实参,后续声明给未有默认值的形参添加实参。
string screen(sz,sz,char = ' ');
string screen(sz = 24, sz = 80, char); //给未有默认实参的形参添加
通常在函数声明中指定默认实参,并将该声明放在合适的头文件中。
局部变量不能作为默认实参,表达式的类型能转换成形参的类型,该表达式可以作为默认实参。
使用默认实参的名字可以在函数调用时改变默认实参的值。
5.2 内联函数和constexpr函数
在函数的返回类型前加上inline,声明成内联函数,有效避免函数调用的开销。
内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。
一般来说,内联机制用于优化规模较小、流程直接、调用频繁的函数。
constexpr函数是指能用于常量表达式的函数。
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz(); //foo是一个常量表达式
该函数的返回类型及所有形参的类型都得是字面值类型,函数体必须有且只有一条return语句。
constexpr函数被隐式地指定为内联函数。
constexpr函数体内可以包含其他语句(空语句、类型别名、using声明),只要语句在运行时不会执行任何操作就行。
constexpr函数不一定返回常量表达式,但是编译器会报错。
内联函数和constexpr函数通常定义在头文件中。
5.3 调试帮助
C++程序可以包含一些用于调试的代码,这些代码只在开发程序时使用。
assert是一种预处理宏(预处理变量),在调试状态下有效。
assert宏定义在cassert头文件中,宏名字在程序中必须唯一。
assert宏常用于检查“不能发生”的条件。
assert(arr.size() > threshold)
assert的行为依赖于一个名为NDEBUG的预处理变量的状态,如果定义了NDEBUG,表示关闭了调试状态,assert什么都不用做。
可以使用#ifndef和#endif来编写属于NDEBUG的条件调试代码。
_ _ FILE_ _ | 存放文件名的字符串字面值 |
_ _LINE_ _ | 存放当前行号的整型字面值 |
_ _TIME_ _ | 存放文件编译时间的字符串字面值 |
_ _DATE_ _ | 存放文件编译日期的字符串字面值 |
_ _func_ _ | 输出当前调试的函数名称 |
6. 函数匹配
函数匹配首先选定本次调用对应的重载函数集合,集合中的函数称为候选函数。
候选函数的特征:一是与被调用的函数同名,二是其声明在调用点可见。
从候选函数中选出能被调用的函数,这些新选出的函数称为可行函数。
可行函数的特征:形参数量与实参数量相等,每个实参类型与对应的形参类型一致。
如果函数含有默认实参,则我们在调用该函数时传入的实参数量可能少于它实际的实参数量。
如果该次调用具有二义性,编译器会拒绝其请求。
6.1 实参类型转换
实参类型到形参类型的转换优先级排序:
1、精确匹配
1.1、实参类型和形参类型相同。
1.2、实参从数组类型或函数类型转换成对应的指针类型
1.3、向实参添加顶层const或者从实参中删除顶层const
2、通过const转换实现的匹配
3、通过类型提升实现的匹配
4、通过算术类型转换或指针转换
5、通过类类型转换实现的匹配
所有的算术类型转换的优先级都一样。
若传入实参存在两种可能的算数类型转换,该函数调用具有二义性。
7. 函数指针
函数指针指向的是函数而非对象,函数的类型有它的返回类型和形参类型共同决定。
声明一个指向函数的指针,只需用指针替换函数名即可。
bool (*pf) (const string &,const string &);
当我们把函数名作为一个值来使用时,该函数自动地转换成指针。
pf = length; //pf指针指向length函数
pf = &length; //取地址符不强制要求
直接使用指向函数的指针调用该函数,无须提前解引用指针。
bool b1 = pf("hello", "goodbye"); //调用函数
bool b2 = (*pf)("hello","goodbye"); //等价的调用
在指向不同函数类型的指针之间不存在转换规则,函数与指针的类型要保持一致。
虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。
void useer(const string &s1, bool pf(const string &,const string &));
可以直接把函数作为实参使用,函数会自动转换成指针。
useer(s1,s2,length);
直接使用函数指针类型显得繁琐,使用类型别名或decltype关键字能简化代码。
typedef bool func(const string &,const string &);
decltype(length) func2; //func和func2是函数类型
返回类型是函数的话,必须把返回类型显式地写成指针形式。
我们可以使用尾置返回类型的方式声明一个返回函数指针的函数:
auto func(int) -> int (*)(int*,int);
当我们将decltype作用于某个函数时,它返回函数类型而非指针类型。