从结构化程序设计来讲,函数是模块划分的基本单位,是对特定功能的一种抽象,程序是一系列函数的集合。在面向对象程序设计中,对象是程序的基本单位,但是在对象中对数据的处理依然由函数来处理,函数是类中用于数据处理的基本单元。有时候我们也称函数为方法,两者是一个概念。一句话,函数就是完成特定功能的代码块,它可以接受数据,处理完后返回结果。对于一些重复的功能,我们可以通过函数来简化,使得代码更加简练,避免错误。对于一些复杂的功能,我们可以通过多个函数来拆分,使得代码更加清晰,容易理解。
函数分为系统库函数和用户自定义函数两种。库函数是由编译系统提供的函数。库函数的原型说明是在对应的头文件中定义的,使用这些函数前需要在程序的前端包含相关的头文件。用户自定义函数则是由程序员根据业务需求而设计的函数。您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定的,但在逻辑上,划分通常是根据每个函数执行一个特定的任务来进行的。
函数声明语法:
< 返回类型 > < 函数名 > ( [形参表] );
函数定义语法如下:
< 返回类型 > < 函数名 > ( [形参表] )
{
< 函数体 >
}
<返回类型>是函数返回值的数据类型,可以是基本数据类型,也可以是用户自定义的数据类型。程序中使用return语句返回数值,该数值的数据类型与< 返回类型 >一致。一个函数也可以不返回任何类型,那么< 返回类型 >使用void即可。
< 函数名 >由程序员自定义,使用字母和下划线构成,不能使用C++关键字。
[形参表] 中的参数由数据类型和变量构成,多个参数之间用逗号间隔。这个变量称为函数的形式参数,简称形参。当然,函数可以没有形参列表。形参表就是一组变量,这是函数要处理的数据。我们之前说到函数就是完成特定功能的代码块,它可以接受数据,处理完后返回结果。接受数据就是通过参数传递(形参表)的形式完成的,返回结果就是通过return关键字完成的。
我们可以看到,声明和定义的区别在于,定义多了函数体,也就是这个函数具体要实现什么功能的代码块。也就是说,函数的定义是必须的,声明可以没有。
函数声明和定义完成之后,我们就可以使用这个函数来完成特定的功能,称之为函数调用。函数调用的返回值就是函数体内return返回的数据,使用对应的数据类型变量接收就行了。
#include <iostream>
// 函数声明
double max(double x, double y);
int main()
{
// 函数调用
double a = 1.5, b = 2.5;
double c = max(a, b);
std::cout << c << std::endl; // 输出 2.5
}
// 函数定义
double max(double x, double y)
{
return x > y ? x : y;
}
C++中函数被调用前,编译器需要预先知道程序中是否存在被调用函数。告知编译器函数存在的方法有两种:一种是在函数调用之前定义被调用函数,即先定义再调用;第二种是在函数调用前先声明函数原型,之后就可以在具体定义函数之前调用该函数。如果我们直接定义函数再调用的话,那么我们需要再调用函数之前进行定义。也就是在main方法的上面进行定义max方法,然后才能在main方法中使用。如果我们先声明再定义的话,那么就需要再main方法之前进行声明,然后在main方法中调用,最后在去定义函数的实现。本案例采用这种方式。
备注:在函数声明或定义中,函数内的参数(x和y),我们称之为形式参数,简称形参。在函数调用的时候,传递的参数(a和b),我们称之为实际参数,简称实参。形参和实参是函数中非常重要的概念,需要清除的知道两者的区别。
函数的调用的时候,需要引用函数名,并为形参指定相应的实参。程序在为形参分配内存空间的同时就完成了实参向形参传递数据。C++中向函数传递的实参类型必须与形参相符或兼容,实参可以是常量,变量或表达式。C++中将实参传递给形参的方法主要有三种:按值传递,地址传递和引用传递。每种参数传递方法都有各自的特性。三种方式的表现形式就在于函数声明或定义的时候形参的数据类型是不同的。
按值传递:系统将实参的值赋予给函数的形参,形参是实参的一个拷贝,两者分别占有独立的内存空间,被调函数对形参的修改不影响实参。
// 按值传递函数定义(放在main函数上面)
void change1(double x) {
x++;
std::cout << "形参x=" << x << std::endl; // 输出 4.5
}
// 按值传递函数调用(放在main函数中执行)
double d = 3.5;
change1(d);
std::cout << "实参d=" << d << std::endl; // 输出 3.5
地址传递:该方法需要将形参声明为指针类型。实际就是把实参变量的内存地址赋予给形参,两者指向同一个内存单元。被调用函数对形参的修改等同于对实参的修改。
// 按地址传递函数定义(放在main函数上面)
void change2(double* x) {
(*x)++;
std::cout << "形参x=" << *x << std::endl; // 输出 4.5
}
// 按地址传递函数调用(放在main函数中执行)
double e = 3.5;
change2(&e);
std::cout << "实参e=" << e << std::endl; // 输出 4.5
引用传递:该方法需要将形参声明为引用类型。由于引用就是变量的别名,因此实参和形参是同一个内存单元,对形参的修改都会同步到实参。
// 按引用传递函数定义(放在main函数上面)
void change3(double& x) {
x++;
std::cout << "形参x=" << x << std::endl; // 输出 4.5
}
// 按引用传递函数调用(放在main函数中执行)
double f = 3.5;
change3(f);
std::cout << "实参f=" << f << std::endl; // 输出 4.5
引用传递和指针传递是不同的,虽然它们都操作到主调函数中的实参变量。对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。因为引用只能在定义时被初始化一次,之后不可变;指针是可变的。也就是说引用相对来说,比较安全一些。
备注:对于尺寸较大的实参,用地址传递和引用传递不仅能够减少系统的时间和空间开销,还能具备修改形参等同于修改实参的效果。然而,在程序设计中有时需要禁止被调用函数修改主调函数的实参,在C++中可以用const关键字来修饰指针或引用的形参。
C++ 不允许变量重名,但是允许多个函数取相同的名字,只要参数表不同即可,这叫作函数的重载(overload)。编译器是根据函数调用语句中实参的个数和类型来判断应该调用哪个函数的。因为重载函数的参数表不同,而调用函数的语句给出的实参必须和参数表中的形参个数和类型都匹配,因此编译器才能够判断出到底应该调用哪个函数。
// 直接定义两个数比较大小
int Max(int a, int b)
{
return a > b ? a : b;
}
// 直接定义三个数比较大小
int Max(int a, int b, int c)
{
a = a > b ? a : b;
return a > c ? a : c;
}
int main()
{
// 调用两个数比较大小
int g = Max(1, 2);
std::cout << g << std::endl;
// 调用三个数比较大小
int h = Max(1, 2, 3);
std::cout << h << std::endl;
}
备注:在函数中调用当前函数,这种调用方式称为递归。如果没有中止条件,递归调用将会无限循环下去(死循环),因此递归一定要有结束条件。
本课程的所有代码案例下载地址:
C++示例源代码(配合教学课程使用)-C/C++文档类资源-CSDN下载
备注:这是我们游戏开发系列教程的第一个课程,主要是编程语言的基础学习,优先学习C++编程语言,然后进行C#语言,最后才是Java语言,紧接着就是使用C++和DirectX来介绍游戏开发中的一些基础理论知识。我们游戏开发系列教程的第二个课程是Unity游戏引擎的学习。课程中如果有一些错误的地方,请大家留言指正,感激不尽!