C++学习笔记——函数探幽

C++内联函数

  内联函数是一种用空间换时间的技术,是C++提高程序运行速度做的改进。运行程序时操作系统将指令载入计算机内存中,并逐条执行这些指令,遇到循环或分支时向前或向后跳转到特定的地址(每条指令都有特定的内存地址)。常规函数也是如此,在调用常规函数时立即存储该指令的地址,并跳转到函数的地址,在函数执行结束后返回到跳转之前的地址继续往下执行。来回的跳跃并记录跳跃位置需要花费一定的开销。

  与常规函数不同,如果使用内联函数,编译器会使用相应的函数代码替换函数调用,此时程序无需跳到另一个位置执行代码,再跳回来。因此内联函数运行速度比常规函数快。但相应的需要占用更多的内存,所以内联函数适合代码量较小的函数,且不能是递归函数。

  inlien是C++新增的特性,C使用预处理语句#difine来提供宏,以此来达到内联函数的功能。但是宏是单纯的文本替换,不能按值传递。

1 inline double square(double x) { return x * x; }

引用变量

  引用变量是C++新增的一种复合类型。引用变量的主要作用是作为函数的形参,此时函数将使用原始数据,而不是副本。提高函数处理大型数据结构的效率(常用于结构、类)。引用和指针很像,表达式 b 和 *p 都可以与 a 互换, &b 和 p 可以和 &a 互换。但在声明引用时必须初始化,指针可以先声明,后赋值。

1 int a = 10;
2 int& b = a;   //声明int类型引用,并将b作为a的别名, int* const b = &a;
3 int* p = &a;

  使用引用作为函数参数时,函数参数中的变量名成为调用程序中的变量别名。按引用传递可以让被调用函数直接访问调用函数中的原始数据,而不是其副本。函数调用使用实参初始化形参。如果想使用引用参数而又不想对原始数据造成影响,则使用常量引用。

1 double refcube(const double& x);

  如果传递的实参与形参类型不匹配,但是可以转换为正确的类型,或者实参的类型正确,但不是左值。C++将生成临时变量(前提形参为const引用),将实参的值传递给临时变量,并将const引用指向临时变量。且临时变量只在该函数调用期间存在。因为如果是使用参数const引用的函数,表明该函数不对原始数据进行修改,而是使用,此时生成临时变量不会造成不利的影响。

  传统的返回机制与按值传递参数类似,计算 return 后面表达式的值,概念上来说,这个值被复制到一个临时位置,然后调用程序使用这个值(编译器可能会优化)。返回引用时直接复制,效率更高。同时要避免返回临时变量的引用,简单的做法是返回作为参数传递给函数的引用。或者使用 new 来分配新的存储空间。

  将返回类型声明为 const 引用可以避免将函数调用作为左值。

  当引用的对象是类,且该类的派生类采用公有继承时,基类的引用可以指向基类或派生类对象(指针同理)。如果数据对象时数组,则使用指针是唯一的选择

 

默认参数

  通过函数原型设置函数参数默认值,同时必须从右向左提供默认值,中间不能跳过任何参数。

1 char* left(const char* str, int n = 1);

 

函数重载

  默认参数使得可以使用不同数目的参数调用同一个函数,函数重载允许使用多个同名的函数——完成相同工作,但使用不同的参数列表。通过上下文确定要使用的函数重载版本。要定义多个同名的函数,它们的特征标(参数数目、参数类型)必须不同,变量名无关紧要。

  如果传递的参数列表不匹配任何函数原型,C++将尝试使用标准类型转换强制进行匹配,此时若有多个函数原型可以转换匹配,C++将拒绝这中函数调用,并将其视为错误。同时,C++将类型类型引用视为同一个特征标。匹配函数时,不区分const和非const变量。

1 double cube(double x);
2 double cube(double& x);
3 
4 count << cube(x); //同时匹配 double x 原型和 double& x 原型

   C++如何跟踪每个重载函数呢?C++对函数的编码方式不同,C++编译器把函数原型中的形参对函数名进行修饰,以此来区别函数名相同的函数。修饰时使用的约定根据编译器而异。

 

函数模板

  使用泛型来定义函数,泛型可用具体的类型替换,将类型作为参数传递给模板,编译器根据模板和传递的参数生成相关的函数定义。注意函数模板不是函数。

  template告诉编译器要定义一个模板,后面跟着一对尖括号代表模板参数列表,typename(或class)指出 T 是一个接收类型的类型参数,这里的 T 起着占位符的作用,如果传给 T 的类型为 int, 则所有 T 替换为 int。模板不创建任何函数,只是告诉编译器如何定义函数。

  在第 11 行代码中,编译器会检查使用的参数类型,并生成相应的函数,此处生成 int 版本的Swap函数。注意函数模板不能缩短可执行程序,而且模板的定义放在头文件中。

复制代码
 1 template <typename T>
 2 void Swap(T& a, T& b) {
 3     T temp;
 4     temp = a;
 5     a = b;
 6     b = temp;
 7 }
 8 
 9 
10 int i = 10, j = 20;
11 Swap(i, j);
复制代码

  当然模板也支持重载,模板参数列表中也并非必须为类型参数,如下面第四个模板声明。

复制代码
 1 template <class T>
 2 void Swap(T& a, T& b);
 3 
 4 template <class T>
 5 void Swap(T& a, T& b, T& c);
 6 
 7 template <typename T>
 8 void Swap(T* a, T* b);
 9 
10 template <typename T>
11 void Swap(T a[], T b[], int n);
复制代码

   模板也有一些局限性,可能无法处理某些类型。如果 T 为数组,则下面模板中的赋值、if条件语句、T c = a*b语句都不成立。解决的一种办法是为特定类型提供具体化的模板定义。

复制代码
1 template<typename T>
2 void func(T a, T b) {
3     a = b;
4     if (a > b) {
5         ...
6     }
7 
8     T c = a * b;
9 }
复制代码

  提供具体化模板函数的定义称为显示具体化。对于给定的函数名,可以有非模板函数、模板函数和具体化模板函数以及函数的重载版本。显示具体化模板函数的原型和定义以 template<> 开头,通过名称指出类型。下面分别为非模板函数、模板函数、具体化模板函数,编译器在选择原型时,优先级为:非模板函数 > 显示具体化模板函数 > 模板函数

复制代码
 1 struct job {
 2     char name[40];
 3     double salary;
 4     int floor;
 5 };
 6 
 7 void Swap(job a, job b);
 8 
 9 template<typename T>
10 void Swap(T&, T&);
11 
12 template<> void Swap<job>(job&, job&); //Swap<job>(job&, job&)中的<job> 可选,因为函数的参数类型已经表明这是 job 的一个具体化
复制代码

   代码中包含模板本身不会生成函数定义,编译器使用模板为特定类型生成函数定义时,得到模板实例。模板不是函数定义,但是使用 int的模板实例是函数定义(隐式实例化)。显式具体化和显式实例化的区别在于,显式具体化声明在template后包含<>, 而显式实例化没有。可以在文件中使用函数来创建显式实例化,如 std::cout << Swap<double>(a, b) << std::endl; 。在同一个文件中,同一类型显式实例化和显示具体化不能同时存在。

  对于函数重载、函数模板和函数模板重载,C++通过重载解析来确定为函数调用哪个定义。首先,编译器创建候选函数列表,从候选函数列表创建可行的函数列表,再从可行的函数列表中选择最佳的可行函数。最佳到最差的顺序:完全匹配 > 提升转化 > 标准转化 > 用户自定义转化。如果有多个匹配的原型,则编译器可能无法完成重载解析的过程。但有时候,即使两个函数都完全匹配,仍然可以完成重载解析,比如 const 和 非const 之间的区别(只适用于指针和引用指向的数据,如下所示)。

 

1 void func(int);
2 void func(const int);  //编译器报错,出现二义性错误
3 
4 void func(int&);
5 void func(const int&); //const 和 非const的区别只适用于指针和引用

  术语“最具体”并不一定意味着显示具体化,而是指编译器推断使用哪种类型时,执行的转换最少。

1 template<class T> void recycle(T t);  #1
2 template<class T> void recycle(T* t); #2
3 
4 recycle(&link); //recycle(&link)调用会和 #2 模板匹配,因为在生成过程中,它需要进行的转换更少

  在使用模板时,可能会出现变量的类型不确定的情况。C++11新增关键字decltype可以解决这种情况。

 1 template<class T1, class T2>
 2 void ft(T1 x, T2 y) {
 3     ...
 4     ?xpy? = x + y; //变量xpy的类型是? 解决办法是使用C++11新增的关键字decltype
 5     //decltype(x + y) xpy = x + y;
 6     ...
 7 }

  decltype的处理实际上更复杂一些。

复制代码
 1     /*
 2         decltype(expression) var;
 3     */
 4     
 5     //如果exression是一个没有用括号括起的标识符。
 6     double x = 5.5;
 7     double y = 7.9;
 8     double& rx = x;
 9     const double* pd;
10       
11     decltype(x) w;              // w的类型为double
12     decltype(rx) u = y;         // u的类型为double&
13     decltype(pd) v;             // v的类型为const double*
14 
15     //如果expression是一个函数调用
16     long indeed(int);           //声明indeed函数
17     decltype(indeed(3)) m;      //m的类型为indeed函数的返回类型(long),实际上编译器不会调用函数,只是会查看函数的原型
18 
19     //如果expression是用括号括起的标识符且是一个左值,则var为指向其类型的引用
20     double xx = 4.4;
21     decltype ((xx)) r2 = xx;   //r2的类型为double&
22     decltype (xx) w = xx;      //w的类型为double
23 
24     //如果前面的条件都不满足,var的类型与expression的类型相同
25     int j = 3;
26     int& k = j;
27     int& n = j;
28     decltype(j + 6) i1;            //i1的类型为int
29     decltype(100L)  i2;            //i2的类型为long
30     decltype(k + n) i3;            //i3的类型为int, k和n虽然为引用,但是表达式 k+n 是两个int类型的和
复制代码

  解决了函数模板中函数体的变量类型问题后,还有函数模板返回类型的问题。如下所示,无法直接使用 decltype(x + y),因为此时还未声明参数x和y,必须在声明参数x,y后才能使用decltype。

1 template<class T1, class T2>
2 ?type? func(T1 x, T2 y) {   //函数的返回类型是?
3     return x + y;
4 }

  解决方法是使用后置返回类型。此时decltyep在参数声明后面,x和y位于作用域内,可以使用它们。

1 double h(int x, int y);
2 auto h(int x, int y) -> double; //后置返回类型,auto是一个占位符
3 
4 template<class T>
5 auto func(T x, T y) -> decltype(x + y) {
6     return x + y;
7 }

 

 

 

 

 

  

 

posted @   owmt  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示