6.函数介绍

文章目录

函数

6.1函数基础

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

形参和实参

尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。

6.1.1局部对象

在c++语言中,名字有作用域,对象有生命周期。理解这两个概念非常重要:

  • 名字的作用域是程序文本的一部分,名字在其中可见。
  • 对象的生命周期是程序执行过程中该对象存在的一段时间。

在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁局部变量的生命周期依赖于定义的方式

自动对象

对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。因此,把只存在于块执行期间的对象称为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
内置类型的未初始化局部变量将产生未定义的值

局部静态对象

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

// 统计自己被调用了多少次
size_t count_calls() {
    static size_t ctr = 0;  // 调用结束后,这个值仍然有效
    return ++ctr;
}

int main() {
    for (size_t i = 0; i != 10; ++i) {
        cout << count_calls() << endl;
    }

    return 0;
}

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

6.1.2函数声明

和其他名字一样,函数的名字也必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。唯一的例外是,如果一个函数永远也不会被用到,那么它可以只有声明没有定义

void print(vector<int>::const_iterator beg, vector<int>::const_iterator end);

函数声明也称作函数原型

在头文件中进行函数声明

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

6.1.3分离式编译

随着程序越来越复杂,希望把程序的各个部分分别存储在不同文件中。为了允许编写程序时按照逻辑关系将其划分开来,c++语言支持所谓的分离式编译,允许把程序分割到几个文件中去,每个文件独立编译。如果修改了其中一个源文件,那么只需重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制。

6.2参数传递

形参初始化的机制与变量初始化一样
和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参
当形参是引用类型时,对应的实参被引用传递。当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。

6.2.1传值参数

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。

指针形参

指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:

void reset(int *ip) {
    *ip = 0;    // 改变了指针ip所指对象的值
    ip = 0; // 只改变了ip的局部拷贝,实参未改变
}

熟悉c的程序员常常使用指针类型的形参访问函数外部的对象。在c++语言中,建议使用引用类型的形参替代指针

6.2.2传引用参数
使用引用避免拷贝

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。另一方面,如果函数无须改变引用形参的值,最好将其声明为常量引用

使用引用形参返回额外信息

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

// 返回s中c第一次出现的位置索引,引用形参occurs负责统计c出现的总次数
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 != s.size(); ++i) {
        if (s[i] == c) {
            if (ret == s.size()) {
                ret = i;
            }

            ++occurs;
        }
    }  

    return ret;
}
6.2.3const形参和实参

和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。但是忽略掉形参的顶层const也会造成函数重载的一些问题。例如,void fcn(int)其实和void fcn(const int)是一样的。

指针或引用形参与const

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

void reset(int &);
void reset(int *);

int main() {    
    int i = 0;    
    const int ci = i;    
    string::size_type ctr = 0;        
    
    reset(&i);  // 调用形参类型是int*的reset函数
    reset(&ci); // 错误:不能用指向const int对象的指针初始化int*
    reset(i);   // 调用形参类型是int&的reset函数
    reset(ci);  // 错误:不能把普通引用绑定到const对象ci上
    reset(42);  // 错误:不能把普通引用绑定到字面值上
    reset(ctr); // 错误:类型不匹配,ctr是无符号类型
    
    return 0;
}
尽量使用常量引用

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

6.2.4数组形参

数组的两个特殊性质对定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时通常会将其转换成指针。因为不能拷贝数组,所以无法以值传递的方式使用数组参数,但是可以把形参写成类似数组的形式

void print(const int*);
void print(const int[]);
void print(const int[10]);  // 这里的维度表示期望数组含有多少元素,实际不一定

和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。

使用标记指定数组长度

管理数组实参的第一种方法是要求数组本身包含一个结束标记,例如c风格字符串(最后一个字符后面跟着一个空字符):

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

这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况。

适用标准库规范

管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针:

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;
    }
}
数组引用和形参

C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用:

// 正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10]) {
    for (auto elem : arr) {
        cout << elem << endl;
    }
}

这种用法无形中限制了函数的可用性,只能将函数作用于指定大小的数组(后面会介绍给引用类型的形参传递任意大小的数组)。

传递多维数组

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略

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

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

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

形参看起来是一个二维数组,实际上是指向指定大小的数组的指针。

6.2.6含有可变形参的函数

有时无法提前预知应该向函数传递几个实参。为了编写能处理不同数量实参的函数,c++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板。
C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参,不过这种功能一般只用于与c函数交互的接口程序。

initializer_list形参

initializer_list用于表示某种特定类型的值的数组,定义在同名的头文件中。

在这里插入图片描述

vector一样,initializer_list也是一种模板类型;和vector不一样的是,initializer_list对象中的元素永远是常量值。如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:

// expected和actual是string对象
if (expected != actual) {
    error_msg({"functionX", expected, actual});
} else {
    error_msg({"functionX", "okay"});
}

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

void error_msg(ErrCode e, initializer_list<string> il) {
	cout << e.msg() << ": ";
	for (const auto &elem : il) {
		cout << elem << " ";
	}
	cout << endl;
}

if (expected != actual) {
    error_msg(ErrCode(42), {"functionX", expected, actual});
} else {
    error_msg(ErrCode(0), {"functionX", "okay"});
}
省略符形参

省略符形参是为了便于c++程序访问某些特殊的c代码而设置的,这些代码使用了名为varargs的c标准库功能。通常,省略符形参不应用于其他目的。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置

// 指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。
// 省略符形参所对应的实参无须类型检查
void foo(parm_list, ...);
void foo(...);
6.3返回类型和return语句
6.3.1无返回值函数

一个返回类型是void的函数也能使用return语句的第二种形式,即return expression;,不过此时return语句的expression必须是另一个返回void的函数。强行令void函数返回其他类型的表达式将产生编译错误。

6.3.2有返回值函数

return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。

值是如何被返回的

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

不要返回局部对象的引用或指针

函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域;同样,返回局部对象的指针也是错误的

// 严重错误:这个函数试图返回局部对象的引用
const string &manip() {
	string ret;
	
	// 以某种方式改变一下ret
	if (!ret.empty()) {
		return ret;	// 错误:返回对局部对象的引用
	} else {
		return "Empty";	// 错误:"Empty"是一个局部临时量
	}
}

要想确保返回值安全,不妨提问:引用所引的是在函数之前已经存在的哪个对象

引用返回左值

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

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

int main() {
    string s("a value");
    cout << s << endl;
    get_val(s, 0) = 'A';
    cout << s << endl;

    return 0;
}
列表初始化返回值

C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。

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

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

主函数main的返回值

为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量:EXIT_SUCCESSEXIT_FAILURE

6.3.3返回数组指针

因为数组不能被拷贝,所以函数不能返回数组,但是可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较繁琐,但是通过类型别名可以简化这一任务:

typedef int arrT[10];
using arrT = int[10];
arrt* func(int i);
声明一个返回数组指针的函数

要想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度:Type (*function(parameter_list))[dimension]

int (*func(int i))[10];

逐层解析:

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

在c++11新标准中还有一种可以简化返回数组指针的函数声明的方法,就是使用尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效。尾置返回类型跟在列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,在本应该出现返回类型的地方放置一个autoauto func(int i) -> int (*)[10]

使用decltype

如果知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型:

int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i) {
    return (i % 2) ? &odd : &even;
}

arrPtr使用关键字decltype表示它的返回类型是个指针,并且该指针所指的对象与odd的类型一致。因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。
有一个地方需要注意:decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想表示返回指针还必须在函数声明时加一个*符号。

6.4函数重载
定义重载函数

不允许两个函数除了返回类型外其他所有的要素都相同。

重载和const形参

顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数的重载,此时的const是底层的

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

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

当传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选择非常量版本的函数
尽管函数重载能在一定程度上减轻为函数起名字、记名字的负担,但是最好只重载那些确实非常相似的操作。有些情况下,给函数起不同的名字能使得程序更容易理解。

cast_const和重载

const_cast在重载函数的情景中最有用,例如:

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

函数的参数和返回类型都是const string的引用。可以对两个非常量string实参调用这个函数,但返回结果仍然是const string的引用。因此,需要一种新的函数,当它的实参不是常量时,得到的结果是一个普通的引用,使用const_cast可以做到这一点:

string &shorterString(string &s1, string &s2) {
    // 如果不进行const_cast转换,将会调用自身的非常量版本
    auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));

    return const_cast<string&>(r);
}
调用重载的函数

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

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

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

string read();
void print(const string&);
void print(double);
void footBar(int ival) {
    bool read = false;  // 新作用域:隐藏了外层的read
    string s = read();  // 错误:read是一个布尔值,而非函数
    // 不好的习惯:通常来说,在局部作用域中声明函数不是一个好的选择
    void print(int);
    print("Value: ");   // 错误:print(const string&)被隐藏掉了
    print(ival);
    print(3.14);    // 正确:调用print(int);print(double)被隐藏掉了
}

一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体
在c++中,名字查找发生在类型检查之前

6.5特殊用途语言特性
6.5.1默认实参

某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参:

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

需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值

使用默认实参调用函数

如果想使用默认实参,只要在调用函数的时候省略该实参即可。函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。例如:

string window;

window = screen(, , '?');   // 错误:只能省略尾部的实参
window = screen('?');   // 调用screen('?', 80, ' '),字符隐式转换成string::size_type

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

默认实参声明

虽然多次声明同一个函数是合法的,但是有一点要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值

// 表示高度和宽度的形参没有默认值
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);
string window = screen();

void f2() {
    def = '*';  // 改变默认实参的值
    sz wd = 100;    // 隐藏了外层定义的wd,但是没有改变默认值
    window = screen();  // 调用screen(ht(), 80, '*')
}
6.5.2内联函数和constexpr函数
内联函数可以避免函数调用的开销

将函数指定为内联函数,通常就是将它在每个调用点上"内联地"展开。在函数的返回类型前面加上关键字inline,这样就可以将它声明成内联函数了:

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

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数,并且很多编译器都不支持内联递归函数。

constexpr函数

constexpr函数是指能用于常量表达式的函数。定义这样的函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句

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

为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。当然,constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行,例如,可以有空语句、类型别名以及using声明。
允许constexpr函数的返回值并非一个常量:

// 如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }

当函数的实参是常量表达式时,它的返回值也是常量表达式;反之则不然

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

如果用一个非常量表达式调用constexpr函数,比如int类型的对象i,则返回值是一个非常量表达式
constexpr函数不一定返回常量表达式。

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

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

6.5.3调试帮助

C++有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assertNDEBUG

assert预处理宏

assert是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:

assert(expr);

首先对表达式求值,如果表达式为假(即0),则输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。
和预处理变量一样,宏名字在程序内必须唯一。含有cassert头文件的程序不能再定义名为assert的变量、函数或者其他实体。在实际编程过程中,即使没有包含cassert头文件,也最好不要为了其他目的使用assert。很多头文件都包含了cassert,这就意味着即使没有直接包含cassert,它也很有可能通过其他途径包含到程序中。
assert宏常用于检查不能发生的条件。

NDEBUG预处理变量

assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查
可以使用一个#define语句定义NDEBUG,从而关闭调试状态。同时,很多编译器都提供了一个命令行选项可以定义预处理变量。

$ CC -D NDEBUG main.c # use /D with the Microsoft compiler

定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此,assert应该仅用于验证那些确实不可能发生的事情。可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。
除了用于assert外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef#endif之间的代码;如果定义了NDEBUG,这些代码将被忽略:

void print(const int ia[], size_t size) {
#ifndef NDEBUG
	// __func__是编译器定义的一个局部静态变量,用于存放函数的名字
    cerr << __func__ << ": array size is " << size << endl;
#endif
}

预处理器定义了几个对于程序调试很有用的名字:

  • __func__:输出当前调试的函数的名字。
  • __FILE__:存放文件名的字符串字面值。
  • __LINE__:存放当前行号的整型字面值。
  • __TIME__:存放文件编译时间的字符串字面值。
  • __DATE__:存放文件编译日期的字符串字面值。
6.6函数匹配
确定候选函数和可行函数

函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数具备两个特征:一是与被调用的函数同名;二是其声明在调用点可见
第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等;二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);

f(5.6); // 调用void f(double, double)
寻找最佳匹配(如果有的话)

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

含有多个形参的函数匹配
f(42, 2.56);    // 错误:调用具有二义性

编译器因为函数调用具有二义性而拒绝其请求:因为每个可行函数各自在一个实参上实现了最好的匹配,从整体上无法判断孰优孰劣。看起来似乎可以通过强制类型转换其中的一个实参来实现函数匹配,但是在设计良好的系统中,不应该对实参进行强制类型转换
调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明设计的形参集合不合理。

6.6.1实参类型转换

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

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

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

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

所有算术类型转换的级别都一样。例如:从intunsigned int的转换并不比从intdouble的转换级别高。例如:

void mainip(long);
void mainip(float);
mainip(3.14);   // 错误:二义性调用
函数匹配和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&)
6.7函数指针

函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:bool lengthCompare(const string &, const string &);。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:bool (*pf)(const string &, const string &);

使用函数指针

当把函数名作为一个值使用时,该函数自动地转换成指针。例如:

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

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

bool p1 = pf("hello", "goodbye");
bool p2 = (*pf)("hello", "goodbye");
bool p3 = lengthCompare("hello", "goodbye");

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

重载函数的指针

当使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配

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

void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)
void (*pf2)(int) = ff;  // 错误:没有任何一个ff与该形参列表匹配
double (*pf3)(int*) = ff;   // 错误:ff和pf3的返回类型不匹配
函数指针形参

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

// 第三个形参是函数类型,它会自动地转换成指向函数的指针
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 &));

可以直接把函数作为实参使用,此时它会自动转换成指针。直接使用函数类型显得冗长而繁琐。类型别名和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返回函数类型,此时不会将函数类型自动转换成指针类型,所以只有在结果前面加上*才能得到指针

返回指向函数的指针

和数组类型,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。与往常一样,要想声明一个返回函数指针的函数,最简单的办法是使用类型别名:

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

必须时刻注意的是,和函数类型的形参不一样,返回类型不会自动地转换成指针,必须显式地将返回类型指定为指针

PF f1(int);
F f1(int);  // 错误:F是函数类型,f1不能返回一个函数
F *f1(int); // 正确:显式地指定返回类型是指向函数的指针

当然,也能直接用这样的形式直接声明:int (*f1(int))(int*, int);。可以看到,f1有形参列表,所以f1是个函数;f1前面有*,所以f1返回一个指针;进一步观察发现,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是int。另一方面,也可以使用尾置返回类型的方式声明一个返回函数指针的函数:auto f1(int) -> int (*)(int*, int);

autodecltype用于函数指针类型

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

string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
// 根据其形参的取值,getFcn函数返回指向sumLength或者largerLength的指针
decltype(sumLength) *getFcn(const string&);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值