C++PrimerPlus第八章学习笔记——函数探幽

前言

此文为本人学习所做一些记录,仅做个人学习之用,加入了我的理解,如发现错误欢迎指正,邮箱:lujialun99 A T gmail.com

内联函数

概念

内联函数,关键字inline,是C++为提高程序运行速度所做的一项改进。对于使用了内联函数的位置,程序无需跳到另一个位置执行函数代码,而是直接将函数代码副本包含进程序内。
内联函数比常规函数的运行速度稍快,但代价是需要更多的内存。

inline指令就象register,它只是对编译器的一种提示,而不是命令。也就是说,只要编译器愿意,它就可以随意地忽略掉你的指令,事实上编译器常常会这么做。例如,大多数编译器拒绝内联”复杂”的函数(例如,包含循环和递归的函数);还有,即使是最简单的虚函数调用,编译器的内联处理程序对它也爱莫能助。(这一点也不奇怪。virtual的意思是”等到运行时再决定调用哪个函数”,inline的意思是”在编译期间将调用之处用被调函数来代替”,如果编译器甚至还不知道哪个函数将被调用,当然就不能责怪它拒绝生成内联调用了)。

内联函数与宏

C语言使用预处理语句来提供宏,这其实是内联代码的原始实现。但宏定义并不是通过传递参数来实现的而是直接的文本替换。
如:

    #define SQUARE(X) X*X
    a=SQUARE(4.5+7.5);//a=4.5+7.5*4.5+7.5

宏不能按值传递,如果宏要执行类似函数的功能,应考虑转化为C++内联函数。

使用内联函数的注意事项

  • 不要过度使用内联函数,尤其是大函数
  • 内联函数中不要含有循环、判断、选择、递归。
  • 内联成员函数可以访问成员变量(public private protected),除了比普通成员函数执行快之外,不要刻意区分内联成员函数和普通成员函数。

 //8.1 inline.cpp -- 内联函数的使用
    #include <iostream>
    inline double square(double x) { return x * x; }
    int main()
    {
        using namespace std;
        double a, b;
        double c = 13.0;

        a = square(5.0);
        b = square(4.5 + 7.5);   // can pass expressions
        cout << "a = " << a << ", b = " << b << "\n";
        cout << "c = " << c;
        cout << ", c squared = " << square(c++) << "\n";
        cout << "Now c = " << c << "\n";
        // cin.get();
        return 0;  
    }

引用变量

引用是已定义变量的别名,主要用途是用作函数的形参。通过引用变量,函数将使用原始数据,而不是临时数据,对处理大型结构和设计类来说是必不可少的。

创建引用变量

注意:在创建引用变量的必须同时进行初始化。

 int rat;
    int &rodent;
    rodent = rat;//这样写是错误的

引用更接近于const指针,对于

 int &rodents=rats;

实际上是下述代码的伪装表示

    int *const pr =&rats;

将引用变量用作函数参数

这种传递参数的方法称为按引用传递,允许函数能够访问和调用函数中的变量。C语言只能按值传递,函数只能使用变量的副本,当然如果使用指针的话就能避开按值传递的限制。
下图显示了按值传递和按引用传递的区别。
Markdown

引用的属性和特别之处

 // cubes.cpp -- regular and reference arguments
    #include <iostream>
    double cube(double a);
    double refcube(double &ra);
    int main ()
    {
        using namespace std;
        double x = 3.0;

        cout << cube(x);
        cout << " = cube of " << x << endl;
        cout << refcube(x);
        cout << " = cube of " << x << endl;
        // cin.get();
        return 0;
    }

    double cube(double a)
    {
        a *= a * a;
        return a;
    }

    double refcube(double &ra)
    {
        ra *= ra * ra;
        return ra; 
    }

该程序输出为:

27 = cube of 3
27 = cube of 27

该程序体现了按值传递与按引用传递的区别,如果我们的意图是想让函数使用传递给它的信息而不对这些信息进行修改,同时想使用引用,则应使用常量引用。如下

     double refcube(const double &ra)

传递引用比按值传递的限制更加严格,如下的代码就是不合理的

    double z=refcube(x+3.0)

函数的实参应当为变量,而x+3.0并不是一个变量,在现代C++中这是错误的。

应尽可能使用const

理由有三:

  1. 使用const可以避免无意中修改数据的编程错误;
  2. 使用const使函数能够处理const和非const实参,否则只能接受非const数据
  3. 使用const引用使函数能够正确生成并使用临时变量;

有关临时变量、引用参数和const详见书262页

将引用用于结构

 //strc_ref.cpp -- using structure references
    #include <iostream>
    #include <string>
    struct free_throws
    {
        std::string name;
        int made;
        int attempts;
        float percent;
    };

    void display(const free_throws & ft);
    void set_pc(free_throws & ft);
    free_throws & accumulate(free_throws &target, const free_throws &source);

    int main()
    {
        free_throws one = {"Ifelsa Branch", 13, 14};
        free_throws two = {"Andor Knott", 10, 16};
        free_throws three = {"Minnie Max", 7, 9};
        free_throws four = {"Whily Looper", 5, 9};
        free_throws five = {"Long Long", 6, 14};
        free_throws team = {"Throwgoods", 0, 0};
        free_throws dup;
        set_pc(one);
        display(one);
        accumulate(team, one);
        display(team);
    // use return value as argument
        display(accumulate(team, two));
        accumulate(accumulate(team, three), four);
        display(team);
    // use return value in assignment
        dup = accumulate(team,five);
        std::cout << "Displaying team:\n";
        display(team);
        std::cout << "Displaying dup after assignment:\n";
        display(dup);
        set_pc(four);
    // ill-advised assignment
        accumulate(dup,five) = four;
        std::cout << "Displaying dup after ill-advised assignment:\n";
        display(dup);
    // std::cin.get();
        return 0;
    }

    void display(const free_throws & ft)
    {
        using std::cout;
        cout << "Name: " << ft.name << '\n';
        cout << "  Made: " << ft.made << '\t';
        cout << "Attempts: " << ft.attempts << '\t';
        cout << "Percent: " << ft.percent << '\n';
    }
    void set_pc(free_throws & ft)
    {
        if (ft.attempts != 0)
            ft.percent = 100.0f *float(ft.made)/float(ft.attempts);
        else
            ft.percent = 0;
    }

    free_throws & accumulate(free_throws & target, const free_throws & source)
    {
        target.attempts += source.attempts;
        target.made += source.made;
        set_pc(target);
        return target;
    }

为何要返回引用?

传统返回机制将返回值复制到一个临时位置,而调用程序将使用这个位置的值。返回引用将直接返回被引用的变量,其效率更高。

返回引用时应注意的问题

避免返回一个指向临时变量的引用,也就是向下面这样的。

    const free_throws & clone2(free_throws & ft)
    {
        free_throws newguy;
        newguy=ft;
        return newguy;
    }

为何将const用于返回类型

在返回类型中省略const可以编写更简短代码,但其含义也更模糊,为避免犯错,应使用const。

将引用用于类对象

将类对象传递给函数时,C++通常做法是使用引用。下面三个函数是个例子,函数的作用为将指定的字符串加入另一个字符串的前面和后面。

  /* function a */
    string version1(const string & s1, const string & s2)
    {
        string temp;
        temp = s2 + s1 + s2;
        return temp;
    }
     /* function b */
    const string & version2(string & s1, const string & s2)   // has side effect
    {
        s1 = s2 + s1 + s2;
        // safe to return reference passed to function
        return s1; 
    }
    /* function c */
    const string & version3(string & s1, const string & s2)   // bad design
    {
        string temp; 
        temp = s2 + s1 + s2;
        // unsafe to return reference to local variable
        return temp;
    }
function 编译 运行 解释
a 通过 正确 使用引用不需要创建新的string对象,提高了效率,使用const限定确保不被修改
b 通过 正确 s1未加const限定将直接修改s1,这就是此函数的副作用
c warning: reference to local variable ‘temp’ returned 输入字符串程序崩溃 引用指向了一个局部变量,而此局部变量函数结束后即被释放

对象、继承和引用

没怎么看懂,— —,略过。

何时使用引用参数

默认参数

请看下面代码:

    // left.cpp 截取某一字符串前n个字符
    #include <iostream>
    const int ArSize = 80;
    char * left(const char * str, int n = 1);//原型指定默认参数
    int main()
    {
        using namespace std;
        char sample[ArSize];
        cout << "Enter a string:\n";
        cin.get(sample,ArSize);
        char *ps = left(sample, 4);//指定参数
        cout << ps << endl;
        delete [] ps;  
        ps = left(sample);//默认参数
        cout << ps << endl;
        delete [] ps;  
        return 0;
    }

    char * left(const char * str, int n)
    {
        if(n < 0)
            n = 0;
        char * p = new char[n+1];
        int i;
        for (i = 0; i < n && str[i]; i++)
            p[i] = str[i];  // copy characters
        while (i <= n)
        p[i++] = '\0';  // set rest of string to '\0'
        return p; 
    }

函数 char * left(const char * str, int n = 1);的作用是截取一个字符串前n个字符,该函数为参数n设置默认参数1。left(hello);将调用默认参数。

在函数原型中指定默认值即可,函数定义不用指定。

对于一个含有多个默认参数的函数来说,必须从右边第一个参数开始添加向左添加,中间不能包含非默认参数。而且在给默认参数赋值时必须从左边第一个默认参数开始赋值,然后依次向右,中间不能跳过任何参数。 下面这个写法就是错误的:

 void f(int a,int b=1;int c=2);
    f(1, , 3);

函数重载

函数重载的关键就是函数的参数列表——也成为函数特征标(function signature)。请看下面一组原型:

    void print(const char*str,int width); //1
    void print(double d,int width);       //2
    void print(long l,int width);         //3
    void print(int i,int width);          //4
    void print(const char *str);          //5

这是一个函数重载的例子,如果有下列语句:

  unsigned int year=222;
  print(year6);

在五个重载的函数原型中,没有一个与之匹配,所以C++会尝试使用强制转换,将该函数与最相近的函数进行匹配进行匹配。 值得注意的是,编译器在检查函数特征标的时候将把类型引用和类型本身视为同一个特征标。

该书中277页有关于const和非const变量重载的描述似乎有些错误,而且描述十分模糊,简单的来说,只有指针和引用才能重载。

 //不重载
 void f(char );
 void f(const char );
 //重载
 void f(char * );
 void f(const char* );

何时使用函数重载

能使用默认参数就使用默认参数,否则使用函数重载。

什么是名称修饰

C++为了跟踪每一个重载函数,会对函数进行名称修饰(name decoration)或称名称矫正(name mangling),它根据函数原型中指定的形参类型对每个函数名进行加密。 例如:

 long MyFunctionFoo(int,float);

修饰为:

MyFunctionFoo@@YAXH

编译器将对函数进行编码,编码根据函数参数数目和类型不同而不同。

函数模板

函数模板是通用的函数描述,它使用泛型来定义函数,其中的泛型可用具体的类型来替换。通过将类型作为参数传递给模板,可使编译器自动生成该类型的函数。因此模板有时也被称为通用编程

由于类型是用参数来表示的,因此模板特性有时也被称为参数化类型(parameterized types)

下面是一个例子:

 template <typename T>  // or class T
    void Swap(T &a, T &b)
    {
        T temp;   // temp a variable of type T
        temp = a;
        a = b;
        b = temp; 
    }

template为模板关键字,template之后必须使用尖括号。在平时的使用中class比typename似乎使用的更多,因为在之前的C++标准中并未包含typename这个关键字。为了避免此处使用class带来的混淆,所以引入了typename,但这里的typename并不完全等价于class,typename另外一个作用为:使用嵌套依赖类型(nested depended name)。C++ primer plus这本书对此也没有过多解释,所以我们就暂时了解到这里。

模板并不创建任何函数,只是告诉编译器如何定义函数。模板也并不能缩短可执行程序,最终的代码不包含任何模板,而只包含为程序而生成的实际函数。使用模板的好处是在需要生成多个函数时函数的定义更简单、更可靠。

重载的模板

模板也可以重载。

    template <typename T> 
    void Swap(T &a, T &b);

    template <typename T> 
    void Swap(T *a, T *b, int n);

从第二个模板函数可以看出,并非所有的模板参数都必须是模板参数类型。

显示具体化(explicit specialization)

C++98给出的具体化方法:

  • 对于给定的函数名,可以有非模板函数、模板函数和显示具体化函数以及他们的重载版本。
  • 显示具体化的原型和定义应以template<>打头,并通过名称来指出类型。
  • 具体化优于常规模板,而非模板函数优于具体化和常规模板。
    下面是用于交换结构体job的非模板函数、模板函数和具体化的原型。
    void Swap(job &, job &);//非模板

    template <class T>//模板
    void Swap(T &, T &);

    template <> void Swap<job>(job &, job &);//具体化

显示具体化中Swap中的是可选的,因为函数的参数类型表明,这是job的一个具体化。因此函数原型也可以这样写:

     template <> void Swap(job &, job &);

实例化和具体化

编译器使用模板为特定类型生成函数定义时得到的是模板实例

下面这种实例化的方式称为隐式实例化。

  template <typename T>
    void Swap(T &a, T &b) 
    {
        T temp;
        temp = a;
        a = b;
        b = temp;
    }
    int i=10,j=20;
    Swap(i,j);

以前C++只允许隐式实例化,现在还允许显示实例化,即直接命令编译器创建特定的实例。如:

    template void Swap<int>(int &,int &);//使用Swap()模板生int类型的函数定义(不用再额外定义)

而显示具体化是像下面这样的:

     template <> void Swap<int>(int &, int &);
     template <> void Swap(int &, int &);

声明上面的函数以后,编译器就不会使用常规模板来为int生成定义,而是专门的函数定义。

注意:试图在同一个文件(或转换单元)中使用同一种类型的显示实例和显示具体化将出错。

隐式实例化、显示实例化和显示具体化统称为具体化。

编译器会怎么样选择函数版本

对于函数重载、函数模板、函数模板重载的调用策略的过程称为重载解析,大致过程如下:
Markdown

编译器首先确定名称相同、参数数目相同和参数能够被强制转换的可行函数,然后根据以下所需要进行的转化从最佳到最差来匹配:

  1. 完全匹配,但常规函数优于模板
  2. 提升转换(例:char和short自动转化为int,float自动转换为double)
  3. 标准转换(例:int转换为char,long转换为double)
  4. 用户定义的转换,如类声明中定义的转换

完全匹配和最佳匹配

进行完全匹配时C++允许某些“无关紧要的转换”。
Markdown

具体有关理解参看C++ primer plus 291页。

关键字decltype

C++11新标准,后面再讨论。

PS:第一次写这么长的博客,花了很多时间也挺累的但是收获很大。

发布了20 篇原创文章 · 获赞 25 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/Timekeeperl/article/details/68954119