C++Primer笔记——6.函数

函数

函数是一个命名了的代码块,我们通过调用函数执行相应的语句。

一、函数基础

一个典型的函数定义包括以下部分:返回类型,函数名,由0个或多个形参组成的列表以及函数体。我们通过调用运算符执行函数。

调用函数

函数的调用完成两项工作:一是用实参初始化对应的形参;二是把控制权转移给被调用的函数。此时,主调函数的执行暂时被中断,被调函数开始执行。

执行函数的第一步是(隐式的)定义并初始化它的形参。当遇到一条 return 语句时函数结束执行过程。

形参和实参

实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。

实参的类型必须与对应的形参类型匹配。因为函数的调用规定实参数量应该与形参数量一致,所以形参一定会被初始化

函数的形参列表

函数的形参列表可以为空,但是不能省略。任意两个形参都不能同名,而且函数最外层作用域中的局部变量也不能使用与 函数形参一样的名字。

函数的返回类型

大多数类型都能被用作函数的返回类型。函数的返回类型不能是 数组或函数类型,但可以是指向数组或函数的指针。

1.局部对象

C++中,变量有作用域,对象有生命周期。

形参 和 函数体内部定义的变量统称为局部变量。

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

自动对象

我们把只存在于 块执行期间 的对象称为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。

**形参是一种自动对象。**函数开始时为形参申请存储空间,因为形参定义在函数体作用域内,所以一旦函数终止,形参也就被销毁。

我们用传递给函数的实参初始化形参对应的自动对象。

局部静态对象

有时有必要让局部变量的生命周期贯穿函数的调用 及其之后的时间。

局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并直到程序终止才会被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

size_t count_call() {
	static size_t ctr = 0;
	return ++ctr;
}

int main() {
	for (size_t i = 0; i != 10; i++) {
		//每一次调用 ctr 都是上一次调用的值
		cout << count_call() << endl;
	}
	system("pause");
	return 0;
}

2.函数声明

函数的名字也必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。因为函数的声明不含函数体,所以也就不需要形参的名字。函数声明也成为函数原型。

void print(vector<int>::const_iterator beg, vector<int>::const_iterator end);
在头文件中进行函数声明

建议函数应该在头文件中声明而在源文件中定义。

3.分离式编译

随着程序越来越复杂,我们希望把程序的各个部分分别存储到不同的文件中。分离式编译允许我们把程序分隔到几个文件中去,每一个文件独立编译。

二、参数传递

每次调用函数都会重新创建它的形参,并用传入的实参对形参进行初始化。

形参的类型决定了形参和实参的交互方式。如果形参是引用类型,它将绑定到对应的实参上;否则将实参的值拷贝后赋给形参。

1.传值参数

当初始化一个非引用类型的变量时,初始值被拷贝给变量。传值参数的机制完全一样,函数对形参的所有操作都不会影响到实参。

指针形参

指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。

//该函数接受一个指针 然后将指针所指的值 置为 0
void reset(int* p) {
	*p = 0; //改变指针 p 所指对象的值
	p = 0;  //只改变了 p 的局部拷贝,实参未被改变
}

int main() {

	int i = 42;
	reset(&i); //改变 i 的值,而非 i 的地址
	cout << "i = " << i << endl; // 输出 i = 0
}

在C++中,建议使用引用类型的形参代替指针。

使用指针的方式交换两个数的值

#include<iostream>
using namespace std;

void mySwap(int* p, int* q) {
	int temp = *p;
	*p = *q;
	*q = temp;
}

int main() {
	int a = 10, b = 20;
	cout << "a = " << a << "  b = " << b << endl;
	mySwap(&a, &b);
	cout << "a = " << a << "  b = " << b << endl;
	system("pause");
	return 0;
}

2.传应用参数

通过引用实参,允许函数改变一个或多个实参的值。

//该函数接受一个int 对象的引用,然后将该对象置为0
void reset2(int& i) {
	i = 0; //改变了 i 所引用对象的值
}

int main() {
	int j = 42;
	reset2(j);
	cout << "j = " << j << endl; //输出 j = 0
	system("pause");
	return 0;
}
使用引用避免拷贝

拷贝大的类类型对象 或 容器对象非常低效,甚至有的类类型(包括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; //记录 c 第一次出现的位置
			occurs++;  //将出现次数 +1
		}
	}
	return ret;  //出现次数通过 occurs 隐式的返回
}

3.const形参和实参

顶层 const 作用于对象本身。当用实参初始化形参时会忽略掉顶层const。即当形参有顶层const时,传给它常量或者非常量对象都可以。

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

调用 fcn 函数时,既可以传入 const int ,也可以传入 int。

指针或引用形参 与 const

我们可以使用一个非常量初始化一个底层 const 对象,但反过来不行;一个普通的引用必须用同类型的对象初始化。

    int i = 2;
	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 i = 0;
	const int ci = i;
	string::size_type ctr = 0;
	reset(&i);               //调用形参类型是 int* 的reset函数
	reset(&ci);              //错误,不能用指向const int 的对象的指针 初始化 int*
	reset2(i);               //调用形参类型是 int& 的reset2函数
	reset2(ci);              //错误,不能把普通引用绑定到 const int 对象 ci 上
	reset2(42);              //错误,不能把普通引用绑定到 字面值上
	reset2(ctr);             //错误,类型不匹配,ctr 是无符号类型

	find_char("Hello,World!", 'o', ctr); //正确,find_char 第一个形参是对常量的引用
尽量使用常量引用

把函数不会改变的形参定义成(普通的)引用是一种常见的错误,这么做会给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的参数类型。

string::size_type find_char2(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; //记录 c 第一次出现的位置
			occurs++;  //将出现次数 +1
		}
	}
	return ret;  //出现次数通过 occurs 隐式的返回
}

void test(){
	find_char2("Hello,World!", 'o', ctr);  //错误,普通引用不能绑定到 字面值上
}

4.数组形参

数组的两个特殊性质对我们定义和使用在数组上的函数有影响,两个性质是:不允许拷贝数组,使用数组时通常会将其转换为指针。

//尽管形式不同,但这三个函数都是等价的
//每一个函数都一个 const int * 的形参
void print(const int*);
void print(const int[]);
void print(const int[10]);

	int i = 0, j[2] = { 0,1 };
	print(&i);     //正确,&i 的类型是 int*
	print(j);      //正确,j 转换为 int* 并指向 j[0]
数组形参与const

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

数组引用形参

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

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

**因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心的使用数组。**但是这一用法也限制了 print 的可用性我们只能将函数作用于大小为10的数组。

    int i = 0, j[2] = { 0,1 };
	int k[10] = { 1,2,3,4,5,6,7,8,9,10 };
	print(&i);    //错误,实参不是含有十个整数的数组
	print(j);     //错误,实参不是含有十个整数的数组
	print(k);     //正确,实参是含有十个整数的数组
传递多维数组

C++ 中没有真正的多维数组,所谓的多维数组其实是数组的数组。

数组的第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略。

//matrix 指向数组的首元素,该数组的元素是由 10个正数 构成的数组
//以下两个定义等价
void print(int(*matrix)[10], int rowSize);
void print(int matrix[][10], int rowSize);

5.含有可变形参的函数

有时我们无法提前预知该向函数传递几个实参。

为了编写能处理不同数量实参的函数。C++ 11 提供了两种主要方法:

  • 如果所有的实参类型相同,可以传递一个名为 initailizer_list的标准库模板。
  • 如果实参类型不同,我们可以编写一种特殊的函数,即可变参数模板
initializer_list 形参

如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用 initializer_list 类型的形参。initializer_list 是一种标准库类型,用于表示某种特定类型的值的数组。

操作含义
initializer_list< T > lst默认初始化,T类型元素的空列表
initializer_list< T > lst {a,b,c,…}lst 的元素数量和初始值一样多;lst 的元素是对应初始值的副本;列表中的元素是 const
lst2 ( lst ) , lst2 = lst拷贝 或 赋值一个initializer_list 对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
lst.size()列表中的元素数量
lst.begin()返回指向 lst 首元素的指针
lst.end()返回指向 lst 尾元素下一位置的指针

initializer_list 对象中的元素永远是常量值,我们无法改变该对象中元素的值。

void error_msg(initializer_list<string> lst) {
	for (auto it = lst.begin(); it != lst.end(); ++it) {
		cout << *it << " ";
	}
	cout << endl;
}

如果想向 initializer_list 形参中传递一个值的序列,则必须把序列放在一对花括号内。

	error_msg({ "functionX","expected","actual" });
省略符形参

省略符形参是为了便于 C++ 访问某些特殊的 C 代码而设置的,这些代码使用了名为 varargs 的 C 标准库。

省略符形参只能出现在形参列表的最后一个位置。

void foo(int a, ...);
void foo(...); 

三、返回类型 和 return语句

return 语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。

1.有返回值函数

没有返回值的 return 语句只能用在返回类型是 void 的函数中。返回 void的函数不要求必须有 return 语句,因为在这类函数的最后一句后面会隐式的执行 return。

通常情况下,void函数如果想在它的中间位置提前退出,可以使用return语句。

2.有返回值函数

只要函数的返回类型不是 void,则该函数的每一条 return 语句都必须返回一个值。

return 语句返回值的类型必须与函数的返回类型相同。

注意: 在含有 return 语句的循环后面也应该有一条 return 语句,如果没有的话该程序就是错误的。

值是如何被返回的

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

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

函数执行完后,他所占的存储空间也被释放掉。因此,函数终止意味着局部对象的引用将不再指向有效的内存区域。

//严重错误,这个函数试图返回局部对象的引用
const string& manip() {
	string ret;
	if (!ret.empty()) {
		return ret;   //错误,返回局部对象的引用
	}
	else return "Empty";  //错误,“Empty” 是一个局部临时量
}

当函数结束时对象占用的空间也就随之释放掉了,所以两条 return 语句都指向了不可再用的内存空间。

返回局部对象的引用是错误的,返回局部对象的指针也是错误的。

引用返回左值

调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值一样使用返回引用的函数调用,我们可以为返回类型是 非常量引用 的函数赋值。

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';   //将 s[0] 的值改为 'A'
	cout << s << endl;     //输出 A value


	system("pause");
	return 0;
}
列表初始化返回值

C++ 11 标准规定,函数可以返回花括号包围的值得列表,此处的列表用来对表示函数返回的临时量进行初始化。


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

如果函数返回的是内置类型,则花括号包含的列表最多包含一个值,而且该值所占空间不应该大于目标类型得空间。

主函数 main 的返回值

我们允许 main 函数没有 return 语句直接结束,如果控制到达了 main 函数的结尾处而且没有 return 语句,编译器将隐式的插入一条返回 0 的return语句。

main 函数的返回值可以看作是状态指示器,返回 0 表示执行成功,返回其他值表示执行失败,其中非 0 值得含义依机器而定。

注意:main 函数不能调用自己。

3. 返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。

函数可以返回数组得指针 或 引用。定义一个返回数组的指针 或 引用得函数比较繁琐,我们可以使用类型别名简化。

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

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

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

如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名之后。

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

C++ 11 标准有一种可以简化上述 func声明的方法,就是使用 尾置 返回类型。

任何函数定义都能使用 尾置 返回,这种形式对于返回类型比较复杂的函数最有效,比如返回类型是是数组的指针或引用。

//func 接受一个 int 类型的实参,返回一个指针,该指针指向含有十个整数的数组
auto func(int i) -> int(*)[10];
使用 decltype

还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用 decltype声明返回类型。

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

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

四、函数重载

如果同一作用域内几个函数名字相同但是形参列表不同,即函数重载

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");    //调用 print(const char *cp)

print(j,end(j) - begin(j));  //调用 print(const int ia[],size_t size)

print(begin(j) ,end(j));  //调用 print(const int *beg,const int *end)

函数的名字仅仅是让编译器知道它调用的是哪一个函数,二函数重载可以在一定程度上程序员起名字,记名字的负担。

注意:main 函数不能重载。

定义重载函数
Record lookup(const Account&);   //根据Account查找记录
Record lookup(const Phone&);     //根据Phone查找记录
Record lookup(const Name&);     //根据Name查找记录

Account acct;
Phone phone;

Record r1 = lookup(acct);     //调用接受 Account 的版本
Record r2 = lookup(phone);    //调用接受 Phone 的版本

对于重载的函数来说,它们应该在型参数量或形参类型上有所不同。

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

Record lookup(const Account&);
bool lookup(const Account&);   //错误:与前面那个函数相比只有返回类型不同
判断两个形参的类型是否相异

有时候两个形参列表看起来不一样,但实际上是相同的:

//每对声明的都是同一个函数
Record lookup(const Account &acct);
Record lookup(const Account&);  //省略了形参的名字

typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&);   //Telno 和 Phone 的类型相同
重载和 const 形参

顶层 const 不影响传入函数的对象。一个拥有顶层 const 的形参无法和另一个没有顶层const的形参区分开来

Record lookup(Phone);
Record lookup(const Phone);   //重复声明了 Record lookup(Phone)

Record lookup(Phone*);
Record lookup(const Phone*); //重复声明了 Record lookup(Phone*);

如果形参是某种类型的指针或引用,则通过区分其指向的是 常量对象 还是 非常量对象 可以实现函数重载,此时的 const 是底层的。

//对于接受 引用 或 指针的函数来说,对象是常量还是非常量对应的形参不同
//以下定义了4个独立的重载函数
Record lookup(Account&);    //函数作用于 Account 的引用
Record lookup(const Account&); //新函数,作用于常量引用

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

在上面的例子中,编译器可以通过实参是否是常量来推断应该调用哪一个函数。

建议:尽管函数重载能在一定程度上减轻我们为函数起名字,记名字的负担,但最好只重载那些非常相似的操作。

const_cast 和 重载

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

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

这个函数的参数 和 返回类型都是 const string的引用。如果我们想要得到一个普通的 string引用时,就需要使用 const_cast

string &shortString(string &s1,string &s2){
    auto &r = short(const_cast<const string&>(s1),const_cast<const string&>(s2));
    return const_cast<string&>(r);
}

调用重载的函数

函数匹配(function matching) 是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定(overload resolution)

编译器首先将调用的实参 与 重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪一个函数。

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

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

1.重载与作用域

注意:一般来说,不建议把函数声明置于局部作用域内。

如果我们在内层作用域中声明名字,他将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。

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),print(double) 被隐藏了
}

五、特殊用途语言特性

1. 默认实参

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

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

需要注意的是,一旦某一个形参是默认实参,则它后面的所有形参都是默认实参。

使用默认实参调用函数
string window;
window = scrren();   //等价于 screen(24,80,' ')
window = scrren(66);   //等价于 screen(66,80,' ')
window = scrren(66,256);   //等价于 screen(66,256,' ')
window = scrren(66,256,'#');   //等价于 screen(66,256,'#')

函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参。

默认实参声明

对于函数声明来说,通常是将其放在头文件中,并且一个函数只声明一次(多次声明同一个函数也是合法的)。需要注意的是:在给定的作用域中一个形参只能被赋予一次默认实参,即 函数的后续声明只能为之前没有默认值的形参赋予一次默认实参。

//表示高度和宽度的形参没有默认值
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 = scrren();    //调用 screen(ht(),80,' ')

2. 内联函数 和 constexpr 函数

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

将函数指定为 内联函数(inline) ,通常就是将它在每一个调用点上“内联的”展开。

//假设我们把 shorterString 函数定义成内联函数,以下的调用
cout<< shorterString(s1,s2) <<endl;

//将在编译过程中展开成类似以下的形式,从而消除了 shorterString 函数的运行时开销
cout<< (s1.size() < s2.size() ? s1 : s2) <<endl; 

在函数的返回类型前面加上 关键字inline,这样就可以将其声明为内联函数。

//内联版本:返回两个string对象中较短的那个
inline const string & shorterString(const string &s1,const string &s2){
    return s1.size() <= s2.size() ? s1 : s2;
}

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

一般来说,内联机制用于优化规模比较小、流程直接、调用频繁的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不太可能在调用点内联地展开。

constexpr 函数

constexpr 函数 是指能用于常量表达式得函数。

定义 constexpr 函数 得方法和其他函数类似,不过需要遵循几项规定:

  • 函数的返回类型以及所有形参的类型都得是字面值类型
  • 函数体中必须有且只有一条 return 语句
constexpr int new_sz() { return 42;}
constexpr int foo = new_sz();  //正确:foo 是一个常量表达式

我们允许 constexpr 函数的返回值不是一个常量

//如果 arg 是常量表达式,则 scale(arg) 也是常量表达式
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)  不是常量表达式

注意:constexpt 函数不一定返回常量表达式。

3. 调试帮助

C++ 程序员有时会用到一种类似于头文件保护的技术,以便有选择的执行、调试代码。

其基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当程序编写完之后,需要屏蔽掉这些调试代码。

这种方法用到两项预处理功能: assertNDEBUG

assert预处理宏

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

//如果 expr为假(即0),assert 输出信息并终止程序的执行
//如果 expr为真(即非0),assert 什么也不做
assert(expr);

和 预处理变量一样,宏名字在程序内必须唯一。

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

assert(word.size() > threshold);
NDEBUG预处理变量

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

定义 NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此, assert应该仅用于验证那些确实不可能发生的事情。

除了用于 assert外,也可以使用 NDEBUG编写自己的条件调试代码。如果 NDEBUG未定义,将执行 #ifdef#endif之间的代码;如果定义了 NDEBUG,这些代码将被忽略掉。

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

六、函数匹配

在大多数情况下,我们容易确定某一次调用应该选用哪个重载函数。然而,当几个重载函数的型参数量相等 以及 某些形参的类型可以由其他类型转换得来时,就不容易了。

确定候选函数 和 可行函数

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

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

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

寻找最佳匹配(如果有的话)

函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。

建议:调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。

1. 实参类型转换

为了确定最佳匹配,编译器将 实参类型 到 形参类型的转换划分为几个等级。
1.精确匹配,包括以下情况:

  • 实参类型与形参类型相同
  • 实参从数组类型 或 函数类型转换成对应的指针类型
  • 向实参添加 顶层const 或者从是惨重删除 顶层const

2.通过 const 转换实现的匹配

3.通过类型提升实现的匹配

4.通过算术类型转换 或 指针转换实现的匹配。

5.通过 类类型转换 实现的匹配

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

我们知道小整型一般都会提升到 ``int```类型 或 更大的整数类型。

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

void ff(int);
void ff(short);

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

所有算术类型转换的级别都一样。

void manip(long);
void manip(float);

//3.14的字面值是 double,既能转换成 long,又能转换成 float
manip(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&)

指针类型的形参也是类似的。

七、函数指针

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

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

//比较两个 string 对象的长度
bool lengthCompare(const string&,const string&);

//pf 指向一个函数,该函数的参数是两个 const string 的引用,返回值是 bool 类型
bool (*pf)(const string&,const string&);//未初始化, *pf 两边的括号必须有
使用函数指针

当我们把函数名作为一个值使用时,该函数自动的转成指针。

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

//此外,还可以直接使用指向函数的指针调用该函数,无需提前解引用指针
bool b1 = pf("hello","goodbye");  //调用 lengthCompare函数
bool b2 = (*pf)("hello","goodbye"); //一个等价的调用
boool b3 = lengthCompare("hello","goodbye");   //另一个等价的调用


//在指向不同函数类型的指针间不存在转换规则,但是我们可以为函数指针赋予一个 nullptr 或者 0,
//表示该指针没有指向任何一个函数
string::size_type sumLength(const string&,const string&);
bool cstring(const char*,const char*);

pf = 0;    //正确:pf不指向任何函数
pf = sumLength;   //错误:返回类型不匹配

pf = cstringCompare;  //错误:形参类型不匹配
pf = lengthCompare;   //正确:函数和指针的类型精确匹配
重载函数的指针

如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪一个函数,指针类型必须与重载函数中的某一个精确匹配。

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&));

//我们可以直接把函数作为实参使用,此时他会自动转换成指针

//自动将函数 lengthCompare 转换成指向该函数的指针
useBigger(s1,s2,lengthCompare);
返回指向函数的指针

和数组类似,虽然不能返回一个函数,但是可以返回指向函数类型的指针。

然而,我们必须把返回类型写作指针的形式,编译器不会自动将函数类型当成对应的指针类型处理。

要想声明一个返回函数指针的函数,最简单的方法是使用类型别名。

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);    //正确:显式地指定返回类型是指向函数的指针
autodecltype用于函数指针类型

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

需要注意的点是,当我们将 decltype作用于某一个函数时,它返回函数类型而非指针类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值