一. 函数
1 函数参数
1.1 参数传递基础
- C++通常按值传递参数
- 用于接收传递值得变量被称为形参;传递给函数的值被称为实参。处于简化的目的,C++标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参,因此参数传递将参数赋给参量。
1.2 数组作为函数参数
基本的函数声明如下:
int sum_arr(int arr[], int n); // arr 数组名 n 数组长度 arr其实是一个指针
C++和C语言一样,也将数组名视为指针。上一篇介绍过,C++将数组名解释为其第一个元素的地址:
int arrays[8];
arrays == &arrays[0]; // 数组名是首元素的地址
该规则有一些例外:
- 首先,数组声明使用数组名来标记存储位置;
- 其次,对数组名使用
sizeof
将得到整个数组的长度(以字节为单位);- 第三,正如上一篇指出的,将地址运算符
&
用于数组名时,将返回整个数组的地址,例如&arrays将返回一个32字节内存块的地址(如果int
长4字节)
那么,以数组作为参数的函数声明也可以是这样:
int sum_arr(int * arr, int n); // arr 数组名 n 数组长度
在C++中,当且仅当用于函数头或函数原型中,int * arr
与int arr[]
的含义才是相同的。 它们都意味着arr
是一个int
指针。然而,数组表示法(int arr[]
)提醒用户,arr不仅指向int,还指向int数组的第一个int。
我们应该始终记住下面两个恒等式:
arr[i] == *(arr+i);
&arr[i] == arr + i
传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。这看似违反了C++按值传递的原则,但实际上并非如此,sum_arr()
函数仍然是传递了一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容。 但另一方面,使用原始数据增加了破坏数据的风险。在经典的C语言中,这确实是一个问题,但C++中的cons
t限定符提供了解决这种问题的办法。稍后将具体介绍const
。
1.3 const
限定符
将const
用于指针有一些很微妙的地方(指针看起来总是很微妙)。我们可以用两种不同的方式将const
关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,即所谓的底层const
;第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置,即所谓的 顶层const
。
int sloth = 3;
const int *ps = &sloth; // 指向常量的指针
int * const finger = &sloth; // 指针常量
finger
只能指向sloth
,但允许使用finger
来修改sloth
的值。
不允许使用ps
来修改sloth
的值,但允许将ps
指向另一个位置。
简而言之,finger
和*ps
都是const
,而*finger
和ps
不是。
如果愿意,还可以声明指向const
对象的const
指针:
double trouble = 2.0;
const double * const stick = &trouble;
指向常量的指针: 防止使用该指针来修改所指向的值。
int age = 39;
const int * pt = &age;
- 常规变量的地址赋给常规指针
- 常规变量的地址赋给指向const的指针
- const变量的地址赋给指向const的指针
- ~~const变量的地址赋给常规指针~~ (不可行)
如果将指针指向指针,情况将更加复杂。假如涉及的是一级间接关系,则将非const指针赋给const指针是可以的。
int age = 39; // age++ is a valid operation
int *pd = &age; // *pd = 41 is a valid operation
const int * pt = pd; // *pt = 42 is a invalid operation
然而,进入两级间接关系时,与一级间接关系一样将const和非const混合的指针赋值方式将不再安全。如果允许这样做,则可以编写这样的代码:
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; // not allowed, but suppose it were
*pp2 = &n; // valid, both const, but sets p1 to poitn at n
*p1 = 10; // valid, but changes const n
上述代码将非const地址(&p1)付给了const指针(pp2),因此可以使用p1来修改const数据。因此,仅当只有一层简接关系(如指针指向基本数据类型)时,才可以将非const地址或指针赋值给const指针。
指针常量: 防止改变指针指向的位置。
int sloth = 3;
const int * ps = &sloth; // 一个指向常量的指针
int * const finger = &sloth; // 一个int类型的指针常量
2. 函数指针
这个我们好像不太常用,但是在回调函数等情形下,是要用到的。比如,我们在写一个下层应用给上层提供服务。但实现过程中,一些数据如何操作取决于调用我们的上层的状态,我们当下是不知道的。这个时候,我们就需要在接口中提供一个参数,用来传递函数地址。 上层应用时,会将自己对应的函数地址通过该参数传递给接口,下层应用在执行到对应步骤时,使用上层调用者提供的函数地址访问对应函数,执行对应函数中的操作。在这个函数中,上层可根据自己的状态,进行具体地操作。 这就是回调函数。
与数据项相似,函数也有地址。函数的地址是存储器机器语言代码的内存的开始地址。通常,这些地址对用户而言,既不重要,也没有什么用处,但对程序而言,却很有用。例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数将能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。
首先通过一个例子来阐释函数指针的用处。
假设要设计一个名为estimate()的函数,估计编写指定行数的代码所需的时间,并且希望不同程序员都将使用该函数。对于所有的用户来说,estimate()中一部分代码都是相同的,但该函数允许每个程序员提供自己的算法来估计时间。为实现这种目标,采用的机制是,将程序员要使用的算法函数的地址传递给estimate()。为此,必须能够完成下面的工作:
- 获取函数的地址;
- 声明一个函数指针;
- 是用函数指针来调用函数;
2.1 获取函数的地址
很简单,只要使用函数名即可。如果think()是一个函数,则think就是该函数的地址。
一定要区分传递的是函数的地址还是函数的返回值:
process(think); // 传递think函数地址
thought(think()); // 传递think函数的返回值
2.2 声明函数指针
声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(函数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。
例如,某函数原型如下:
double pam(int);
则正确的指针类型声明如下:
double (*pf) (int);
将pam
替换为了(*pf)
。由于pam
是函数,因此(*pf)
也是函数。而如果(*pf)是函数,则pf
就是函数指针。
一定要用括号将
*pf
括起来。括号的优先级比*
运算符高,因此*pf (int)
意味着pf()
是一个返回指针的函数,而(*pf)(int)
意味着pf
是一个指向函数的指针。
2.3 使用指针来调用函数
使用(*pf)
时,只需将它看做函数名即可:
double pam(int); // 函数原型
double (*pf)(int); // 声明函数指针
pf = pam; // 将pam函数地址赋给函数指针pf
double x = pam(4); // 用函数名调用函数
double y = (*pf)(5); // 用函数指针来调用函数
实际上,C++也允许像使用函数名那样使用pf:
double y = pf(5); // 也是通过函数指针pf来调用pam()
但最好还是用第一种方式,因为它给出了强有力的提示——代码正在使用函数指针。
但可以发现一个函数指针的声明需要包括返回值、参数等等,十分冗长,C++11之后我们可以使用auto来简化这种冗长的数据类型
另外,也可以使用typedef
进行简化
const double * f1(const double ar[], int n);
typedef const double *(*p_fun)(const double*, int); // p_fun 现在是一个类型名了
p_fun p1 = f1; // p1是指向f1函数的函数指针
3 内联函数(inline
)
【关键字:inline
】
- 在函数声明前加上关键字inline;
- 在函数定义前加上关键字inline;
二者选其一,即可声明为内联函数。
常规函数和内联函数之间的主要区别在于C++编译器如何将它们组合到程序中。要了解内联函数与常规函数之间的区别,必须深入到程序内部。
编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(如有循环或分支语句时),将跳过一些指令,向前或向后跳到特定地址。
常规函数调用也使程序跳到另一个地址(函数的地址),并在函数结束时返回。
下面更详细地介绍这一过程的典型实现。执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
而C++内联函数提供了另外一种选择。内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。
如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的一小部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此,尽快节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
简而言之,适合使用内联函数的情况就是满足下面两个条件:
- 函数体短小执行速度快;
- . 经常调用该函数。
否则,效果不大,还会比较耗费空间。
在后面的类
class
,在类的声明内部定义的函数,默认都是内联的。
4 引用变量
引用即别名。
int rats;
int & rodents = rats;
必须在声明引用变量时进行初始化
引用更接近指针常量,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。
4.1 将引用用作函数参数
引用经常被用作函数参数,使得函数中的变量名称为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。
如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。
double refcube(const double &ra);
声明两个函数,分别是值传递和按引用传递。
double cube(double a);
double refcube(double& ra);
按值传递的函数可使用多种类型的实参。例如,下面的调用都是合法的:
double z = cube(x+2.0);
z = cube(8.0);
int k = 10;
z = cube(k);
double yo[3] = {2.2, 3.3, 4.4};
z = cube(yo[2]);
传递引用的限制更严格。毕竟,如果ra
是一个变量的别名,则实参应是该变量。下面的代码不合理,因为表达式x+3.0
并不是变量:
double z = refcube(x+3.0);
如果试图使用像refcube(x+3.0)
这样的函数调用,将发生什么情况呢?在现代的C++中,这是错误的,大多数编译器都将指出这一点;而有些较老的编译器将发出这样的警告:
Warning: Temporary used for parameter 'ra' in call to refcube(double& )
之所以做出这种比较温和的反应是由于早期的C++确实允许将表达式传递给引用变量。有些情况下,仍然是这样做的。这样做的结果如下:由于x+3.0
不是double
类型的变量,因此程序将创建一个临时的无名变量,并将其初始化为表达式x+3.0
的值。然后,ra
将成为该临时变量的引用。下面详细讨论这种临时变量,看看什么时候创建它们,什么时候不创建。
临时变量、引用参数和const
首先,什么时候将创建临时变量呢?如果引用参数是const
,则编译器将在下面两种情况下生成临时变量:
- 实参的类型正确,但不是左值;
- 实参的类型不正确,但可以转换为正确的类型。
左值是什么?
左值参数是可以被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。
非左值包括字面常量(用引号括起的字符串除外,他们由其地址表示)和包含多项的表达式。
在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况。现在,常规变量和const变量都可以视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
为什么对于常量引用,这种生成临时变量的行为是可行的,其他情况下却不行呢?
简而言之,
- 如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法就是,禁止创建临时变量,现在的C++标准正是这样做的。
- 如果目的只是使用传递的值,而不是修改它们,那么完全应该将参数声明为常量引用,而且临时变量不会造成任何不利影响,反而会使函数在可处理的参数种类方面更通用。
应尽可能使用
const
将引用参数声明为常量数据的引用的理由有三个:
- 使用
const
可以避免无意中修改数据的变成错误;- 使用
const
使函数能够处理const
和非const
实参,否则将只能接受非const数据;- 使用
const
引用使函数能够正确生成并使用临时变量。
C++新增了另一种引用——右值引用(rvalue reference)。这种个引用可指向右值,是使用&&
声明的:
double && rref = std::sqrt(36.00);
double j = 15.0;
double && jref = 2.0*j + 18.5;
std::cout << rref << '\n'; // display 6.0
std::cout << jref << '\n'; // display 48.5
新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现。我目前用的比较少,制作简单了解即可。
将引用用于struct
、class
当初引入引用主要是为了用于这些类型的,而不是基本的内置类型。
4.2 将引用用作函数返回值
传统返回机制与按值传递函数参数类似:计算关键字return
后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。
那么,如果返回值是一个结构体或类时,也要把整个结构体或类的实例复制到一个临时位置,在将这个拷贝复制给调用函数中接收返回值的变量。但是在返回值为引用时,将直接把要返回的结构体或类复制到调用函数中接收返回值的变量,其效率更高。
返回引用时需要注意的问题
- 应避免返回函数终止时不再存在的内存单元的引用。(比如在函数中声明的临时变量)
- 返回引用时,返回类型左值。假设我们要使用引用返回值,但又不允许将返回值作为左值进行修改,那么只需将返回类型声明为
const
引用。
4.3. 何时使用引用参数
使用引用参数的主要原因有两个。
- 程序员能够修改调用函数中的数据对象;
- 通过引用传递而不是整个数据对象,可以减少拷贝开销,提高程序的运行速度。
当数据对象较大时(如结构体和类对象),第二个原因最重要。这些同样也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。
那么,什么时候应使用引用、什么时候应使用指针呢?什么时候应按值传递呢?下面是一些指导原则:
对于使用传递的参数而不对参数作修改的函数。
- 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向
const
的指针。 - 如果数据对象是较大的结构体,则使用
const
指针或const
引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。 - 如果数据对象是类对象,则使用
const
引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
对于要对参数进行修改的函数:
- 如果数据对象是内置数据类型,则使用指针。如果看到诸如
fixit(&x)
这样的代码(其中x
是int
),则很明显,该函数将修改x
。 - 如果数据对象是数组,则只能使用指针。
- 如果数据对象是结构体,则使用引用或指针。
- 如果数据地向是类对象,则使用引用。
5 函数重载
函数多态是C++在C语言的基础上新增的功能。
默认参数让我们能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让我们能使用多个同名的函数。
函数重载的关键是函数的参数列表——也称为函数特征标(function signature)。参数数目或参数类型不同,才是函数重载。
看下面的示例:
void sink(double & r1); // 匹配可修改的左值
void sink(const double & r2); // 匹配可修改的左值、const左值、可修改的右值、const右值
void sink(double && r3); // 匹配右值
涉及到函数匹配问题。将调用最匹配的版本。
那什么是最匹配的版本呢?这就又说来话长了,咱们把函数模板复习完之后一起说~