函数的定义格式为
返回值类型 函数名(参数表)
{
语句块;
}
1.参数传递
函数参数分为形参和实参两种。
形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。实参出现在主调函数中,进入被调函数中,实参变量也不能使用。形参和实参的功能是做数据传递。发生函数调用时,主调函数把实参的值传送给被调函数的形参,从而实现主调函数向被调函数的数据传递。
函数调用时,实参必须与形参兼容。C在函数调用时一般有两种传递方式,指针传递和值传递,严格来说,指针传递也是按值传递的,复制的是地址。C++中一般有三种传递方法:值传递,指针传递和引用传递。
给函数传递实参遵循变量初始化原则,非引用类型的形参以相应实参的副本进行初始化,形参的任何修改都都作用于于形参副本,而不会影响到实参。可将形参指定为引用类型以避免传递副本的开销,对引用形参的任何修改都会影响到实参本身,故应将不需要修改实参的引用定义为const引用。
将引用作为函数参数有如下特点:
1)传引用给函数,形参是作为实参变量或对象的一个别名来,所以对形参的操作就是对实参的操作。
2)使用引用传参,内存中不会产生实参副本,而是直接对实参操作。而使用一般变量传参,形参是实参的副本,故需要为形参分配存储单元。如果传递的是对象,还要调用拷贝构造函数。因此参数传递数据较大时,用引用效率高,所占空间少。
3)用指针作为函数参数虽然也能起到引用效果,但是在被调函数中同样要为形参分配存储单元,且需要重复使用 *指针变量名 形式进行计算,可读性差。在主调函数调用点处必须使用变量地址作为实参。
传递指针的引用:
假设我们想编写一个函数,实现两个指针的交换,应该如何做呢?用*定义指针,&定义引用,例子如下:
Void ptrswap(int * &v1, int * &v2 )
{
Int * tmp = v2;
V2 = v1;
V1 = tmp;
}
形参int *&v1的定义应该从右至左理解:v1是一个引用,与指向int型对象的指针相关联。也就是说,v1只是传递进ptrswap函数的任意指针别名。
2.内联函数
一般在代码中用inline修饰,内联函数有两种:
成员函数成为内联函数:
在类中定义的成员函数默认为内联函数,可显式加上inline标识符,也可不加。
在类中声明的成员函数,加inline则为内联函数,没加inline而在类外定义时加了inline,
也为内联函数。
普通函数成为内联函数:
声明或定义前加inline其成为内联函数。
通常在编译时,调用内联函数的地方不进行函数调用,而是使用函数体替换调用处的函数名,类似宏替换,称为内联扩展。内联扩展可消除调用时的时间开销。
内联函数一般适用于优化小的,只有几行且经常被调用的函数。
3.默认参数
默认参数就是函数声明或定义时,直接对参数赋值。调用函数是若没有指定与形参对应的实参,就自动使用默认参数。
默认参数特点如下:
1)默认参数只能在函数声明中设定一次,且只在无函数声明时,才可以在函数定义中设定。
设定顺序是从右到左,即若一个参数有默认值,则其右边的参数都要有默认值。
2)默认参数调用时从左到右逐个调用。
void mal(int a, int b = 3, int c = 5); //默认参数
mal(3, 8, 9); //调用时指定参数,则不使用默认参数
mal(3,5); //指定两个参数,从左到右调用,相当于 mal(3, 5, 5);
mal(3); //指定一个参数,从左到右调用,相当于 mal(3, 3, 5);
mal(); //错误,a没有指定默认值
mal(3, ,9);//错误,应按从左到右逐个调用,此处第二参数没有调用就调用的第三个参数
3)默认值可以是全局变量,静态变量甚至是函数,但不能是静态变量。因为默认参数调用在编译时确定,而局部变量位置与默认值在编译时无法确定。
可变参数: 略。
4.函数重载
在同一作用域内,具有相同函数名不同参数列表的函数,被称为重载函数。重载函数通常用来命名一组功能相似的函数,可减少函数名使用量,避免名字空间污染,提高可读性。
进行函数重载时,要求同名函数在参数个数上不同,或参数类型上不同,否则无法实现重载。
5.泛型编程
所谓泛型编程就是以独立于任何特定类型的方式编写代码,使用泛型编程时,我们需要提供实例化所操作的类型或值。
在泛型编程中,我们所编写的类和函数能够多态地用于跨越编译时不相关的类型。一个类或者一个函数可以用来操纵多种类型的对象。标准库中的容器、迭代器和算法是很好的泛型编程的例子。标准库用独立于类型的方式定义每个容器、迭代器和算法,因此几乎可以在任意类型上使用标准库的类和函数。在C++中,模板是泛型编程的基础,是创建类或函数的类型或公式。
6. 函数模板
模板定义以关键字templete开始,后接模板形参表,模板形参表是用尖括号括起来的一个或形参的列表,之间用逗号隔开,模板形参表不能为空,可以在类或函数的定义中使用那些类型或值。
如 callWithMax函数声明一个名为T的类型形参,在函数内部,可以使用名字T引用一个类型,T表示哪个实际类型由编译器根据所用的函数参数确定。
Templete <typename T>
Void callWithMax(const T &a, const T &b)
{
F(a>b? a:b);
}
类型形参T跟在关键字class或typename之后定义,此处二者无区别。
使用时,编译器会推断哪个(或哪些)模板实参绑定到了模板形参。一旦编译器确定了实际的模板实参,就称它实例化了一个函数模板的实例。编译器将确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。
7.类模板
可以想定义函数模板一样定义类模板。如下
template <class Type> class Queue
{
public:
Queue(); //默认构造函数
Type &front (); //取队首元素
const Type &front() const;
void push(const Type &); //入队
void pop(); //出队
bool empty() const; //判队空
private:
...
};
使用类模板时,必须为其形参显示指定实参:
Queue<int> qi;
编译器通过实参来实例化这个类的特定类型版本,即编译器通过用户提供的实际特定类型代替type,重新编写Queue类。
6.函数的递归
如果在一个函数、过程或数据的定义中又调用了他自身,那么这个函数、过程或数据结构称为递归定义的,简称递归。
递归的精髓在于能否将原始问题转化为属性相同但规模较小的问题。通常对于大型的复杂问题,通过递归只需少量代码就可以描述出解题过程中所需要的多次重复计算。在递归过程中,系统为每一层返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出。因为包含多次重复计算,故递归效率低下,但其优点是代码简单,容易理解。通常可以通过栈将递归算法转换成非递归算法。
必须注意递归是不能循环定义的,其必须满足下面4两个条件:
1)递归表达式(递归体)
2)边界出口(递归出口)