《C++Primer》第六章——函数

第六章:函数

6.1 函数基础

1.典型的函数定义包括:返回类型、函数名字、由0个或多个形参组成的列表、函数体,一共四个部分
2.函数是一个命名了的代码块,通过调用函数执行相应的代码,调用运算符的形式是一对圆括号,作用一个表达式,该表达式是函数或指向函数的指针,圆括号之内是一个用逗号隔开的实参列表,通过实参初始化函数的形参
3.函数返回类型:函数的返回类型不能数组类型或函数类型,但可以使指向数组或函数的指针
4.在C++语言中,名字有作用域,对象有生命周期

  • 名字的作用域是程序文本的一部分,名字在其中可见
  • 对象的生命周期是程序执行过程中该对象存在的一段时间
    5.自动对象:对于不同局部变量对应的对象,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在块末尾是销毁,将这种只存在于块执行期间的对象称为自动对象
  • 形参是一种自动对象,函数开始时为形参申请存储空间,函数终止则形参销毁
    6.局部静态对象:有时,为了令局部变量的声明周期贯穿函数调用及之后的时间,可以将局部变量定义为 static 类型从而获得这样的对象,局部静态对象在程序执行路径第一次经过对象定义语句时初始化,知道程序终止时才被销毁,在此期间即使对象所在函数结束执行也不会对该局部静态对象有影响
  • 如果局部静态变量没有显示的初始值,将执行值初始化,内置类型的局部静态变量初始化为0
size_t count_calls()
{
	//控制流第一次经过ctr的定义之前,ctr被创建并初始化为0,每次调用则将ctr加1并返回新值
	static size_t ctr = 0;
	return ++ctr;
}
int main()
{
	for(size_t i = 0; i != 10; ++i)
		cout << count_calls() << endl;
	return 0;
}

7.函数声明:和其他名字一样,函数的名字必须在使用之前声明,类似于变量,函数只能定义一次但可以声明多次;
建议变量在头文件中声明,在源文件中定义,与之类似,函数也应该在头文件中声明而在源文件中定义,定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配

6.2 参数传递

1.形参初始化的机理与变量初始化一样,形参的类型决定了形参和实参的交互方式,若形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参

  • 当形参是引用类型,称它对应的实参被引用传递或函数被传引用调用,引用形参是它对应的实参的别名
  • 当实参的值被拷贝给形参时,形参和实参是两个互相独立的对象,称为值传递或者函数被传值调用
    注:熟悉C的程序员常常使用指针类型的形参访问函数外部的对象,在C++中,建议使用引用类型的形参替代指针
    2.const 形参和实参:和其他初始化过程一样,当用实参初始化形参时,会忽略顶层 const,传递给它常量对象或非常量对象均可
void fcn(const int i){/*fcn能读取i,但不能向i写值*/}
//C++允许定义若干相同名字的函数,但前提是不同函数形参列表应有明显区别,因为顶层const被忽略掉了,所以这两个fcn函数参数完全一样即重复定义
void fcn(int i){/*...*/}//错误,重复定义了fcn(int)
  • 形参的初始化方式和变量初始化方式一样,因此可以使用非常量初始化一个底层的 const 对象,但是反过来不行
  • 把函数不会改变的形参定义成普通的引用是一种比较常见的错误,因此尽量使用常量引用
    3.数组形参:数组有两个特殊性质,不允许拷贝数组以及使用数组时通常会将其转换成指针,因此当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针
    因为数组是以指针形式传递给函数的,函数一开始并不知道数组确切尺寸,调用者应额外提供一些信息,管理指针形参共有三种常用技术
    1)使用标记指定数组长度:要求数组本身包含一个结束标记,C风格字符串就是用了这种方法,C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符,函数处理C风格字符串时遇到空字符停止
void print(const char *cp)
{
	if(cp)					//若cp不是一个空指针
		while(*cp)
			cout << *cp++;
}

这种方法适用于带有明显标记的且不会与普通数据混淆的情况
2)使用规范库规范:这种技术是传递指向数组首元素和尾后元素的指针

void print(const int *beg, const int *end)
{
	while(beg != end)
		cout << *beg++ << endl;		//输出当前元素并将指针向前移动一个位置
}

3)显示传递一个表示数组大小的形参:专门定义一个表示数组大小的形参

//通过形参size的值确定要输出多少元素
void print(const int ia[], size_t size)
{
	for(size_t i = 0; i != size; ++i)
		cout << ia[i] << endl;
}

4.main: 处理命令行选项:有时我们确实要给 main 传递参数,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作
int main(int argc, char *argv[]} {…}
第一个形参 argc 表示数组中字符串的数量;第二个形参 argv 是一个数组,它的元素是指向C风格字符串的指针
因为第二个形参是数组,所以 main 函数也可以定义为:
int main(int argc, char **argv} {…}
当实参传递给 main 函数之后,argv 的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参;当使用 argv 中的实参时,一定要记得可选的实参从 argv[1] 开始,argv[0] 保存程序的名字,而非用户输入
5.含有可变形参的函数:为了编写能处理不同数量实参的函数,C++提供了两种主要方法,

  • 如果所有实参类型相同,可以传递一个名为 initializer_list 的标准库类型
  • 实参类型不同,则可以编写一种特殊的函数,即可变参数模板
    C++还有一种特殊的形参类型即省略符,可以传递可变数量的实参
    1)initializer_list 形参:若函数的实参数量未知但全部实参的类型都相同,则可以使用 initializer_list 类型的形参,initializer_list 也是一种模板类型,定义其对象时必须说明列表中所含元素的类型,且 initializer_list 对象中的元素永远是常量值,无法改变其中的值
void error_msg(initializer_list<string> il)
{
	for(auto beg = il.begin(); beg != il.end(); ++beg)
		cout << *beg << " ";
	cout << endl;
}

//两次调用传递的参数数量不同,第一次调用传入了三个值,第二次调用只传入两个
if(expected != actual)
	error_msg({"functionX", expected, actual});
else
	error_msg({"functionX", "okay"});

2)省略符形参:为了便于C++程序访问某些特殊的C代码而设置的
省略符形参只能出现在形参列表的最后一个位置

//指定了foo函数的部分形参类型,对应于这些形参的实参将会执行正常的类型检查,省略符形参对应的实参无需类型检查
void foo(parm_list, ...);
void foo(...);

6.3 返回类型和 return 语句

1.不要返回局部对象的引用或指针:函数完成后,它所占用的存储空间也随之被释放掉,因此函数终止意味着局部变量的引用将指向不再有效的区域

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

2.引用返回左值:函数的返回类型决定函数调用是否为左值,调用一个返回引用的函数得到左值,其他返回类型得到右值,可以像其他左值那样来使用返回引用的函数调用,因此我们能为返回类型是非常量引用的函数的结果赋值,但如果返回类型是常量引用,则不能给调用的结果赋值

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

3.主函数 main 的返回值:如果函数的返回类型不是 void,那么必须返回一个值,但有个例外,即允许 main 函数没有 return 语句直接结束,如果控制到达了 main 函数的结尾处而没有 return 语句,编译器将隐式地插入一条返回 0 的 return 语句
main 函数的返回值可以看做是状态指示器,返回 0 表示执行成功,返回其他值表示执行失败,其中非 0 值得具体含义依机器而定
4.返回数组指针
1)使用类型别名

typedef int arrT[10];	//arrT是一个类型别名,表示的类型是含有10个整数的数组
using arrT = int[10];	//arrT的等价声明

arrT* func(int i);		//func返回一个指向含有10个整数的数组的指针

2)若不使用类型别名,必须牢记被定义的名字后面数组的维度,数组的维度必须跟在函数名字之后,而函数的形参列表也跟在函数名字后面且形参列表应该优先于数组的维度,所以返回数组指针的函数形式如下:
Type (*function(parameter_list)) [dimension]
Type 表示元素的类型,dimension 表示数组的大小
e g: 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 类型
3)使用尾置返回类型:C++11新标准提供了尾置返回类型,任何函数定义都能使用尾置返回,但对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或引用
尾置返回类型跟在形参列表后并以一个->符号开头,为了表示函数真正的返回类型跟在形参列表之后,在本应该出现返回类型的地方防止一个 auto:

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

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

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
//decltype 并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想返回指针还需加*
decltype(odd) *arrPtr(int i)
{
	return (i % 2) ? &odd : &even
}

6.4 函数重载

1.重载函数:同一作用域内的几个函数名字相同形参列表不同,编译器根据传递的实参类型推断想要的是哪个函数,不允许两个函数除了返回类型外其他所有要素都相同,,若有两个函数的形参列表一样但返回类型不同,则第二个函数的声明是错误的

Record lookup(const Account&);
bool lookup(const Account&);	//错误,与第一个函数仅返回类型不同

2.重载与 const 形参
1)顶层 const 不影响传入函数的对象,因此一个有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来

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

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

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

Record lookup(Account&);	//函数作用于Account的引用
Record lookup(const Account&);	//新函数,作用于Account的常量引用

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

因为非常量可以转换成 const,因此上面的4个函数均可作用于非常量的对象或指向非常量对象的指针,但当传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
3.const_cast 和重载

const string &shorterString(const string &s1, const string &s2)
{
	return s1.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 string 的引用,但这个应用事实上是绑定在了某个非常量的实参上,最后再将其转换回一个普通的 string&,显然是安全的。
4.重载和作用域
如果在内层作用域中声明名字,将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名

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

当编译器处理调用 read 的请求时,找到的是定义在局部作用域中的 read,但这个名字是个布尔变量,无法调用一个布尔值,因此非法
当调用 print 函数时,编译器会先寻找对该函数名的声明,找到的是接受 int 值得局部声明,一旦在当前作用域中找到了所需名字,编译器会忽略掉外层作用域中的同名实体

6.5 特殊用途语言特性

1.默认实参
1)调用含有默认实参的函数时,可以包含该实参,也可以忽略该实参
2)默认实参作为形参的初始值出现在形参列表中,我们可以为一个或多个形参定义默认值,但需要注意,一旦某个形参被赋予了默认值,其后面的所有形参都必须有默认值
3)函数解析时,实参按照其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)

window = screen(, , '?');	//错误,只能省略尾部的实参

4)当设计含有默认实参的函数时,应尽量让不怎么使用默认值得形参出现在前面,而让经常使用默认值的形参出现在后面
2.内联函数
1)在大多数机器上,一次函数调用包含一系列工作:调用前要先保存寄存器,并在返回时恢复,可能需要拷贝实参,程序转向一个新的位置继续执行
2)在函数返回类型前加关键字 inline,将函数指定为内联函数,通常就是将其在每个调用结点上内联的展开
3)内联机制用于优化规模较小、流程直接、频繁调用的函数
3. constexpr 函数
1)constexpr 函数是指能用于常量表达式的函数
2)constexpr 函数的返回值类型及所有形参的类型都得是字面值类型(算数类型、引用、指针),而且函数体中必须有且仅有一条 return 语句
3)执行初始化任务时,编译器把对 constexpr 函数的调用替换成其结果值,为了能在编译过程中随时展开,constexpr 函数被隐式地指定为内联函数
4)constexpr 函数不一定返回常量表达式。
5)内联函数和 constexpr 函数通常定义在头文件中
4. assert 预处理宏
1)assert 是一种预处理宏,即一种预处理变量
assert(expr);
首先对 expr 求值,若表达式为假(0),assert 输出信息并终止程序执行;若表达式为真,assert 什么也不做;因此,assert 宏常用于检查不能发生的条件。
2)assert 宏定义在 cassert 头文件中,预处理名字由预处理器而非编译器管理,因此可以直接使用预处理名字而无需提供 using 声明。
5. NDEBUG预处理变量:如果定义了 NDEBUG,则 assert 什么都不做,默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查,可以使用一个 #define 语句定义 NDEBUG,从而关闭调试状态。(p 216)

6.6 函数匹配(p 217)

6.7 函数指针

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

//pf指向一个函数们该函数的两个参数均为const string的引用,返回值是bool类型
bool (*pf)(const string&, const string&)

*pf两端的括号必不可少,若不写括号,则 pf 是一个返回值为 bool 指针的函数
2.使用函数指针
1)当把函数名作为一个值使用时,该函数自动转换为指针

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

2)可以直接使用指向函数的指针调用该函数,无需提前解引用指针

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

3)指向不同函数类型的指针不存在转换规则,参数列表和返回类型必须完全一样

string::size_type sumLength(const string&, const string&);
bool cstringCompare(const char*, const char*);
pf = 0;					//正确:pf不指向任何函数
pf = sumLength;			//错误:返回类型不匹配
pf = cstringCompare;	//错误:形参类型不匹配
pf = lengthCompare;		//正确:函数和指针的类型精确匹配

3.重载函数的指针:编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数的某一个精确匹配

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

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

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

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

直接使用函数指针类型冗长而繁琐,类型别名和 decltype 可以使我们简化使用函数指针的代码

//Func 和 Func2 是函数类型
typedef bool Func(const string&, const string&);	//Func代表一种函数,返回值为bool,形参类型为两个const string的引用
typedef decltype(lengthCompare) Func2;				//等价的类型

//FunP 和 FuncP2 是指向函数的指针
//FuncP代表一个函数指针,该指针指向的返回值为void,形参类型为两个const string的引用的函数
typedef bool(*FuncP)(const string&, const string&);	
typedef decltype(lengthCompare) *FuncP2;			//等价的类型

//useBigger的等价声明,其中使用了类型别名
void useBigger(const string&, const string&, Func);		//编译器自动地将Func表示的函数类型转换为指针
void useBigger(const string&, const string&, FuncP2);

5.返回指向函数的指针:虽不能返回一个函数,但是能返回指向函数类型的指针,我们必须把返回类型携程指针形式编译器不会将函数返回类型当成对应的指针类型处理

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

从内向外的顺序阅读这条声明语句,f1有形参列表,所以f1是一个函数,f1前面有*,所以f1返回一个指针,进一步发现,指针的类型本身也包含形参列表因此指针指向函数,该函数返回类型是int
当然,也可以使用尾置返回类型的方式声明一个返回函数指针的函数

auto f1(int) -> int (*)(int*, int);
  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值