四、函数
函数的使用
以一个求阶乘的函数为例讲述如何编写和使用函数。
1.头文件,函数的声明
我们提倡 分离式编译 (separate compilation) ,因此我们会将程序分割到数个文件中。头文件包含了该函数的声明。
#ifndef FACT_H
#define FACT_H
#include <iostream>
using namespace std;
unsigned int fact(int a);
#endif
fact.h是我们定义的头文件的名称。其中ifndef和endif语句块是一种预处理机制,防止重复包含头文件带来的潜在危害。
在头文件中需要包含哪些库是视其所生命的函数而定的,在这里只用到了一些iostream的内容,因此只include它一个。
在这个头文件中我们声明了一个会返回unsigned int类型的、名称为fact,形参为一个整型变量的函数。在此处仅仅是声明,并不设计该函数的具体实现。
将函数的声明直接放在使用该函数的源文件中也是合法的,但是这么做也许会很繁琐并且容易出错。如果将函数的声明放在头文件中,既能保证同意函数的所有声明保持一致,在将来万一我们想要修改函数的接口也会更加的方便。
2.源文件,函数的定义
在这个例子中,函数的定义如下:
#include "fact.h"
unsigned int fact(int a)
{
unsigned int temp = 1;
while (a > 1)
{
temp *= a--;
}
return temp;
}
首先,为了将源文件与头文件关联起来,我们必须将含有函数声明的头文件包含进定义函数的源文件中,即第一行 include 语句。
随后我们开始定义这个计算阶乘的函数。它的返回,函数名,形参与头文件中一致,大括号内是函数体,涉及到函数的具体实现,最后返回了一个temp,其类型必须与函数的返回类型一致。
3.主程序,函数的调用
#include "fact.h"
using namespace std;
int main()
{
int a = 0;
cin >> a;
cout << fact(a) << endl;
return 0;
}
要调用fact函数,必须先在源文件中include声明其的头文件。
我们用调用运算符() 来执行函数。函数的调用完成两项工作:一是用实参初始化函数的形参,二是将控制权转移给被调用函数。
用实参初始化函数的形参是指程序会首先创建一个名字为a的int变量,并用调用运算符内的实参初始化这个变量。该变量的生命周期仅在fact函数被调用期间,一旦函数执行结束,该变量就会被“销毁”。
参数传递
1.值传递
如上文所示的程序,主函数中定义的变量 a 的值被拷贝后传递给了fact函数中的形参 a ,在这种情况下形参和实参是两个相互独立的对象,这样的传递方式称为 值传递 。
当初始化一个非引用类型的变量时,初始值被拷贝给变量,此时对变量的改动不会影响初始值。
unsigned int fact(int a)
{
unsigned int temp = 1;
while (a > 1)
{
temp *= a--;
}
return temp;
}
int main()
{
int a = 0;
cin >> a;
cout << fact(a) << endl;
cout << a << endl;
return 0;
}
现在分析:虽然在fact中我们对形参a做了更改:a-- ,但是如同上文所说,在值传递的情况下程序是通过建立副本的方式实现功能的,因此主函数中的变量a并没有被改变。
2.引用传递
当形参是引用类型的时候,这时的实参被 引用传递 了。此时的形参被绑在了对应的实参上,也就是说我们可以通过形参更改实参的值。
void reset(int &i)
{
i = 0;
}
int main()
{
int a = 10;
reset(a);
cout << a << endl;
return 0;
}
根据值传递的结论,倘若reset的形参为非引用类型,则这段代码打印出来的值应该是10。实际上由于在这里使用了引用形参,因此改变 i 的值就是改变了 a 的值。
由于拷贝大的类类型对象和容器是比较低效的行为,甚至有的类不支持拷贝操作,因此建议在这种情况使用引用形参访问该类型对象。
一个实例:
#include "bubblesort.h"
void bubblesort(int (&r)[10])
{
for (int i = 0; i != 9; ++i)
{
for (int j = 0; j != 10 - i - 1; ++j)
{
int temp;
if ( r[j] > r[j + 1])
{
temp = r[j];
r[j] = r[j + 1];
r[j + 1] = temp;
}
}
}
}
#include "bubblesort.h"
int main()
{
int arr[] = {1,10,2,7,9,4,6,3,8,5};
bubblesort(arr);
for (auto r : arr)
{
cout << r << " ";
}
cout << endl;
return 0;
}
这是一个冒泡排序的例子,只能传递长度为10的数组, 扩展性太差 对于可变数组的传递以后会涉及。
3.关于const形参
由于普通的引用形参会改变实参的值,因此在不需要改变的情况下建议优先使用带有const的引用形参。否则会给使用者一个错觉,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。
string size_type find_char(const string &s,char c);
这个函数的目的是在字符串 s 中寻找字符 c 。此时定义的 s 类型为对字符串的常量引用,此时的函数既能接受 const 对象,也能接受字面值。
string size_type find_char(string &s,char c);
find_char("hello world",'o');
除此以外还会有更加隐蔽的问题,当在另外一个函数中调用这个函数的时候:
bool is_sentence(const string &s)
{
return find_char(s,'.') == s.size()-1;
}
在这个新函数中形参为常量引用,而 find_char 则为普通引用,因此会报错。结局办法可以是把 is_sentence 函数的形参写成普通引用(但这只是掩耳盗铃,在下一次调用时依然无法避免类似的问题),也可以在 is_sentence 函数内部定义一个 s 的副本,亦或者更改 find_char 的形参为常量引用。
4.指针形参
不想写了,类似于引用传递。形参中输入的应该时实参的地址,可以实现实参的修改。
指针类型的形参目前是在传递数组的时候遇到。由于数组的名字表示的是首元素的地址,是一个指针,因此可以用指针形参传递。
局部对象
形参和函数体内部定义的变量统称为 局部变量 。它们仅在函数的作用域内可见,因此是“局部”的。
1.自动对象
这是普通的局部变量,当程序读到变量定义的语句时创建该变量,到达定义所在的块的末尾时销毁它。这样只存在于块执行期间的对象称为 自动对象 。
2.局部静态变量
某些时候有必要让变量贯穿始终,使用 static 类型从而获得这样的对象。
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;
}
在 count_calls 函数结束后 ctr 对象依然存在,不会消失。
函数的返回
1.引用作为返回
- 不要返回局部变量的引用
int & test1()
{
int a = 10;
return a;
}
int main()
{
int &r = test1();
cout << r << endl;
return 0;
}
由于局部变量 a 在 test1 运行结束后被释放了,所以函数无法正确返回一个不存在的变量的引用。
- 引用可以作为左值
如果一个函数的返回值是引用,那么这个函数的调用可以作为左值使用
int & test2()
{
static int a = 10;
return a;
}
int main()
{
int &r = test2();
cout << r << endl;
test2() = 100;
cout << r << endl;
return 0;
}
结果为:
函数的参数
1.默认参数
默认参数可以有多个,但是必须统一放在形参列表尾部。
int func(int a, int b, int c)
{
return a + b + c;
}
int func1(int a, int b = 0, int c = 0)
{
return a + b + c;
}
int func2(int a, int b = 0, int c)
{
return a + b + c;
}
默认参数只能存在于函数声明或者函数实现其中之一。
int func3(int a, int b, int c = 10);
int func(int a, int b, int c = 10)
{
return a + b + c;
}
2.占位参数
void func(int a, int)
{}
其中的第二个参数就是占位参数,在调用时必须补全这个参数。
int main()
{
int a = 10;
func(a,8);
}
占位参数还可以有默认参数值,这样在调用时就可以不用补了。
void func(int a, int = 8)
{}
int main()
{
int a = 10;
func(a);
}
|