导图社区 Cplusplus函数详解
这是一篇关于Cplusplus函数详解的思维导图,主要内容有函数的定义、调用、声明、函数的形参和实参。
编辑于2021-08-11 20:50:21"重构人生的12条黄金法则:从拒绝平庸到活出独特自我首先打破认知枷锁,用‘反愿景’明确人生底线绝对自由等于混乱,平庸是最昂贵的代价接着用高标准重塑身份,让卓越成为习惯能力提升要聚焦实战,以生活为项目炼就深度通才每日推进关键任务,用持续创造撬动自由保持创业心态,把不确定性当跳板,用自我实验突破极限记住:所有努力只为活出不可复制的生命剧本,包括接纳过程中的重大失误。
在职场沟通场景中,日报的本质是「工作价值的结构化呈现工具」。 许多人之所以会陷入写作困境,核心矛盾并非表达能力不足,而是日常工作缺乏目标导向的思维框架与成果沉淀的行为模式。 这里我将尝试以麦肯锡式思维逻辑为底层框架,拆解从工作规划到日报输出的全流程方法论,帮助职场人建立「思考有模型、执行有标准、呈现有效率」的工作体系。
C 类的封装方法:类的定义,类的构造函数,成员函数,重载构造函数,析构函数,静态成员等详细案例解析和理论罗列。
社区模板帮助中心,点此进入>>
"重构人生的12条黄金法则:从拒绝平庸到活出独特自我首先打破认知枷锁,用‘反愿景’明确人生底线绝对自由等于混乱,平庸是最昂贵的代价接着用高标准重塑身份,让卓越成为习惯能力提升要聚焦实战,以生活为项目炼就深度通才每日推进关键任务,用持续创造撬动自由保持创业心态,把不确定性当跳板,用自我实验突破极限记住:所有努力只为活出不可复制的生命剧本,包括接纳过程中的重大失误。
在职场沟通场景中,日报的本质是「工作价值的结构化呈现工具」。 许多人之所以会陷入写作困境,核心矛盾并非表达能力不足,而是日常工作缺乏目标导向的思维框架与成果沉淀的行为模式。 这里我将尝试以麦肯锡式思维逻辑为底层框架,拆解从工作规划到日报输出的全流程方法论,帮助职场人建立「思考有模型、执行有标准、呈现有效率」的工作体系。
C 类的封装方法:类的定义,类的构造函数,成员函数,重载构造函数,析构函数,静态成员等详细案例解析和理论罗列。
C++函数(上)
函数的定义
函数由4部分构成
返回类型
函数的返回类型指的是函数会返回数据的类型。
如果某个函数不返回任何值,则其返回类型是void,定义这样的函数的目的不是求一个值,而只是为了执行一组操作。
函数名
函数名要求能够描述这个函数的功能或者定义该函数的目的。不准确或者没有意义的函数名往往会误导开发者,并导致程序难以阅读
而准确、有意义的函数名则可以提高程序的可读性,也是对开发者的一种指导。
参数列表
函数的参数位于一个括号中,并且用逗号分隔。括号中的部分就称做函数的参数列表。
函数体
函数体是一个语句块,即由花括号“{”和“}”包含的一组语句。这些语句用来实现函数的功能或目的。
如果函数要返回一个数据,则函数体中至少需要一个return语句。
return 结果表达式;
return (结果表达式);
等价的
如果函数不需要返回数据,即函数的返回类型是void,则在函数体中不需要return语句。即便是加了return语句,也不能带任何表达式。
语法
返回类型 函数名(参数1, 参数2, ...) { 语句1; 语句2; .... 语句n; return 结果表达式; }
意义
使用函数的最大好处就是可重复使用。
函数的调用
所谓调用函数,就是使用函数的功能返回一个值或者执行一组语句。调用函数时,在函数名后跟一个括号,其中是各个参数,用逗号分隔
返回类型变量 = 函数名(参数列表);
调用有返回值的函数
函数名(参数列表)
调用void类型函数,或者虽然有返回值但不需要保存
示例
函数的声明和定义
函数的声明
C++标准规定:函数在调用之前,必须先声明。函数的声明包括返回类型、函数名和参数列表。
函数声明就是为了告诉编译器存在这样一个函数,并且可以在后面的程序中使用。
与函数定义不同,函数的声明不包含函数体。函数的声明也称做函数原型
在函数的声明和定义中,不能没有参数列表。如果函数不需要参数,则可以用空参数列表或只带一个void关键字的参数列表
示例
int function();
int function( void );
等价表示不接受任何参数
声明函数时,参数列表中的每个参数可以有名字,也可以没名字,即只带一个类型关键字
示例
int max( int, int );
int max(int a, int b);
等价
尽管声明函数时不需要给出参数的名字,但是如果参数有名字,则可以提示该参数的含义,开发者可以根据参数的名字传入合适的参数。
尽管声明时不需要给出参数的名字,但在定义时必须有,否则会引起编译错误。而且参数名在函数定义时也有着特殊的意义
调用函数前先声明
函数声明的作用就是告诉调用者如何使用该函数,即函数接受什么类型的参数、参数的个数以及函数的返回类型。函数声明只是函数定义的一部分,它缺少函数体。
如果在调用前函数已经定义,则不必另外声明,因为编译器已经知道了该函数的全部信息。
示例
示例
无需声明
内部声明
外部声明
即告诉编译器有一个名为max的函数,接受两个整型参数,并返回一个整数。在后面的代码中,调用max函数时,编译器就会根据这个函数头在整个源文件中查找这个max函数的定义。
在头文件中声明函数
而对于复杂的大型程序,往往有很多源文件,而且一个源文件中定义的函数,往往会被其他源文件使用,因此就需要在其他源文件中声明该函数。
在有多个源文件的程序中,往往把函数的声明放在头文件中。当别的源文件要声明函数时,只要包含头文件即可(用#include指令)。
使用各种库函数时也是如此。使用库中的某个函数时,首先要声明,即包含相应的头文件。调用时,编译器会根据函数的声明,到库中查找函数的定义。
函数的定义
函数的定义也称做函数实现
函数的定义一般在源文件中实现
一个函数的定义由返回类型、函数名、参数列表和函数体组成
前面的3个部分称为函数的声明或函数原型
函数中的return语句除了返回数据之外,还有一个重要的功能,就是结束函数的运行,在return语句之后的语句不会被执行。
函数的定义不允许嵌套
尽管函数定义不允许嵌套,但可以在函数体中声明另外一个函数。
规范
书写代码时,应当尽量用清楚、明白的算法流程,而不是采用复杂的表达式。
函数的形参和实参
形参
形参是函数定义时的参数。之所以称为形参,是因为这些参数实际并不存在,只是在形式上代表运行时实际出现的参数。
实参
实参是函数调用时传入的参数,也是程序运行时实际存在的参数。
使用默认实参
默认实参是声明函数时给参数指定的默认值。指定默认实参的方法类似于初始化一个变量。
void function (int a, int b = 123);
对于声明中带有默认实参的函数,调用时可以不给该参数传实参。虽然调用时没有指定实参,但在函数运行时,该参数仍然有值,这个值就是声明时的默认实参。
调用者指定的实参会覆盖参数的默认值,也就是说函数运行时会按照调用者指定的值运行。
一个参数只能在一个文件中被指定一次默认实参,但可以在多次声明中依次向前指定其他参数的默认实参。
参数类型检查
调用函数时,编译器会检查实参与形参的类型是否匹配。如果不匹配,则检查是否存在实参类型到形参类型的隐式类型转换。如果存在,则按照隐式类型转换规则进行转换;否则,编译器就会报告一个编译错误。
总结
形参和实参是相对的概念,有时会相互转化。
当函数A()调用函数B()时,由于是在A()的函数体中,所以A()的参数对于A()来讲是形参
但是在调用B()函数时,可以使用A()的参数,此时这个参数对于B()来讲就是实参
示例
void outputMin (int x, int y) { int z = min(x, y); }
对于outputMin函数,x和y是形参;
对于min函数(第3行的调用),x和y是实参。
参数传递
值传递
实参传递给函数后,系统将构建一份实参的副本,其值与实参的值相同。此后函数将针对这份副本进行操作,对原始的实参没有任何影响。C++函数的这种约定称为“值传递”。
函数的形参就是实参的副本。
示例
或许swap函数作者的意图是交换两个参数的值,即期待程序输出“x == 1”和“y == 0”
但是实际结果却没变
这说明swap函数的调用并没有改变两个实参变量的值。
缺点
传值方式传递的只是实参的值,而不是实参本身。这样做一方面会有效率的问题,因为对于大的数据类型有一次赋值过程;另一方面在函数中也并不能改变实参的值,因为函数中只是构建了实参的一个副本。用指针和引用作为参数
指针传递
在函数的参数列表中,可以使用指针类型的参数。
void function (int *)
函数的参数就是一个int类型的指针
传递给指针参数的实参可以是一个指针变量,也可以是一个变量的地址。
用指针作为参数,遵从的也是值传递的原则。实际传递的值是指针的值,而不是指针所指变量的值。
优点
提高了传递参数的效率。
函数可以修改实参指针所指变量的值。
可以消除值传递带来的空间和时间上的浪费。
示例
数组函数
用数组作为函数参数
即函数的形参是数组
定义函数时数组参数作为指针使用,而这个指针就指向数组实参的首地址,也就是数组中第一个元素的地址。
函数内如果修改了数组参数中某个元素的值,也就修改了对应的数组实参中元素的值。
应用
C++程序中常用的排序函数就利用了数组参数的这个特性,它在函数中对数组参数进行排序,函数运行完后,数组中的元素就是排序后的结果。
void function(int a[10] )
用数组作为函数参数时,数组的长度是没有意义的,也就是说上述函数的声明同以下的声明是等效的
正因为在定义函数时,数组参数被当做指针,所以数组参数也可以用指针参数表示:
void function(int a[]);
void function(int a[100])
void function(int *p);
前三个是等价的
所以在定义函数时,不能依赖于数组参数中数组的长度。实际上,编译器会自动将数组参数作为指针进行解释,这个指针指向一块儿连续的内存。而这样的一个指针中不会保存长度信息,所以函数声明时,数组参数的长度是没有意义的。为了弥补这个缺点,可以在参数列表中再附加一个参数,用以传递数组的长度,
void function(int a[], int n)
其中第二个参数n就是数组的长度
调用方法
数组作为实参
指针作为实参
示例
对一个整型数组按照递增的顺序进行排序。
选择排序算法
思想
依次确定第i(i=0,1,…,n-1)个位置上的元素,这个元素的值也就是第i个元素到第n-1个元素中的最小值。
程序实现
在函数 sort 的定义中直接修改了形参数组中的元素,由于数组参数被当做指针使用,所以实际修改的就是实参数组中的元素。
引用传递
使用引用参数的好处同使用指针参数的好处是相同的,也是提高了传递参数的效率,而且也可以在函数内部修改实参变量的值。
函数中的变量
变量是函数的主要内容,变量的类型和作用范围将对函数的功能起到很大的影响
分类
局部变量
定义函数时,在函数体中声明的变量称为局部变量。之所以称为局部变量,是因为这些变量的作用范围仅限于函数体内,在函数外部不能访问。
在上述程序中,函数function中定义了一个变量a,其作用域仅仅限于function的函数体内。由于变量a并不存在,所以“a”这个标识符也就没有定义。
函数体其实就是一个语句块。C++规定在语句块内声明的变量,其作用域仅限于这个语句块内,所以函数体中声明的变量,其作用域不会超出函数体的范围。
形参的作用域也仅限于函数体内,所以形参也可以当做局部变量使用。
全局变量
函数之外声明的变量称为全局变量。全局变量的作用域是整个程序,即在任何一个函数中都可以访问。全局变量一旦被修改,在其他函数中访问到的值也会随之而变。
规范中是不建议的哈,原因就在这
运行上述程序将输出“1,2,3,”,而不是“0,1,2,”。在上述程序中,变量gVar声明(第1行)在所有函数之外,是全局变量。程序中任何地方都可以访问gVar
鉴于全局变量的特性,可以用全局变量来保存函数的结果,即原来用返回值当做函数的结果值,现在可以用全局变量当做函数的结果值。
尽量减少全局变量的使用。虽然在程序中使用全局变量非常方便,可以使数据在函数间共享。但是这样做也使得一个函数的逻辑依赖于另外一个函数的逻辑,函数间的耦合性非常高,导致程序复杂化。另外,如果一个函数赋给全局变量一个错误的值,则会引起其他函数出错,而且这样的错误根源也难以查找
初始化
同局部变量不同,全局变量的初始化是强制性的。如果在定义全局变量时没有给出初始化的值,则全局变量自动初始化为0。
如果程序中定义了多个全局变量,而且都位于同一个源文件中,则这些全局变量会按照其定义时的顺序进行初始化。
如果程序中的全局变量分散在多个源文件中,则其初始化的顺序是不一定的,C++标准没有对此做出规定。
多个源文件共享全局变量
全局变量的作用域虽然是整个程序,但在使用时仍然有特殊的要求。假设有两个源文件file1.cpp和file2.cpp,其中file1.cpp中定义了一些全局变量,如果file2.cpp中的函数要使用这些全局变量,则必须在使用前声明。
extern 类型 全局变量名;
其中extern关键字表明这个全局变量是在别的文件中定义的,需要在本文件中使用。
在全局变量前加上extern关键字只是用来声明一个全局变量,而不是定义全局变量。如果只有extern声明,没有定义,则全局变量仍然不存在。编译器会报告“某某全局变量没有定义”的错误。
静态变量
其特点是变量定义时带有static关键字
函数内的静态变量也称为局部静态变量,其作用域只限于函数内部,别的函数不能访问。局部静态变量的生命周期同全局变量一样,在整个程序运行期间都有效。
函数内的静态变量
全局的静态变量
示例
这是因为 iVal 是一个局部静态变量,其生命周期在程序运行期间一直有效,所以函数 function 每一次运行结束后,并没有销毁 iVal。而且,函数function每一次访问到的iVal的值都是上一次函数运行的结果。例如,function第一次运行后,iVal的值是1,第二次运行访问iVal的值就是1,同样第三次访问到的值就是2。
输出“0,1,2,”
应用
基于这个特性,可以利用局部静态变量保存每一次函数运行时的状态,以供函数再次被调用时使用。虽然全局变量也可以做到这一点,但是任何函数都可以访问,不利于控制。而局部静态变量只有本函数能够访问,可以有效地限制其作用域。
某些严格的安全系统对用户试图登录的次数有限制,可以用静态变量记录这个次数,超过限定次数后则阻止用户继续尝试,用全局变量则给了其他函数修改变量的机会,不符合安全性的要求。
示例
用静态变量记录函数被调用的次数(这里是用户试图登录系统的次数)
全局静态变量
同一般全局变量类似,全局静态变量也是在函数外部定义的变量,只是定义之前带有 static关键字。
跟一般全局变量不同的是:全局静态变量的作用域仅限于定义这个变量的源文件,而不是整个程序。
假设程序中有file1.cpp和file2.cpp两个源文件,其中file1.cpp中定义了全局静态变量gVar,则在file2.cpp中试图访问gVar时,会遇到一个编译错误:“变量gVar未定义”,这就是因为gVar是定义在file1.cpp中的全局静态变量,只能在file1.cpp中访问。
不同的源文件中是不能共享全局静态变量的,但是可以共享全局变量
递归函数
如果一个函数在其定义中又调用自身,则称为递归函数,调用自身的过程叫做递归。递归分为直接递归和间接递归。
直接递归是指函数直接调用自身,间接递归则指A函数调用了B函数,而B函数又调用 A 函数。
函数递归应当有个终止条件,即当某个条件满足时,递归就应当停止。否则递归没完没了地继续下去,程序就会陷入死循环。
坏处
虽然递归在解决某些问题方面非常方便,但也是有代价的。每次递归调用都要重新创建一份函数的副本。如果函数中有大量的变量,并且递归层次很多,则内存很快就会被消耗殆尽。因此,应用递归时,函数应当尽量简单,而且要尽量减少递归的层次。
这是重点问题啊
如何消除递归爆炸
内联函数
源代码编译完之后,函数就变成了一个指令的集合。调用函数时,系统将跳转到这些指令集的首地址开始运行。当函数返回时,系统就跳回到函数调用处的下一条指令继续执行。不管调用多少次,每次系统都跳转到同一地址,程序中也只有一个函数的复制。
虽然函数节省了空间,但也不是没有代价。在调用函数的两次跳转过程中,存在一些影响性能的系统开销。如果函数本身非常短小,只有一两条指令,则跳转花费的时间就会占到较大的比重。如果可以避免跳转,则程序的执行效率就会大大提高。
求和在计算机中是最基本的运算,只要一条指令就可以完成。如果写成函数,则需要附加两次跳转、参数传递、函数返回等操作,这将极大地影响效率。所以,对于如此简单的功能,最好不要使用函数,而是直接计算。
作用
在C++中,如果在函数的声明前加上inline关键字,则称为内联函数。对于内联函数,编译器不创建真实的函数,而只是在函数调用处展开(即将函数的代码直接复制到调用处)。
这样,在“调用”函数时就不用跳转了,避免了使用真实函数的代价。
对于add函数,如果其声明为:
inline int add(int a, int b);
函数调用如下
int x = add(1, 2);
编译后
int x = 1 + 2
尽管在调用处展开内联函数,同复制函数代码是一样的,但还是使用内联函数方便,否则当需要修改代码时,就要在所有用到的地方进行修改。
函数定义
如果函数是在头文件中定义的,则编译器会自动将该函数视为内联函数。
inline关键字和在头文件中定义函数,只是对编译器的一种建议。到底要不要将函数作为内联函数,取决于编译器的判断。
有的函数是不适合作为内联函数的,如递归函数,或者有很多语句的函数。这样的函数即便是加了inline关键字,或者定义在头文件中,编译器依然会将其当做非内联的一般函数对待。
示例
在头文件中定义一个内联函数,求两个整数中的最小值
min函数在头文件method.h中定义,编译器自动将其识别为内联函数。并且函数非常短小,所以会在调用处展开。
函数重载
在C++程序中不允许有相同的函数出现,否则调用时无法区分到底使用哪一个。区分两个函数靠的不仅是函数名,还有函数的参数列表。如果多个函数拥有相同的函数名,但参数列表不同,则称为函数重载
函数标志符与参数列表可以唯一确定一个函数
虽然上述 4 个函数的名字都是“function”,但参数列表不同,所以可以共存于一个程序中。只是在调用时,需要传入不同的实参,以便调用所需的目标函数。
函数的返回类型不能用来区别函数。如果两个函数仅返回类型不同,则第二个出现的函数会被认为是对第一个函数的错误重复。
重载的意义
其实重载早在 C 语言中就已经存在了,例如各种算术运算符。整数和浮点数在内存中的表示方式是不一样的,但在进行各种算术运算时用的运算符却都是一样的
1+ 2
1.0 + 2.0
C++语言将重载进行了进一步扩展,不仅运算符可以重载,而且函数也可以重载。这样,一个函数名就可以应用到不同的参数列表上,从而对不同的参数列表采取不同的操作。
定义一组函数,其目的相似,但是参数(类型或数量)不同,此时就应当使用函数重载。因为目的相似,所以可以用同一个函数名来标识。或许读者会想到给函数名加上不同的前缀或后缀来区别函数,从而避免重载,但那样做无疑会使程序变得复杂。
“函数重载”重载的其实是函数名,因此准确的说法是“函数名重载”。不过“函数重载”已经受到广泛的认同,在后文中将不加区别地使用这两种说法。
示例
函数重载解析
如果一个函数名被重载,那么在调用时选择函数的过程就是重载解析。
确定实参列表的属性,确定候选函数的集合。
根据实参的个数和类型确定合适的函数。
如果函数重载解析过程的第二步没有找到任何合适的函数,则函数调用就是错误的。
选择精确匹配的函数。
为了选择这个函数,从实参类型到相应可行函数参数所用的转换都要划分等级。
应用在实参上的转换,不比调用其他可行函数所需的转换差。
在某些实参上的转换,要比其他可行函数对该参数的转换好。
函数的指针
指向函数的指针,函数的指针也是一个变量
可以指向不同的函数
功能
通过函数指针可以调用其指向的函数,从而使函数调用更加灵活
函数的地址
函数也是有地址的。编译之后的函数,其实是一组指令的集合。这样一组指令在程序运行时存在于内存中,其起始地址就是该函数的地址,也称做函数的入口地址。
表示方法
可以用函数名来表示函数的地址
在函数名之前加上取地址符号“&”表示函数的地址。
函数名“Add”以及“&Add”都表示Add函数的地址
内存地址0x401390即函数Add在内存中的地址。
可以使用强制类型转换,将该地址保存在一个长整型变量中:
也可以通过库函数或者cout对象将Add函数的地址输出到显示器上
定义函数的指针
声明
在函数声明的基础上做一个小小的改变,就可以将其变成一个函数指针的声明。
例如对于Add函数,只要将函数名Add替换成一个函数指针名,例如“pf”,并在其前面加上“(*”、在后面加上“)”
int (*pf)(int a, int b)
定义了函数指针变量pf
函数指针名“*pf”两侧的括号不能省略,否则就成了一个返回“int *”类型的函数声明。正是这个括号使得星号“*”和标识符“pf”组成一个整体,表示pf是一个指针。