6.1函数基础
一个典型的函数定义包括:返回类型、函数名字、0个或多个形参组成的列表、函数体。
函数执行的操作在语句块中说明,成为函数体。
通过圆括号()调用运算符来执行。
局部静态对象:定义成static
在函数调用时,只初始化一次,函数结束仍然有效。
函数声明
函数只能定义一次,但是可以声明多次。
函数声明无需函数体,用分号替代。
函数的三要素(返回类型,函数名,形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称函数原型。
建议在头文件中声明,在源文件中定义。
分离式编译
允许把程序分割到几个文件,每个文件独立编译。
factMain调用fact,生产可执行文件
编译的过程如下:
$ CC factMain.cc fact.cc #generates factMain.exe
$ CC factMain.cc fact.cc -o #generates main.exe
如果修改了其中一个源文件,只需要重新编译改动的文件。
分离式编译产生一个.obj或.o文件,含义是该文件包含对象代码。
接下来编译器负责把对象文件链接在一起形成可执行文件
实际编译过程:
$ CC -c factMain.cc #generates factMain.o
$ CC -c fact.cc #generates fact.o
$ CC factMain.o fact.o #generates factMain.exe or a.out
$ CC factMain.o fact.o -o main #geneartes main or main.exe
6.2参数传递
每次调用函数都会重新创建形参,并用传入的实参对形参进行初始化,即拷贝。
如果形参是引用类型,则实参被引用传递,函数被传引用调用。
如果形参是实参的拷贝,则成为值船体,函数被传值调用。
指针形参
执行指针拷贝操作时,拷贝的时指针的值,拷贝之后两个指针时不同的。
void reset(int *ip)
{
*ip = 0; //改变ip所指对象的值
ip = 0; //改变了ip的局部拷贝,实参未改变。
}
int i = 42;
reset(&i); //i = 0;改变的是i的值,不会改变i的地址
传引用参数
void reset(int &i)
{
i = 0; //改变了i所引对象的值
}
int j = 42;
reset (j); // j = 0;
拷贝大的类型或容器比较低效,有的类型(IO)还不支持拷贝,函数只能引用形参访问改类型对象。
如果函数要返回多个值,一个好方法就是给函数一个额外的引用参数,令其保存结果。
const形参和实参
首先要注意顶层const的使用
const int ci = 42; //不能改变ci,const是顶层的
int i = ci; //拷贝时可以忽略顶层const
int * const p = &i; //const是顶层的,p无法再赋值
*p = 0; //通过p改变对象内容是允许的,i = 0
当用实参初始化形参时会忽略顶层const。
如果形参有const,传入的常量对象或者非常量对象都是可以的。
指针或引用参与const
void reset(int &i)
{
i = 0; //改变了i所引对象的值
}
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i); //可以
reset(&ci); //错误,不能指向const
reset(i); //可以
reset(ci); //错误
reset(42); //错误,不能把普通字面值作为引用
reset(ctr); //错误,类型不匹配
尽量使用常量引用
如果把引用不设置未常量,容易误导人以为可以修改实参。
同时会限制参数,因此如果不需要修改就使用常量引用。
数组形参
数组有两个性质:1、不允许拷贝。2、使用数组时会转换成指针。
因此无法值传递使用数组参数,只能用类似形式。
//三个函数等价,每个函数都有const int*类型的形参
void print(const int*)
void print(const int[]])
void print(const int[10]]) //这里的维度表示期望含有的元素,实际不一定
调用时
int i = 0;
int j[2] = {0, 1}
print(&i); //正确,&i的类型时int*
print(j); //正确,j转换成int*指向j[0]
因此一开始不知道数组的确切尺寸
1、使用标记指定数组长度
管理数组实参的一种方法是要求数组本身包含一个结束标记,如C风格字符串,最后有一个空字符表示结束。
适用于有明显结束标记的数组
void print(const char *cp)
{
if(cp) //CP不是空指针
while(*cp) //只有指针所指不是空字符
count << *cp++ //打印
}
2、使用标准库规范
传递指向数组首元素和伪元素的指针。
void print(const int *beg; const int *end)
{
while(beg != end)
count << *beg << endl;
}
调用时传入两个指针
int j [2] = {0,1};
print(begin(j),end(j));
3、显式传递数组大小的参数
实参专门定义一个数组大小的形参
只要size不超过实际数组大小,就是安全的。
void print(const int ia[]; size_t size)
{
for(size_t i = 0; i != size; ++i)
cout << ia[i] << endl;
}
数组形参和const
当函数不需要对数组读写时,形参应该是指向const的指针。只有函数需要改变元素值时,才把形参定义指向非常量的指针。和指针类似。
数组引用形参
形参可以是数组的引用。(括号不可少)
void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem << endl;
}
函数体只能作用于大小为10的数组
传递多维数组
处理的是数组的数组,首元素本身是一个数组,指针是一个指向数字的指针。
void print(int (*matrix)[10], int rowSize)
等价定义
void print(int matrix[][10], int rowSize)
main:处理命令行选项
有时需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数要执行的操作。
例如:假定main函数位于可执行文件prog内,我们可以向程序传递下面的选项:
prog -d -o ofile data0
命令行通过两个(可选的)形参传递给main函数
int main(int argc, char *argv[])
第一个形参表示数组中字符串的数量。
第二个形参是一个数组,它的元素是指向C风格字符串的指针。
也可以定义成
int main(int argc, char **argv0)
其中argv指向char*
当实参传递给main后,argv的第一个元素指向程序的名字或者一个空字符串。接下来的元素一次传递命令行提供的实参。最后一个指针之后的元素值保证为0.
上述命令行为例
argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data";
argv[5] = 0;
含有可变形参的函数
为了编写能处理不同数量实参的函数,C++ 11有两种主要方法:
1、如果所有实参类型相同,可以传递一个名为initializer_list的标准库函数。
2、如果实参类型不同,可以编写一种特殊函数,即可变参数模板。
C++还有一种特殊的形参类型(省略符),可以传递可变数量的实参。
这种功能一般只用于与C函数交互的接口程序。
initializer_list形参
表示某种特定类型的值的数组。
提供的操作有:
initializer_list<T> lst //默认初始化;T类型元素的空列表
initializer_list<T> list{a,b,c}//lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
lst2(lst) ;// 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;
lst2 = lst; //拷贝后,原始列表和副本共享元素
lst.size(); //数量
lst.begin(); //首元素指针
lst.end(); //尾元素下一个位置的指针。
和vector一样,initializer_list也是模板类型,需要说明所含元素类型。不过initializer_list对象中的元素永远是常量,无法修改。
initializer_list<int> li;
如下编写输出错误信息的函数,使其可以作用于可变函数的实参:
void error_msg(initializer_list<string> il)
{
for (auto beg = li.begin(); beg != il.end(); ++beg)
{
cout << *beg << "";
}
cout << endl
}
如果想向initializer_list形参中传递一个值,要用花括号。
error_msg({"functionX", expected, actual})
含有initializer_list形参的函数也可以同时拥有其他形参。
省略符形参
是为了c++程序访问某些特殊的C代码设置的,这些代码使用了varargs的标准库。
通常省略符形参不应用其他目的。
只能出现在形参列表最后一个位置
void foo(parm_list, ...);
6.3、返回类型和return语句
无返回值函数
只能用在void函数中。void的函数最有一句后面会隐式地执行return。
一般用法是中间位置提前退出,类似break。
有返回值函数
只要函数类型不是void,则必须返回一个对应的值。
函数返回的是一个拷贝的临时对象。
函数也可以返回引用
const string &shorterString(const string &s1. const string &s2)
{
retrun s1.size() <= s2.size() ? s1 : s2;
}
不要返回局部对象的引用
函数完成后,所占用的存储空间就释放了,因此局部变量的引用就不在有效。
引用返回左值
调用一个返回引用的函数得到左值,其他返回类型得到右值。
char &get_val(string &str, string::size_type ix)
{
return str[ix];
}
get_val(s, 0) = 'A';
如果返回的是常量引用就不能赋值。
列表初始化返回值
c++11允许返回花括号包围的值的列表。
vector<string> process()
{
//expected和actual是string
if(expected.empty())
return{};
else if (expected == actual)
return {"functionX", "okay"};
else
return {"functionX", expected, actual}
}
主函数main的返回值
允许main函数没有return(有一个隐藏的retrun 0)
main函数的返回值看作状态指示器,返回0表示成功,其他表示失败。
为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量。
int main()
{
if(some_failure)
return EXIT_FAILURE;
else
return EXIT_SUCCESS;
}
递归
函数调用自身就是递归。
递归函数必须有一个路径是不包含递归调用的,否则就会死循环。
返回数组指针
数组不能拷贝,因此函数不能返回数组,只能返回指针或引用。
声明一个返回数组指针的函数
int (*func(int i))[10];
使用尾置返回类型
c++简化方法,使用尾置返回类型
fun函数返回指针,指针指向了10个整数的数组
auto func(int i) -> int(*)[10]
使用decltype
如果知道函数返回的指针将指向哪个数组,就可以使用decltype来声明返回类型。
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even;
}
arrPtr使用decltype表示它的返回类型是指针,并且所指对象与odd类型一致。因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。
6、4函数重载
统一作用域内的几个函数名字相同但是形参列表不同,称之为重载。
有顶层const的形参和没有的无法区分
Record lookup(Phone);
Record lookup(const Phone); //重复声明错误
如果形参是指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现重载,此时const是底层的。
Record lookup(Phone*); //函数作于指向Phone的指针
Record lookup(const Phone*); //函数作用于指向常量的指针
const_cast和重载
当函数的实参不是常量时,得到的结果是一个普通的引用,就可以使用const_cast
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}
先把实参强制转成const的引用,然后调用函数,返回对const string的引用,再将其转换回一个普通的string&,是安全的。
调用重载的函数
把函数调用与一组重载函数中的某一个关联起来,叫函数匹配,也叫重载确定。
调用重载函数有3种可能的结果:
1、找到一个最佳匹配的函数
2、找不到任何一个匹配的,编译器发出无匹配的错误
3、有多于一个函数可以匹配也会发生错误,称为二义性调用
重载和作用域
函数只会调用最近的作用域中的重载函数。
6、5特殊用途语言特性
默认实参
定义成如下
string screen(sz ht = 24; sz wid = 80; char backgrnd = ' ')
一旦每个形参被赋予了初值,剩下的都必须有默认值。
实际调用时只能省略尾部的实参。
window = screen(66)
window = screen('?') /实际调用screen('?', 80,' ')
默认实参声明
通常放在头文件,且一个函数只声明一次。
每个形参只能被赋予一次默认值,如果后续声明为了添加默认实参,也是允许的。
默认实参初始值
局部变量不能作为默认实参,除此之外只要表达式的类型能转换成形参所需要的类型,该表达式就能作为默认实参。
sz wd = 80;
char def = '';
sz ht();
string screen(sz = ht(); sz = wd; char = def);
如果:
void f2()
{
def = '*; //更新了传递的值
sz wd = 100; //隐藏了外层的wd,但与传递的wd无关
window = screen(); //调用screen(ht(), 80, '*');
}
内联函数和constexpr
调用函数有一系列工作,会比较慢
内联函数可以避免函数调用的开销
在函数返回类型前加上inline,声明成内联函数
inline const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= se.size() ? s1 : s2;
}
一般用于优化规模较小,流程直接,频繁调用的函数。很多编译器不支持内联递归函数。
constexpr函数
指能用于常量表达式的函数。
定义时候的约定:
1、函数的返回类型及所有形参类型都是字面值类型
2、函数体中必须有且只有一个return
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz();
在编译中直接替换成结果值,constexpr函数被隐式地指定为内联函数。
constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作。
把内联函数和constexpr函数放在头文件
内联函数和constexpr函数可以多次定义,但是它的多个定义必须完全一致。
调试帮助
程序可以包含一下用于调试的代码,在发布时要屏蔽,用到两个预处理功能。assert和NDEBUG
assert
是一种预处理宏,即一个预处理变量。使用一个表达式作为条件
assert(expr);
如果表达式为假,assert输出信息并终止程序;否则什么也不做。
定义在cassert头文件中,名字由预处理器管理,因此不需要提供using声明。
常常用于检查“不能发生”的条件。
NDEBUG
assert的行为依赖于NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没定义。
可以使用#define定义NDEBUG,从而关闭调试状态。
很多编译器都提供了一个命令行选项是我们可以预处理变量
$ CC -D NDEBUG main.c # use /D with the Microsoft compiler
这条命令等价于在开头写#define NDEBUG
还可以使用NDEBUG编写自己的调试代码,如果NDEBUG没定义,将指向#ifndef和#endif之间的代码
void print(const int io[], sizt_t size)
{
#ifndef NDEBUG
cerr << __func__ << ": array size is" << size << endl;
#endif
}
__func__输出当前函数的名字。
编译器为每个函数都定义了一些名字
__FILE__ //存放文件名的字符串字面值
__LINE__ //存放当前行号的整型字面值
__TIME__ //存放文件编译时间的字符串字面值
__DATE_- //存放文件编译日期的字符串字面值
6、6函数匹配
有时候重载函数太多,不容易选择。
确定候选函数和可行函数
函数匹配第一步是选本次调用对应的重载函数集。
两个特征:1、同名 2、声明在调用点可见。
第二步考察实参,然后选出可行函数。
两个特征:1、形参与调用提供的实参个数相等。2、类型也相同
第三步:寻找最佳匹配。逐一检查提供的实参类型,寻找最匹配的可行函数。
void f(int);
void f(double, double = 3.14);
f(3.14); //调用第二个
含有多个形参的函数匹配
如果有且只有一个函数满足下列条件则匹配成功:
1、函数的每个实参匹配都不劣于其他可行函数需要的匹配。
2、至少有一个实参的匹配优于其他可行函数的匹配。
如果没有函数可以,就是错误的,会报错。
void f(int, int);
void f(double, double = 3.14);
f(3, 3.14) //直接报错,有二义性。
实参类型转换
为了确定最近匹配,编译器将实参类型到形参类型的转换分了几个等级:
1、精确匹配(类型相同、转换成对应指针、添加顶层const或删除顶层const)
2、通过const转换实现的匹配
3、类型提升
4、算数转换或指针转换
5、通过类类型转换
需要算数提升和转换的匹配
如果有两个函数,优先算术提升
void ff(int);
void ff(short);
ff('a'); //提升为int调用int的
算术类型转换的级别都一样
void manip(long);
void manip(float);
manip(3.14); //错误,二义性调用
函数匹配和const实参
如果重载函数的区别在于引用类型的形参是否引用了const,或者指针类型的形参是否指向const,那么就会根据实参是否是变量来决定使用哪个。
如果传入const对象,所以唯一可行的函数是常量引用函数。
如果传入普通对象,两个函数都可调用,优先使用非常量版本的函数。
6、7函数指针
指向的是函数而非对象。
bool (*pf)(const string &, const string &);
pf是一个指向函数的指针,该函数的参数是两个const string引用,返回值是bool。
使用函数指针
把函数名作为一个值使用时,函数自动转换成指针
pf = lengthCompare; //pf指向这个函数
pf = &LengthCompare; //等价的,取地址符时可选的
调用时可以直接使用,不需要解引用
boo1 b1 = pf("Hello", "goodbye");
boo1 b2 = (*pf)("Hello", "goodbye");
bool b3 = lengthCompare("Hello", "goodbye");
//三者等价
函数指针可以赋值为nullptr或者0,表示没有指向函数。
指向时函数的返回类型,形参类型必须精确匹配。
重载函数的指针
使用重载函数时,指针类型必须与重载函数中的某一个精确匹配
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff;
void (*pf2)(int) = ff;
void (*pf3)(int*) = ff;
函数指针形参
可以直接把函数作为实参使用,自动转换成指针
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
useBigger(s1, s2, lengthCopmare);
直接使用函数指针类型很冗长,用类别别名和decltype可以简化函数指针
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; //等价类型,是函数类型
typedef bool (*Func2)(const string&, const string&);
typedef decltype(lengthCompare) *Func2; //等价类型,是指向函数的指针类型
然后可以如下重新声明函数
void useBigger(const string &s1, const string &s2, Func); //编译器自动将Func表示的函数类型转换成指针。
void useBigger(const string &s1, const string &s2, Func2);
返回指向函数的指针
一种方法是使用别名
using F = int(int*, int); //F是函数
using PF = int(*)(int* ,int); //F是指针
PF fl(int); //正确,PF是指针,fl返回指向函数的指针
F fl(int); //错误,F是函数,不能返回一个函数
F *fl(int); //正确,显式返回
完整的如下:
int (*fl(int))(int*, int);
fl是个函数,fl返回一个指针,指针指向的函数返回int。
使用尾置返回
auto fl(int) -> int (*)(int*, int);
将auto和decltype用于函数指针类型
如果明确知道返回的函数是哪一个,就使用decltype简化。
将decltype作用于某个函数时,本身返回函数类型而非指针,因此要加上*
size_type sumLength(const string&, const string&);
decltype(sumLength) * getFun(const string &);