天天看点

C++Primer-第六章 函数

第六章 函数

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

6.1 函数基础

一个典型的函数定义包括以下部分:返回类型、函数名、由0个或者多个形参组成的列表以及函数体。

  • 编写函数
int fact(int val)
{
	int ret=1;			//局部变量,用于保存计算结果
	while(val>1)		
	{
		ret*=val--;		//
	}
	return ret;
}
           
  • 调用函数

    函数的调用完成两项工作:

    • 用实参初始化函数对应的形参
    • 将控制权转移给被调用函数,此时主函数的执行被暂时中断。
  • 形参和实参
    • 实参是形参的初始值。
    • 实参的类型必须与对应的形参类型匹配
  • 函数的形参列表

    函数的形参列表可以为空,但是不能省略。

void f1(){/***/}				//隐式定义空形参列表
void f2(void){/****/}			//显示地定义空形参列表
int f3(int v1,v2){/****/}		//错误: 参数类型不能省略
int f4(int v1,int v2){/****/}	//正确
           

6.1.1 局部对象

在C++语言中,名字有作用域,对象有声明周期。

名字的作用域是程序文本的一部分,名字在其中可见。

对象的声明周期是程序执行过程中该对象存在的一段时间。

  • 自动对象

    只存在于块执行期间的对象称为自动对象。当块执行结束后,块中创建的自动对象的值就会变成未定义的了。(是不是可以理解为销毁? 销毁要根据具体情况来考虑,比如释放或者垃圾回收)

  • 局部静态对象

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

size_t count_calls()
{
	static size_t ctr=0;		//调用结束后,这个值仍然有效
	return ++ctr;
}
           

6.1.2 函数声明

  • 和其他名字一样,函数的名字也必须在使用之前声明。
  • 函数的声明和函数的定义非常类似,唯一的区别是函数声明无函数体,用一个分号替代即可。
  • 函数的声明不包含函数体,所有也就无需形参的名字。
  • 函数的声明建议在头文件中声明,在源文件中定义。

6.1.3 分离式编译

随着程序的越来越复杂,可以把文件中的函数或者其他类等按照逻辑关系将其划分开来,然后每个文件独立编译

6.2 参数传递

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

形参初始化的机理与便利初始化一样
  • 当形参是引用类型时,我们说它对应的实参是引用类型或者函数被传入引用个调用。
  • 当实参的值别拷贝给形参时,形参和实参是两个独立的对象。我们说这样的实参是值传递。

6.2.1 传值参数

  • 非引用变量

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

int n=0;		//int 类型的初始变量
int i=n;		//i是n的值的副本
i=42;			// i的值改变,n的值不变
           
  • 指针形参

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

int n=0,i=42;
int *p=&n,*q=&i;			// p指向n; q指向i
*p=42;						//n的值改变;p不变
p=q;						// p现在指向了i;但是i和n的值都不变

//函数接受一个指针,然后将指针所指的值置为0
void reset(int *ip)
{
	*ip=0;			// 改变指针ip所指对象的值
	ip=0;			// 只改变了ip的局部拷贝,实参未被改变
}
           
#include <iostream>
using namespace std;

void test(int *a, int *b)
{
	cout << "&a:" << &a << endl;
	cout << "*a:" << *a << endl;
	*a = 7, *b = 8;
}

int main()
{
	int a = 3, b = 4;
	cout << "&a:" <<&a<< endl;
	cout << "a:" << a << endl;
	test(&a, &b);
	return 0;
}
结果:
&a:00ACFDC0
a:3
&a:00ACFCDC
*a:3
           

6.2.2 传引用参数

引用的操作实际上是作用在引用的对象上,引用形参丙丁初始化它的对象。

  • 使用引用避免拷贝
bool isShorter(const string &s1,const &string s2)
{
	reutrn s1.size() < s2.size();
}
           

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能铜鼓引用形参访问该类型的对象。

如果函数无须改变引用形参的值,最好将其声明为常量引用。
  • 使用引用形参返回额外信息

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

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.3 const形参和实参

当形参是const时,一定要注意const是否是顶层const,顶层const作用于对象本身:

const int ci=1024;		//不能改变ci,const是顶层const
int i=ci;				//正确:当拷贝ci时,忽略它的顶层const
int *const p=&i;		//错误:const是顶层的,不能给p赋值
*p=0;					//正确:通过p改变独享的内容是运行的
           
  • 指针或引用形参与const

    形参的初始化方式和变量的初始化方式是一样的。

  • 尽量使用常量引用

    把函数不会改变的形参定义成引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。所有把形参定义为const类型的能避免错误。

6.2.4 数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响:

  1. 不允许拷贝数组
  2. 使用数组时会将其转换成指针

6.2.5 main: 处理命令行选项

main函数有两个参数

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

6.2.6 含有可变形参的函数

  • 使用函数
void error_msg(ErrCode e,initializer_list<string> il)
{
	cout<<e.msg()<<":";
	for(const auto &elem:il)
		cout<<elem<<" ";
	cout<<endl;
}
           
  • 省略符形参
void foo(param_list,...);
void foo(...);
           

6.3 返回类型和return语句

C++Primer-第六章 函数

6.4 函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。

void print(const char *cp);
void print(const int *beg,const int *end);
void print(const int ia[],size_t size);
           
main函数不能重载

6.5 特殊用途语言特性

6.5.1 默认实参

某些函数有这样一种形参,在函数的很多次调用中他们都被赋予一个相同的值,此时,我们把这个反复出现的值称为幻术的默认实参。

typedef string::size_type sz;		//取个别名
string screen(sz ht=24,sz wid=80,char background=' ');
           

6.5.2 内联函数和constexpr函数

我们把规模较小的操作定义成函数有很多好处:

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

    将函数指定为内联函数,通常就是将他在每个调用点上内联地展开。

6.6 函数匹配

  • 确定候选函数和可行函数
    • 函数匹配:
      • 选定本次调用对应的重载函数集,集合中的函数称为候选函数。

        候选函数具备两个特征:

        • 与被调用的函数同名
        • 其声明在调用点可见
      • 考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数 称为可行函数。

        可行函数有两个特征:

        • 其形参数量与本次调用提供的实参数量相等
        • 每个实参的类型与对应的形参类型相同,或者能转换成形参的类型
      • 从可行函数中选择与本次调用最匹配的函数。

6.7 函数指针

函数指针指向的是函数而非对象。

要想声明一个可以指向函数的指针,只要需要用指针替换函数名即可。

//比较两个string对象的长度
bool lengthCompare(const string &,const string &);		//该函数的类型是bool

//pf指向一个函数,该函数的参数是两个const string 的引用,返回值是bool类型
bool (*pf)(const string &,const string &);
           
*pf两段的括号必不可少。如果不写这对括号,则pf是一个返回值为bool指针的函数。
  • 使用指针函数
pf = lengthCompare;		//pf指向名为 lengthCompare的函数
pf = &lengthCompare;	//等价的赋值语句:取地址符是可选的
           
  • 重载函数的指针
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 &));
           

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

继续阅读