文章目录
前言
从这一章开始就是C++特有的内容的体现了,这一节全都是C++特有的函数形式。这一节将介绍:内联函数、引用变量、缺省参数、函数重载以及函数的模板。其中引用变量和函数模板是用得比较广泛的。这一节内容基于《CPP Primer Plus》第八章函数探幽(Adventures in Functions),如果是只学习一个学期C++(没有C基础)或非计算机系的同学可以不用看哦~因为这一部分一般不考?......
鸽了许久,因为最近也有点忙哦,请谅解~😯
内联函数
内联函数是C++为提升程序运行速度所做的一项改进。我们知道,在普通调用函数的时候,程序运行速度会比求解等价表达式要慢得多。调用函数前可能要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行等繁琐的操作。
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用。也就是说,编译器将使用相应的函数代码替换函数调用.对于内联代码, 程序无需跳到另一个位置处执行代码再跳回来。因此,内联函数的运行速度比常规函数稍快;但代价是需要占用更多内存。
除了效率、占用内存的区别外,内联函数和普通函数并没有太大的区别,用关键字 inline 放在函数定义(注意是定义而非声明)的前面即可将函数指定为内联函数。
例如普通函数求两个数的最大值:
int max(int x,int y)
{
return (x>y)?x:y;
}
则这个函数改写成内联函数则:
int max(int x,int y);//声明不一定需要inline
inline int max(int x,int y)//定义需要加上inline关键字
{
return (x>y)?x:y;
}
但是我们使用内联函数的时候,也需要注意一些问题:
- 只有当函数小于等于10 行时才将其定义为内联函数。
- 在内联函数内不允许使用循环语句和开关语句;
- 内联函数的定义必须出现在内联函数第一次调用之前;
- 类结构中所在的类说明内部定义的函数是内联函数。
- 有些函数即使声明为内联的也不一定会被编译器内联。
引用变量
创建引用变量
C++新增了一种复合类型——引用变量。引用(reference)是已定义的变量的别名(另一个名称)。例如你在户口上登记的是你的名字,你的家人、朋友平时也许会叫你的小名,你的名字和小名都是指你,此时这个小名就是一个引用。那它有什么用呢?引用变量的主要用途是用作函数的形参。
我们给一块空间取了一个名字叫做 a ,后来又给它取了个小名叫做 b。相当于之后,我们对 b 进行任何操作,也会对 a 进行修改。因为它们指代的是同一个空间,我们用 & 来声明引用变量。
例如:
int a;
int &b=a;
在这里 & 不是取地址操作符,而是类型标识符的一部分。正如 char* , * 也是类型标识符的一部分,表示一个指向 char 类型的指针变量,而我们的 int& 表示一个指向 int 类型的引用变量。
不过要注意,使用引用变量时:
- 必须在声明引用变量时进行初始化。
- 引用变量一旦与某个变量关联起来,就会一直效忠于他。
- 当 b 成为 a 的引用之后,它就不会被修改,尝试更改时其实是进行了赋值。
例如:
int a = 5;
int& b = a;
int c = 20;
b = c;
此时程序运行结果:a、b、c都是20,因为b=c,此时使b对应的原变量a进行赋值,即a=c=20,因为b是a的引用,a改变了,b也同时改变,故全都是20.
引用用作函数参数
引用经常被用作函数参数,使得函数中的变点名成为调用程序中的变员的别名。这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变盘. C++新增的这项特性是对C语言的超越, C 语言只能按值传递.按值传递导致被调用函数使用调用程序的值的拷贝。当然, C 语言也允许避开按值传递的限制, 采用按指针传递的方式,不过其实好像本质一样?(因为传递的地址,也是一个值?)。
接下来我们以交换值的例子,引入引用用作函数参数。
#include <iostream>
using namespace std;
void swapr(int& a, int& b);
void swapp(int* p, int* q);
void swapv(int a, int b);
int main() {
int a = 1;
int b = 2;
swapr(a, b); //能够交换a和b
swapp(&a, &b); //能够交换a和b
swapv(a, b); //不能够交换a和b
}
void swapr(int& a, int& b) {
int temp;
temp = a;
a = b;
b = temp;
}
void swapp(int* p, int* q) {
int temp;
temp = *p;
*p = *q;
*q = temp;
}
void swapv(int a, int b) {
int temp;
temp = a;
a = b;
b = temp;
}
引用和指针方法都成功地交换了两个数内容, 而按值传递的方法没能完成这项任务。
- 指针的方法(swapp),定义中定义为两个指针,那我们在调用的时候就是需要提供两个地址作为参数,交换的时候交换的是两个指针对应的地址。
- 按值传递的方法(swapv),根据函数传值调用的原则,其实是传递过来了原来两个变量的拷贝,修改并没有对原变量造成修改。
- 引用的方法(swapr),其中传递过来的是两个别名,指代的是和原变量同样的空间,所以交换引用相当于交换了原变量的值。
引用这样的做法会改变原变量的值,如果我们不想修改变量的值又想传递一个引用的时候该怎么做呢?
我们来看一个求立方的例子:
int cube(const int& r) {
r *= r * r;
return r;
}
int main() {
int a = 3;
cout << "cube of " << a << " = "<< cube(a) << endl;
return 0;
}
我们想要求 a 的立方,并且之后还要用到 a ,这时候并不希望改变 a 。如果我们在这个地方不用const标记,则会改变原来a的值。为了解决这个问题,当我们希望不改变原值时,尽量采用 const 来修饰。
那有人就问了,这样引用变量不就是个别名,为什么要有这种不修改的规则呢?
这其实是因为,引用变量作为参数还可以省时间、省空间。我们知道,平时函数传参时,都需要将原来的变量拷贝一份至一个临时变量,再将这个临时变量作为形参传入函数,但现在从始至终,都是原来的那个变量,在大型项目中更加省时、省空间。
引用作为左值
你有没有想过,引用能作为可修改的左值吗?或者说我们在调用函数的时候可以传递引用表达式吗?例如上面的求立方的函数,这样的表达式可以吗?
cout<<cube(a+3);
此时在这种情况下编译器将生成一个临时匿名变量,并让调用的引用指向它,这种临时变量只在函数调用期间存在,此后编译器可以随意将其删除。
但是只有下面这两种情况会生成临时变量:
- 实参类型正确,但不是左值。
- 实参类型不正确,但可以转换为正确的类型。
引用作返回值
你可能会好奇,引用变量能不能作为返回值?这是当然可以的,例如上面求立方的那个例子。但这样和原本的返回有什么区别呢?
如果我们设置r不是引用变量,而是普通变量,在返回的时候,r就已经不存在了,程序会把r存到一个临时变量中,再把临时变量赋值到调用处,这样就多了几步存在效率问题,但是如果是用引用变量,因为操作的都是同一块空间,效率就会高很多。
但是,它也不是万能的。引用作返回值,不能返回一个临时变量的引用,需要变量在这个函数结束后还在,例如静态或全局变量。
什么时候使用引用变量
使用引用参数的主要原因一般有两个:
- 程序员能够修改函数中的数据对象。
- 通过传过引用而不是整个数据对象, 可以提高的程序的运行速度。
当对象比较大时,为了提高程序运行的速度,更应该采用引用变量。所以还有以下建议:
传递值而不修改值时
- 内置数据类型:由于较小,可直接按值传递;
- 数组:采用 const 修饰的指针;
- 较大的结构:使用 const 指针或 const 引用,可提高效率、节省时间空间;
- 类对象:const 引用。
需要修改原数据
- 内置数据类型:可使用指针;
- 数组:只能使用指针;
- 较大的结构:使用指针或引用;
- 类对象:const 引用。
缺省参数
下面介绍C++的另一项新内容——默认参数(缺省参数)。默认参数指的是当函数调用中省略了实参时自动使用的一个值,这个值即我们所说的缺省值。
我们也许会好奇过,我们在定义函数的时候,为什么不可以在参数部分给一个值呢?例如:void wow(int n=1)
这样的定义是正确的吗?
在C语言中编译器会报错,而在C++中有了默认参数这个概念,编译器会把它当成默认参数。我们在调用函数的时候,例如:wow();
其实相当于就是调用了wow(1);
当然,如果我们调用函数wow(2),这时候传入的还是2,那个缺省值1只在缺省的时候生效。默认参数就给我们提供一个便捷的方式。
我们在使用默认参数的时候需要注意一点:缺省定义只能在末尾的参数上。
举个例子:定义函数int harpo (int n, int m =4, int j = 5);
我们有下面几种调用情况:
- beeps = harpo(2); // 实际上是harpo(2,4,5)
- beeps = harpo(1,8) // 实际上是harpo(1, 8, 5)
- beeps = harpo(8,7,6) // harpo(8,7,6).
- beeps = harpo(3, , 8) // 无效,缺省值只能在末尾的参数上
函数重载
默认参数的使用能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)则可以能够使用多个同名的函数。术语"函数重载"指的是可以有多个同名的函数, 因此对名称进行了重载。
函数重载的关键是函数的参数列表——也称为函数特征标(funcnon slgnature)。 如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。
函数的重载使得 C++ 程序员对完成类似功能的不同函数可以统一命名,减少了命名所花的心思。例如,可能会需要一个求两个整数的最大值的函数,也可能还要写一个求三个实数的最大值的函数,这两个函数的功能都是求最大值,那么就都命名为 Max 即可,不需要一个命名为 MaxOfTwoIntegers,另一个命名为 MaxOfThreeFloats。
在调用同名函数时,编译器怎么知道到底调用的是哪个函数呢?编译器是根据函数调用语句中实参的个数和类型来判断应该调用哪个函数的。因为重载函数的参数表不同,而调用函数的语句给出的实参必须和参数表中的形参个数和类型都匹配,因此编译器才能够判断出到底应该调用哪个函数。
所以:同名函数只有参数表不同才能算重载。两个同名函数的参数表相同而返回值类型不同不是重载,而是重复定义,是不允许的。
函数模板
面向对象的继承和多态机制有效提高了程序的可重用性和可扩充性。在程序的可重用性方面,程序员还希望得到更多支持。函数模板可以用来创建一个通用功能的函数,以支持多种不同形参,简化重载函数的设计。
例如,两个交换函数:
//交换整型函数
void Swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
//交换浮点型函数
void Swap(double& a, double& b)
{
double temp = a;
a = b;
b = temp;
}
我们可以看到这两个函数只有类型不同,这个时候我们就可以用一个函数模板代替,函数模板需要在模板函数前添加template关键字。
template<typename T>//或者class T
返回值类型 函数名(形式参数表)
{……;}//函数体
当然,我们定义多个T也可以,例如:template <typename x, typename y>
模板类型参数代表一种类型,由关键字class 或 typename(建议用typename)后加一个标识符构成,这两个关键字的意义相同,他们表示后面的参数名代表一个潜在的内置或用户定义的类型。模板函数中类型将用自己定义的类型代替(T)。举个例子,像之前的交换函数,我们写成函数模板:
template <typename T>
void Swap(T & x, T & y)
{
T temp = x;
x = y;
y = temp;
}
你对比一下原来的函数,只有类型改变了,在调用的时候会根据给定的可用类型进行自动适应。
也许你会有一个疑问,既然如此那我都使用函数模板不就可以提高适应性了吗?其实函数模板也有一些限制:
- 注意运算符的通用性。例如传入变量我们可以进行赋值,要是传入数组不就会出现错误了吗?
- 程序bug的概率增加了,可能还无法显示在哪错的。
那我们普通函数和函数模板该怎么选择呢?
- 如果普通函数和函数模板同名时,且函数模板和普通函数都可以实现时,优先调用普通函数程序优先匹配普通函数。
- 可以通过空模板参数列表来强制调用函数模板。
- 函数模板也可以发生重载。
- 如果函数模板可以产生更好的匹配,优先调用函数模板。
后面还增加有一个decltype和使用auto的模板方法,这里就先不说了。
后记
这一部分就是C++新增的很多功能了,关于函数的部分大部分还是和C语言差不多,但这些新增的功能会在一定程度上给我们带来方便。
Comments NOTHING