函数是一个命了名的代码块,通过调用函数执行相应的代码。
函数基础
int【返回类型】 fact【函数名】 (int val, double dval【形参列表】)
{
}【函数体】
函数调用符(call operator): 作用于一个表达式,该表达式是函数或者指向函数的指针
注:①函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转交给被调用的函数。
②:主调函数(calling function)执行被暂时中断,被调函数(called function)开始执行
③:尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。
④任意两个形参不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。
void f1(void); //显示的定义空形参列表
int f3(int v1, int v2); //正确
int f4(int v1,v2); //错误:必须把两个类型都写出来
⑤函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
局部对象
在C++语言中,名字有作用域,对象有生命周期(lifetime):
- 名字的作用域是程序文本的一部分,名字在其中可见。
- 对象的生命周期是程序执行过程中该对象存在的一段时间。
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象。当到达定义所在的块末尾时销毁它。
自动对象:只存在块执行期间的对象——形参:函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
局部静态对象
局部静态对象(local static object):在程序的执行路径第一次经过对象定义语句初始化,直到程序终止才被销毁。在此期间即使对象所在的函数结束执行也对它没有影响。
size_t count_call()
{
static size_t ctr = 0;
return ++ctr;
}
int main()
{
for (size_t i = 0; i != 10; ++i)
{
cout << count_call() << endl;
}
getchar();
return 0;
}
如果局部静态变量没有显示的初始值,它将执行值初始化,内置类型的局部静态变量初试值为0;
函数声明/函数原型(function prototype)
函数声明与函数定义非常类似,唯一区别就是用一个分号(;)代替函数体
函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数的全部信息。
【建议】变量在头文件中声明,在源文件中定义。与之类似,函数也应该在头文件中声明在源文件中定义。
参数传递
和其他变量一样,形参的类型决定了形参和实参交互的方式:
- 实参被值传递(passed by value)、函数被传值调用(called by value):
实参的值被拷贝给形参,形参和实参是两个相互独立的对象。
- 实参被引用传递(passed by reference)、函数被传引用调用(called by reference):
引用形参是对应的实参的别名。
传值参数
当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针可以间接访问所指对象,据此修改他所指对象的值。
void reset(int *ip)
{
*ip = 0; //改变指针ip所指的值
ip = 0; //只改变了ip的局部拷贝,实参没有改变
}
void main(void)
{
int i =42;
reset(&i);
cout << i << endl; //输出i=0;
}
在C++语句中不建议这么做!!建议如下!!!!
传引用参数
void reset(int &i)
{
i = 0; //改变了i引用的对象
}
void main(void)
{
int j =42;
reset(j);
cout << j << endl; //输出j=0;
}
优点:
1.使用引用可以避免拷贝:拷贝大的类类型对象或者容器对象比较低效,甚至有些类类型(包括IO类型在内)根本不支持拷贝操作。
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
这里:如果函数无需改变引用形参的值,最好将其声明为常量引用【之后详述】。
2.使用引用形参返回额外信息
//记录某一字母出现的总次数和第一次出现位置
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 != size(); ++i)
{
if(s[i] == c)
{
if(ret == s.size())
ret = i; //记录第一次出现位置
++occurs; //通过绑定返回出现次数
}
}
return ret; //总次数
}
const形参和实参
①当使用实参初始化形参时会忽略掉顶层const,传给顶层const常量对象和非常量对象都是可以的
const int ci = 42;
int i = ci;
int *const p = &i;
*p = 0;
void fcn(const int i)
②在C++语言中,允许定义若干相同名字的函数,但要求不同函数的形参列表应该有明显的区别。
void fcn(const int i) //顶层const 会被忽略
void fcn(int i) //报错。重复定义
【背景知识回忆】1.顶层const的拷贝不受限制,但是底层const的拷贝对象必须具有相同的底层const资格
2.允许一个常量引用绑定非常量的对象、字面值、甚至一个表达式;
const int &r = 42;
故下面内容将更好理解:
把函数不会改变的形参定义为(普通的)引用【应为const引用】是一种比较常见的错误。
导致:1.误导函数可以修改它的实参的值;
2.极大限制函数所能接受的实参类型【不再能接受const对象、字面值、需要类型转换的对象】
string::size_type find_char(string &s, char c, string ::size_type &occurs)
{
//…………
}
find_char("Hello" , 'o' , ctr); //编译错误,原因如上2
数组形参
【背景知识回忆】数组的两个特性:1.不能拷贝数组,所以我们无法以值传递的方式使用数组指针。
2.使用数组时(通常)会将其转换成指针,所以在函数传递时传递的是指向数组首地址的指针。
//这三种print函数等价
print(const int*);
print(cosnt int[]);
print(const int[10]);
以上每个函数的唯一形参类型都是 const int*类型。
int i = 0; j[2] = {1,0};
print(&i);
print(j);
数组的大小对函数调用没有影响。所以函数一开始不知道数组的确切尺寸,需要提供额外信息。有三种常用技术:
- 使用标记指定数组长度【C风格字符串中\0】
void print(const char* cp)
{
if(cp)
while(*cp)
cout << *cp++;
}
- 传递数组首元素和尾元素后的指针
int j[] = {0,1};
int *beg = begin(j);
int *last = end(j);
void print(const int *beg, const int *end)
{
while(beg != end)
cout << *beg++ << endl;
}
print(beg, last);
-
传递一个数组大小的形参
int j[2] = {0,1};
int *beg = begin(j);
int *last = end(j);
void print(const int ia[], size_t size)
{
for(size_t i = 0; i != size; ++i)
cout << ia[i] << endl;
}
print(j, last - end);
C++形参也可以是数组的引用。
void print(int (&arr)[10])
{
for (auto elem : arr)
{
cout << elem << endl;
}
}
int k[10] = {0,1,2,3,4,5,6,7,8,9};
print(k);
16章接受引用类型的形参传递任意大小的数组
传递多维数组
void print(int (*matrix)[10], int rowSize)
{
//………………
}
//等价定义
void print(int matrix[][10], int rowSize)
形参指向含有10个整型数组的指针
main:处理命令行选项
有时我们需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数要执行的操作。
prog -d -o ofile data0
此例中,argc等于5.
argv[0] = "prog" argv[4] = "data0" argv[5] = 0【最后一个指针之后的元素值保证为零】
int main(int argc, char *argv[])
{
//…………
}
argv:指向C风格字符串的指针,
argc:字符串的数量。
注:当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序名,非用户输入;
含有可变形参的函数
有时候我们无法预知应该向函数传递几个实参,C++11提供了两种主要方法:
- 所有实参类型相同,传递一个名为initializer_list的标准库类型。
与vector一样,initializer_list也是一种模板类型。
#include <initializer_list>
std::initializer_list<string> ls;
std::initializer_list<int> li;
如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内
error({v1,v2,"string"});
当然,含有initializer_list形参的函数也可以同时拥有其他形参。
省略符形参【说啥呢???,坑,待填】
为了便于C++程序访问某些特殊的C代码而设计的,这些代码使用了名为varargs的C标准库功能
C编译器文档会描述其如何使用,不应用于其他目的。只出现在形参列表的最后一个位置。
返回类型和return语句
终止当前正在执行的函数并将控制权返回到调用该函数的地方
return ;
return expression;
无返回值函数
返回void的函数不要求非得有return语句,因为这类函数的最后一句后面会有隐式的执行return
通常,void函数如果想在它的中间位置提前提出,可以使用return语句。
void swap(int &v1, int &v2)
{
if(v1 = v2)
return;
int tmp = v2;
v2 = v1;
v1 = tmp;
//无需显式的return;
}
有返回值函数
只要函数类型不是void,则该函数内的每条return语句必须返回一个值,其类型与函数返回类型相同/隐式转换。
值是如何被返回的
方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size ? s1 : s2;
}
其中,返回形参和返回类型都是const string的引用。不管是调用函数还是返回结果,都不会真正拷贝string对象。
不要返回局部变量对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉。因此,函数最终意味着局部变量的引用将指向不再有效的内存区域。
const string &manip()
{
string ret;
if(!ret.empty())
return ret; //错误!返回局部对象的引用
else
return "Empty"; //错误!返回一个局部临时量
}
同理,返回一个局部对象的指针也是错误的。
返回类类型的函数和调用运算符
调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律。
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size ? s1 : s2;
}
auto sz = shorterString(s1,s2).size();
如果函数返回指针、引用或类的对象,我们就可以使用函数调用的结果访问结果对象的成员。
引用返回左值
函数的【返回类型】决定函数调用是否是【左值】。调用一个返回是【(非常量)引用的函数】得到左值,其他返回类型得到右值。
把函数调用放在赋值语句的左侧看起来有点奇怪,但其实没什么特别的(咋可能!)。返回值是引用,因此调用是个左值。
列表初始化返回值
函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。
vector <string> process()
{
//expected和actual为string对象
if(expected.empty)
return {};
else if(expected == actual)
return {"function","okey"};
else
return {"function", actual};
}
返回内置类型: 包围的列表最多包含一个值,值所占的空间不应该大于目标空间;
类类型: 由类本身定义初始值如何使用
主函数main的返回值
我们允许main函数没有return语句直接结束。如果控制到达了main函数的结尾处而没有return语句,编译器将隐式的插入一条返回0的return语句。
递归
如果一个函数调用了它自身,不管这种调用是直接的还是间接地,那都称该函数为递归函数(recursive function)。
int factorial(int val)
{
if (val > 1)
{
return factorial(val - 1) * val;
}
return 1;
}
在递归函数中,一定有一条路径是不包含递归调用的;否则,函数将“永远递归下去”。函数将不断地调用它自身直到程序栈空间耗尽为止。
注:main函数不能调用它自己。
返回数组指针
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针和引用。
- 类型别名
typedef int arrT[10];
using arrt = int[10];
arrT* func(int i); //返回一个指向含有10个整数类型的数组指针
- 声明一个返回数组指针的函数
想要在声明func时不使用类型别名,我们必须牢记被定义的名字后面数组的维度
Type (*function(parameter_list))[dimension]
int (*func(int i))[10];
- 使用尾至返回类型(trailing return type)
C++新标准,对于返回类型比较复杂的函数最为有效,比如返回类型是数组的指针或者数组的引用。
尾至返回类型跟在形参列表后面并以->符号开头。
auto func(int i) ->int(*) [10];
- 使用decltype
我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。
decltype并不负责把数组类型转换成响应的指针,所以decltype的结果是一个数组,要想表示arrPtr返回指针还必须在函数声明中加一个*
函数重载
函数重载(overloaded):同一个作用域内的几个函数名字相同但形参列表不同。
void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);
编译器会根据传递的实参类型推断想要的是哪个函数。
int j[2] = {0,1};
print("Hello World");
print(begin(j), end(j));
print(j,end(j) - begin(j));
当然,main函数不能重载。
对于重载函数来说,他们应该在形参数量或形参类型上有所不同。
判断两个形参的类型是否相异
有时候两个形参列表看起来不一样,但实际上是相同的。
Record lookup(const Account &acct);
Record lookup(const Account&); //忽略的形参名字
typedef Phone Telno;
Record lookup(const Telno &);
Record lookup(const Phone &); //类型相同
Record lookup(const Phone);
Record lookup(Phone); //顶层const
Record lookup(Phone * const);
Record lookup(Phone *); //顶层const
如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现其函数重载
Record lookup(Account &);
Record lookup(const Account &);
Record lookup(Account *);
Record lookup(const Account *);
我们只能把const对象传递给const形参(const不能转换成其他类型)
因为非常量可以转换成const,所以上面的4个函数都能作用于非常量对象或者指向非常量对象的指针【编译器会选用非常量版本的函数 】。
【背景知识补充】
①第四章:一旦我们去掉了某个对象的const属性,编译器就不再阻止我们对该对象进行写操作了,如果对象本身不是一个常量,使用强制类型转换获得写权限是合法行为
②
③【auto】在一条语句中定义多个变量,切记,【符号(*)和(&)只从属于某个声明符】【符号(*)和(&)只从属于某个声明符】【符号(*)和(&)只从属于某个声明符】
int i =0, &r = i;
auto a = r; //a是一个整型
const int ci =i, &cr = ci;
auto b = ci; //b是一个整型(顶层const特性被忽略)
auto e = &ci; //e是一个指向整型常量的指针(底层const)
//重点!
auto &m = ci; //m是对整型常量的绑定
auto *p = &ci; //P是一个指向整型常量的指针
const_cast和重载
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() > s2.size() ? s1 : s2;
}
上段代码中,输入参数和返回类型都是const string的引用
修改为:非常量的string实参调用函数,但返回仍是const string的引用
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() > s2.size() ? s1 : s2;
}
string & shorterString(string &s1, string &s2)
{
auto &s3 = shorterString(const_cast<const string&> (s1), const_cast<const string&> (s2)); //s3前引用符!不能少!
return const_cast<string &> (s3);
}
使用const_cast可以对两个非常量的string实参调用这个函数,但返回的结果仍然是const string的引用。
调用重载的函数
函数匹配(function matching): 把函数调用和一组重载函数中的某一个关联起来,函数匹配也叫做重载确认。
当调用重载函数的时候有三种可能的结果:
- 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no matching)的错误信息。
- 有多于一个函数可以匹配,但每一个都不是明显的最佳匹配。此时也发生错误,二义性调用(ambiguous call)。
重载与作用域
重载对于作用域的一般性质没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用局中声明的实体。
在不同作用域中无法重载函数名;
错误1:当编译器处理调用read的请求的时候,找到的是定义在局部作用域中的read。这个名字是个布尔变量,显然我们无法调用一个布尔值
错误2:当我们调用print函数时,编译器首先寻找对该函数名的声明,找到的是接受int值的那个局部函数。一旦在当前作用域找到所需的名字,编译器就会忽略掉外层作用域中的同名实体。
假设我们把print(int)和其他print函数声明放在同一个作用域中,则它将成为另一种重载形式。
特殊用途语言特性
默认实参(default argument):
调用含有默认实参的函数,可以包含该实参,也可以省略该实参。
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd ='');
注:
①默认实参作为形参的初始值出现在形参列表中,一旦某个形参被赋予了默认值,后面所有的参数都必须有默认值。(在设计时,一个重要任务就是让经常使用的形参出现在后面)
②通常的习惯是将函数声明放置在头文件中,并且一个函数只声明一次(多次声明也合法)。
但给定作用域中一个形参只能被赋予一次默认实参(后续声明只能为之前那些没有默认值的形参添加默认实参)
string screen(sz, sz, char = ' '); //合法
string screen(sz, sz, char = '*'); //非法
string screen(sz = 80, sz = 24, char); //合法
③用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时。【main()内变量也属于局部变量】
内联函数
内联函数(inline):通常就是将它在每个调用点上“内联地”展开。
cout << shorterString(s1, s2) << endl;
//内联后等价 消除了函数运行开销
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
inline const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在在调用点内敛展开。
constexpr函数
指能用于常量表达式的函数,要求:【函数的返回类型及所有形参的类型都得是字面值类型】【函数体中必须有且只有一条return语句】
constexpr int new_sz() {return 41;}
constexpr int foo = new_sz(); //foo一个常量表达式
执行该初始化任务时, 编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr被隐含指定为内联。
constexpr函数体可以包含其他语句,只要这些语句在运行时不执行任何操作就可以。可以有空语句、类型别名以及using声明;
调试帮助
程序可以包含一些用于调试的代码,但这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法只用到两项预处理功能:assert和NDEBUG
assert:是一种预处理宏(preprocessor marco)【cassert.h中】
assert(expr);
对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真,assert什么也不做。
NDEBUG:assert的行为依赖于此预处理变量,如果定义了NDEBUG则assert什么也不做,默认状态下没有定义NDEBUG,此时assert执行运行检测。
函数匹配
当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时, 这项工作就不那么容易了
void f();
void f(double);
void f(int, int);
void f(int, double = 3.14);
f(42, 5.6);
第一步:选定本次调用的重载函数集,其中的函数称为【候选函数】——两个特征:①与被调用的函数同名 ②其声明在调用点可见
第二步:选出能被这组实参调用的函数,其中的函数称为【可行函数】——两个特征:①其形参数量与本次调用的实参数量相等②每个实参的类型对应的形参类型相同。
void f(int, int);
void f(int, double = 3.14);
第三步:从可行函数中选择与本次调用最匹配的函数。
编译器依次检查每个实参以确定哪个函数是最佳匹配。如果只有一个函数满足下列条件,则匹配成功:
①该函数每个实参的匹配都不劣于其他可行函数需要的匹配
②至少有一个实参的匹配优于其他可行函数提供的匹配
如果在检测了所有实参之后没有任何一个函数脱颖而出,则该函数调用是错误的。编译器报告二义性错误。
调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
编译器将实参类型到形参类型的转换划分为几个等级
1.精准匹配:①实参类型和形参类型相同②实参从数组类型或函数类型转换成相应的指针类型③实参的顶层const操作
2.通过const转换实现的匹配
3.通过类型提升实现的匹配
void ff(int);
void ff(short);
ff('a'); //匹配int
4.通过算术类型转换或指针转换实现的匹配
void manip(long);
void manip(float);
manip(3.14);
存在两种算术类型转换,调用具有二义性。
5.通过类类型转换实现的匹配
函数指针
函数指针指向的是函数而非对象。函数指针指向某种特定类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
bool lengthCompare(const string &, const string &);
该函数的类型是: bool(const string &, const string &)
声明指向该函数的指针为:
bool (*pf)(const string &, const string &); //未初试化
pf前面有个*,因此pf是指针;右侧是形参列表,表明pf指向的是函数;再观察左侧,发现函数的返回类型是bool。
因此:pf是一个指向函数的指针,该函数的参数是两个const string的引用,返回值是bool类型。
当我们把函数名作为一个值使用时,该函数自动的转换成指针。
pf = &lengthCompare; //取地址符可以省略
pf = lengthCompare;
直接使用指向函数的指针调用该函数。
bool b1 = (*pf)("hello", "goodboy");
bool b1 = pf ("hello", "goodboy"); //解引用符可以省略
在指向不同函数类型的指针间不存在转换规则。
string::size_type sumLenth(const string &, const string &);
pf = 0; //合法
pf = &sumLenth; //非法
函数指针形参
与数组相同,不能定义函数类型形参,但可以是指向函数的指针。
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &,const string & ));
等价于
void useBigger(const string &s1, const string &s2, bool pf(const string &,const string & ));
我们可以直接把函数作为实参使用,此时它会自动转化成指针。
useBigger(s1, s2, lengthCompare);
使用类型别名和decltype可以简化代码
typedef bool Func(const string &,const string &);
typedef decltpye(lengthCompare) Func2;
typedef bool (*FuncP)(const string &,const string &);
typedef decltpye(lengthCompare) *FuncP2;
void useBigger(const string &s1, const string &s2, Func);
void useBigger(const string &s1, const string &s2, FuncP2);
在第一条语句中,编译器自动将Func表示的函数转换成函数指针。
返回指向函数的指针
与数组类似,虽然不能返回一个函数,但能返回指向函数的指针。
using F = int(int* , int ); //F是函数类型
using PF = int(*)(int*, int); //PF是指针类型
PF f1(int);
F *f1(int); //等价
以上是使用类型别名(最简便),也可以用一下形式
int (*f1(int))(int *, int); //返回值为函数指针的函数
由内向外的顺序阅读这条声明:f1有形参列表,所以f1是个函数;f1前面有*,所以f1返回一个指针;进一步观察:指针本身也包含函数列表,因此指针指向函数,该函数返回类型为int。
对比:
bool *f1(int);
答案:返回值为bool指针的函数
与此同时,可以使用尾置返回类型方式
auto f1(int) ->int(*)(int*, int);
将auto和decltype用于函数指针类型
假定有两个函数,他们的返回类型都是string::size_type,并且各有两个const string&类型的形参,此时我们可以编写第三个函数,接受一个string类型的参数,返回一个指针,该指针指向两个函数中的一个。
string :: size_type sumLength(const string&, const string&);
string :: size_type largeLength(const string&, const string&);
decltype(sumLength) *getFcn(const string &);
术语表
constexpr:可以返回常量表达式的函数,一个constexpr函数被隐式地声明成内联函数。
initializer_list:一个标准的类,表示的是一组花括号包围的类型相同的对象,对象之间以逗号隔开。