GESP C++ 四级第一章:再谈函数(上)

在《二级三级》的第28章,我们已经初步学习了函数的知识,明白了函数的概念,并且知道了函数分为两大类:预定义函数和自定义函数。对于预定义函数,我们主要关注如何调用它来完成特定功能;而要进一步发挥函数的威力,关键在于掌握自定义函数的创建和使用。 本章继续深入学习函数,这些知识主要服务于自定义函数的编写和理解,包括:

  • 函数的优缺点
  • 函数定义
  • 变量的作用域
  • 参数传递
  • 函数重载

函数的优点和缺点

在三级的第28章中,我们已经提到了使用函数的一些优点,这里再做一个总结。总的来说,使用函数有以下几个优点。

  • 代码重用。函数允许你将常用的代码段封装起来,以便在程序的多个地方重复使用,而无需每次都重新编写相同的代码。这大大提高了编程效率,并减少了程序维护的工作量。这是函数最大的优点。
  • 提高可读性。通过将复杂的逻辑或操作封装在函数中,并用简洁的函数名称来调用它们,可以使代码更加清晰和易于理解。这有助于其他开发者(或未来的你)更快地理解代码的功能和结构。如果不使用函数,就要通过注释来说明。
  • 另外,如果不使用函数而把所有的代码都放到主函数里,那么主函数中会有很多嵌套,层次将有可能变得很深,严重影响可读性。
  • 促进团队协作:在团队开发项目中,函数使得不同开发者可以并行工作。每个开发者可以负责编写和测试自己的函数,然后将它们集成到整个程序中。这种分工合作提高了开发效率。

那么,使用函数有缺点吗?我们是否应该将"两个数相加"这样的简单操作也封装成函数呢?答案显然是否定的,因为函数确实存在一些缺点。最主要的考量是性能开销:每次调用函数都需要一些额外的操作,这会导致其效率略低于直接调用的代码。但需要注意的是,这种效率差异通常微乎其微,除非在几千、数万次的循环中反复调用,否则其影响如同沧海一粟,可以忽略不计。

函数定义

函数定义由函数原型函数体两部分组成,如下所示:

第一行称为函数原型,后面由花括号括起来的部分称为函数体。

cpp 复制代码
返回值类型 函数名(参数1类型 参数名, 参数2类型 参数名, ...)
{
       代码块
       return 返回值;           //类型必须与返回值类型相同
}

函数原型用于指定函数的名称、传入参数列表以及返回值类型。函数的参数是可选的,可以有0至多个参数。每个参数都应该写明类型。函数名称前面的返回值类型表示当函数执行完之后,返回何种类型的值。函数的返回类型不可以省略,即使函数不需要返回任何值,也需要指定返回类型为void。从 C++ 11 开始,可以使用 auto 推导返回类型,但这仍然是指定了返回类型。

函数体由{ }中的一组语句组成,实现数据处理的功能。函数体中一般包含return语句,如果返回值类型为void,则不用返回语句,或直接调用 return 返回。函数中可以有多个return语句,一旦遇到 return 语句,无论后面有没有代码,函数立即运行结束,将数值返回。

下面是一个实现两个整数相加的函数(这里只是为了演示如何定义一个函数,在实际代码中,两个数相加没必要封装成一个函数):

cpp 复制代码
int add(int x, int y)
{
       return x + y;
}

函数在被调用之前,调用者需要知道它的信息,因而传统的做法是,把函数定义放在main函数之前(假设需要在 main函数中调用这个函数)。但是,一个函数可能会有很多代码,一个程序可能会有很多函数,如果我们把所有的函数定义都放在 main 函数前面,那么main函数就会在程序代码中处于比较靠后的位置,这会影响程序的可读性。为了避免这种情况,可以仅把函数声明放在函数调用之前,完整的函数定义放在函数调用之后。像下面这样:

cpp 复制代码
int add(int x, int y);         //函数声明
int main() {
    ...... //代码省略
}
int add(int x, int y) {         //完整的函数定义放在调用函数之后
    return x + y;
}

函数声明只包括函数原型,不包括函数体,它的作用是告诉编译器这个函数是存在的,可以调用它,至于它的定义在哪里,暂时不用关心。

如果我们定义了很多函数,我们还可以把这些函数声明放在一个单独的文件里,并在main 函数前面通过 #include 语句把它包含进来。这个其实就是头文件的概念。

所以,函数在被调用之前,只要编译器能够看到函数声明就可以了,它可以直接写在函数调用之前,也可以在一个单独的头文件里,也可以在一个函数定义里。

变量的作用域

我们在一级中曾经提到过,两个变量不可以重名。其实严格来说,是指在同一个作用域里不能重名。变量的作用域,是指变量可以被使用的范围。变量的作用域有很多种,今天我们先学习两种。

局部变量

局部变量,是指在函数内部或者复合语句块内部定义的变量,它只在函数内部或者复合语句块内部有效,函数外部或者复合语句块外部无法访问。到目前为止,我们使用的变量大多是局部变量(除使用数组时,会把数组定义成全局变量)。下面代码中的变量 n、sum、i、a 都是局部变量,其中i和a的作用域在for循环的内部,在for循环外面是不能访问的:

cpp 复制代码
int main() {
      int n, sum=0;
      cin >> n;
      for (int i=1; i<=n; i++) {
          int a;
          cin >> a;
          sum += a;
      }
      cout << sum << endl;
      return 0;
}

在定义局部变量时,可以遵循"延迟定义"或者"按需定义"原则,即不需要在函数的一开始把所有的变量全部定义好,而是等到真正要使用时才定义,这样一方面增加了可读性,另一方面节约了内存。看下面的例子:

cpp 复制代码
int main() {
      int m, n;
      cin >> m >> n;
      if (n == 0)   return -1;
      double r = (double)m/n;
      ...
}

在这段代码中,并没有在一开始就定义变量r,而是等到第一个分支语句if (n == 0)执行完以后才定义的。这样如果用户第二个数输入了0,程序立马就退出了,根本不需要定义 r 了。

局部变量在定义时并不会被自动初始化,其初始值是不确定的(通常为内存中的随机值)。因此,为了确保程序的正确性和可靠性,开发人员在使用局部变量之前,必须显式地为其赋予一个明确的初始值。

全局变量

与局部变量相对的,是全局变量。全局变量定义在函数外部,默认在整个程序范围内有效。那"函数外部"具体是哪儿呢?一般就是所有函数的前面,在 using namespace 之后,那样所有的函数就都能访问了。

全局变量的默认初始化

全局变量除了它的作用域是整个程序以外,它与局部变量还有一个不同的地方。我们知道,局部变量如果定义了但没有赋初值的话,它们的值是不确定的。但是,全局变量会自动初始化成每种数据类型的默认值,对于数值型变量,会自动初始化成0,对于字符串变量,会自动初始化成空串。如果是数组,那么它的每个元素也会自动初始化成元素的数据类型的默认值。这个似乎是全局变量的优点,但一个好的程序,应该尽量少地使用全局变量。

全局变量的声明

对于局部变量来说,声明变量与定义变量是同一个意思,更准确地说,声明变量与定义变量是合在一起的,无法分离成两个独立的阶段。但是对于全局变量,声明和定义是两个不同的阶段,它们可以分开进行,也可以合二为一。

变量声明(Declaration),仅引入变量名和类型,告诉编译器这个变量是存在的,并不分配存储空间。变量声明使用 extern 关键字,如下所示:

cpp 复制代码
extern int g_count;

同一个变量,可以允许多次声明。

变量定义(Definition),提供变量名和类型,并分配内存和初始化(显式初始化或默认初始化)。每个变量只能定义一次。

所以,严格说来,全局变量的定义可以放在任何位置,全局变量的声明才必须放在使用它之前的位置。像下面这样:

cpp 复制代码
extern int g_count;
int main() {
    cout << g_count << endl;
}
int g_count = 5;

跟函数一样,也可以把全局变量的声明放在一个头文件里,然后在需要使用这个全局变量的地方包含这个头文件。全局变量的这种声明和定义的分离也不是必须的,只有当多个文件需要使用这个全局变量时,才需要单独的变量声明。

变量重名

前面说过,在同一个作用域下,两个变量是不能重名的,这也就意味着,在不同的作用域下两个变量是可以重名的。如果两个重名的变量都是局部变量,在不同的作用域里,那么当然互不干涉,不会引起任何歧义。但是,如果两个重名的变量一个是局部变量,一个是全局变量,那么会发生什么情况呢?

局部变量隐藏全局变量

我们看下面的例子:

cpp 复制代码
int n = 5;                           //全局变量
int main() {
      cout << n << ' ';
      int m = 5;
      if (m > 1) {
          int n = 10;
          cout << n << ' ';
      }
      cout << n << endl;
      return 0;
}

这段代码中一共有3次输出n的值,分别为第3行、第7行和第9行,它们输出的值是什么呢?运行这段代码,输出5 10 5。第二个是10,不是5,是 if 分支语句内部的局部变量n 的值,不是外面的全局变量 n的值。但是当分支语句结束了,再输出 n 时,又变成全局变量的值了,因为此时局部变量n已经无效了。

所以结论是:局部变量会"隐藏"或者"屏蔽"全局变量,即当存在同名的局部变量时,如果在这个局部变量的作用域内访问变量名称,访问的是局部变量而不是全局变量。

明确使用全局变量

如果在上述的分支语句内部,我就是想要访问全局变量,那么应该怎么办呢?可以在变量名前面加上两个冒号"::",这样就能访问到全局变量了。我们在上述代码的第7行前面加入下面的语句:

cpp 复制代码
cout << ::n << ' ';

那么输出的结果为5 5 10 5。

通过关键字改变变量的作用域

局部变量全局化

局部变量的作用域只在定义它的函数或者复合语句内部。这句话其实有两层含义。第一,在函数外部或者复合语句外部不能访问这个变量;第二,一旦函数或者复合语句执行结束了,这个变量所占用的内存空间也就被回收了,等到下次这个函数或者复合语句再次被调用时,变量重新被分配内存空间,重新被初始化。

但是,如果在定义这个变量时,前面加上 static 关键字,那么这个变量仍然只能在函数或者复合语句内部访问,但这个变量的内存空间却一直不会被回收,因而它的值会一直保留着,具有了全局变量的某些属性。比较下面两份代码:

代码1,cnt没有使用static关键字

cpp 复制代码
int count() {
      int cnt = 0;
      cnt ++;
      cout << cnt << endl;
}
int main() {
      for (int i=1; i<=3; i++)
          count();
      return 0;
}

代码2,cnt使用了static 关键字

cpp 复制代码
int count() {
      static int cnt = 0;
      cnt ++;
      cout << cnt << endl;
}
int main() {
      for (int i=1; i<=3; i++)
          count();
      return 0;
}

代码1的运行结果如图1-1所示,代码2的运行结果如图1-2所示:

图1-1 图1-2

代码1中,cnt每次被重新被初始化成了0,然后加1;代码2中,只有第一次被初始化成了0,后面它的值就一直保留着,并且一直往上累加,有点像全局变量。

全局变量局部化

全局变量默认在整个程序范围内有效,这在大部分情况下正是全局变量的优势。但也有某些情况,人们把某个变量定义成了全局变量,但却并不希望它在整个程序范围内有效。比如,有一个很大的程序,有好几个人一起开发,把代码分成了好几个文件。假设小乐负责的是a.cpp文件,小马负责的是b.cpp 文件,他们碰巧都在他们的文件内部定义了一个叫做 count的全局变量,但这两个变量各有各的含义,并且仅仅在它们自己的文件用到。这种情况下,如果不做任何处理,程序编译时就会出错,会认为有两个重名的全局变量。

但是,如果在这两个全局变量前面加上static关键字,像下面这样:

a.cpp:

cpp 复制代码
static int count = 0;
int funcA() { ...... }

b.cpp:

cpp 复制代码
static int count = 0;
int funcB() { ...... }

这样就把它们的作用域限定在了文件内部,文件内部的所有函数仍然都能访问到这个变量,但文件外部访问不到,a.cpp文件看不到b.cpp文件中的count变量,b.cpp文件也看不到a.cpp文件中的count变量,这样它们就具有了某种局部变量的特性。

(未完待续)

相关推荐
微露清风4 小时前
系统性学习C++-第九讲-list类
c++·学习·list
大佬,救命!!!5 小时前
C++多线程同步与互斥
开发语言·c++·学习笔记·多线程·互斥锁·同步与互斥·死锁和避免策略
散峰而望5 小时前
C++入门(一)(算法竞赛)
c语言·开发语言·c++·编辑器·github
C_Liu_5 小时前
13.C++:继承
开发语言·c++
凡同学。6 小时前
通信人C++自学
c++·应届生秋招·后端四件套
威桑6 小时前
C++ Linux 环境下内存泄露检测方式
linux·c++
报错小能手7 小时前
C++笔记(面向对象)RTTI操作符
开发语言·c++·笔记
GOATLong7 小时前
git使用
大数据·c语言·c++·git·elasticsearch
十五年专注C++开发8 小时前
Qt-Nice-Frameless-Window: 一个跨平台无边框窗口(Frameless Window)解决方案
开发语言·c++·qt