《C++Primer中文版》第六章:函数

目录

 

第六章:函数

6.1函数基础

6.1.1局部对象

6.1.2函数声明

6.1.3分离式编译( separate compilation)​

6.2参数传递

6.2.1传值参数

6.2.2传引用参数

6.2.3const形参和实参

6.2.4数组形参

6.2.5main:处理命令行选项​

6.2.6 含有可变形参的函数

initializer_list 形参c++11

6.3返回类型和return语句

6.3.1无返回值函数

​6.3.2有返回值函数

6.3.3返回数组指针

6.4函数重载

6.4.1重载与作用域​

6.5特殊用途语言特性

6.5.1默认实参 default argument

6.5.2内联函数和 constexpr函数

6.5.3调试帮助

6.6函数匹配

6.6.1实参类型转换

6.7函数指针


第六章:函数

函数是一个命名了的代码块,我们通过调用函数执行相应的代码。函数可以有0个或多个参数,而且(通常)会产生一个结果。

重载函数:同一个名字可以对应儿几个不同的函数。

6.1函数基础

一个 典型的函数( function)定义包括以下部分:返回类型(returm type)函数名字由0个或多个形参( parameter)组成的列表以及函数体。其中,形参以逗号隔开,形参的列表位于一对圆括号之内。函数执行的操作在语句块中说明,该语句块称为函数体( function body)

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


编写函数

// val 的阶乘是val* (val - 1)*(val -2) ...*((val - (val - 1))* 1)
int fact (int val)
{
    int ret = 1;  //局部变量,用于保存计算结果
    while (val > 1)
        ret *= val--; //把ret和val的乘积赋给ret,然后将val减1
    return ret ; //返回结果
}

调用函数

int main()
{
    int j = fact(5); // j等于120,即fact(5)的结果
    cout << "5 !is"<< j << endl;
    return 0;
}

函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。

此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。

1.执行函数的第一步是(隐式地)定义并初始化它的形参。因此,当调用fact函数时,首先创建一个名为val的int变量,然后将它初始化为调用时所用的实参5。

2.当遇到一条return语句时函数结束执行过程。和函数调用一样,return语句也完成两项工作:一是返回return语句中的值(如果有的话)二是将控制权从被调函数转移回主调函数。函数的返回值用于初始化调用表达式的结果,之后继续完成调用所在的表达式的剩余部分。

形参和实参

fact ("hello") ;//错误:实参类型不正确
fact() ;// 错误: 实参数量不足
fact(42,10,0) ;//错误:实参数量过多
fact(3.14) ;//正确:该实参能转换成int类型

函数的形参列表

为了与C语言兼容,也可以使用关键字void

void f1(){ /* ...*/ }//隐式地定义空形参列表
void f2(void){ /* ...*/ }//显式地定义空形参列表
/*
*阶乘
*/
#include <iostream>
using namespace std;

int fact(int val)
{
	if (val < 0)
		return -1;
	int ret = 1;
	//从1连乘到val
	for (int i = 1; i != val + 1; ++i)
		ret *= i;
	return ret;
}
int main()
{
	int num;
	cout << "请输入一个整数:";
	cin >> num;
	cout << num << "的阶乘是:" << fact(num)<< endl;
	return 0;
}

6.1.1局部对象

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

如我们所知,函数体是一个语句块构成一个新的作用域,我们可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量(local variable)。它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏(hide)在外层作用域中同名的其他所有声明中。


自动对象
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象(automatic object)。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
形参是一种自动对象函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
我们用传递给函数的实参初始化形参对应的自动对象。对于局部变量对应的自动对象来说,则分为两种情况:如果变量定义本身含有初始值,就用这个初始值进行初始化;否则,如果变量定义本身不含初始值,执行默认初始化。这意味着内置类型的未初始化局部变量将产生未定义的值。


局部静态对象( local static object)

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

例题

#include <iostream>
using namespace std;

int myCnt()
{
	static int iCnt = -1;
	++iCnt;
	return iCnt;
}
int main()
{
	cout << "请输入任意字符后按回车键继续" << endl;
	int ch;
	cin >> ch;
	while (ch)
	{
		cout << "函数的执行次数是:" << myCnt() << endl;
		--ch;
	}
		
	return 0;
}

6.1.2函数声明

函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型( function prototype)。

在头文件中进行函数声明
函数也应该在头文件中声明而在源文件中定义。

6.1.3分离式编译( separate compilation)

编译和链接多个源文件

去百度吧

6.2参数传递

当形参是引用类型时,我们说它对应的实参被引用传递( passed by reference)或者函数被传引用调用(called by reference)。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
 

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用( called by value)
 

6.2.1传值参数

指针形参

6.2.2传引用参数

使用引用避免拷贝

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

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

//返回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隐式地返回
}

调用:

6.2.3const形参和实参

和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的

void fcn(const int i) { /* fcn能够读取i,但是不能向i写值*/ }
void fcn(int i) { /* ...*/} //错误:重复定义了fcn(int)

所以这样重载算是一个。

指针或引用形参与const


尽量使用常量引用

问题:如果把一个本来应该是常量引用的形参设成了普通引用类型,有可能遇到几个问题:

一是容易给使用者一种误导,即程序允许修改实参的内容;

是限制了该函数所能接受的实参类型,无法把const对象、字面值常量或者需要类型转换的对象传递给普通的引用形参
 

6.2.4数组形参

//尽管形式不同,但这三个print函数是等价的
//每个函数都有一个const int* 类型的形参
void print (const int*) ; 
void print (const int[] ) ;     //可以看出来,函数的意图是作用于一个数组
void print (const int [10]) ;   //这里的维度表示我们期望数组含有多少元素,实际
                                //不一定

如果我们传给函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。

使用标记指定数组长度

void print (const char *cp)
{
    if (cp) //若cp不是一个空指针
        while (*cp)  //只要指针所指的字符不是空字符
            cout << *cp++ ;  //输出当前字符并将指针向前移动一个位置

}

使用标准库规范

#include <iostream>
using namespace std;
void print(const int *beg, const int *end)
{
	//输出beg到end之间(不含end)的所有元素
	while (beg != end)
		cout << *beg++ << endl; //输出当前元素并将指针向前移动一个位置
}

int main()
{
	int j[2] = { 0, 1 };
	// j转换成指向它首元素的指针
	//第二个实参是指向j的尾后元素的指针
	print(begin(j),end(j));  // begin和end函数

	return 0;
}

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

#include <iostream>
using namespace std;

// const int ia[]等价于const int* ia
// size 表示数组的大小,将它显式地传给函数用于控制对ia元素的访问
void print(const int ia[],size_t size)
{
	for (size_t i = 0; i != size; ++i) {
		cout << ia[i] << endl;
	}

}

int main()
{
	int j[] = { 0,1,4,6 };
	//大小为2的整型数组
	print(j, end(j) - begin(j));
	return 0;
}

数组引用形参

#include <iostream>
using namespace std;

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

int main()
{
	int j[10] = { 0,1,4,6 };
	//大小为2的整型数组
	print(j);
}

传递多维数组

// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10],int rowSize) { /* ...*/ }
我们也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,
所以最好不要把它包括在形参列表内:
//等价定义
void print (int matrix[] [10],int rowSize) { /* ... */ }

实现了三个版本的print函数,第一个版本不控制指针的边界,第二个版本由调用者指定数组的维度,第三个版本使用C++11新规定的begin和end函数限定数组边界。满足题意的程序如下所示:

#include <iostream>
using namespace std;

//参数是常量整型指针
void print1(const int *p)
{
	cout << *p << endl;
}

//参数有两个,分别是常量整型指针和数组的容量
void print2(const int *p,const int sz )
{
	int i = 0;
	while (i != sz)
	{
		cout << *p++ << endl;
		++i;
	}
}

//参数有两个,分别是数组的首尾边界
void print3(const int *b, const int *e)
{
	for (auto q = b; q != e; ++q)
		cout << *q <<"   ";
}

int main()
{
	int i = 0,j[2] = { 2, 1 };
	print1(&i);
	print1(j);
	print2(&i,1);
	//计算得到数组j的容量
	print2(j, sizeof(j) / sizeof(*j));
	auto b = begin(j);
	auto e = end(j);
	print3(b,e);
	return 0;
}

6.2.5main:处理命令行选项

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

练习6.25:编写一个main函数,令其接受两个实参。把实参的内容连接成一个string对象并输出出来。

#include <iostream>
#include <string>
using namespace std;
int main(int argc, char **argv)
{
	string str;
	for (int i = 0; i != argc; ++i)
		cout << "argc[ " << i << "]:" << argv[i] << endl;


	for (int i = 0; i != argc; ++i)
		str += argv[i];
	cout << str << endl;
	return 0;
}

6.2.6 含有可变形参的函数

initializer_list 形参c++11

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


// expected和actual是string对象
//必须把序列放在一对花括号内
if (expected != actual)
error_msg({ " functionX", expected, actual });
else
error_msg({ "functionX", "okay" });


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标准库功能。通常,省略符形参不应用于其他目的。你的C编译器文档会描述如何使用varargs。

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

练习6.27:编写一个函数,它的参数是initializer_ list<int> 类型的对象,函数的功能是计算列表中所有元素的和。

#include <iostream>
using namespace std;
int iCount (initializer_list<int> il)
{
    int count = 0;
    //遍历il的每一个元素
    for (auto val : il)
        count += val;
    return count;
}
int main()
{
    //使用列表初始化的方式构建initializer_list<int>对象
    //然后把它作为实参传递给函数iCount
    cout << "1,6,9 的和是:"<< iCount({1, 6,9}) << endl;
    cout << "4,5,9,18的和是:"<< iCount({4, 5,9,18}) << endl;
    return 0;
}

6.3返回类型和return语句

6.3.1无返回值函数


6.3.2有返回值函数

值是如何被返回的

//如果ctr的值大于1,返回word的复数形式
string make_plural(size_t ctr, const string &word,
                                    const string &ending)
{
    return (ctr > 1) ? word + ending : word;
}

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

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

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

//调用string对象的size成员,该string对象是由shorterString函数返回的
auto sz = shorterString(s1, s2) .size() ;

左结合律。因此,如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员。
 

引用返回左值

#include <iostream>
#include <string>
using namespace std;

char &get_val(string &str, string::size_type ix)
{
	return str[ix];// get_val 假定索引值是有效的
}

int main()
{
	string s("a value");
	cout << s << endl; //输出a value
	get_val(s, 0) = 'A'; //将s[0]的值改为A
	cout << s << endl;   //输出A value
	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 { " functionX", expected, actual};
}

主函数main的返回值
main函数的返回值可以看做是状态指示器。

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

int main()
{
    if (some_failure)
         return EXIT_FAILURE; //定义在cstdlib头文件中
    else
          return EXIT_SUCCESS;//定义在cstdlib头文件中
}

递归

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。

// 计算val 的阶乘,即1*2*3...* val
int factorial (int val)
{
    if (val > 1)
        return factorial (va1-1) * val;
    return 1 ;
}

在递归函数中,一定有某条路径是不包含递归调用的;否则,函数将“永远”递归下去,换句话说,函数将不断地调用它自身直到程序栈空间耗尽为止。我们有时候会说这种函数含有递归循环(recursion loop)。在factorial函数中,递归终止的条件是val等于1

练习6.32:

#include <iostream>
using namespace std;
int &get(int *arry, int index) { return arry[index]; }
int main()
{
    int ia[10];
    for (int i = 0; i!= 10; ++i)
        get(ia, i) = i;
    for(auto val : ia)
        cout << val <<endl;
    return 0;
}

练习6.33:编写一个递归函数,输出vector对象的内容。

#include <iostream>
#include <vector>
using namespace std;
//递归函数输出vector<int>的内容
void print (vector<int> vInt,unsigned index)
{
    unsigned sz = vInt.size() ;
    if (!vInt. empty() && index < sz)
    {
        cout << vInt[index] << endl;
        print(vInt,index + 1) ;        
    }

}

int main ()
{
   vector<int> v = {1,3,5, 7,9,11,13, 15};
    print(v,0) ;
    return 0;
}

6.3.3返回数组指针

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

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

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

可以按照以下的顺序来逐层理解该声明的含义:
●func(int i)表示调用func函数时需要一个int类型的实参。
●(*func(inti))意味着我们可以对函数调用的结果执行解引用操作。
●(*func(int i)) [10]表示解引用func的调用将得到-一个大小是10的数组。
●int (*func(int i)) [10]表示数组中的元素是int类型。

使用尾置返回类型
在C++11新标准中还有一
种可以简化上述func声明的方法,就是使用尾置返回类型( railing return type)。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:
// func 接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组.
auto func(int i) -> int(*) [10] ;

因为我们把函数的返回类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针,并且该指针指向了含有10个整数的数组。


使用decltype c++11
还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。例如,下面的函数返回一个指针,该指针根据参数i的不同指向两个已知数组中的某一个:

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的结果是个数组,要想表示arrPtr返回指针还必须在函数声明时加一个*符号。

练习:

6.4函数重载

定义重载函数
对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。在上面的代码中,虽然每个函数都只接受一个参数,但是参数的类型不同。


不允许两个函数除了返回类型外其他所有的要素都相同。假设有两个函数,它们的形参列表一样但是返回类型不同,则第二个函数的声明是错误的:

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 (Phone* const) ;//重复声明了Record lookup (Phone* )

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

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

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

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

const_cast 和重载

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

当它的实参不是常量时,得到的结果是一个普通的引用:就是下面这种调用方法

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 的引用,然后调用了shorterString函数的const版本。const 版本返回对const string 的引用,这个引用事实上绑定在了某个初始的非常量实参上。因此,我们可以再将其转换回一个普通的string&,这显然是安全的。


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

现在我们需要掌握的是,当调用重载函数时有三种可能的结果:
●编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
●找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。
●有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用( ambiguous call)
 

6.4.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)被隐藏掉了
}

void print (const string &) ;
void print (double) ;// print函数的重载形式
void print (int) ;// print函数的另一种重载形式
void fooBar2 (int ival)
{
    print ("Value: ") ;//调用print (const string &)
    print (ival) ;//调用print (int)
    print (3.14) ; //调用print (double)
}

6.5特殊用途语言特性

6.5.1默认实参 default argument

可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

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

使用默认实参调用函数

string window;
window = screen() ; //等价于screen(24,80,' ')
window = screen(66) ;//等价于screen(66,80,' '
window = screen(66, 256) ;// screen(66,256,' ")
window = screen(66, 256,'#'); // screen(66,256,' #')
window = screen(, ,'?') ;//错误:只能省略尾部的实参
window = screen('?') ;//调用screen(' ?', 80, ' ')

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

 

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

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

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

void f2 ()
{
    def ='*'; //改变默认实参的值
    sz wd = 100; //隐藏了外层定义的wd,但是没有改变默认值
    window = screen() ;//调用screen(ht(), 80,'*')
)

6.5.2内联函数和 constexpr函数

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


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

每个调用点上“内联地"展开。
 

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

constexpr函数( constexpr function )c++11

函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:

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

执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,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)不是常量表达式

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

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

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

6.5.3调试帮助

assert预处理宏( preprocessor marco )    : cassert头文件
 

assert (expr) ;

首先对expr求值,如果表达式为假(即0),assert 输出信息并终止程序的执行。如果表达式为真(即非0),assert 什么也不做。
assert宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阈值。此时,程序可以包含一条如下所示的语句:   assert (word.size()〉threshold) ;

NDEBUG预处理变量

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


定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此, assert应该仅用于验证那些确实不可能发生的事情。我们可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。

__func__是编译器定义的一个局部静态变量,用于存放函数的名字
__FILE__存 放文件名的字符串字面值。
__LINE__存放当前行号的整型字面值。
__TIME__存放文件编译时间的字符串字面值。
__DATE__存放文件编译日期的字符串字面值。

练习6.47

#include <iostream>
#include <vector>
using namespace std;
//递归函数输出vector<int>的内容
void print (vector<int> vInt, unsigned index)
{
    unsigned sZ = vInt.size() ;
    //设置在此处输出调试信息
    #ifndef NDEBUG
    cout << "vector 对象的大小是: "<< sZ << endl;
    #endif // NDEBUG
    if (!vInt.empty() && index < sZ)
    {
            cout << vInt[index] << endl;
            print (vInt,index + 1) ;
    }

}
int main()
{
    vector<int> v = {1,3,5, 7,9,11,13,15} ;
    print(v,0) ;
    return 0;
}

6.6函数匹配

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

候选函数(candidate function):候选函数具备两个特征:一是 与被调用的函数同名二是其声明在调用点可见。在这个例子中,有4个名为f的候选函数。
可行函数(viable function):有两个特征: 一是其形参数量与本次调用提供的实参数量相等二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。

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

它的基本思想是,实参类型与形参类型越接近,它们匹配得越好。
含有多个形参的函数匹配

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

6.6.1实参类型转换

编译器将实参类型到形参类型的转换划分成几个等级

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

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

函数匹配和const实参

6.7函数指针

使用函数指针

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


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

重载函数的指针

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

Func和Func2是函数类型  FuncP和FuncP2是指针类型,所以只有在结果前面加上*才能得到指针。

自动将函数转换成指向该函数的指针

返回指向函数的指针

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

可以使用尾置返回类型的方式:

将auto和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 &) ;

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

例子:

#include <iostream>
#include <vector>
using namespace std;
//加法
int func1(int a, int b)
{
	return a + b;
}

//减法
int func2(int a, int b)
{
	return a - b;
}
//乘法
int func3(int a, int b)
{
	return a * b;
}

//除法
int func4(int a, int b)
{
	return a / b;
}

void Compute(int a, int b, int(*p) (int, int))
{
	cout << p(a, b) << endl;
}




int main()
{
	int i = 5, j = 10;
	decltype (func1) *p1 = func1, *p2 = func2, *p3 = func3, *p4 = func4;
	vector<decltype(func1)*> vF = { p1, p2,p3,p4 };
	for (auto p : vF) //遍历vector中的每个元素,依次调用四则运算函数
		Compute(i, j, p);
	return 0;
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值