一、函数基础
一个函数(function)定义包括:返回类型、函数名字、0或若干个形参组成的列表以及函数体。
实参是形参的初始值,并且实参的类型必须与对应的形参类型匹配,函数的形参列表可以为空,但是不能省略,一般是书写一个空的形参列表,也可以使用关键字void表示函数没有形参
void f1(){/*...*/} //隐式地定义空形参列表
void f2(void){/*...*/} //显示地定义空形参列表
大多数类型都能作为函数的返回类型,void类型表示不返回任何值,函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
1.2 局部对象
C++中,名字有作用域,即程序文本的一部分;对象有声明周期,是程序执行过程中该对象存在的一段时间。
形参和函数体内部定义的变量统称为局部变量(local variable),它们仅在函数的作用域内可见,并隐藏在外层作用域中同名的其他所有生命之中。
自动对象(automatic object)指只存在于执行期间的对象,即当函数控制路径经过变量定义语句时创建该对象,当达到定义所在块末尾时销毁它,这时自动对象的值就变成未定义的了,形参是一种自动对象。
有时候需要令局部变量的生命周期贯穿函数调用及之后的时间,这时可以将局部变量定义为static类型,即局部静态对象(local static object),其在程序执行路径第一次经过对象定义语句时初始化,并直到程序终止才被销毁。
1.2 函数声明
函数的名字必须在使用之前声明,与变量类似,函数只能定义一次,但可以声明多次,函数的声明与定义的唯一区别是函数声明无须函数体,用一个分好替代即可,函数声明也称作函数原型(function prototype)。
一般地,与变量相似,函数在头文件中声明,在源文件中定义,定义函数的源文件应该含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
1.3 分离式编译
随着程序越来越复杂,一般吧程序的各个部分分别存储在不同的文件中,为了允许编写程序时按照逻辑关系将其划分开来,C++语言支持分离式编译(separate compilation)。
二、参数传递
形参的类型决定了形参与实参交互的方式,如果形参是引用类型,它将绑定到对应的实参上,否则将实参的值拷贝后赋给形参。
当形参是引用类型时,称它对应的实参被引用传递,引用形参也是它绑定对象的别名,即形参是其对应实参的别名。当实参的值被拷贝给形参时,形参和实参是两个独立的对象,我们称实参被值传递。
2.1 传值参数
初始化一个非引用类型的变量时,初始值被拷贝给变量,此时对变量的改动不会影响初始值。
指针的行为和其他非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值,拷贝之后两个指针是不同的指针。
int n = 0,i = 42;
int *p = &n,*q = &i; //p指向n,q指向i
*p = 42; //n的值改变,p不变
p=q; //p指向i,但i和n的值都不变
指针形参的行为与上述相似:
void reset(int *ip)
{
*ip = 0; //改变指针ip所指对象的值
ip = 0; //只改变了ip的局部拷贝,实参未被改变
}
调用reset函数之后,实参所指的对象被置为0,但是实参本身并没有改变:
int i = 42;
reset(&i); //改变i的值而非i的地址
cout << "i = " << i << endl; //输出i = 0
同时C++中建议使用引用类型的形参替代指针。
2.2 传引用参数
引用形参的行为与对引用的操作类似,通过使用引用形参,允许函数改变一个或多个实参的值。
//该函数接收一个int对象的引用,然后将对象的值置为0
void reset(int &i) //i是传给reset函数的对象的另外一个名字
{
i = 0; //改变了i所引对象的值
}
int j = 42;
reset(j); //j采用传引用方式,它的值被改变
①使用引用避免拷贝
拷贝大的类类型对象或者对象比较抵消,甚至有的类类型根本就不支持拷贝操作,这时函数只能通过引用形参访问该类型的对象,比如编写一个函数比较两个string对象的长度,为了避免直接拷贝过长的string对象,可以使用引用形参,且因为无需改变string内容,所以最好把形参定义成对常量的引用:
bool isShorter(const string &s1,const string &s2)
{
return s1.size() < s2.size();
}
②使用引用形参返回额外信息
一个函数只能返回一个值,但是引用形参可以为我们一次返回多个结果。比如定义一个函数能够返回string对象中某个指定字符第一次出现的位置,并且也返回该字符出现的总次数。一种方法是定义一个新的数据类型,让它包含位置和数量两个成员,而除此之外,我们还可以传入一个额外的引用实参,令其保存字符出现的次数。
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; //出现次数通过occurs隐式地返回
}
auto index = find_char(s,'o',ctr);
2.3 const形参和实参
当形参是const时,与之前讨论的顶层const作用于对象本身是类似的,即当用实参初始化形参时会忽略顶层const,传给它常量对象或者非常量对象都可以:
const int ci = 42; //不能改变ci,const是顶层的
int j = ci; //正确,拷贝ci时,忽略其顶层const
int * const p = &j; //const是顶层的,不能给p赋值
*p = 0; //正确,通过p改变对象的内容是允许的,i变成0
void fcn(const int i){/*fcn能够读取i,但是不能向i写值*/}
void fcn(int i){/*...*/} //错误,重复定义了fcn(int)
C++中允许有相同名字函数,但是函数的形参列表应有明显区别,上述函数因为顶层const被忽略了,所以两个函数的参数可以是一样的,所以第二个函数时错误的。
①指针或引用形参与const
形参的初始化方式和变量的初始化方式是一样,可以使用非常量初始化一个低层const对象,但是反过来不行,同时一个普通的引用必须用同类型的对象初始化。
int j = 42;
const int *cp = &j; //正确,但是cp不能改变j
const int &r = j; //正确,但是r不能改变j
const int &r2 = 42; //正确
int *p = cp; //错误,p的类型和cp的类型不匹配
int &r3 = r; //错误,r3的类型和r不匹配
int &r4 = 42; //错误,不能用字面值初始化一个非常量引用
const int cj = j;
string::size_type ctr = 0;
reset(&j); //调用形参类型是int*的reset函数
reset(&cj); //错误,不能用指向const int对象的指针初始化int*
reset(j); //调用形参类型是int&的reset函数
reset(cj); //错误,不能把普通引用绑定到const对象cj上
reset(42); //错误,不能把普通应用绑定到字面值上
reset(ctr); //错误,类型不匹配,ctr是无符号类型
//正确,find_char的第一个形参是对常量的引用
find_char("hello world!", 'o',ctr);
②尽量使用常量引用
把函数不会改变的形参定义成普通的引用会带给函数的调用者一种误导,即函数可以修改它的实参值,并且使用非常量引用也会极大限制函数所能接收的实参类型,如上面的不能把const对象、字面值或需要类型转换的对象传递给普通的引用形参。
2.4 数组形参
数组有两个非常重要的性质:①不允许拷贝数组,所以无法以值传递的方式使用数组参数;②使用数组时通常会将其转换成指针,所以当我们为函数传递一个数组时,实际传递的是指向数组首元素的指针。
虽然不能以值的方式传递数组,但是看可以把形参写成类似数组的形式,当给传给函数的是一个数组,实参会自动地转换成指向数组首元素的指针,数组大小对函数的调用没有影响:
//尽管形式不同,但这三个print函数时等价的,每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]); //可以看出函数的意图是作用域一个数组
void print(const int[10]); //这里维度表示我们期望数组有多少个元素,实际不一定
①使用标记指定数组长度
管理数组实参的第一种方法要求数组本身包含一个借宿标记,这种方法典型示例是C风格字符串,其储存在字符数组中,并且在最后一个字符后面跟着一个空字符,函数处理C风格字符串时遇到空字符停止:
void print(const char *cp)
{
if (cp) //若cp不是一个空指针
while (*cp) //只要指针所指的字符不是空字符
cout << *cp++; //输出当前字符并将指针向前移动;一个位置
}
②使用标准库规范
管理数组实参的第二种技术是传递指向数组首元素和尾元素的指针,使用该方法可以按照如下形式输出元素内容:
void print(const int *beg,const int *end)
{
//输出beg到end之间(不含end)的所有元素
while (beg !=end)
cout << *beg++ << endl; //输出当前元素并将指针向前移动一个位置
③显示传递一个表示数组大小的形参
第三种管理数组实参的方法是专门定义一个表述数组大小的形参:
//const int ia[]等价于const in* ia
//size表示数组的大小,将它显示地传给函数用于控制对ia元素的访问
void print(const int ia[],size_t size)
{
for (size_t i = 0;i !=size; ++i){
cout << ia[i] << endl;
}
}
int j[] = {0,1};
print(j,end(j) - begin(j));
需要注意的是,关于引用的讨论同样适用于指针,即函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。
④数组引形参
C++语言允许将变量定义数组的引用,同样形参也可以是数组的引用,引用形参绑定到对应的实参上,即绑定到数组上:
//形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10])
{
for (auto elem :arr)
cout << elem << endl;
}
2.5 main:处理命令行选项
如果我们需要给main函数传递实参,一种常见的情况是用户通过设置一组选项来确定所要执行的操作,假定main函数位于可执行文件prog之内,我们可以向程序传递下面的选项:
prog -d -o ofile data0
这些命令选项通过两个形参传递给main函数:
int main(int argc,char *argv[]) { ... }
//or
int main(int argc,char **argv) { ... }
第一个形参argc表示数组中字符串的数量;第二个形参argv是一个数组,它的元素是指向C风格字符串的指针,所以main函数也可以定义成第二种形式,其中argv指向char*,当实参传给main函数之后,argv的第一个元素指向程序的名字或者第一个空字符串,接下来的元素一次传递命令提供的实参,最后一个指针知乎的元素值保证为0。需要注意的是当使用argv中的实参时,可选实参从argv[1]开始,argv[0]保存程序的名字,而非用户输入。
argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
agrv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
2.6 含有可变形参的函数
为了可以编写可以处理不同数量实参的函数,C++11提供了两种主要的方法;如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,那么可以编写一种特殊的函数,即可变参数模板。
①initializer_list形参
initializer_list用于某种特定类型数值的数组,其类型定义在同名的头文件中。与vector一样initializer_list也是一种模板类型,定义initializer_list对象时,必须说明列表中所含元素的类型:
initializer_list<string> ls; //元素类型是string
initializer_list<int> li; //元素类型是int
与vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变其对象中的值;我们使用如下形式编写输出错误信息的函数,使其可以作用域可变数量的实参:
void error_msg(initializer_list<string> li)
{
for (auto beg = li.begin();beg != li.end(); ++beg)
cout << *beg << " " ;
cout << endl;
}
参考文献:
①C++ primer 第五版。
本文介绍了C++中的函数基础,包括函数定义、局部对象和函数声明。此外,详细探讨了参数传递,包括传值参数、传引用参数、const形参、数组形参的处理,以及main函数的命令行选项和可变形参的函数。强调了引用形参在避免拷贝和返回额外信息中的作用,并推荐使用const引用。
3889

被折叠的 条评论
为什么被折叠?



