1 函数
文章目录
函数调用机制:
- 准备工作:将实参,局部变量,返回地址,若干寄存器,入栈
- 执行代码
- 清理现场
- 将入栈变量出栈,
1.1 函数参数
1.1.1 默认参数
:**
默认参数性质
- 参数数可以是常量,也可以是表达式
- 默认参数只可以放到形参列表后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值
- 如果函数声明有默认值,那么实现时不可以有默认值。编译器禁止声明和定义时同时定义缺省参数值
- 原因:声明是用户可以看到的部分,客户非常信任地使用这个特性
- 实践证明,缺省参数可以在定义中,也可以在声明中,只要全局的声明和定义中只有一个带参数即可
默认参数的作用:
- 通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。
#include<iostream>
using namespace std;
//常量默认参数
void func(int n, float b=1.2, char c='@'){
cout<<n<<", "<<b<<", "<<c<<endl;
}
//表达式默认参数
float d = 10.8;
void func(int n, float b=d+2.9, char c='@'){
cout<<n<<", "<<b<<", "<<c<<endl;
}
//2. 如果函数声明有默认值,函数实现的时候就不能有默认参数
int func2(int a = 10, int b = 10);
int func2(int a, int b) {
return a + b;
}
int main(){
//为所有参数传值
func(10, 3.5, '#');
//为n、b传值,相当于调用func(20, 9.8, '@')
func(20, 9.8);
//只为n传值,相当于调用func(30, 1.2, '@')
func(30);
return 0;
}
1.2.2 占位参数
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
语法: 返回值类型 函数名 (数据类型){}
在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术
示例:
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
cout << "this is func" << endl;
}
int main() {
func(10,10); //占位参数必须填补
system("pause");
return 0;
}
1.2 函数重载
1.2.1 函数重载规则
重载条件:
- 函数名称相同
- 参数列表不同。参数列表不同包括参数的个数不同、类型不同或顺序不同。
- 引用可以作为重载条件加上const;可基于函数的引用形参是指向 const 对象还是指向非 const 对象
- const 修饰函数可实现重载,普通函数和常函数。
注意事项:
- 参数名称不同不可以作为重载条件。
- 返回值类型不可以作为重载条件。
本质:
重载决议:C++代码在编译时会根据参数列表对函数进行重命名,
例如void Swap(int a, int b)
会被重命名为_Swap_int_int
,void Swap(float x, float y)
会被重命名为_Swap_float_float
。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错。
本质:函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样。
1.2.2 重载的二义性
二义性是指在编译过程中无法找出最匹配的函数,或者说编译器在函数匹配过后还是有多个函数满足要求,无法确定该执行那一个引发的错误。
1.参数数目引发的二义性(默认参数问题)
int get(){
return 5;
}
int get(int a = 5){
return a;
}
//调用get()
//不给参数和有默认参数会造成歧义。
2.参数隐式转换引发二义性
可以使用explict关键字禁用隐式转换
int get(int m){
return m;
}
long get(long m){
return m;
}
//double d = 1.234;
//调用get(d);double既可以隐式转换未long,也可以是int,或者说一般的数值类型之间都可以进行隐式类型转换,故无法确定那一个更加匹配。
3.引用作为重载条件
void func(int &a)
{
cout << "func (int &a) 调用 " << endl;
}
void func(const int &a)
{
cout << "func (const int &a) 调用 " << endl;
}
int a=10;
func(a);调用1 1.是变量可读可写 2.是可读 无二意性
func(10);调用2 1. int &a=10;语法错误,引用必须指向一个内存
2. const int a=10;合法,创建临时向量 存储10,然后引用指向。
1.3 内联函数
函数调用机制:
- C++程序的执行过程可以被当做多个函数的调用过程,从main函数开始,到main函数结束;然而函数的调用将会产生开销,因此短代码的函数开销不可忽略,而长函数的函数开销可以忽略。
- 准备工作:将实参,局部变量,返回地址,若干寄存器,入栈
- 执行代码
- 清理现场
- 将入栈变量出栈,
**背景:**当函数体代码比较多,函数调用机制时间可以被忽略
当函数体代码比较少,函数调用机制时间相对较长,不可忽略。
解决方法:内联函数
消除时空开销,提高效率。在函数编译时将内联函数调用处,直接用内联函数的代码替换。如此,便不需要函数调用。
注意事项:
内联函数特性
- 内联函数不应该有声明,内敛函数省略函数原型,直接将函数的整个定义放入提供函数原型的地方。(声明时进行定义,不需要将声明的定义分离)
- 函数定义时使用inline有效,声明inline编译器无视
- 编译器对是否内联有自己的想法,程序员仅仅是提供建议,最终是否内联还是取决于编译器
- 内联函数是一种空间换时间的做法,编译期间,内联函数代码展开,省去函数调用开销,代码很长或者有循环/递归的函数不适宜使用作为内联函数。可能会造成代码膨胀。
- 在debug模式下,内联函数默认是不会展开的,因为debug模式主要是用来调试代码的
内联函数失去函数的本质
- 函数是一段可以重复使用的代码,它位于虚拟地址空间中的代码区,也占用可执行文件的体积,而内联函数的代码在编译后就被消除了,不存在于虚拟地址空间中,没法重复使用
#include <iostream>
using namespace std;
//声明处inline关键字会被忽略
inline void swap(int*,int*);
//内联函数,交换两个数的值
inline void swap(int *a, int *b){
int temp;
temp = *a;
*a = *b;
*b = temp;
}
int main(){
int m, n;
cin>>m>>n;
cout<<m<<", "<<n<<endl;
swap(&m, &n);
cout<<m<<", "<<n<<endl;
return 0;
}
1.3.1 内联函数代替带参数的宏
宏的调用机制是字符串的替换,而不是按值传递。
当n = 9 时,SQ(n) = 81
如果把SQ(n)
换成SQ(n+1)却得不到100,因为
sq = n+1*n+1;
#include <iostream>
using namespace std;
#define SQ(y) y*y
int main(){
int n, sq;
cin>>n;
sq = SQ(n+1);
cout<<sq<<endl;
return 0;
}
1.3.2内联函数代替宏
-
宏可以带参数,宏的机制是字符串替换
注意踩坑 #define SQ(y) y*y 当n = 9 时,SQ(n) = 81 如果把SQ(n)换成SQ(n+1)却得不到100,因为sq = n+1*n+1; 要想得到正确的结果,还应该对宏加以限制,在两边增加( ),如下所示: #define SQ(y) ( (y)*(y) )
-
C++推荐使用内联函数代替宏,函数调用比字符串替换相比快捷
-
头文件和内联函数
- 内联函数可以定义在头文件中,且该文件可以被多次include
- 非内联函数不可以定义在头文件中,且头文件多次include会有重复定义错误
1.3.3宏
-
宏的优点:宏函数在预处理阶段展开了,减少了函数调用的开销(传参,参数压栈以及栈帧花销)…
-
宏的缺点:宏函数可能会存在一定的副作用;在预处理阶段进行替换,不会参与编译,少了类型检测;宏函数不能调试…
-
在C++中,宏常量可以用const修饰的常量来代替,宏函数可以用内联函数来代替
1.3.4const修饰变量
-
在C语言中,const修饰的变量不能成为常量,只能称为不能被修改的变量
-
在C++中,被const修饰的变量称为常量,且有宏替换的功能
-
被const修饰的变量的替换发生在编译阶段,在所有使用const常量的位置用常量的值替换该常量