C++内联函数

内联函数是C++提高程序运行速度所做的一项改进。
代码编译之后会产生机器指令,每条指令都有特定的内存地址。当遇到函数调用时,将会发生地址的跳转,在函数调用完之后又会跳转回来。(过程可参照汇编语言,在跳转之前先要进行状态的保存,程序指针的保存;返回时同样需要状态的恢复,程序指针的恢复)而这来回的跳转,以及信息的保存需要一定的开销。内联函数提供另一种选择:将函数的编译代码和与其它程序“内联”起来(编译器将使用相应的函数代码替换函数调用),使得内联函数调用时无需进行地址的跳到。但是其代价就是产生了很多函数代码副本,占用内存更多(空间换时间)。
内联函数最佳使用情况:函数执行时间很短的并且经常被调用的函数。(也就是一些短小的函数)
使用方式(至上满足以下一条):

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

通常做法是:省略原型,将整个定义放在本应提供原型的地方。
在以下情况下inline不会生效

  • 函数过大;
  • 函数递归;

    内联函数和宏

    1. #define SQUARE(X) ((X)*(X))
    2. inline double square(double x){ return x*x; }
    通常情况下,宏不能按值传递,如果使用C语言的宏执行了类似函数的功能,通常应将其转换为C++的内联函数。

    引用变量

    引用在之前介绍过,它其实就是为一个已知变量起一个别名。(和指针有点类似)其主要用途是作为函数的形参。
    1. int rats;
    2. int number = 10;
    3. int& rodents = rats;
    4. rodents = number;
    上面的rodentsrats都指向相同的值和内存单元。
    引用必须在声明时进行初始化。下面语句不被容许!(值得注意的是常量指针也是如此,此处的常量指针是指指针值不能修改的指针,不是指向const类型的指针)
    逆向思考,如果容许引用先声明后赋值,那么岂不是这个引用就能被修改去作为其它变量的引用呢?这是不被容许的!同理,常量指针也是如此!
    后面对引用变量的赋值,其实是值的传递。
    1. int rats;
    2. int& rodents;
    3. rodents = rats; //Invalid

    引用作为参数

    1. #include<array>
    2. #include<iostream>
    3. using namespace std;
    4. void swap(int&, int&);
    5. int main(){
    6. int a = 10;
    7. int b = 20;
    8. cout << "before swap:" << endl;
    9. cout << "a is: " << a << endl;
    10. cout << "b is: " << b << endl;
    11. swap(a, b);
    12. cout << "after swap:" << endl;
    13. cout << "a is: " << a << endl;
    14. cout << "b is: " << b << endl;
    15. }
    16. void swap(int& a, int& b){
    17. int temp = a;
    18. a = b;
    19. b = temp;
    20. }
    函数输出:

    before swap: a is: 10 b is: 20 after swap: a is: 20 b is: 10

注意事项:

  • 利用引用进行参数传递时,传入函数的如果是表达式,则不是很合理,无法通过编译;

    • 注意上面所述情况,出现在引用没有用const修饰时!
      1. int process_data(int&);
      2. ......
      3. process_data(x+2); // 不合理

      临时变量、引用参数和const

      如何实参和引用参数不匹配,C++将生成临时变量,目前只有当参数为const引用时,C++才允许这样做。
      生成临时变量的条件:
  • 引用参数为const;

  • 实参的类型正确,但不是左值;

    1. int process_data(const int&);
    2. ......
    3. int x = 10;
    4. process_data(x+2);
    • ·左值:可被引用的数据对象,例如变量、数组元素、结构成员、引用、指针等;
    • 非左值:字面常量和包含多项的表达式;
  • 实参的类型不正确,但可以转换为正确的类型;

    • 当然,类型不正确,可以转为正确类型,实参不是左值也会创建临时变量;
      1. int process_data(const int&);
      2. ......
      3. long x = 10;
      4. process_data(x);

      为何有const限制

      其实在早期的C++中是没有const限制的,但是这导致了一种问题:
      1. int swap(double&, double&);
      2. ......
      3. int x = 10;
      4. int y = 20;
      5. swap(x, y);
      分析上述代码,是希望将x,y进行交换,但是由于满足临时变量生成条件,在函数中会生成临时变量,这导致x,y并没有被交换,这和编程者的意图相悖,由于编译器没有报错,使得编程者很难发现这个问题!如果有了const限制,那么swap函数本身就不能对x,y进行操作,在对x,y进行操作时会报错,这使得错误容易发现!在没有const限制时,就不会创建临时变量,这就时才会编程者修改x,y(允许修改和不创建临时变量使得其满足编程者要求);
      当然在不能创建临时变量的情况下,如果有类型不正确将会报错!
      显然,多使用const可以避免一些易于出现的错误!
  • 使用const可以避免无意中修改数据的编程错误;

  • 使用const使得函数能够处理const和非const类型的数据,否则只能接受非const数据;(和const指针类似);
  • 使用const能够正确生成并创建临时变量。

    右值引用

    使用&&声明:

    1. const int c = 100;
    2. int&& cr = c * 10;

    数组引用

    1. int array[] = {1,2,3};
    2. int (&array_r)[3] = array;

    注意引用数组不合法!

    结构引用

    返回引用

    1. double& sum_data(const double* dp, int size){
    2. double result=0;
    3. for(int i=0; i<size;i++){
    4. result += dp[i];
    5. }
    6. return result; // 局部变量会被销毁
    7. };

    以上代码不合理,返回的局部变量的引用,局部变量是会被销毁的!

    1. double& sum_data(const double* dp, int size, double &sum){
    2. sum=0;
    3. for(int i=0; i<size;i++){
    4. sum += dp[i];
    5. }
    6. return sum; // 既会返回sum也会修改传进来的sum值
    7. }

    神奇用法:

    1. sum_data(array, 3, sum) = 10

    由于sum_data返回的是引用,是左值(通常函数返回时右值),引用是可以被修改!
    为了防止这种情况导致的错误,如果函数返回的引用不需要修改,那么建议使用const修饰!

    对象、继承和引用

    继承例子:ostream -> ofstream

    使用引用参数的原因

  • 编程者能够修改调用函数中的数据对象;

  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度;

    什么时候使用哪种传递

  • 如果数据对象很小,则按值传递;

  • 如果数据对象是数组,则使用指针(唯一选择),并考虑是否需要加上const
  • 如数据对象是较大的结构,则使用指针或者引用,以提高速率,并考虑是否需要const修饰;
  • 类设计常常使用引用;
  • 如果数据对象是内置结构则使用指针;
  • 如果数据对象是结构,则使用引用或指针;
  • 如果数据对象是类对象,则使用引用;

    默认参数

    函数默认值,就是在定义函数时就给部分参数进行赋初值:

    1. int harpo(int n, int m=4, int j=5);

    注意事项:

  • 如果给某个参数赋了默认值,那么其右边的所有参数必须赋默认值!

    函数重载

    函数重载也即是函数多态,同一个函数名,由于参数的不同,就可以实现不同的操作!也就是可以存在多个同名的函数,即对函数名称进行了重载! ```cpp void print(const char* str, int width); // #1 void print(double d, int width); // #2 void print(long l, int width); // #3

print(“hello”, 5); // #1 print(12.3, 10); // #2

  1. 如果调用`print((int)10, 10)`,则显然没有与之匹配的原型。如果#2print的唯一匹配原型,虽然前面的调用没有匹配原型,但是C++会尝试进行标准类型转换进行强制匹配(就是尝试类型转换,使得与之匹配)。但是由于上面的print有多个以数组为参数的函数,所以有多种转换方法,此时会报错!<br />假设有如下四个原型:
  2. ```cpp
  3. void dribble(char* bits);
  4. void dribble(const char* bits);
  5. void dabble(char* bits);
  6. void drivel(const char* bits);
  • dribble有两个原型:当输入为const的时候与后者匹配,非const时与前者匹配;
  • dabble只有一个原型,只能和非const得输入匹配;(const得特性,前面已提过)
  • drivel只有一个原型,由于形式参数const修饰,所以既可以和const参数匹配,也可以非const;

注意事项:

  • 重载得两个函数,名称必须相同,返回类型可以不同,特征标必须不同(传入参数类型);

    函数模板

    函数模板是通用的函数描述,他们使用泛型来定义函数,其中的泛型可用具体的类型替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型的方式进行编程,所以也被成为通用编程。模板特性:参数化类型(以类型作为参数)。
    为何需要模板?设想编写了int型的swap函数,如果数据类型是double,是char又当如何呢?当然,编译器自己转换类型可以在一定程度上解决这个问题(非常局限),但是对象像指针、引用这些在很多情况下不会进行类型转换,会直接报错。这时,模板将参数作为参数就可以解决此类问题!— 节省时间,安全可靠!

    模板编写

    1. template <typename AnyType> // or class AnyType -- 没有逗号,没有分号
    2. void Swap(AnyType& a, AnyType& b){
    3. AnyType temp = a;
    4. a = b;
    5. b = a;
    6. }

    语句:template <typename AnyType>声明要建立一个模板,类型名为AnyType,类型名可以自己设置,其中的typename可以用class替代(两者等价)。模板本身不会创建函数,只是告诉编译器如何定义函数。
    要让编译器知道程序需要一个特定形式的交换函数,只需在程序中使用Swap函数即可,编译器将检查所使用的参数类型,并生成相应函数(类型事实上并没有显示地在调用函数时进行传递,编译器自己检查!)

    1. #include<iostream>
    2. using namespace std;
    3. template<typename T>
    4. void swap1(T& a, T& b);
    5. int main(){
    6. int a = 10;
    7. int b = 20;
    8. double c = 1.5;
    9. double d = 2.7;
    10. cout << "before swap1: " << endl;
    11. cout << "a: " << a << " b: " << b << " c: " << c << " d: " << d << endl;
    12. swap1(a, b);
    13. swap1(c, d);
    14. cout << "a: " << a << " b: " << b << " c: " << c << " d: " << d << endl;
    15. cout << "a: " << a << " b: " << b << endl;
    16. }
    17. template<typename T>
    18. void swap1(T& a, T& b){
    19. T temp = a;
    20. a = b;
    21. b = temp;
    22. }

    注意事项:

  • template<typename T>在原型和函数定义之前都应该出现!

    重载的模板

    需要对多个不同类型使用同一种算法时需要用到模板,但是并不是所有类型都是用同一种算法,重载模板很好地解决了这个问题。由于之前介绍过重载:特征标不同! ```cpp template void Swap(T& a, T& b);

template void Swap(T a, T b, int n); // 数组交换,只能遍历每个元素!

  1. - 数组不能作为参数进行传递,数组之间不可以进行赋值;
  2. ```cpp
  3. int a[3] = {1,2,3};
  4. int b[3] = a; // Invalid
  • 结构,类等可以相互之间进行赋值;

    1. struct person{
    2. int height;
    3. int width;
    4. int age;
    5. }
    6. person a = {1,2,3};
    7. person b = a; // Valid

    模板的局限性

    其实模板的局限性在于:我们定义模板,首先会假设可执行那些操作,比如赋值操作等,但是对于特定的类型可能存在某些操作不能用的情况,从而使得当以某些类型作为参数传入模板函数时会报错!(模板适用性被压缩)其中的一个解决方法就是为特定的类型提供具体化的模板定义。
    可能你会认为模板的重载能够解决这个问题,但是很多情况下其特征标是相同的,无法通过重载解决!

    显式具体化模板

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

  • 显式具体化的原型应以template<>打头,并通过名称来指出类型;
  • 具体化优先于常规模板,而非模板优先于模板函数;

    1. // job是一个结构体
    2. // 非模板
    3. void Swap(job& a, job& b);
    4. // 常规模板
    5. template <class T>
    6. void Swap(T& a, T& b);
    7. // 显示化模板
    8. template <> void Swap<job>(job& a, job& b); // 也可以:template <> void Swap(job& a, job& b);
    1. #include<iostream>
    2. #include<new>
    3. const int BUF = 512;
    4. const int N = 5;
    5. char buffer[BUF];
    6. template <class T>
    7. void display(T data);
    8. template <> void display<int>(int data);
    9. using namespace std;
    10. int main(){
    11. using namespace std;
    12. display(100); // 会选择int类型的显示具体化模板
    13. }
    14. template <class T>
    15. void display(T data){
    16. cout << data << endl;
    17. }
    18. template <> void display<int>(int data){
    19. cout << "this is int!" << endl;
    20. }

    那么如果要写成显示化模板,为何不直接用非模板呢?(貌似两者基本上没有不同)

    实例化和具体化

    代码中包含函数模板本身不会生成函数,它只是用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板的实例。比如像之前利用模板定义的函数进行运算,我们在调用函数时并没有指定模板接受参数的类型,编译器为我们自动实现了此操作,这即是隐式实例化。除了隐式实例化,还可以进行显示的实例化,这意味着可以命令编译器创建特定的实例。

    1. template<typename T>
    2. void swap1(T& a, T& b);
    3. // 显示实例化函数
    4. template void swap1<int>(int, int);
    1. #include<iostream>
    2. using namespace std;
    3. template<typename T>
    4. void swap1(T& a, T& b); // 注意这两行合成一句话!
    5. // 指向模板的指针,指定了类型
    6. void (*pf)(int& a, int& b) = swap1;
    7. int main(){
    8. int a = 10;
    9. int b = 20;
    10. double c = 1.5;
    11. double d = 2.7;
    12. cout << "before swap1: " << endl;
    13. cout << "a: " << a << " b: " << b << " c: " << c << " d: " << d << endl;
    14. (*pf)(a, b);
    15. swap1(c, d);
    16. cout << "after swap1: " << endl;
    17. cout << "a: " << a << " b: " << b << " c: " << c << " d: " << d << endl;
    18. }
    19. template<typename T>
    20. void swap1(T& a, T& b){
    21. T temp = a;
    22. a = b;
    23. b = temp;
    24. }

    请注意显式实例化和显式具体化的差别!利用实例化函数进行运算:

    1. // 隐式实例化
    2. swap1(a,b);
    3. // 显式实例化
    4. swap1<int>(a,b);
  • swap1<int>(a,b);即是一个实例化,利用整型实例化模板;

  • swap1(a,b);也是一个实例化,但是其为隐式的;

    编译器函数选择

    对于函数重载、函数模板和函数模板重载,C++有一个选择策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时,这一过程称为重载解析。其大致步骤如下:
  1. 创建候选函数列表;
  2. 使用候选函数列表创建可行函数列表;这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。
  3. 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

假设有如下的一个调用:
may('B');
那么编译器首先寻找名称为may的函数或者模板,找到之后进行参数数量的匹配,过滤掉需要必须要传入的参数与调用不同的函数或者模板,然后按照如下顺序进行参数类型筛选,选择最佳匹配:

  • 完全匹配,但常规函数优先于模板;
  • 提升转换(例如charshort自动转换为intfloat自动转换为double);
  • 标准转换(例如int转为charlong转为double);
  • 用户定义转换,如声明中定义的转换;

(这几个转换没怎么看懂…)如果没有最佳匹配将会报错!

1. 完全匹配和最佳匹配

进行完全匹配时,C++允许某些“无关紧要的转换”(也即是说,参数类型并不是完完全全一样)。这类转换如下表所示,可以看出来这类转换和之前定义函数完成的自动转换一样,给我们的感觉就是的确可以这样(处理倒数第二列?)。

从实参 到形参
Type Type&
Type& Type
Type[] Type*
Type(argument list) Type(*)(argument list)
Type const Type
Type volatile Type
Type* const Type
Type* volatile Type*

例如如下代码:

  1. struct blot{int a; char b[10];}
  2. void recycle(blot); // #1
  3. void recycle(const blot); // #2
  4. void recycle(blot&); // #3
  5. void recycle(const blot&); // #4
  6. // ...
  7. blot ink = {25, "sports"};
  8. // ...
  9. recycle(ink)

recycle(ink)存在多个完全匹配,前面的函数原型和它都是完全匹配,当存在多个完全匹配时,如果没有最佳匹配将会报错。
完全匹配之间的优先级:

  • 指向非const数据的指针和引用优先与非const指针和引用参数匹配;(const只是在指针和引用时存在这种特性)
  • 非模板函数优先于模板函数;
  • 完全匹配的都是模板函数,则找最具体的模板作为最佳匹配;(最具体:指的是编译器推断出使用哪种类型时执行的转换最少)

基于以上特征:如果只存在#1和#2的定义,那么将会出现二义性;如果只定义了#3和#4,那么将会选择#3;

  1. template <class Type> void recycle(Type t); // #1
  2. template <class Type> void recycle(Type* t); // #1
  3. struct blot{int a; char b[10];}
  4. //...
  5. blot ink = {25, "sports"};
  6. //...
  7. recycle(&ink)

显然上面的两个模板都和调用完全匹配,然而#1中的Type将会是:blot*,儿#2中的Type将会是blot,显然#2提供的信息更为具体。找出最具体模板的规则称为函数模板的部分排序规则。

2、关键字decltype和auto

decltype介绍

  1. template <class T1, class T2>
  2. void ft(T1 x, T2 y){
  3. // ...
  4. decltype(x + y) xpy = x + y;
  5. // ...
  6. }

由于此时x和y的类型未知,所以很难确切地写出x + y的类型,decltype关键字解决了这个问题。为了确定类型编译器必须遍历一个核对表,假设有如下申明:decltype(expression) var;那么核对表的简化版如下:

  • 第一步:如果expression是一个没有用括号括起的标识符,则var与标识符的类型相同,包括const等限定词;

    1. double x = 20.0;
    2. double* xp = &x;
    3. decltype(xp) var; // 则var是double*类型
  • 第二步:如果expression是一个函数调用,那么var类型和函数返回值类型相同;

    1. double indeed(int);
    2. decltype (indeed(3)) var; // var是double类型

    注意:这个过程并不会实际调用函数。编译器通过查看函数的原型来获取返回类型,而无需实际调用函数。(这也是原型的好处之一罗)

  • 第三步:如果expression是一个左值,则var为指向其的引用。为了与第一步区分开来,此时这个左值需要用括号进行区分;

    1. double xx = 4.4;
    2. decltype((xx)) r2 = xx; // r2是double &类型
    3. decltype(xx) w = xx; // w是double类型

    事实上括号并不会改变左值的值和左值性。

  • 第四步:如果前面的条件都不满足,则var的类型与expression的类型相同;(表达式计算之后的类型)

    1. int j = 3;
    2. int& k = j;
    3. decltype(j+6) var1; // var1是int型
    4. decltype(k+7) var2; // var2是int型

    auto使用

    1. template <class T1, class T2>
    2. auto ft(T1 x, T2 y) -> decltype(x + y)
    3. {
    4. // ...
    5. return x + y;
    6. }
  • 不能将auto替换为decltype(x + y),因为该处的x和y还没有被声明;

  • 这将返回类型移到了参数声明后面,->double被称为后置返回类型,其中的auto是占位符,表示后置返回类型提供的类型;
  • 由于->double在参数声明之后,所以可以使用他们;

事实上很多规则和性质,自己想一想就能明白其中的价值及原因。