【C++笔记】07 函数

 

1 函数的声明与定义

函数声明原型的结构是:

返回值类型 函数名(参数表);

函数定义的格式为:

返回值类型 函数名(参数表)
{
    语句块;
}

2 参数传递

函数的参数分为形参和实参两种:

  • 形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。

  • 实参出现在主调函数中,进入被调函数后,实参变量也不能使用。

形参和实参的功能是做数据传送。发生函数调用时,主调函数把实参的值传送给被调函数的形参,从而实现主调函数向被调函数的数据传送。

一般来说,函数调用时,

  • C中有两种传递:值传递和指针传递(传递地址)。

    注:严格来说,只有一种传递(值传递),指针传递也是按值传递的,复制的是地址。

  • C++中有三种传递:值传递、指针传递和引用传递。

2.1 使用引用作为函数参数

给函数传递实参遵循变量初始化的规则。非引用类型的形参以相应实参的副本(值)初始化。对(非引用)形参的任何修改仅作用于局部副本,并不影响实参本身。为了避免传递副本的开销,可将形参指定为引用类型。对引用形参的任何修改会直接影响实参本身。应将不需要修改相应实参的引用形参定义为const引用。

使用引用作为函数参数具有如下特点:

  1. 传递引用给函数,这时,被调函数的形参就作为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作;
  2. 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而是用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递的效率和所占空间较好
  3. 使用指针作为函数的参数虽然也能达到使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用“*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

2.2 传递指针的引用

void func(int* &v1);

形参int* &v1理解为:v1为一个引用,与指向int型对象的指针相关联,即v1为传递进func函数的任意指针的别名。

3 内联函数 inline

3.1 成员函数成为内联函数

类中定义的成员函数全部默认为内联函数,可以显式加上inline标识符或者不加。在类中声明的成员函数,如果加了inline,则其为内联函数;如果没加inline,而在类外定义该成员函数时加了inline,该成员函数也为内联函数。

3.2 普通函数成为内联函数

在普通函数声明或定义前加inline使其成为内联函数。

通常编译时,调用内联函数的地方,将不进行函数调用,而是使用函数体替换调用处的函数名,形式类似宏替换,这种替换称为内联扩展。内联扩展可以消除函数调用时的时间开销。

注:一般来说,内联机制适用于优化小的、只有几行的且经常被调用的函数。大多数的编译器都不支持递归函数的内联。

4 默认参数

在函数声明或定义时,直接对参数赋值,这就是默认参数。在函数调用时没有指定与形参相对应的实参时,就自动使用默认参数。如:

int mal(int a, int b=3, int c = 6);
mal(1);//等价于mal(1,3,6);

需要注意的是:

  1. 默认参数只可在函数声明中设定一次。只有在无函数声明时,才可以在函数定义中设定;
  2. 默认参数定义的顺序为自右到左。即如果一个参数设定了默认值时,其右边的参数都要有默认值;
  3. 默认参数调用时,遵循参数调用顺序,自左到右逐个调用;
  4. 默认值可以是全局变量、全局常量,甚至是一个函数,但不可以是局部变量。因为默认参数调用是在编译时确定的,而局部变量位置与默认值在编译时无法确定。

5 可变参数

参数个数可变的函数可使用“…”做参数占位符。如:

int printf(const char* format, ...);

一个接受可变参数的函数实现多个数相加可实现为:

int add(int num, ...)
{
    int sum = 0;
    int *p = (int*)&num + 1;//重点
    for(int i = 0; i < num; ++i)
        sum += *p++;
    return sum;
}
int main()
{
    int i = 1; j = 2; k = 3;
    cout << add(3, i, j, k);
    return 0;
}

6 函数重载

函数重载是指在同一个作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。

void print(int i)
{
    cout << "print a integer: " << i << endl;
}
void print(string str)
{
    cout << "print a string: " << str << endl;
}

重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对程序的可读性有很大的好处。

进行函数重载时,要求同名函数在参数个数上不同,或者在参数类型上不同。否则,将无法实现重载。

7 泛型编程与函数模板

7.1 泛型编程

指以独立于任何特定类型的方式编写代码。泛型编程中,我们所编写的类和函数能够多态地用于跨域编译时不相关的类型。标准库中的容器、迭代器和算法是很好的泛型编程的例子。

7.2 函数模板

模板定义以关键字template开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。模板形参表示可以在类或函数的定义中使用的类型或值。

template <typename T>
void callWithMax(const T& a, const T& b)
{
    f(a > b ? a : b);
}

类型形参T跟在关键字class或typename之后定义,在这里class和typename没有区别。模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。模板形参选择的名字没有本质含义。可以给模板形参赋予的唯一含义是区别形参是类型形参还是非类型形参。如果是类型形参,我们就知道该形参表示未知类型,如果是非类型形参,我们就知道它是一个未知值。模板非类型形参是模板定义内部的常量值

上述模板函数如果加上inline(inline放在模板形参表之后、返回类型之前,不能放在template之前),使其成为模板内联函数。此模板内联函数可以代替以下宏定义:

#define CALL_WITH_MAX(a,b) f((a)>(b) ? (a):(b))

注:宏定义与内联函数的区别:1)宏定义是在预处理阶段进行代码替换,而内联函数是在编译阶段插入代码;2)宏定义没有类型检查,而内联函数有类型检查

使用函数模板时,编译器会推断哪个(或哪些)模板实例绑定到模板形参。一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。编译器将确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。

7.3 类模板

类模板的定义:

template <class T>
class Queue
{
public:
    Queue();
    T& front();
    const T&front() const;
    void push(const T&);
    void pop();
    bool empty() const;
    
private:
    //...
};

使用类模板时,必须为模板形参显式指定实参:

Queue<int> qi;

编译器使用实参来实例化这个类的特定类型版本,即编译器用用户提供的实际特定类型代替T,重新编写Queue类。

8 函数的递归

简单地说,如果在一个函数、过程或数据结构的定义中又调用了它自身,那么这个函数、过程或数据结构称为是递归定义的,简称递归。

递归通常可以把一个大型的复杂问题,层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需要少量的代码就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量。但通常情况下,效率并不高(原因是递归调用过程中包含很多重复的计算)。

递归模型不能是循环定义的,必须满足以下两个条件:

  1. 递归表达式(递归体)
  2. 边界条件(递归出口)

递归的精髓在于能否将原始问题转换为属性相同但规模较小的问题

在递归的调用过程中,系统为每一层返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出等。

可以将递归算法转换为非递归算法,通常需要借助栈来实现这种转换。

9 extern “C”

在C++程序中调用被C编译器编译后的函数,为什么要加extern “C”?

C++语言是一种面向对象编程语言,为了支持重载机制,在编译生成的汇编码中,要对函数的名字进行一些处理,加入比如函数的返回值类型等信息。而在C语言中,只是简单的函数名字而已,不会加入其他的信息,也就是说:C++和C语言对产生的函数名字的处理是不一样的。这样在连接阶段若是按照C++的函数命名规则去查找C编译器编译的函数,就会出现链接错误。

extern “C”告诉编译器该函数是用C编译器编译的,请用C的方式来链接它们,从而解决了此问题。用法是在声明函数时,前面加上extern “C”,如下:

extern "C" void f1();

extern "C" {
    void f1();
}

或者还可以将这些函数放在头文件中,然后在include时声明:

extern "C" {
    #include "header.h"
}