首页 C++ Primer Plus-第8章 函数探幽
文章
取消

C++ Primer Plus-第8章 函数探幽

    没想到这一章竟然持续了将近10天!虽然差不多是看了三天,因为前面一周感冒纯开摆,顺便还去长广溪呆了两天YWY

8.1 C++内联函数

  • 内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在手编写方式,而在于C++编译器如何将它们组合到程序中

  • 编译过程的最终产品是可执行程序——由一组机器语言指令组成,运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址

  • 常规函数调用使程跳到另一个地址(函数的地址),并在函数结束时返回

    • 执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址

    • 并将函数参数复制到堆栈

    • 跳到标记函数起点的内存单元,执行函数代码(也许还需要将返回值放入寄存器中)

    • 跳回到地址被保存的指令处

      来回跳跃需要一定的开销

  • 内联函数的编译代码与其他程序代码“内联”起来了,也就是说编译器将使用相应的函数代码替换函数调用,不需要跳跃,因此更快,但代价是占用更多内存

  • 如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可节省非内联调用使用的大部分时间

  • 使用内联函数:

    • 在函数声明前加上关键字inline

    • 在函数定义前加上关键字inline

  • 通常做法是省略原型,将整个定义放在本应提供原型的地方

  • 内联函数不能递归

  • inline是C++新增的特性,C语言使用#define来提供宏(但宏不能按值传递)

8.2 引用变量

  • 引用是已定义的变量的别名

  • 通过将引用变量用作参数,函数将使用原始数据

  • int &指向int变量的引用

  • 必须在声明引用时将其初始化

  • int & rodents = rats;其实是int * const pr = &rats;的伪装表示

  • 引用常被用作函数参数,这种传递参数的方法称为按引用传递

  • 如果编写使用基本数值类型的函数,应采用按值传递的方式,当数据比较大时,引用参数将很有用

  • 如果函数形参是引用,那么实参应该是变量,因为比如不能将值赋给表达式和字面常量

  • 如果实参和引用参数不匹配,C++将生成临时变量,而且仅当参数为const引用时才允许这样做:

    • 实参类型正确,并且是非左值(字面常量(除了字符串,因为是指针)、表达式)

    • 实参的类型不正确,但可以转换为正确的类型

    原因其实也很简单,如果不加const意味着想要使用引用修改实参,但C++生成了临时变量,所以是没法修改的。加了const就是不想修改实参,那么生成临时变量去算也没关系,这样的话,该函数只需要在必要的时候生成临时变量,其他情况不生成,又能节约时间还能增加函数使用空间。临时变量只在函数调用期间存在。

    注意:如果函数调用的参数不是左值或与相应的cost引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。

  • 应尽可能使用const

    • 可以避免无意中修改数据的编程错误

    • 使函数能够处理const和非const实参,否则将只能接受非const数据

    • 使用const引用使函数能够正确生成并使用临时变量

  • 引用非常适合用于结构和类

  • 函数头的返回类型如果不是引用,那么将返回一个拷贝;如果返回类型为引用,意味着返回对象本身

  • 为何要返回引用?返回引用与传统返回机制的不同:

    • 传统返回机制与按值传递函数参数类似:计算关键字return后面的表达式,并将结果返回给调用函数,从概念上来讲,这个值被复制到一个临时位置,而调用程序将使用这个值

    • 返回引用直接把结果返回,不会复制到临时位置

    • 注意:应避免返回 函数终止时不再存在的 内存单元引用(比如函数中新建了个对象,返回了这个对象,但该对象在函数结束后自动就销毁了)

      • 解决办法就是:

      • 返回一个作为参数传递给函数的引用

      • 用new来分配新的存储空间

        1
        2
        3
        4
        5
        6
        
        const node & clone(node & n)
        {
            node * p;
            *p = n;//此时复制了n,此处隐藏了new,实际上使用了new
            return *p;
        }
        
      • 这样使得后面很容易忘记使用delete释放内存

  • 将const用于引用返回类型

    • 一方面不能成为左值

    • 另一方面不能成为函数非const形参的实参

  • 将C-分割字符串用作string对象引用参数

    • string类定义了一种char*到string的转换功能,这使得可以使用C-风格字符串(如"abcd")来初始化string对象

    • 使用const引用形参,如果类型不匹配但能转换为引用类型,那么就会创建正确的临时变量,如果不加const只加引用,那么会报错

  • 基类引用可以指向派生类对象,例如参数类型为ostream &的函数可以接受ostream对象或ofstream对象

    •  setf()可以设置各种格式化状态(ostream类中的方法)

      • setf(ios_base::fixed)定点表示法

      • setf(ios_base::showpoint)显示小数点模式

    • width()(ostream类中的方法)设置下一次操作使用的字段宽度,这种设置只在下一个值时有效,然后恢复默认

  • 何时使用引用参数:

    • 能够修改函数中的数据对象

    • 提高运行速度

  • 使用引用、指针、按值传递的指导原则

    • 对于使用传递的值而不做修改的函数

    • 如果数据对象很小,比如内置数据类型或小结构,则按值传递

    • 如果数据对象是数组,则使用指针,并将指针声明为指向const的指针

    • 如果数据对象是较大的结构,则使用const指针或const引用以提高程序效率

    • 如果数据对象是类对象,则使用const引用。传递类对象参数的标准方式是按引用传递

    • 对于修改调用函数中数据的函数

    • 如果数据对象是内置数据类型,则使用指针

    • 如果数据对象是数组,则只能使用指针

    • 如果数据对象是结构,则使用引用或指针

    • 如果对象是类对象则使用引用
    • 当然,以上都是指导原则,有可能做出其他选择。比如cin对于基本类型使用引用,因此可以使用cin>>n而不是cin>>&n

8.3 默认参数

  • 默认参数指的是当函数调用中省略了实参时自动使用的一个值

  • 通过函数原型来设置默认值,将值赋给原型中的参数,例如char * left(const char * str, int n = 1);

  • 对于带参数列表的函数(即有多个参数),必须从右向左添加默认值

  • 实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数,因此下面的调用是不允许的beeps=harpo(3, ,8);

8.4 函数重载

  • 函数重载的关键是函数的参数列表——也称为函数特征标(function signature),如果两个函数的参数数目和类型相同,同时参数的排列顺序也想通,则它们的特征标相同,而变量名是无关紧要的

  • C++允许定义名称相同的函数,条件是它们的特征标不同

  • 使用被重载的函数时,需要在函数调用中使用正确的参数类型(也就是说不会自动转换类型)

  • 为避免混乱,类型引用和类型本身视为同一特征标

  • 匹配函数时,并不区分const和非const,这是因为非const可以赋给const;如果重载了一个const形参的函数和一个非const形参的函数,那么就会匹配正确类型,如果只写了const的函数,那么非const实参也可以赋给const形参;如果只写了非const形参函数,那么const实参无法赋值

  • 特征标相同且返回类型不同的不能重载

  • void stove(double && r3);这个可以与右值参数匹配,如两个double的和,如果没有定义这个函数,那么就会调用stove(const double &)

  • 名称修饰(name decoration)或名称矫正(name mangling):它根据函数原型中指定的形参类型对每个函数名进行加密;编译器会将奖惩转换为不太好看的内部表示来描述接口,比如long f(int , float);转换为?f@@YAXH。添加的符号随函数特征标而异,而修饰时使用的约定随编译器而异

8.5 函数模板

  • 函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数

  • 建立一个交换模板:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    template <typename AnyType>
    void Swap(AnyType &a, AnyType &b)
    {
        AnyType tmp;
        tmp = a;
        a = b;
        b = tmp;
    }
    template <class AnyType>
    void Swap(AnyType &a, AnyType &b)
    {
        AnyType tmp;
        tmp = a;
        a = b;
        b = tmp;
    }
    
    • 第一行指出要建立一个模板,并将类型命名为AnyType;关键字tmplatetypeename是必需的,除非使用关键字class代替typename
  • 注意,函数模板不能缩短可执行程序

  • 并非所有的模板参数都必须是模板参数类型,可以是具体类型

  • 第三代具体化

    • 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本

    • 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型

    • 具体化优先于常规模板,而非模板函数优先于具体化和常规模板

      1
      2
      3
      4
      5
      
      void swap(job &, job &);//非模板
      template <typename T>
      void swap(T &, T &);//模板
      template <> void swap<job>(job &, job &);//显式具体化模板
      template <> void swap(job &, job &);//<job>可省略 
      
  • 实例化和具体化

    • 函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案

    • 实例化:

      • 编写模板后,调用模板函数如swap(i,j),那么会导致编译器生成swap()的一个实例,即生成了一个函数定义,这种实例化方式称为隐式实例化(implicit instantiation)

      • 显式实例化(explicit instantiation):命令创建特定的实例template void swap<int>(int, int);;也可以使用函数来创建显式实例化,比如swap<int>(),即使用swap()模板来生成int类型的函数定义,那么如果参数的类型不同,就可以通过类型转换而尝试运行,当然也有可能出其他的错误,但是,隐式实例化遇到两个不同类型的参数会直接报错(swap模板的两个形参类型一样)

    • 显式具体化(explicit specialization)template <> void swap<int>(int &, int &) ;//(其中<int>可省略)这个声明的意思是不要使用swap模板来生成int的函数定义,而是应该使用专门为int类型显式地定义的函数定义

    • 隐式实例化、显式实例化、显式具体化统称为具体化

  • 重载解析(overloading resolution):对于函数重载、函数模板和函数模板重载,C++需要一个策略——决定为函数调用使用哪一个函数定义,尤其是有多个参数时

    1. 创建候选函数列表,其中包含与被调用函数名称相同的函数和模板函数

    2. 使用候选函数列表创建可行函数列表(类型隐式转换的也加入可行列表)

    3. 确定是否有最佳可行函数
      • 最佳到最差顺序:
    4. 完全匹配,但常规函数优于模板

    5. 提升转换(如char自动转为int,float自动转double)

    6. 标准转换(如int转char,long转double)

    7. 用户自定义的转换
      • 如果有多个匹配的原型,没有最佳的可行函数(如果存在更具体的,那么也算是有最佳的可行函数),则编译器会生成一条错误信息。比如ambiguous(二义性)
    • 自己选择

      • lesser<>(m,n);此时是示意编译器选择模板函数而不使用非模板函数

      • lesser<int>(m,n);此时是示意编译器选择模板函数并将实参强制转换为int

  • 模板函数拓展

    • 为解决类型问题,发明了关键字decltype:

      1
      2
      3
      4
      5
      6
      7
      
      template<typename t1, typename t2>
      void f(t1 x, t2 y)
      {
          decltype(x)z;//设置z的类型和x一样
          decltype(x + y) xpy;//设置xpy的类型与x+y一样
          xpy = x + y;
      }
      
    • 语法:decltype(expression) var;

      1. 如果expression是一个没有用括号括起来的标识符,那么var的类型与该标识符类型相同

      2. 如果expression是一个函数调用,则var类型与函数的返回类型相同

      3. 如果expression是一个左值,那么var为指向其类型的引用

    • 同样,因为无法预先知道x和y的类型,可以将返回类型设置为decltype(x + y),但此时未声明x和y,不在作用域(编译器看不到),为此C++11使用auto与后置返回类型(trailing return type)来操作:

      1
      2
      3
      4
      5
      
      template<class t1, class t2>
      auto gt(t1 x, t2 y) -> decltype(x + y)
      {
         return x + y; 
      }
      
本文由作者按照 CC BY 4.0 进行授权

C++ Primer Plus-第7章 函数——C++的编程模块

C++ Primer Plus-第9章 内存模型和名称空间