C++ Primer 第六章笔记 函数
1. 函数基础
一个函数由定义包含以下部分:返回类型,函数名字,由0个或者多个形参组成的列表以及函数体。
int fact(int val)
{
int ret=1;
while(val>1)
ret * = val--;
return ret;
}
执行函数的第一步是定义并初始化它的形参,因此当调用fact函数时,首先创建一个名为val的int变量,然后将它初始化为调用时所用的实参。实参必须能转换为指定的类型
函数体是一个语句块,构成一个新的作用域,我们可以在其中定义变量,形参和函数体内部定义的变量统称为局部变量,仅在函数的作用域内可见。
自动对象
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾销毁它,我们将只存在于块执行期间的对象称为自动对象。当块的执行结束之后,块中创建的自动对象的值变成未定义的。
形参是一种自动对象,函数开始时为形参申请存储空间,因为形参定义在函数的作用域之内,所以一旦函数被终止,形参被销毁。
###局部静态对象###
局部对象:某些时候,需要使局部变量的生命周期贯穿函数被调用及以后的时间。可以将局部变量定义为static类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,知道程序终止时销毁,函数结束执行对它没有影响。
size_t count_calls()
{
static size_t ctr = 0;
return ++ctr;
}
int main()
{
for (int i = 0; i < 10; ++i) {
std::cout << count_calls() << std::endl;
}//依次打印1,2,3
return 0;
}
静态局部变量只赋值一次,以后每次调用函数不再赋初值而是保留上次函数调用结束时的值。
虽然静态局部变量在函数调用后仍然存在,但是在其他函数中是不能引用它的。
分离式编译
C++语言支持分离式编译,允许讲程序分割到几个文件中,每个文件独立编译。
比如Chapter.h中声明了fact函数,而fact.cc定义了fact函数,fact.cc中包含了头文件Chapter.h中,另外我们在名为factMain.cc的文件中创建main函数,并调用fact函数。要生成可执行文件,必须告诉编译器所用到的代码在哪。
编译过程如下
gcc factMain.cc fact.cc #生成factMain.exe或者a.out
gcc factMain.cc fact.cc -o main #生成main或者main.exe
如果我们修改了其中一个源文件,只需要重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这个过程通常会产生一个后缀名为.obj或者.o的文件,后缀名的含义是该文件包含对象代码。
实际编译过程如下
gcc -c factMain.cc #生成factMain.o
gcc -c fact.cc #生成fact.o
gcc factMain.o fact.o #生成factMain.exe或者a.out
gcc factMain.o fact.o -o main #生成main或者main.exe
2. 参数传递
形参的类型决定了形参和实参交互的方式,如果形参是引用类型,它将绑定到对应的实参上,否则将实参的值拷贝后赋给形参,对变量的改动不会影响大初始值。
传值参数
当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时对变量的改动不会影响初始值
指针形参
指针的行为与其他非引用类型一样,当形参的类型是指针时,拷贝的是指针的值,拷贝之后两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。
void reset(int *ip)
{
*ip = 0;
}
int main()
{
int i = 42;
reset(&i);
std::cout << "i = " << i << std::endl;
}
传引用参数
void reset(int &i)
{
i=0;//改变了i所引用对象的值
}
使用引用避免拷贝
拷贝大的类型对象或者容器对象比较低效,甚至有的类类型不支持拷贝操作,这个时候可通过引用形参来访问该类型的对象。
还可以使用引用返回额外的信息
一个函数只能返回多个值,然而有时候需要同时返回多个值,引用形参可以使我们能返回多个结果(还有一种办法是定义一种新的数据类型,包含位置和数量两个成员)。
const形参和实参
顶层const作用于对象本身
const int ci =24;
int i = ci;//正确,当拷贝ci时,忽略了它的顶层const
与变量的初始化过程一眼,当用实参初始化形参时会忽略掉底层const,即当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
void fcn(const int i){/*fcn能够读取i,但是不能向i写值*/}
在C++语言中,允许定义若干个具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层const被忽略掉了,所以下面的两个fcn函数的参数可以完全一眼,因此第二个fcn重复定义,错误。
尽量使用常量引用
将函数不会改变的形参定义为普通引用是一种常见的错误,将误导函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用会极大地限制函数所能接受的实参类型,比如不能将const,字面值或者需要类型转换的对象。而且,加入其他函数将他们的形参定义为常量引用,那么无法在此类函数中调用 参数类型为普通引用的函数。
数组形参
注意数组的两个特殊性质:不允许拷贝数组以及使用数组时通常会转换为指针。所以不能以值传递的方式使用数组参数,因为数组会被转换为指针,所以我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
//下面三个print函数是等价的,又有一个const int*的形参
void print(const int*)
void print(const int[])
void print(const int[10])//这里的10是我们期望数组的维度,实际上不一定。如果我们传递给函数的是一个数组,实参将自动转换成指向数组首元素的指针,数组大小对函数的调用没有影响。
因为数组是以指针的形式传递给数组的,一开始函数不知道数组的长度,需要提供额外的信息,管理指针形参有三种常见的方法。
- 根据结束标记判断,比如C语言风格字符串存储在字符数组中,并且最后一个有效字符跟着一个空字符。
void print(const char *cp)
{
if (cp) {
while (*cp) {
std::cout << *cp++ << std::endl;
}
}
}
- 使用标准库规范。
void print(const int *beg,const int *end)
{
while (beg != end) {
std::cout << *beg++ << std::endl;
}
}
- 显示传递一个表示数组大小的形参
void print(int (&arr)[10])
{
for (auto elem : arr)
cout<< elem <<endl;
}
&arr 两端的括号必不可少,int &arr[10]将arr声明为元素是引用的数组。
具有可变形参的函数
如果函数的实参数量未知,但是全部实参类型相同,我们可以试用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。
和vector不一样的是,initializer_list对象中的元素永远是常量值。
3. 返回类型和return语句
返回值类型为void的函数,不要求一定有return函数,这类函数最后依据隐式执行return。
值是如何被返回的
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
//挑出两个string对象中较短的那个,返回其引用
const string &shorterString(const string &s1,const string &s2)
{
return s1.size()<s2.size()?s1:s2;
}
不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉,因此,函数终止意味着局部变量的引用将指向不再有效的内存区域:
引用返回左值
函数的返回类型决定了给函数调用是否是左值,调用一个返回引用的函数得到左值。其他返回类型得到右值。可以为返回类型是非常量引用的函数的结果赋值。
char &get_val(string &str, string::size_type ix)
{
return str[ix]; // get_val assumes the given index is valid
}
int main()
{
string s("a value");
cout << s << endl; // prints a value
get_val(s, 0) = 'A'; // changes s[0] to A
cout << s << endl; // prints A value
return 0;
}
返回数组指针
因为数组不能被拷贝,所以函数不能返回数组。不过函数可以返回数组的指针或者引用。 定义一个返回数组的指针或引用的函数比较繁琐;但有一些方法可以简化这一任务,最直接的方法是使用类型别名。
typedef int arrT[10];
using arrT=int[10];
arrT=func(int i);
声明一个返回数组指针的函数
int (*p2)[10]=&arr; //p2是一个指针,它指向含有10个整数的数组
和声明变量类似,返回数组指针的函数形式如下所示
Type (*function (parameter_list))[dimension]
Type表示数组中元素的类型,dimension表示数组的大小
例如 int (*func(int i))[10];
使用尾置返回类型
任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个 -> 符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们本应该出现返回类型的地方放置一个 auto。
当复杂度提升后这种设计效果不明显,此时应该考虑decltype。
//传统方法
int (* func1(int arr[][3], int n))[3] {
return &arr[n];
}
auto func1(int arr[][3], int n) -> int(*)[3] {
return &arr[n];
}
使用decltype
如果我们知道返回的指针指向哪个数组,可以使用decltype声明返回类型。
int odd[]={1,3};
int even[]={2,4};
decltype(odd) *arrPtr(int i)
{
return (i%2)?odd:even;
}
decltype表示返回类型是个指向含有5个整数的数组的指针,但是注意decltype并不负责将数组类型转换为相应的指针,表示 arrPtr 返回指针还必须在函数声明时加一个 * 符号。
4. 函数重载
重载和const形参
顶层 const 不影响传入函数的对象。一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来:
Record lookup(Phone);
Record lookup(const Phone);//重复声明
另一方面,如果形参是某种类型的指针或者引用,可以通过区分其指向的对象是常量对象还是非常量对象实现函数重载,此时的const是底层的。
Record lookup(Account*);
Record lookup(const Account*);//新函数,作用与指向常量的指针..引用类似
const_cast和重载
const string &shorterString(const string &s1,const string &s2)
{
return s1.size()<s2.size()?s1:s2;
}
如果我们需要一种新的shorterString函数,当它的实参不是常量时,得到的结果是一个普通的引用,使用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);
}
重载和作用域
将所有print放在同一个作用域内,print(int)是正确的重载形式,此时编译器可看到所有三个函数。
void print(const string&);
void print(double);
void print(int);
void fooBar2(int val)
{
print("Value:");
print(val);
print(3.14);//调用print(double)
}
但是如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名
string read();
void print(const string&);
void print(double);
void fooBar2(int val)
{
bool read=false;//函数read被隐藏
string s=read();//read是一个布尔值,而非一个函数
void print(int);//新作用域,隐藏了之前的print
print("Value:");//错误,之前的void print(const string&)被隐藏了
print(val);
print(3.14);//调用print(double)
}
5.特殊用途语言特性
默认实参
typedef string::size_type sz;
string screen(sz ht =24,sz wid =80,char backgnd=' ');
如果我们想使用默认实参,直接省略该实参就可以了,需要注意默认实参的位置,一般将经常使用的默认实参出现在最后面。
默认实参声明
编译器禁止声明和定义同时定义缺省参数值。通常应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
内联函数和constexpr函数
引入内联函数的目的是为了解决程序中函数调用的效率问题,这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的i节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要留神:
- 在内联函数内不允许使用循环语句和开关语句;
- 内联函数的定义必须出现在内联函数第一次调用之前;
- 在类定义中定义的函数都是内联函数。
constexpr:常量表达式是指值不会变化并且在编译过程中就能得到计算结果的表达式。
constexpr变量,允许将变量声明为constexpr类型,以便由编译器验证变量的值是否是一个常量表达式。
constexpr函数是值可用于常量表达式的函数,定义constexpr函数的方法和其他函数的方法类似。函数返回类型及所有形参的类型都是字面值类型。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
和其他的函数不一样,内联函数和constexpr函数可以在程序中多次定义。但是它的多个定义必须是一致的。因此内联函数和constexpr函数通常定义在头文件中。
调试帮助
assert 是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为类似于内联函数,assert 宏使用一个表达式作为它的条件:
assert(expr);
首先对expr求值,如果表达式为假(即0)assert输出信息并终止程序。
NDEBUG 预处理变量
assert 的行为依赖一个名为 NDEBUG 的预处理变量的状态。如果定义了 NDEBUG,则 assert 什么也不做。默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。
可以使用一个 #define 语句定义 NDEBUG,从而关闭调试状态。同时,很多编译器都提供了一个命令行选项使我们可以定义预处理变量:
$ CC -D NDEBUG main.C #等价于在 main.c 文件的一开始写 #define NDEBUG
6.函数匹配
略
7.函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。
//比较两个string对象的长度
bool lengthCompare(const string &,const string &);
//声明一个可以指向该函数的指针 pf
bool (*pf)(const string &,const string &);
*pf两端的括号不可少,如果没有括号,pf是一个返回值为bool指针的函数。
bool *pf(const string &,const string &);//pf返回的是bool指针
使用函数指针
将函数名作为一个值使用时,该函数自动转换为指针。
pf = lengthCompare;
pf = &lengthCompare; //等价的赋值语句,取地址符是可取的
//可以使用指向函数的指针调用该函数,无须提前解引用指针
bool b1 = pf("hello","goodbye");
bool b2 = (*pf)("hello","goodbye");
在指向不同函数类型的指针不存在转换规则。但是和往常一样,我们可以为函数指针赋一个 nullptr 或者值为 0 的整形常量表达式,表示该指针没有指向任何一个函数:
string::size_type sumLength(const string&, const string&);
bool cstringCompare(const char*, const char*);
pf = 0; // ok: pf points to no function
pf = sumLength; // error: return type differs
pf = cstringCompare; // error: parameter types differ
pf = lengthCompare; // ok: function and pointer types match exactly
重载函数的指针
编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中某一个精确匹配
void ff(int*);
void ff(unsigned int);
void (*pf2)(int) =ff; //错误,没有任何一个ff和该形参列表匹配
double (*pf3)(int*) = ff; //错误,ff和pf3的返回类型不匹配
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来时函数类型,实际上却时被当成指针使用:
// third parameter is a function type and is automatically treated as a pointer to function
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
// equivalent declaration: explicitly define the parameter as a pointer to function
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
直接把函数作为实参使用,此时它会自动转换成指针:
正如 useBigger 的声明语句所示,直接使用函数指针类型显得冗长而烦琐。类型别名和 decltype 能让我们简化使用了函数指针的代码:
// Func and Func2 have function type
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; // equivalent type
// FuncP and FuncP2 have pointer to function type
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; // 等价类型
Func 和 Func2 是函数类型,而 FuncP 和 FuncP2 是指针类型。需要注意的是,decltype 返回函数类型,此时不会讲函数类型自动转换成指针类型。因为 decltype 的结果是函数类型,所以只有在结果前面加上 * 才能得到指针。可以使用如下的形式重新声明 useBigger:
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, FuncP2);
返回指向函数的指针
和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。与往常一样,要想声明一个返回函数指针的函数,最简单的方法是使用类型别名:
using F = int(int*, int); //F是函数类型
using PF = int(*)(int*, int); // PF是指针
其中我们使用类型别名将 F 定义成函数类型,将 PF 定义成指向函数类型的指针。必须注意的是,和函数类型不一样,返回类型不会自动地转换成指针。我们必须显示地将返回类型定义成指针:
PF f1(int); //对,返回指向函数的指针
F f1(int); //错误,不能直接返回函数
F *f1(int); //正确,显示地指定返回类型是指向函数的指针
将auto和decltype用于函数指针类型
string::size_type sumLength(const string&,const string&);
string::size_type largeLength(const string&,const string&);
decltype(sumLength) *getFcn(const string &);