C++__函数与预处理
时间 2021-01-17
标签
C++
函数
预处理
在一个程序文件中可以包含若干个函数。无论把一个程序划分为多少个程序模块,但只能有一个main函数。程序总是从main函数开始执行的。在程序运行过程中,由主函数调用其他函数,其他函数也可以互相调用。
从用户使用的角度看,函数有两种:
① 系统函数,即库函数。这是由编译系统提供的,用户
② 用户自己定义的函数。用以解决用户的专门需要。
从函数的形式看,函数分两类:
① 无参函数。调用函数时不必给出参数。
② 有参函数。在调用函数时,要给出参数。在主调函数
和被调用函数之间有数据传递。
2 定义函数的一般形式
类型标识符 函数名([void])
{声明部分
语句
}
类型标识符 函数名(形式参数列表)
{声明部分
语句
C++要求在定义函数时必须指定函数的类型 (在C语言中,如在定义函数时不指定函数类型,系统默认为int型)。
3 函数参数和函数的值
- 在定义函数时,函数名后面括号中的变量称为形式参数(formal parameter,简称形参);在主调函数中调用一个函数时,函数名后面括号中的参数(可以是一个表达式)称为实际参数(actual parameter,简称实参)。
- 在调用函数时,主调函数和被调用函数之间有数据传递关系。
有关形参与实参的说明:
① 定义函数时所指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,也就不存在数据。只有在发生函数调用时,函数中的形参才被分配内存单元,以便接收从实参传来的数据。在调用结束后,形参所占的内存单元被释放。
② 实参可以是常量、变量或表达式。但实参必须有确定的值,以便在调用函数时将实参的值赋给形参。
③ 在定义函数时,必须在函数首部指定形参的类型。
④ 实参与形参的类型应相同或赋值兼容。如果实参与形参的类型不同,则按不同类型数值的赋值规则进行转换。字符型与整型可以互相通用。
⑤ 实参变量对形参变量的数据传递是单向的“值传递”,即只由实参传给形参,而不能由形参传回来给实参。
注意:
实参单元与形参单元是不同的单元。在调用函数时,编译系统临时给形参分配存储单元,用来接收从实参传来的数据。调用结束后,形参单元被释放,但实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。
2 函数的返回值
有关函数返回值的说明:
① 函数的返回值是通过函数中的return语句获得的。return语句将被调用函数中的一个确定值带回到主调函数中。return语句后面的括号可以要,也可以不要。return后面的值可以是一个表达式。
② 函数返回值的类型是在定义函数时指定的,应为某一个确定的类型。
③ 如果函数返回值的类型和return语句中表达式值的类型不一致,则以函数类型为准,即函数类型决定返回值的类型。对数值型数据,可以自动进行类型转换。
4 函数的调用
函数名([实参列表])
① 如果调用的是无参函数,则“实参列表”可以没有,但括号不能省略。
② 如果实参列表包含多个实参,则各参数间用逗号隔开。实参与形参的个数应相等,类型应匹配 (相同或赋值兼容)。实参与形参是按顺序一对一对应地传递数据。
注意:如果实参列表包括多个实参,对实参求值的顺序并不是确定的。许多C++系统是按自右至左的顺序求值的。
把函数调用单独作为一个语句,并不要求函数带回一个
值,只是要求函数完成一定的操作。
在一个函数中调用另一个函数需要具备的条件:
① 首先被调用的函数必须是已经存在的函数。
② 如果调用库函数,一般应在本文件开头用#include
命令将有关头文件“包含”到本文件中来。
③ 如果调用用户自己定义的函数,而该函数与调用它
的函数在同一个程序单位中,且位置在调函数之后,则必须
在调用此函数之前对被调用的函数作声明。
所谓函数声明(declare),就是在函数尚未定义的情况
下,事先将该函数的有关信息通知编译系统,以便使编译能
正常进行。
函数的定义是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。
函数的声明则是把函数的名字、函数类型以及形参的个数、类型和顺序(不包括函数体)通知给编译系统,以便在对包含有函数调用的语句进行编译时,根据这些信息对其进行对照检查(如函数名是否正确,实参与形参的类型和个数是否一致)。
4 函数原型
在函数声明中也可以不写形参名,而只写形参的类型,
这种函数声明称为函数原型(function prototype)。
函数原型的一般形式为
函数类型 函数名(参数类型1,参数类型2…);
【例】
float add(float,float);
使用函数原型是C和C++的一个重要特点,其主要作用
是:根据函数原型在程序编译阶段对调用函数的合法性进行
全面检查。如果发现与函数原型不匹配的函数调用就报告编
译出错。
注意:应当保证函数原型与函数首部写法上的一致,即函数类型、函数名、参数个数、参数类型和参数顺序必须相同。在函数调用时函数名、实参类型和实参个数应与函数原型一致。
说明:编译系统并不检查函数声明中的参数名。因此函数声明中的参数名是什么都无所谓。
关于函数定义位置不同的影响
关于调用函数的说明:
① 如果被调用函数的定义出现在主调函数之前,可以不必加以声明。因为编译系统已经事先知道了已定义的函数类型,会根据函数首部提供的信息对函数的调用作正确性检查。
② 函数声明的位置可以在调用函数所在的函数中,也可以在函数之外。如果函数声明放在函数的外部,并在所有函数定义之前,则在各个主调函数中不必对所调用的函数再作声明。
编程建议:一般都把main函数写在最前面,这样对整个程序的结构和作用一目了然,统览全局,然后再具体了解各函数的细节。此外,应养成对所有用到的函数作声明的习惯。这是保证程序正确性和可读性的重要环节。
5 内置函数
- 调用函数需要一定的时间和空间开销,其过程如下图所示。
- 在程序运行到该句的时候会直接将函数中的语句写入其中,
-
注意:
① 内置函数的声明可以是在声明函数和定义函数时都加inline,也可以只在其中一处声明inline。
② 使用内置函数可以节省运行时间,但却增加了目标程序的长度。因此一般只将规模很小(一般为5个语句以下)而调用频繁的函数声明为内置函数。
③ 内置函数中不能包括复杂的控制语句,如循环语句和switch语句。
说明:声明内置函数,只是编程者对编译系统提出的一个建议,而不是指令。并非指定函数为inline,编译系统就必须把它作为inline处理,而是编译系统会根据具体情况决定是否这样做。因此,只有那些规模较小而又被频繁调用的简单函数,才适合于声明为inline函数。
- 6 函数的重载
C++允许用同一函数名定义多个函数,即对一个函数名重新赋予它新的含义,使一个函数名可以多用,这就是函数的重载(function overloading)。
注意:
重载函数的参数个数、参数类型或参数顺序3者中必须至少有一种不同,函数返回值类型可以相同也可以不同。
建议:在使用重载函数时,同名函数的功能应当相同或相近,不要用同一函数名去实现完全不相干的功能,虽然程序也能运行,但可读性不好,使人莫名其妙。
- 7 函数模板
所谓函数模板(function template),实际上是一个通用函数,该函数在建立时其函数类型和形参类型不具体指定,而是用一个虚拟的类型来代表。在调用函数时系统会根据实参的类型来取代函数模板中的虚拟类型, 从而实现不同的函数功能。
定义函数模板的一般形式为:
template < typename T1,typename T2,……>
或
template <class T1,class T2,……>
凡是函数体相同的函数都可以用一个函数模板来代替,不必定义多个函数,只需在模板中定义一次即可。
注意:
虽然用函数模板比函数重载更方便,程序更简洁。但它只适用于函数的参数个数相同而类型不同,且函数体相同的情况。
-
8 有默认参数的函数
在函数调用时,形参一般是从实参那里取得值,因此实参的个数应与形参相同。有时多次调用同一函数时使用同样的实参,C++提供简单的处理办法,即给形参一个默认值,这样形参就不必一定要从实参取值。
如果函数有多个形参,可以使每个形参有一个默认值,也可以只对一部分形参指定默认值,另一部分形参不指定默认值。
- 默认值的位置问题
- 注意:实参与形参的传值是从左至右顺序进行的。因此指定默认值的参数必须放在形参列表中的最右端,否则函数调用时会出错。
- 使用带有默认参数的函数可简化编程,使程序比较灵活,并能提高运行效率。但使用中要注意两点:
- ① 如果函数的定义在函数调用之前,则应在函数定义中给出默认值。如果函数的定义在函数调用之后,则在函数调用之前需要有函数声明,此时必须在函数声明中给出默认值,在函数定义时可以不给出默认值。
- ② 一个函数不能既作为重载函数,又作为有默认参数的函数。因为当调用函数时如果少写一个参数,系统无法判定是利用重载函数还是利用默认参数的函数,出现二义性,系统无法执行。
9 函数的嵌套调用
C++不允许对函数作嵌套定义,也就是说在一个函数的定义中不能包含另一个函数的定义。 C++程序中每个函数的定义都是互相平行和独立的。
虽然C++不能嵌套定义函数,但可以嵌套调用函数,即在调用一个函数的过程中,又调用另一个函数。
- 注意:如果被调用函数的定义在主调函数之后,那么应在主调函数之前对被调用函数作声明。
10 函数的递归调用
在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用(recursive) 。C++允许函数的递归调用。包含递归调用的函数称为递归函数。
注意:
应避免函数无终止的自身调用。可以在使用递归调用时使用if语句来控制递归调用的次数,即只有在某一条件成立时才继续执行递归调用,否则就不再继续。
11 局部变量和全局变量
每一个变量都有其有效作用范围,这就是变量的作用域。在作用域以外是不能访问这些变量的。
- 在一个函数内部定义的变量是内部变量,它只在本函数范围内有效,即该变量只有在本函数内才能使用它们,在此函数以外是不能使用这些变量的。同样,在复合语句中定义的变量只在本复合语句范围内有效。这类变量称为局部变量(local variable)。
说明:
① 主函数main中定义的变量只在主函数中有效,不会因为在主函数中定义而在整个文件或程序中有效。主函数也不能使用其他函数中定义的变量。
② 不同函数中可以使用同名的变量,它们代表不同的对象,互不干扰。
③ 可以在一个函数内的复合语句中定义变量,这些变量只在本复合语句中有效,这种复合语句也称为分程序或程序块。
④ 形式参数也是局部变量。其他函数不能调用。
⑤ 在函数声明中出现的参数名,其作用范围只在本行的括号内。实际上,编译系统对函数声明中的变量名是忽略的,即使在调用函数时也没有为它们分配存储单元。
在函数内定义的变量是局部变量,而在函数之外定义的变量是外部变量,称为全局变量(global variable,也称全程变量)。全局变量的有效范围为从定义变量的位置开始到本源程序文件结束。
说明:
(1)全局变量的作用是增加函数间数据联系的渠道。
(2)建议在不必要时不使用全局变量,因为:
① 全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。
② 它使函数的通用性降低了,因为在执行函数时要受到外部变量的影响。
③ 使用全局变量过多,会降低程序的清晰性,人们往往难以清楚地判断出每个瞬时各个全局变量的值,程序容易出错。
(3)如果在同一个源文件中,全局变量与局部变量同名,则在局部变量的作用范围内,全局变量被屏蔽,即它不起作用。
总结:变量有4种不同的作用域:文件作用域、函数作用域、块作用域和函数原型作用域。文件作用域是全局的,其他三者是局部的。
12 变量的存储类别
变量除了具有作用域(从空间的角度来分析)这一属性外,还有另一种属性——存储期(从变量值存在的时间角度来分析),也称生命期。
存储期是指变量在内存中的存在期间。存储期可以分为静态存储期和动态存储期,它是由变量的静态存储方式和动态存储方式决定的。
所谓静态存储方式是指在程序运行期间,系统对变量分配固定的存储空间。而动态存储方式则是在程序运行期间,系统对变量动态地分配存储空间。
内存中供用户使用的存储空间可以分为三部分,即:
① 程序区
② 静态存储区
③ 动态存储区
程序区用来存放程序指令,静态存储区和动态存储区用来存放数据。
全局变量全部存放在静态存储区中。在程序开始执行时给全局变量分配存储单元,程序执行完毕就释放这些空间。在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放。
在动态存储区中存放以下数据: ① 函数形式参数。② 函数中的自动变量(未加static声明的局部变量)。③ 函数调用时的现场保护和返回地址等。 对于这些数据,在函数调用开始时分配存储空间,函数结束时释放这些空间。在程序执行过程中,这种分配和释放是动态的。
说明:如果在一个程序中包含若干个函数,每个函数中局部变量的存储期并不等于整个程序的执行周期,它只是整个程序执行周期的一部分。
2 自动变量
C++中的变量除了具有数据类型属性外,还有存储类别属性。所谓存储类别指的是数据在内存中存储的方法,即静态存储和动态存储,具体包含4种:自动的(auto)、静态的(static)、寄存器的(register)和外部的(extern)。根据变量的存储类别,可以知道变量的作用域和存储期。
函数的形参和函数中的局部变量(不加关键字static声明)属于自动变量。这类变量在函数调用时,系统给它们动态分配存储空间,数据存储在动态存储区中。在函数调用结束时自动释放这些空间。
复合语句中定义的变量也属于自动变量,它们在变量定义时分配存储空间,在复合语句结束时自动释放空间。
自动变量用关键字auto作存储类别的声明。
intf(inta) //定义f函数,a为形参
{ auto int b,c=3; //定义b和c为整型的自动变量 }
说明:
① 在定义自动变量时,存储类别和数据类型的顺序任意。
② 关键字auto可以省略,系统会默认为自动存储类别。
有时希望函数中局部变量的值在函数调用结束后不消失而保留原值,即其占用的存储单元不释放,在下一次调用该函数时,该变量保留上一次函数调用结束时的值。这种局部变量应该指定为静态局部变量(static local variable)。
对静态局部变量的说明:
① 静态局部变量在静态存储区内分配存储单元,在程序整个运行期间都不释放。
② 静态局部变量赋初值是在编译时进行,即只赋初值一次。在程序运行时每次调用函数都不再重新赋初值而只是保留上次函数调用结束时的值。而自动变量赋初值,不在编译时进行,而是在函数调用时进行,每调用一次函数重新给一次初值。
③ 如果在定义局部变量时不赋初值,那么对静态局部变量来说,编译时自动赋初值0(数值型变量)或空字符(字符型变量)。而对自动变量来说,它的值是一个不确定的值。
④ 虽然静态局部变量在函数调用结束后仍然存在,但其他函数不能引用它,即在其他函数中它是“不可见”的。
使用静态局部变量的两种情况:
① 需要保留函数上一次调用结束时的值。
② 如果初始化后,变量只被引用而不改变其值,则这时用静态局部变量比较方便,以免每次调用时重新赋值。
注意:采用静态存储会多占内存,降低程序的可读性,并且当函数调用次数过多时,往往弄不清静态局部变量的当前值。因此,非必要切勿多用静态局部变量。
一般情况下,变量的值存放在内存中。当程序中用到哪个变量的值时,由控制器发出指令将内存中该变量的值送到CPU中的运算器。经过运算器运算后,如果需要存数,再从运算器将数据送到内存存放。
为了提高执行效率,C++允许将局部变量的值放在CPU的寄存器中,需要用时直接从寄存器取出参加运算,不必再到内存中去存取。这种变量叫做寄存器变量,用关键字register作声明。
说明:在程序中定义寄存器变量只是对编译系统的建议。当今的优化编译系统能够识别使用频繁的变量,自动地将这些变量放在寄存器中。
5 用extern声明外部变量
外部变量 (即全局变量)是在函数的外部定义,其作用域是从变量的定义处开始,到本程序文件的末尾。在此作用域内,全局变量可以被本文件中各个函数所引用。系统编译时将全局变量分配在静态存储区。
用extern来声明全局变量,可以扩展全局变量的作用域。
⑴ 在一个文件内声明全局变量
在一个源程序文件中,如果函数想在外部变量定义之前引用该外部变量,则应该在引用之前用关键字 extern对该变量作外部变量声明,表示该变量是一个将在下面定义的全局变量,这种声明称为提前引用声明。
⑵在多个文件的程序中声明外部变量
当一个程序包含两个源程序文件,而在两个文件中都要使用同一个外部变量时,如果分别在两个文件中各自定义该外部变量,则在进行程序连接时会出现“重复定义”的错误。正确做法是:在其中一个文件中定义该外部变量,而在另一文件中用extern对该变量作外部变量声明。
注意:用extern扩展全局变量的作用域,虽然能为程序设计带来方便,但应十分慎重,因为在执行一个文件中的函数时,可能会改变了该全局变量的值,从而会影响到另一文件中的函数执行结果。
6 用static声明静态外部变量
有时在程序设计中希望某些外部变量只限于被本源程序文件引用,而不能被其他文件引用。那么可以在定义外部变量时加一个static关键字,将该变量声明为静态外部变量。
一个变量除了具有数据类型属性外,还有3种属性:
① 存储类别 C++允许使用auto、static、register和extern 4种存储类别。
② 作用域 指程序中可以引用该变量的区域。
③ 存储期 指变量在内存的存储期限。
以上3种属性是有联系的,程序设计者只能声明变量的存储类别,而变量的作用域和存储期通过存储类别可以确定。
注意:auto、static和register 3种存储类别只能用于变量的定义语句中,而extern只能用来声明已定义的外部变量,不能用于变量的定义。
使用方式都是在变量类型之前。前三个是定义 ,后一个是引用
变量属性间关系的分析:
(1)
从作用域角度,变量可分为局部变量和全局变量。它们采用的存储类别如下:
(2)
从变量存储期,变量有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储则是在调用函数时临时分配单元。
(3) 从变量值存放的位置,变量可分为
(4)
关于作用域和存储期的概念。对一个变量的性质可以从两个方面分析,一是从变量的作用域,一是从变量的存储期。前者是从空间的角度,后者是从时间的角度。二者有联系但不是同一回事。作用域和存储期的示意图如下图所示。
如果一个变量在某个文件或函数范围内有效,则称该文件或函数为该变量的作用域,在此作用域内可以引用该变量,即变量在此作用域内“可见”,这种性质称为变量的可见性。
如果一个变量值在某一时刻是存在的,则认为这一时刻属于该变量的存储期,即该变量在此时刻“存在”。
各种类型变量的作用域和存在性的情况如下表所示。
(5) static声明使变量采用静态存储方式,但它对局部变量和全局变量所起的作用不同。
对局部变量来说,static使变量由动态存储方式改变为静态存储方式。而对全局变量来说,它使变量化(局部于本文件),但仍为静态存储方式。从作用域角度看局部,凡有static声明的,其作用域都是局限的,或者局限于本函数内(静态局部变量),或者局限于本文件内(静态外部变量)。
14 关于变量的声明和定义
一个函数一般由声明部分和执行语句组成。声明部分的作用是对有关的标识符(如变量、函数、结构体、共用体等)进行属性说明。对于函数,声明和定义有明显的区别:函数的声明是函数的原型,而函数的定义是函数功能的确立。对函数的声明是可以放在声明部分中的,而函数的定义是一个文件中的独立模块。
变量出现在声明部分有两种情况:一种是需要建立存储空间的,即定义性声明,简称为定义(如inta; );另一种是不需要建立存储空间的,即引用性声明(如extern int a;)。
一般为了叙述方便,把建立存储空间的声明称为定义,而把不需要建立存储空间的声明称为声明。
15 内部函数和外部函数
- 如果一个函数只能被本源程序文件中的函数所调用,就称为内部函数。定义内部函数的一般格式为:
- 函数本质上是全局的,但也可以指定函数只能被本源程序文件调用,而不能被其他文件调用。根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。
- (1) 在定义函数时,如果在函数首部的最左端加上关键字extern,则表示该函数是外部函数。
16 预处理命令
C++提供的“预处理命令”,可以改进程序设计环境,提高编程效率。
预处理命令是C++统一规定的,但它们不是C++语言本身的组成部分,不能直接对它们进行编译(因为编译程序不能识别它们)。因此程序在编译之前先进行“预处理”,即根据预处理命令对程序作出相应的处理,经预处理后程序中不再包含预处理命令,然后再进行编译连接。现在的C++编译系统都包括了预处理、编译和连接等部分。
C++提供的预处理功能主要有以下3种:
① 宏定义
② 文件包含
③ 条件编译
- 宏定义的作用是将一个指定的标识符(即宏名)来代表一个字符串,一般是用一个短的名字代表一个长的字符串。C++提供了#define命令来实现“宏定义”的操作。
#define 标识符 字符串