C++函数
本篇主要介绍C++相比于C函数的一些特性。
一、基础
一个典型的函数定义包括以下部分:返回类型(return type)、函数名字(function)、0个或者多个形参(parameter)。
调用函数:通过调用运算符 ( )
函数的调用完成两项工作:一是实参初始化函数对应的形参,二是将控制权转移给被调用函数,此时主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。
二、参数传递
2.1、引用形参
当形参是引用类型时,对应的实参被应用传递(passed by reference)或者函数被传引用调用(called by reference)。和其他引用一样,引用形参也是他绑定的对象的别名,也就是引用形参时它对应的实参的别名。
在程序执行的过程中,拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内),根本不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
//比较两个shring对象的长度
bool isShorter(const string &s1,const string &s2)
{
return s1.size() < s2.size();
}
比较两个string对象的长度函数中,以为string对象可能会非常长,所以应该尽量避免直接拷贝它们,使用引用形参比较合适。
如果不需要改变引用形参的值,最好将其声明为常量引用。
2.2、const形参和实参
当形参是const时,需要主要顶层const的相关事项。顶层const作用于对象本身。
const int ci = 42; //不能改变ci,const是顶层的
int i = ci; //正确,当拷贝ci时,忽略了它的顶层const
int * const p = &i; //const是顶层的,不能给pi赋值,但可以给i赋值
*p = 0; //通过给p改变对象的内容是允许的,现在i变成0,p=&ci是不允许的
和其他的初始化过程一样,当用实参初始化形参时会忽略掉顶层const。也就是形参的顶层const被忽略掉了。当形参有const时,传给它常量对象或者非常量对象都是可以的。
2.3、含有可变形参的函数
initializer_list形参 一种模板类型
如果函数的实参数量未知,但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。
initializer_list与vector不一样的是,initializer_list对象中的元素永远是常量值。
#include <initializer_list>
using std::initializer_list;
...
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin();beg != il.end(); ++beg)
{
cout << *beg << ' ';
}
cout << endl;
}
如果想向initializer_list形参中传递一个值的序列,必须把序列放在一对花括号内:
error_msg({"function","expected","actual"});
省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。省略符形参应该只适用于C和C++通用的类型。
省略符形参智能出现在形参列表的最后一个位置。
void foo(parm_list,...);
void foo(...);
三、返回类型和return语句
3.1、值如何被返回
返回一个值的方式和初始化一个变量或者形参的方式完全一样:返回值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
const string isShorter(const string &s1 , const string &s2)
{
return s1.size() < s2.size() ? s1 : s2;
}
以上函数返回类型是string,返回值被拷贝到调用点,该函数将返回s1/s2的副本或者一个未命名的临时string对象。
const string &isShorter(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"; //错误,"Empty"也是一个局部临时变量
}
引用返回左值
char &get_char(char &data)
{
return data;
}
int main()
{
char a_char;
get_char(a_char) = 'A'; //将a_char的值改为'A'
}
//如果main函数末尾没有return,编译器将隐式地插入一条返回0的return语句。
3.2、递归
如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都成为函数为递归函数(recursive function)。
//递归函数,将vector容器内的全部元素输出
void print_vector(vector<int>::const_iterator begin, vector<int>::const_iterator end)
{
if (begin != end)
{
cout << *begin++ << ' ';
return print_vector(begin, end);
}
else
{
cout << endl;
return;
}
}
我们递归调用print_vector函数以输出vector内的元素,当begin==end,递归终止,返回。
在递归函数中,一定有某条路径是不包含递归调用的,否则,函数将“永远”递归下去,或者说函数将不断地调用它自身直到程序栈空间消耗尽为止。——递归循环
3.3、返回数组指针
因为数组不能被拷贝,所以函数不能返回数组,不过可以返回数组指针或引用。
//优先级()>[]>*
int arr[10]; //定义一个含有10个整数的数组
int *p1[10]; //定义一个含有10个指针的数组
int (*p2)[10]; //定义一个指针,指向10个整数的数组
如果我们向定义一个返回数组指针的函数,数组的维度必须跟在函数名字之后。
Type (*function(parameter_list))[dimension];
//Type——表示元素的类型
//dimension——表述数组的大小
//(*function(parameter_list))两端的括号必须同时在,没有括号函数的返回类型是指针的数组。
int (*func(int i))[10]; //返回一个数组指针,指向10个整数的数组
使用尾置返回类型(trailing return type) C++11
尾置返回类型跟在形参列表后面,并以一个 -> 符号开头,为了表示函数真正的返回类型跟在形参列表之后,我们在本该出现返回类型的地方放置一个auto:
auto func(int i) -> int (*)[10];
func返回一个指针,并且该指针指向含有10个整数的数组。
四、函数重载
4.1、基本定义
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数(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"); //调用void print(const char *cp);
print(j,end(j)-begin(j)); //调用void print(const int ia[], size_t size);
print(begin(j),end(j)); //调用void print(const int *beg, const int *end);
对于重载的函数来说,它们应该在形参数量或者形参类型上有所不同。
不允许两个函数除了返回类型外其他所有的要素都相同。假设有两个函数,它们的形参列表一样但是返回类型不同,则第二个函数的声明是错误的。
int calData(int Adata, int Bdata);
double calData(int Adata, int Bdata); //错误,与上一个函数相比只有返回类型不同
4.2、重载和const形参
顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来;
record lookup(phone);
record lookup(const Phone); //重复声明了Record lookup(phone)
record lookup(phone *);
record lookup(Phone* const); //重复声明了Record lookup(phone)
这两组函数声明中, 每一组的第二个声明和第一个声明是等价的。
注:下面这个是普通变量和(指针/引用对象)的差别。
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时const是底层的。
//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
//定义了4个独立的重载参数
record lookup(Account &); //函数作用于Account的引用
record lookup(const Account&); //新函数,作用于常量引用
record lookup(Account*); //新函数,作用于指向Account的指针
record lookup(const Account*); //新函数,作用于指向常量的指针
上面的例子中,编译器可以通过实参是否是常量来推断应该调用哪个函数,**因为const不能转化成其他类型,所以只能把const对象传递给const形参。**相反,因为非常量可以转换成const,所以上面4个函数都可以作用于非常量对象或者指向非常量对象的指针,不过当我们传递一个非常量对象或者指向非常量的指针时,编译器会有限选用非常量版本的函数。
(const_cast 可以把实参强制转换成const对象)。
如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const(指向/引用常量或者非常量),则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。
如果实参是指向常量的指针(引用),调用形参是const*的函数;如果实参是非常量对象,调用形参是普通指针(引用)的函数。
4.3、调用重载的函数
定义了一组重载函数之后,需要以合理的实参调用它们。函数匹配(function matching)是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也可以叫重载确定(overload resolution)。编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,根据比较结果决定到底调用哪个函数。
重载函数的三种可能结果:
- 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择,此时也将发生错误,称为二义性调用(ambiguous call)。
五、特殊用途语言
5.1、默认实参
某些函数中有这样一种形参,在函数的很多次调用中它们都被赋予了一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(default argument)。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
typedef string::size_type sz;
string screen(sz ht=24,sz wid=80,char backgrnd=' ');
string window;
window = screen(); //等价于screen(24,80,' ');
window = sceeen(66); //等价于screen(66,80,' ');
window = screen(66,256); //等价于screen(66,256,' ');
window = screen(66,256,'#'); //等价于screen(66,256,'#');
函数调用时实参按照其位置解析,默认实参负责填补函数缺少的尾部实参。(靠右位置)
在给定的作用域中,一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认形参,而且该形参右侧的所有形参必须有默认值。
5.2、内联函数的constexpr函数
在函数调用之前,需要先保存寄存器,并在返回时恢复;可能需要拷贝实参,程序转向一个新的位置继续执行。
使用内联函数(inline),通常是将它在每个调用点上“内联地”展开。
//定义为内联函数
inline int add(int a,int b)
{
return a+b;
}
cout << add(adata,bdata) << endl;
//等效于
cout << adata + bdata << endl;
内联说明只是向编译器发出的一个请求,编译器可以忽略这个请求。
constexpr函数(constexpr function)是指能用于常量表达式的函数,定义constexpr函数的方法与内联函数类似,需要遵循:函数的返回类型及所有形参的类型都是字面值类型,并且函数体中必须有且只有一条return语句。
constexpr int new_sz()
{
return 42;
}
constexpr int foo = new_sz(); //正确,foo是一个常量表达式
把new_sz定义为无参数的constexpr函数,因为编译器能在程序编译时验证我们把new_sz函数返回的是常量表达式,所以可以用new_sz函数初始化constexpr类型的变量foo。
为了在编译过程随时展开,constexpr函数被隐式地指定为内联函数。
一般把内联函数和constexpr函数放在头文件内。
5.3、调试帮助
C++程序有时候会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。程序包含一些用于调试的代码,以便开发使用,当应用程序编写完成准备发布时,需要先屏蔽调试代码。这里用到两种预处理功能:assert和NDEBUG。
assert宏:
assert是一种预处理宏(preprocessor marco),所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert使用一个表达式作为它的条件:
assert(expr);
首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真,assert什么也不做。
assert包含在cassert头文件中,预处理名字由预处理器而非编译器管理,因此可以直接使用预处理名字而无须提供using声明。也就是使用assert而不是std::assert,也不需要提供using声明。
宏和预处理变量一样,宏在程序中的名字必须唯一,含有assert头文件的程序不能在定义名为assert的变量或者函数。
在程序调试中,assert常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阈值。
assert(word.size() < length);
NDEBUG变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态,如果定义了NDEBUG,则assert什么也不做,默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
#define NDEBUG;
$CC -D NDEBUG main.c //等效于在main.c中写一行#define NDEBUG
除了用于assert之外,耶尔可以使用NDEBUG编写自己的条件调试代码。
void print (const int ia[] , size_t size)
{
#ifndef NDEBUG
cerr << __func__ << ": array size is " << size << endl;
#endif
//...
}
C++编译器定义了几个对于程序调用很有用的名字:
__func__ \\当前调用函数的名字
__FILE__ \\存放文件名的字符串字面值
__LINE__ \\存放当前行号的整形字面值
__TIME__ \\存放文件编译时间的字符串字面值
__DATE__ \\存放文件编译日期的字符串字面值