0x05_函数

发布时间:2022-06-28 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了0x05_函数脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

函数

我们通过调用运算符来执行函数,调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。

函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时主调函数的执行被暂时中断,被调函数开始执行。执行函数的第一步时(隐式地)定义并初始化它的形参。当遇到一条return语句时函数结束执行过程。和函数调用一样,return语句完成两项工作:一是返回return语句中的值,二是将控制权从被调函数转移回主调函数。

函数的形参名是可选的,但是由于我们无法使用未命名的形参,所以形参一般有名字。当函数确实有个别形参不会被用到,则此类形参通常不命名以表示在函数体内不会使用它。但即使某个形参不被函数使用,也必须为它提供一个实参。

函数基础

局部对象

某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过对象语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。

函数声明

因为函数的声明不包含函数体,所以也就无须形参的名字。尽管如此,协商形参的名字可以帮助使用者更好地理解函数的功能。

函数的三要素:返回类型、函数名、形参类型描述了函数的接口。函数声明也称作函数原型。

在头文件中进行函数声明:

函数应该在头文件中声明而在源文件中定义。能确保同一函数的所有声明保持一致,而且一旦想改变函数的接口,只需改变一条声明即可。定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

参数传递

当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。引用形参是它绑定的对象的别名。当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象,我们说这样的实参被值传递或者函数被传值调用。

传值参数

当初始化一个非引用类型的变量时,初始值被拷贝给变量,此时对变量的改动不会影响初始值。指针的行为和其他非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针,但都指向同一个内存为止,通过指针可以修改它所指对象的指。

传引用参数

通过使用引用形参,允许函数改变一个或多个实参的值。拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

如比较两个string对象的长度,因为stirng对象可能非常长,应尽力避免直接拷贝它们,这时使用引用形参是明智的选择。又因为比较长度无须改变string对象的内容,所以把形参定义成对常量的引用。

bool isShorter(const string &s1, const string &s2);

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为一次返回多个结果提供了有效的途径。

const形参和实参

当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:

void fcn(const int i) { /* fcn能够读取i,但是不能向i写值 */ };

当用实参初始化形参时,调用者会忽略形参的顶层const,而这可能造成意想不到的后果:

void fcn(const int i) {}
void fcn(int i) {}                 // 错误:重复定义了fcn(int)

因为顶层const被忽略掉了,所以在上面代码中传入两个fcn函数的参数可以完全一样,因此第二个fcn是错误的。

指针或引用形参与const

形参的初始化方式和变量的初始化方式是一样的,可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。

int i = 42;
const int *cp = &i;                // 正确:但是cp不能改变i
const int &r = i;                  // 正确:但是r不能改变i
const int &r2 = 42;                // 正确
int *p = cp;                       // 错误:p的类型和cp的类型不匹配
int &r3 = r;                       // 错误:r3的类型和r的类型不匹配
int &r4 = 42;                      // 错误:不能用字面值初始化一个非常量引用

要想调用形参为引用的函数,只能使用int类型的对象,而不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象。类似的,要想调用形参为指针版本的函数,只能使用int *。C++允许用字面值初始化常量引用,所以当函数的引用形参是常量引用时,可以传递一个字面值。

尽量使用常量引用

把函数不会改变的形参定义成普通引用是一种比较常见的错误,这样给函数的调用着一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大的限制函数所能接收的实参类型,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

// 不良设计:第一个形参的类型应该是const string&
string::size_type find_char(string &s, char c, string::size_type &occurs);

// 此时只能将find_char函数作用于string对象,下面的调用会编译错误
find_char("Hello world", 'o', ctr);

加入其他函数将它们的形参定义成常量引用,则此find_char无法在此函数中正常使用:

bool is_sentence(const string &c) {
    string::size_type ctr = 0;
    return find_char(s, '.', ctr) == s.size() - 1 && ctr == 1;
}

这段代码将在编译时发生错误,原因在于s是常量引用,但find_char被定义成只能接收普通引用。

数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响:不允许拷贝数组以及使用数组时通常会将其转换成指针。所以我们无法以值传递的方式使用数组参数,且实际上为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:

void print(const int*);
void print(const int[]);
void print(const int[10]);

尽管表现形式不同,但上面三个函数是等价的:每个函数的唯一形参都是const int *类型的。

因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。

使用标记指定数组长度

第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串。C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止。

void print(const char *cp) {
    if (cp) {
        while (*cp)
            cout << *cp++;
    }
}

使用标准库规范

第二种技术是传递指向数组首元素和尾后元素的指针,只要调用者能正确地计算指针所指的位置,那么上述代码就是安全的。在这里我们使用标准库begin和end函数提供所需的指针。

void print(const int *beg, const int *end) {
    while (beg != end) {
        cout << *beg++ << endl;
    }
}

显式传递一个表示数组大小的形参

void print(const int ia[], size_t size) {
    for (size_t i = 0; i != size; ++i) {
        cout << ia[i] << endl;
    }
}

只要传递给函数的size值不超过数组实际的大小,函数就是安全的。

数组形参和const

当函数不需要对数组元素执行写操作时,数组形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参

C++语言允许将变量定义成数组的引用,所以形参也可以是数组的引用,此时,引用绑定到对应的实参上,即绑定到数组上。

void print(int (&arr)[10]) {}            // 其中&arr的两端必不可少

因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是这一用法也无形中限制了print函数的可用性,我们只能将函数作用于大小为10的数组。在以后的学习中将介绍如何编写这个函数,使其可以给引用类型的形参传递任意大小的数组。

传递多维数组

在C++语言中实际上并没有真正的多维数组,所谓多维数组其实是数组的数组。和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略。

void print(int (*matrix)[10], int rowSize) { /* ... */ }

我们也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表中:

void print(int matrix[][10], int rowSize) { /* ... */ }         // 等价定义

main:处理命令行选项

main函数是演示C++程序如何向函数传递数组的好例子。有时我们需要给main传递实参,如通过命令行执行程序,这些命令行选项通过两个(可选的)形参传递给main函数:

int main(int argc, char *argv[]) { ... }

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针:第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:

int main(int argc, char **argv) { ... }

当实参传递给main函数后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递给命令行提供的实参,最后一个指针之后的元素值保证为0,即argv[0] = "main", argv[argc] = 0;

含有可变形参的函数

有时无法提前预知应该向函数传递几个实参,为了编写能处理不同数量实参的函数,C++新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,可以编写一种特殊的函数,即可变参数模板。

C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。这种功能一般只用于与C函数交互的接口程序。

initialize_list形参

如果函数的实参数量未知但是全部实参的类型都相同,可以使用initializer_list类型的形参。它是一种标准库类型,用于表示某种特定类型的值的数组,其类型定义在同名的头文件中,提供的操作表如下:

0x05_函数

和vector一样,initializer_list也是一种模板类型,定义时必须说明列表中所含元素的类型。但和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变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({"functionX", expected, actual});   // expected和actual是string对象
error_msg({"functionX", "okay"});

含有initializer_list形参的函数也可以同时拥有其他形参。如:

void error_msg(ErrCode e, initializer_list<string> il);

error_msg(ErrCode(42), {"functionX", expected, actual});
error_msg(ErrCode(0), {"functionX", "okay"});

省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常省略符形参不应用于其他目的。

省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:

void foo(parm_list, ...);
void foo(...);

第一种形式执行了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无须类型检查。在第一种形式中,形参声明后面的逗号是可选的。

返回类型和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";                // 错误:"Empty"是一个局部临时量
}

上面的两条return语句都将返回未定义的值,即将引发未定义的行为。对于第一条return语句来说,它返回的是局部对象的引用,第二条语句中,字符串字面值转换成一个局部临时string对象。当函数结束时临时对象占用的空间也就随之释放掉,所以两条return语句都指向了不再可用的内存空间。 同样,返回局部对象的指针也是错误的,一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。

返回类类型的函数和调用运算符

调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律。因此,如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员。

auto sz = shorterString(s1, s2).size();

引用返回左值

函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值,可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值。

char &get_val(string &str, string::size_type ix) {
    return str[ix];
}

get_val(s, 0) = 'A';

如果返回类型是常量引用,我们不能给调用的结果赋值。

列表初始化返回值

C++11新标准规定,函数可以返回花括号包围的值的列表。

vector<string> process() {
    // expected 和 acutal是string对象
    if (expected.empty()) {
        return {};
    } else if (expected == actual) {
        return {"functionX", "okay"};
    } else {
        return {"functionX", expected, actual};
    }
}

如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。

主函数main的返回值

如果函数的返回类型不是void,那么它必须返回一个值。但是这条规则有个例外:我们允许main函数没有return语句直接结束,如果控制到达了main函数的结尾处且没有return语句,编译器将隐式地插入一条返回0的return语句。

main函数的返回值可以看做是状态指示器,返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,可以使用这两个变量分别表示成功与失败:

int main() {
    if (some_failure)
        return EXIT_FAILURE;
    else
        return EXIT_SUCCESS;
}

因为是预处理变量,所以既不能在前面加上std::,也不能在using声明中出现。

返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过函数可以返回数组的指针或引用。要想定义一个返回数组的指针或引用的函数比较繁琐,可以使用类型别名简化:

typedef int arrT[10];           // arrT是一个类型别名,表示的类型是含有10个整数的数组
using arrT = int[10];           // arrT的等价声明
arrT* func(int i);              // func返回一个指向含有10个整数的数组的指针

声明一个返回数组指针的函数

要想在声明func时不使用类型别名,必须牢记被定义的名字后面数组的维度:

int arr[10];                    // arr是一个含有10个整数的数组
int *p1[10];                    // p1是一个含有10个指针的数组
int (*p2)[10] = &arr;           // p2是一个指针,指向含有10个整数的数组

如果想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后,然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此返回数组指针的函数形式如下:

Type (*function(parameter_list))[dimension];

// 区别于函数指针
bool (*pf)(const string &, const string &);

可以按照以下顺序来逐层理解:

  • func(int i)表示调用func函数时需要一个int类型的实参。
  • (*func(int i))意味着我们可以对函数调用的结果执行解引用操作。
  • (*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组。
  • int (*func(int i))[10]表示数组中的元素是int类型。

使用尾置返回类型

在C++新标准中还有一种可以简化上述func声明的方式,即使用尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效。尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:

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;
}

需要注意的是,decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想表示arrPtr返回指针还必须在函数声明时加一个*符号。

函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数。

main函数不能重载。

对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。不允许两个函数除了返回类型外其他所有的要素都相同。

重载和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是底层的:

// 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
Record lookup(Account &);                     // 函数作用于Account的引用
Record lookup(const Account &);               // 新函数,作用于常量引用

Record lookup(Account *);                     // 新函数,作用于指向Account的指针
Record lookup(const Account *);               // 新函数,作用于指向常量的指针

编译器可以通过实参是否是常量来推断应该调用哪个函数。因为const不能转换成其他类型,所以我们只能把const对象(或指向const的指针)传递给const形参。相反的,因为非常量可以转换成const,所以上面的函数都能作用于非常量对象或者指向非常量对象的指针。不过当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。

const_cast和重载

const_cast在重载函数的情景中最有用。如下函数:

// 比较两个string对象的长度,返回较短的那个引用
const string &shorterString(const string &s1, const string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

这个函数的参数和返回类型都是const string的引用。我们可以对两个非常量的string实参调用这个函数,但返回的结果仍然是const string的引用。我们需要一种新的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);
}

调用重载的函数

函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫重载确定。编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。

当调用重载函数时有三种可能的结果:

  • 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
  • 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息。
  • 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用。

重载与作用域

一般来说,将函数声明置于局部作用域内不是一个明智的选择。

重载对作用域的一般性质并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。

string read();
void print(const string &);
void print(double);                   // 重载print函数
void fooBar(int ival) {
    bool read = false;                // 新作用域:隐藏了外层的read
    string s = read();                // 错误:read是一个布尔值,而非函数
    void print(int);                  // 新作用域:隐藏了之前的print
    print("Value:");                  // 错误:print(const string &)被隐藏掉了
    print(ival);                      // 正确:当前print(int)可见
    print(3.14);                      // 正确:调用print(int);
}

在C++中,名字查找发生在类型检查之前。

特殊用途语言特性

默认实参

调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。例:

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');

其中我们为每一个形参都提供了默认实参,默认实参作为形参的初始值出现在形参列表中。我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

使用默认实参调用函数

如果想使用默认实参,只要在调用函数时省略该实参就可以。函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。例如要想覆盖backgrnd的默认值,必须为ht和wid提供实参。

当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。

默认实参声明

对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的,需要注意的是:在给定的作用域中一个形参只能被赋予一次默认实参。即函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。

string screen(sz, sz, char = ' ');
// 我们不能修改一个已经存在的默认值
string screen(sz, sz, char = '*');             // 错误:重复声明
// 可以添加默认实参
string screen(sz = 24, sz = 80, char);         // 正确:添加默认实参

默认实参初始值

局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:

// wd、def和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);

用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时。

内联函数和constexpr函数

把规模较小的操作定义成函数有很多好处,主要包括:

  • 阅读和理解函数的调用要比读等价的条件表达式容易得多。
  • 使用函数可以确保行为的统一,每次相关操作都能保证按照同样的方式进行。
  • 如果我们需要修改计算过程,修改函数比找到等价表达式的所有出现的地方再逐一修改要容易。
  • 函数可以被其他应用重复利用,省去了重新编写的代价。

但使用函数也有一个潜在的缺点:调用函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数可避免函数调用的开销

将函数指定内联函数,通常就是将它的每个调用点上“内联地”展开,从而消除函数的运行时开销。通过在函数的返回类型前加上关键字inline,就可以将它声明成内联函数:

inline const string &shorterString(const string &s1, const string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。

内联函数过长会引起代码膨胀,一般来说,内联机制用于优化规模较小,流程直接、频繁调用的函数。

constexpr函数

constexpr函数是指能用于常量表达式的函数。constexpr函数要遵循约定:函数的返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句:

constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();                    // 正确:foo是一个常量表达式

执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。

constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作,如空语句、类型别名以及using声明。

我们允许constexpr函数的返回值并非一个常量:

constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }

当scale的实参是常量表达式时,它的返回值也是常量表达式,反之不对:

int arr[scale(2)];                // 正确:scale(2)是常量表达式
int i = 2;                        // i不是常量表达式
int a2[scale(i)];                 // 错误:scale(i)不是常量表达式

如果用一个非常量表达式调用scale函数,比如int类型的对象i,则返回值是一个非常量表达式。当把scale函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求,如果结果恰好不是常量表达式,编译器将发出错误信息。

constexpr函数不一定返回常量表达式。

把内联函数和constexpr函数的定义放在头文件内

和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟编译器想要展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致,所以通常定义在头文件中。

调试帮助

程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用,当应用程序编写完成准备发布时,要先屏蔽掉调试代码。

assert预处理宏

它的行为类似于内联函数,assert宏使用一个表达式作为它的条件:

assert(expr);

首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行,如果表达式为真(即非0),assert什么也不做。

assert宏常用于检查不能发生的条件,如一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阈值:

assert(word.size() > threshold);

NDBUG预处理变量

assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。

assert应该仅用于验证那些确实不可能发生的事情,我们可以把assert当成调试程序的一种辅助手段,但是不能用它来替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。

C++编译器定义了几个对程序调试很有用的名字:

  • __func__存放当前的函数名的const char数组。
  • __FILE__存放文件名的字符串字面值。
  • __LINE__存放当前行号的整型字面值。
  • __TIME__存放文件编译时间的字符串字面值。
  • __DATE__存放文件编译日期的字符串字面值。

函数匹配

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6);                             // 调用void f(double, double);

确定候选函数和可行函数

函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。

在这个例子中,有4个名为f的候选函数。

第二步是考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。

我们能根据实参的数量从候选函数中排除掉两个。不使用形参的函数和使用两个int形参的函数都不适合本次调用。

如果函数含有默认实参,则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。

在使用实参数量初步判别了候选函数后,接下来考察实参的类型是否与形参匹配。实参与形参匹配的含义可能是它们具有相同的类型,也可能是实参类型和形参类型满足转换规则。

  • f(int)是可行的,因为实参类型double能转换成形参类型int。
  • f(double, double)是可行的,因为它的第二个形参提供了默认值,而第一个形参类型与函数使用的实参类型完全一致。

如果没找到可行函数,编译器将报告无匹配函数的错误。

寻找最佳匹配

函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。基本思想是:实参类型与形参类型越接近,它们匹配的越好。

含有多个形参的函数匹配

对于名为f的函数,参数(42, 2.56)调用时发生的情况如下:

选择可行函数的方式和只有一个实参时一样,编译器选择那些形参数量满足要求且实参类型和形参类型能够匹配的函数,此例中,可行函数包括f(int, int)f(double, double)。接下来,编译器依次检查每个实参以确定哪个函数是最佳匹配。如果有且只有一个函数满足下列条件,则匹配成功:

  • 该函数每个实参的匹配都不劣于其他可行函数需要的匹配。
  • 至少有一个实参的匹配优于其他可行函数提供的匹配。

如果在检查了所有的实参后,没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。

在上面的调用中,只考虑第一个实参时我们发现函数f(int, int)能精确匹配;要想匹配第二个函数,int类型的实参必须转换成double类型。显然需要内置类型转换的匹配劣于精确匹配,因此仅就第一个实参来说,f(int, int)f(double, double)更好。

接着考虑第二个实参,此时f(double, double)是精确匹配;要想调用f(int, int)必须将第二个实参从double类型转换成int类型。因此仅就第二个实参来说,f(double, double)更好。

编译器最终将因为这个调用具有二义性而拒绝其请求:因为每个可行函数各自在一个实参上实现了更好的匹配,从整体上无法判断孰优孰劣。看起来似乎我们可以通过强制类型转换其中一个实参来实现函数的匹配,但是在设计良好的系统中,不应该对实参进行强制类型转换。

实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下:

  1. 精确匹配,包括以下情况:
    1. 实参类型和形参类型相同。
    2. 实参从数组类型或函数类型转换成对应的指针类型。
    3. 向实参添加顶层const或者从实参中删除顶层const。
  2. 通过const转换实现的匹配。
  3. 通过类型提升实现的匹配
  4. 通过算术类型转换或指针转换实现的匹配。
  5. 通过类类型转换实现的匹配。

需要类型提升和算术类型转换的匹配

假设有两个函数,一个接受int、另一个接受short,则只有当调用提供的是short类型的值时才会选择short版本的函数。有时,即使实参是一个很小的整数值,也会直接将它提升成int类型;此时使用short版本反而会导致类型转换:

void ff(int);
void ff(short);
ff('a');                                   // char提升成int;调用f(int)

所有算术类型转换的级别都一样,如从int向unsigned int的转换并不比从int向double的转换级别高。考虑:

void manip(long);
void manip(float);
manip(3.14);                               // 错误:二义性调用

字面值3.14的类型是double,它既能转换成long也能转换成float。因为存在两种可能的算术类型转换,所以该调用具有二义性。

函数匹配和const实参

如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数:

Record lookup(Account &);                  // 函数的参数是Account的引用
Record lookup(const Account &);            // 函数的参数是一个常量引用
const Account a;
Account b;
lookup(a);                                 // 调用lookup(const Account &);
lookup(b);                                 // 调用lookup(Account &);

在第一个调用中,我们传入的是const对象a。因为不能把普通引用绑定到const对象上,所以此例中唯一可行的函数是以常量引用作为形参的那个函数,并且调用该函数与实参a精确匹配。

在第二个调用中,我们传入的是非常量对象b,对于这个调用来说,两个函数都是可行的,因为我们既可以使用b初始化常量引用也可以用它初始化非常量引用。然而用非常量对象初始化常量引用需要类型转换,接受非常量形参的版本则与b精确匹配。因此,应该选用非常量版本的函数。

指针类型的形参也类似,如果两个函数的唯一区别是它的指针形参指向常量或非常量,则编译器能通过实参是否是常量决定选用哪个函数:如果实参是指向常量的指针,调用形参是const *的函数;如果实参是指向非常量的指针,调用形参是普通指针的函数。

函数指针

函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参共同决定,与函数名无关。例如:

bool lengthCompare(const string &, const string &);

该函数的类型是bool (const string &, const string &)。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可。

bool (*pf)(const string &, const string &);

使用函数指针

当我们把函数名作为一个值使用时,该函数自动地转换成指针。例如,按照下列形式可以将lengthCompare地地址赋给pf:

pf = lengthCompare;                             // pf指向名为lengthCompare的函数
pf = &lengthCompare;                            // 等价的赋值语句:取地址符是可选的

此外,我们能直接使用指向函数的指针调用该函数,无须提前解引用指针:

bool b1 = pf("hello", "goodbye");               // 调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye");            // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye");    // 另一个等价的调用

在指向不同函数类型的指针间不存在转换规则。但是和往常一样,我们可以为函数指针赋一个nullptr或者值为0的整型常量表达式,表示该指针没有指向任何一个函数。

重载函数的指针

当使用重载函数时,上下文必须清晰界定到底应该选用哪个函数,如果定义了指向重载函数的指针:

void ff(int *);
void ff(unsigned int);

void (*pf1)(unsigned int) = ff;                 // pf1指向ff(unsigned)

编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配(返回值和参数)。

函数指针形参

和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用:

// 第三个形参是函数类型,它会自动地转换成指向函数的指针
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能让我们简化使用了函数指针的代码:

// Func和Func2是函数类型
typedef bool Func(const string &, const string &);
typedef decltype(lengthCompare) Func2;              // 等价的类型

// FuncP和FuncP2是指向函数的指针
typedef bool(*FuncP)(const string &, const string &);
typedef decltype(lengthCompare) *FuncP2;            // 等价的类型

需要注意的是,decltype返回函数类型,此时不会将函数类型自动转换成指针类型,只有在结果前面加上*才能得到指针。可以使用如下的形式重新声明useBigger:

void useBigger(const string &, const String &, Func);
void useBigger(const string &, const string &, FuncP2);

这两个声明语句声明的是同一个函数,在第一条语句中,编译器自动地将Func表示地函数类型转换成指针。

返回指向函数的指针

和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。

要想返回函数指针的函数,最简单的办法是使用类型别名:

using F = int(int *, int);               // F是函数类型,不是指针
using PF = int (*)(int *, int);          // PF是指针类型

// 和函数类型的参数不一样,返回类型不会自动地转换成指针,必须显式地将返回类型指定为指针
PF f1(int);               // 正确:PF是指向函数地指针,f1返回指向函数的指针
F f1(int);                // 错误:F是函数类型,f1不能返回一个函数
F *f1(int);               // 正确:显式地指定返回类型是指向函数地指针

也可以直接声明f1:

int (*f1(int))(int *, int);

auto f1(int) -> int (*)(int *, int);

将auto和decltype用于函数指针类型

如果明确知道返回的函数是哪一个,就能使用decltype简化书写函数指针返回类型的过程。

string::size_type sumLength(const string &, const string &);
string::size_type largerLength(const string &, const string &);

// 根据其形参的取值,getFcn函数返回指向sumLength或者largeLength的指针
decltype(sumLength) *getFcn(const string &);

需要牢记将decltype作用于某个函数时,它返回函数类型而非指针类型。因此需要显式地加上*以表明我们需要返回指针,而非函数本身。

脚本宝典总结

以上是脚本宝典为你收集整理的0x05_函数全部内容,希望文章能够帮你解决0x05_函数所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。
标签: