我爱编程

C++第二弹---函数

2018-06-20  本文已影响1人  黄巴巴

内联函数

值得注意的是,内联函数仅仅是对编译器的内联建议,编译器是否觉得采取你的建议取决于函数是否符合内联的有利条件。如何函数体非常大,那么编译器将忽略函数的内联声明,而将内联函数作为普通函数处理。

关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。

例如:

Class A
{
 Public:
    inline int add(int a, int b)
    {
       return (a + b);
    };
}
Class A
{
 Public:
    int add(int a, int b);
};
inline int A::add(int a, int b)
{
   return (a + b);
}

C++内联函数提供了替代函数调用的方案,通过inline声明,编译器首先在函数调用处使用函数体本身语句替换了函数调用语句,然后编译替换后的代码。因此,通过内联函数,编译器不需要跳转到内存其他地址去执行函数调用,也不需要保留函数调用时的现场数据。

优点:

  1. 它通过避免函数调用所带来的开销来提高你程序的运行速度。
  2. 当函数调用发生时,它节省了变量弹栈、压栈的开销。
  3. 它避免了一个函数执行完返回原现场的开销。
  4. 通过将函数声明为内联,你可以把函数定义放在头文件内。

缺点:

  1. 因为代码的扩展,内联函数增大了可执行程序的体积。
  2. C++内联函数的展开是中编译阶段,这就意味着如果你的内联函数发生了改动,那么就需要重新编译代码。
  3. 当你把内联函数放在头文件中时,它将会使你的头文件信息变多,不过头文件的使用者不用在意这些。
  4. 有时候内联函数并不受到青睐,比如在嵌入式系统中,嵌入式系统的存储约束可能不允许体积很大的可执行程序。
  1. 当对程序执行性能有要求时,那么就使用内联函数吧。
  2. 当你想宏定义一个函数时,那就果断使用内联函数吧。
  3. 在类内部定义的函数会默认声明为inline函数,这有利于 类实现细节的隐藏。

关键点

  1. 内联声明只是一种对编译器的建议,编译器是否采用内联措施由编译器自己来决定。甚至在汇编阶段或链接阶段,一些没有inline声明的函数编译器也会将它内联展开。
  2. 编译器的内联看起来就像是代码的复制与粘贴,这与预处理宏是很不同的:宏是强制的内联展开,可能将会污染所有的命名空间与代码,将为程序的调试带来困难。
  3. 所有中类中定义的函数都默认声明为inline函数,所有我们不用显示地去声明inline。
  4. 虚函数不允许内联。
  5. 虽然说模板函数放中头文件中,但它们不一定是内联的。(不是说定义在头文件中的函数都是内联函数)。

只有当函数只有 10 行甚至更少时才将其定义为内联函数.
复杂的内联函数的定义, 应放在后缀名为 -inl.h 的头文件中.

重载函数

重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。

当您调用一个重载函数时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数的过程,称为重载决策。

下面的实例中,同名函数 print() 被用于输出不同的数据类型:

#include <iostream>
using namespace std;
class printData 
{
   public:
      void print(int i) {
        cout << "整数为: " << i << endl;
      }
      void print(double  f) {
        cout << "浮点数为: " << f << endl;
      }
      void print(string c) {
        cout << "字符串为: " << c << endl;
      }
};
int main(void)
{
   printData pd;
   // 输出整数
   pd.print(5);
   // 输出浮点数
   pd.print(500.263);
   // 输出字符串
   pd.print("Hello C++");
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

整数为: 5
浮点数为: 500.263
字符串为: Hello C++

  1. 函数名称必须相同。
  2. 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。

参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。

  1. 函数的返回类型可以相同也可以不相同。
  2. 仅仅返回类型不同不足以成为函数的重载。
  1. 如果参数类型以及返回类型完全匹配,则选择普通函数或者模板显式特化函数作为调用的函数实例。
  2. 否则,如果模板函数能够推导出一个参数类型以及返回类型完全匹配的函数实例,则选择函数模板。
  3. 否则,如果调用函数的实参以及返回类型能够进行隐式转换成与普通函数或者模板显式特化函数的类型匹配,则选择普通函数或者模板显式特化函数。
  4. 如果以上三条都不能匹配,则函数匹配失败,发生编译错误。

递归函数

int test(int x)
{
    int y;
    y = test(x);
    return(2*y);
}

还有间接调用,比如:

int first(int x)
{
    int b;
    b = second(x);
    return(2*b);
}
int second(int y)
{
    int a;
    a = first(y);
    return(2*a);
}

从上面的程序可以看到,这样执行后会出现无终止的自身调用,所以程序应该加入对用的判断机制,让递归在有限次数后停止。

举个例子:
用递归的方式求n!

#include <iostream>
using namespace std;
long fac(int);
int main()
{
    int n;
    long y;
    cout <<"请输入";
    cin >>n;
    y = fac(n);
    cout<<n<<"!="<<y<<endl;
    getchar();
    getchar();
    return 0 ;
}

long fac(int n)
{
    long f;
    if (n <0)
    {
        cout<<"错误!!!"<<endl;
    }
    else if(n== 0||n == 1) 
    f =1;
    else 
    f=fac(n-1)*n;
    return f;
}

递归退出的条件:

    else if(n== 0||n == 1) 
    f =1;
  1. 须有完成函数任务的语句
    例如,下面的代码定义了一个递归函数:
void count(int val) //递归函数可以没有返回值
{
  if(val>1)
    count(val-1);
  count<<"ok: "<<val<<endl;  //此语句完成函数任务
}
  1. 一个确定是否能避免递归调用的测试
    例如上例代码中,“if(val>1)”便是一个测试,如果不满足条件,就不进行递归函数。
  2. 一个递归调用语句
    该递归调用语句的参数应该逐渐逼近不满足条件,以致最后断绝递归。例如上例代码中,“count(val-1);”是一个递归调用,参数值正在逐渐变小,这种发展趋势能使“if(val>1)”最终不满足。
  3. 先测试,后递归调用
    在递归函数定义中,必须先测试,后递归调用。也就是说,递归调用是有条件的,满足条件后才可以递归。例如,下面的代码无条件调用函数自己,造成无限制递归,终将使栈空间溢出。
#include <iostream>
using namespace std ;
void count(int val)
{
  count(val-1); //无限制递归
  if(val>1)  //此语句无法到达
    cout<<"ok: "<<val<<endl;
}

默认参数函数

在C++中,定义函数时可以给形参指定一个默认的值,这样调用函数时如果没有给这个形参赋值(没有对应的实参),那么就使用这个默认的值。也就是说,调用函数时可以省略有默认值的参数。如果用户指定了参数的值,那么就使用用户指定的值,否则使用参数的默认值。

所谓默认参数,指的是当函数调用中省略了实参时自动使用的一个值,这个值就是给形参指定的默认值。

下面是一个简单的示例:

#include<iostream>
using namespace std;
//带默认参数的函数
void func(int n, float b=1.2, char c='@'){
cout<<n<<", "<<b<<", "<<c<<endl;
}
int main(){
//为所有参数传值
func(10, 3.5, '#');
//为n、b传值,相当于调用func(20, 9.8, '@')
func(20, 9.8);
//只为n传值,相当于调用func(30, 1.2, '@')
func(30);
return 0;
}

运行结果:
10, 3.5, #
20, 9.8, @
30, 1.2, @

本例定义了一个带有默认参数的函数 func(),并在 main() 函数中进行了不同形式的调用。为参数指定默认值非常简单,直接在形参列表中赋值即可,与定义普通变量的形式类似。

指定了默认参数后,调用函数时就可以省略对应的实参了。

默认参数除了使用数值常量指定,也可以使用表达式指定,例如:

float d = 10.8;
void func(int n, float b=d+2.9, char c='@'){
cout<<n<<", "<<b<<", "<<c<<endl;
}

C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提。

下面的写法是正确的:

void func(int a, int b=10, int c=20){ }
void func(int a, int b, int c=20){ }
但这样写不可以:
void func(int a, int b=10, int c=20, int d){ }
void func(int a, int b=10, int c, int d=20){ }

默认参数并非编程方面的重大突破,而只是提供了一种便捷的方式。在以后设计类时你将发现,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。

上述例子中,我们在函数定义处指定了默认参数。除了函数定义,你也可以在函数声明处指定默认参数。

默认值可以是全局变量、全局常量,甚至是一个函数。

上一篇下一篇

猜你喜欢

热点阅读