目录
概念
在函数声明或函数定义的时候
可以给某些参数默认值
这样,在调用函数时,如果用户没有传值,函数将使用默认值。
用珍珠奶茶来举例:珍珠奶茶默认放珍珠(缺省值 / 默认值),如果不特别说明改其他小料的话,就默认放珍珠
语法
int Add(int a = 3, int b = 2)
{
return a + b;
}
//
int main()
{
int x = Add();
//没有传参,默认参数为3和2,所以x为3 + 2 =5
int y = Add(10, 20);
//传参传了a和b,默认参数不生效,所以y为10 + 20 =30
int z = Add(8);
//传参传了a且没传b
//a的默认参数不生效,b的默认参数生效
//所以z为 8 + 2 =10
return 0;
}
规定:实参传参时,从左往右给值,函数形参给缺省值时,从右往左给值
(下面有详细说明)
意义
- 提高灵活性:调用者可以选择性地提供参数,而不是被迫为每个参数都提供值。
- 简化调用:对于那些通常情况下具有相同值的参数,可以通过默认参数来简化函数的调用,减少重复性代码。
分类
如果要做函数的声明和定义的分离,那么声明和定义当中只能有一个有缺省参数
(毕竟如果两边都给了不同的缺省参数,编译器就不知道用哪个缺省参数了)
全缺省
定义:每个函数参数都给缺省值
语法:
void Func(int a = 10, int b = 20, int c = 30)
{
cout<< a <<endl;
cout<< b <<endl;
cout<< c <<endl;
}
要求:当函数传参时,传参的顺序是从左往右给,不可跳跃着给
即:
int main
{
Func(3, 5);//a为3,b为5,c为30
以下是错误示范:
Func( ,4, );
Func(2, , 8);
}
半缺省
定义:部分函数参数给值,部分不给
语法:
void Func(int a, int b = 20, int c = 30)
{
cout<< a <<endl;
cout<< b <<endl;
cout<< c <<endl;
}
要求:当函数参数给缺省值时,给缺省值的顺序是从右往左给,不可跳跃着给
即:
以下是错误示范
void Func(int a = 10, int b, int c)/没有从右往左给
{
cout<< a <<endl;
cout<< b <<endl;
cout<< c <<endl;
}void Func(int a = 10, int b, int c = 30)跳着给
{
cout<< a <<endl;
cout<< b <<endl;
cout<< c <<endl;
}
实际应用
假设我们要搞一个栈
当栈初始化的时候
需要开辟空间
C的方式
- 要么给一个默认开辟的空间大小(例如默认开四个空间)
- 要么每次创建栈的时候都要传值(说明要开辟多大的空间)
以上的两种方法均有缺陷;
方法1的缺陷
栈1要开辟10个空间
栈2要开辟100个空间
默认开辟空间为4时:
栈2在进行开辟空间的过程中会产生大量消耗(调用多次扩容函数)
默认开辟空间为100时:
栈1就会白白浪费了90个空间
方法2的缺陷
创建50个栈就需要手动传参50次
太麻烦了
C++用缺省参数的方式
使用上述C方式的方法2:每次创建栈的时候都要传值
不同的是:开辟的空间大小提前给缺省值
void Init(struct Stack* pst, int size = 4);
{
pst = (struct Stack*)malloc(sizeof(int) * size);
/……
}
一般情况下就用缺省值
如果缺省值无法满足我的需求,那就手动传值
int main()
{
struct Stack st1;
struct Stack st2;
Init(&st1);//开辟了4个空间的栈
Init(&st2, 100);//开辟了100个空间的栈
return 0;
}
有缺省参数的声明和定义的分离
结论:如果要做函数的声明和定义的分离,缺省参数要放在声明
(毕竟如果两边都给了不同的缺省参数,编译器就不知道用哪个缺省参数了)
原因
表层
预处理,编译,汇编,链接构建一个可执行程序的主要步骤
在预处理阶段,文件内的头文件将会被直接拷贝过来(头文件展开)
此时,被拷贝过来的函数只有函数的声明,并没有定义
在编译阶段,进行的是对语法的检查
其中一个语法检查,就是检查函数参数和个数是否匹配
例:
int Add(int a, int b = 3)
{
return a + b;
}
以上内容是Add.cpp
//
以下内容是test.cpp
int Add(int a, int b);从Add.h头文件拷贝过来的函数声明
int main()
{
int c = Add(3);
return 0;
}
这个项目一共有三个文件:Add.cpp 、 Add.h 、 test.cpp
在这个项目中,我将Add.h头文件包含到了test.cpp中
在预编译期间,Add.h头文件在test.cpp中展开了
在编译阶段,要对语法进行检查
在test.cpp中,调用了Add(3),但头文件拷贝过来的的Add(int a, int b)只有两个参数
所以,因为参数个数不匹配,所以就报错:
对声明和定义分离的深度剖析(与缺省参数无关)
此内容仅了解即可
为了更深度地学习声明和定义分离
我们需要了解以下的过程中究竟发生了什么
未进行声明和定义的分离:
C++代码
以上的步骤,在汇编指令的角度为以下步骤:
汇编代码:
在汇编指令中,有几个比较关键的部分(除了关键部分,可以暂时忽视)
分别是两个转折点:Call 和 Jmp
每当我们在进行调用函数时,实际上会被转化成Call 和 Jmp汇编指令
函数的调用本质上是去Call一个地址
当函数调用时,Call会根据某个地址(07211CCh),找到指定的Jmp指令,然后Jmp指令根据Add(07218D0h)实际的地址
跳转到Add函数的第一条指令位置(07218D0h),然后就把剩下的一系列的指令从内存中依次取出
最后发送给计算机的大脑(CPU)去依次执行,执行的这个过程本质上就是函数的实现
这样就可以通过调用Add(3 , 3)来完成Add的功能了
函数的地址在底层中实际就是这个函数的第一条指令的地址
(所以,如果函数没有定义,只有声明,是不会产生函数的地址的)
进行了声明和定义的分离:
此时就存在一个问题:
在未进行声明和定义分离时,我们可以很轻松的在test.cpp中找到Add的地址
但是进行了声明和定义分离后,尽管在预处理阶段已经把Add的声明拷贝到了test中,但是因为在test中找不到Add的定义(只有声明),也就代表着在test中没法找到Add的地址
毕竟Add的定义在Add.cpp中
所以在声明和定义分离的情况中,有一个很尴尬的问题
有的人不用,用的人没有(test.cpp需要地址却没有地址可用,Add.cpp不需要地址却不使用地址)
这样的话,当函数进行调用时(转化成Call某个地址),Call没有地址可用了,也就没法调用函数了
这样的话编译器就会报错了
为了解决这个问题
编译器让所有的源文件进行合作:
汇编阶段的生成符号表和链接阶段的符号表的合并和重定位可以有效的解决这个问题
(简化版↓↓↓)
预处理:
- 进行预处理指令的操作(宏替换、条件编译、删注释),
- 头文件展开
编译:
- 检查语法,语法不合格就报错(函数的参数类型和个数不匹配、找不找得到标识符)
- 生成汇编指令
汇编:
- 生成符号表(会把所有源文件里面的全局函数取出来,构成一个表)
链接:
- 符号表的合并和重定位(将所有源文件里面的符号表合并起来(名字相同的就合在一起),那些找不到地址的函数就可以在这里找到)
所以,尽管test中没有找到Add的地址,但是如果在链接阶段合并符号表时找到了地址,那test就可以继续调用Add函数了
因此,在test中,即使没有找到函数的定义,也不会报错
相当于test向编译器做了个承诺
但如果到了链接期间,仍然没有找到Add的地址(即Add没有做出来),就还是会报错的
形象的例子:
你和朋友一起组装一个玩具城堡,你负责设计和制作城堡的塔楼,而你的朋友负责设计和制作城堡的城墙。
你设计了一个部件,比如说是塔楼的顶部,但是具体的塔楼结构还没有被制作完成,因此你只提供了塔楼顶部的设计图和承诺(函数的声明)。
你的朋友需要在城墙的一部分上添加一个连接塔楼的门。尽管你的朋友只得到了你提供的设计图,但是他知道塔楼的顶部部件会在最终的城堡中存在,因此他可以在城墙上安装门,而不用等到你的塔楼完全制作完成。
即使你的朋友在城墙设计的过程中,并没有看到完整的塔楼,但是他知道这个部件最终会被添加进来,所以他可以继续进行设计和制作,而不会出现错误(尽管test中没有找到Add的地址,但有了函数的声明,所以也选择相信能找到Add的地址,不会报错误)。
但如果最终你并没有制作塔楼的顶部,这个城堡就是一个失败的城堡了(如果到了链接期间,仍然没有找到Add的地址,就还是会报错的)