C++学习笔记(二)——面向过程编程的C++之函数

本文深入探讨了C++中的函数参数传递,特别是按值传递、数组作为参数及const限定符的使用。介绍了数组名被视为指针的特性,以及如何使用const保护数据。此外,讲解了函数指针的概念,如何声明和使用,以及在回调函数中的应用。接着,详细阐述了内联函数的作用和使用,以及为何在类中定义函数默认为内联。最后,讨论了引用变量,包括其作为函数参数和返回值的优势与注意事项,以及函数重载的基本原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. 函数

1 函数参数

1.1 参数传递基础

  1. C++通常按值传递参数
  2. 用于接收传递值得变量被称为形参;传递给函数的值被称为实参。处于简化的目的,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 * arrint arr[]的含义才是相同的。 它们都意味着arr是一个int指针。然而,数组表示法(int arr[])提醒用户,arr不仅指向int,还指向int数组的第一个int。

我们应该始终记住下面两个恒等式:

arr[i] == *(arr+i);
&arr[i] == arr + i

传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。这看似违反了C++按值传递的原则,但实际上并非如此,sum_arr()函数仍然是传递了一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容。 但另一方面,使用原始数据增加了破坏数据的风险。在经典的C语言中,这确实是一个问题,但C++中的const限定符提供了解决这种问题的办法。稍后将具体介绍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,而*fingerps不是。

如果愿意,还可以声明指向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个副本。

如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的一小部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此,尽快节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用

简而言之,适合使用内联函数的情况就是满足下面两个条件:

  1. 函数体短小执行速度快;
  2. . 经常调用该函数。

否则,效果不大,还会比较耗费空间。


在后面的类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

新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现。我目前用的比较少,制作简单了解即可。

将引用用于structclass

当初引入引用主要是为了用于这些类型的,而不是基本的内置类型。

4.2 将引用用作函数返回值

传统返回机制与按值传递函数参数类似:计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。
那么,如果返回值是一个结构体或类时,也要把整个结构体或类的实例复制到一个临时位置,在将这个拷贝复制给调用函数中接收返回值的变量。但是在返回值为引用时,将直接把要返回的结构体或类复制到调用函数中接收返回值的变量,其效率更高。

返回引用时需要注意的问题

  1. 应避免返回函数终止时不再存在的内存单元的引用。(比如在函数中声明的临时变量)
  2. 返回引用时,返回类型左值。假设我们要使用引用返回值,但又不允许将返回值作为左值进行修改,那么只需将返回类型声明为const引用。

4.3. 何时使用引用参数

使用引用参数的主要原因有两个。

  • 程序员能够修改调用函数中的数据对象;
  • 通过引用传递而不是整个数据对象,可以减少拷贝开销,提高程序的运行速度。

当数据对象较大时(如结构体和类对象),第二个原因最重要。这些同样也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。

那么,什么时候应使用引用、什么时候应使用指针呢?什么时候应按值传递呢?下面是一些指导原则:

对于使用传递的参数而不对参数作修改的函数。

  • 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
  • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
  • 如果数据对象是较大的结构体,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
  • 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。

对于要对参数进行修改的函数:

  • 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中xint),则很明显,该函数将修改x
  • 如果数据对象是数组,则只能使用指针。
  • 如果数据对象是结构体,则使用引用或指针。
  • 如果数据地向是类对象,则使用引用。

5 函数重载

函数多态是C++在C语言的基础上新增的功能。
默认参数让我们能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让我们能使用多个同名的函数。

函数重载的关键是函数的参数列表——也称为函数特征标(function signature)。参数数目或参数类型不同,才是函数重载。

看下面的示例:

void sink(double & r1);  // 匹配可修改的左值
void sink(const double & r2); // 匹配可修改的左值、const左值、可修改的右值、const右值
void sink(double && r3); // 匹配右值

涉及到函数匹配问题。将调用最匹配的版本。
那什么是最匹配的版本呢?这就又说来话长了,咱们把函数模板复习完之后一起说~

6 函数模板(第三部分讲)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值