缺省函数
缺省函数又分为全缺省函数和半缺省函数,这里的半缺省不是指缺一半的参数,而是部分缺省。
缺省函数可以理解为:在形参内,存在赋值的参数称为缺省,可以理解为这个参数是备胎。在实参调用时,假如这个有实参那么就以实参值为准,若没有实参,则是形参的赋值。
全缺省函数
在调用函数的时候,实参可以为空。也就是说,在定义这个函数的时候,这个函数的形参自己已经赋值了。
半缺省函数
缺少部分参数 (必须从右往左连续缺省),而函数调用时的实参,必须从左往右传参。
因为假如形参存在缺省,那么那个缺省的形参可以不传值,如果没有缺省,那么实参必须传值。
假如形参的缺省不符号从右往左连续缺省,那么实参的传值就无法判断是传给哪个形参。
函数重载
不同意C语言,C++可以定义多个函数名相同的函数。但是他们的参数有三不同,顺序不同or类型不同or数量不同。(只是返回值不同,不能构成函数重载)
程序运行的流程
概念很简单,有一个面试题:为什么函数重载在C语言不可以,但是C++却可以?
一个程序在编译到链接有四个过程:
1、预处理 -> 头文件的展开/宏替换/条件编译/去掉注释
2、编译 -> 检查语法,生成汇编代码
3、汇编 -> 汇编代码转成二进制的机器码
4、链接 -> 将两个目标文件链接到一起
一般来说一个项目会有三个文件:一个包含头文件,一个是函数的实现,一个是主函数的逻辑。
不妨令他们分别为list.h ;list.c ;test.c。(list.h中包含了头文件,函数的声明)list.c 和test.c 只需要包含list.h即可。
在预处理的过程中,会把list.c 和 test.c 变为 list.i 和 test.i。
编译过程会把 list.i 和 test.i 转化为list.s 和 test.s
汇编过程会把list.s 和 test.s 转化为 list.o 和 test.o
最后把这两个文件链接到一起。
test.c -> test.o的过程,首先会把头文件展开,这个主函数里面有调用函数的指令,但是在编译的时候并不会执行,之后检查是否有语法错误,在预处理的时候,就已经把头文件展开了,也就相当于这个主函数前面有函数的声明。也就不会报错。再到汇编,转化为二进制的机器码,这个时候调用这个函数也是不需要访问地址的,也没有拿到这个函数的地址,因为这个函数的定义的地方在list.c中,两个文件还没有链接,所以找不到。
两个文件变为汇编之后,会有一个符号表,这个符号表内会有两个文件中的函数名以及这个函数的地址(函数的定义是有多行代码的,这个地址指的是这个函数定义的代码中的第一行代码的地址)。test.o中有:mian:0x32321412 list.o中有:add:0x35637182(假如有一个add函数)
然后通过链接,把两个文件链接到一起,生成一个可执行程序。然后将在编译过程中,把没有访问函数地址的地方,去其他文件中寻找这个函数的地址。如果没找到就报错(链接错误)
而为什么C语言不支持重载,C++支持重载的原因就在这。c++有一个函数名修饰规则。
C语言(在Linux中gcc之后)函数名并没有变,而c++(g++之后)函数名变了,变为【_z+函数长度+函数名+类型首字母】。比如一个函数的声明是:int add(int a, int b)。g++之后变成_z3addii。假如参数是引用,比如int& a。这个类型的首字母和int的一样,为i。假如是int* a。那么就为pi。
注意这个名字修饰规则是在linux下的,在windows下比较复杂,这里不与讲述。
假如是C语言,它汇编之后,函数名没有被修饰,那么在调用的时候就不知道该调用哪个了。而c++引用了这个名字修饰规则,不同的参数 ,不同的顺序,就会有不同的函数名,所以可以找到自己该访问的函数地址。
那么还有一个问题:既然C++和C的函数名修饰不同,那么我如何写一个函数,既可以让C++使用,也可以让C使用呢?在函数声明前加上extern "C"
一个例子 Google写了一个函数,tcmalloc()。这函数C和C++都可以使用。这个函数的声明是这样的:extern "C" void* tcmalloc(size_t n)。假如是没有extern "C"的化,在汇编之后的符号表会是这样的:0x地址 :_z8tcmallocui。C++能找到这个地址,C语言是按照tcmalloc去找,显然找不到。那么加上extern "C"之后,这个符号表就变成了:tcmalloc。C就能找到,而C++看见这个函数声明前有extern "C",那它也按照C的方式去找。当然,tcmalloc不可以重载。
引用
定义
引用就是一个变量的别名,不开辟空间,也指向初始化的空间。
常引用
引用需要符合权限的大小。const的一个变量表示只读,这个时候该变量的引用需要使用常引用
const int a = 1;
const int& ra = a;
引用不可扩大变量的权限,且必须初始化。
隐式转换
int a = 1;
double d = a;
这样的是可以的,因为这个赋值包含一个隐式转换,就是创建一个临时变量double,临时变量具有常性,而且d的修改不会影响a。
int a = 1;
double& rd = a;
这样是不行的, 因为这的rd是一个引用,而临时变量具有常性,所以需要使用
const double& rd = a。 那么这样就不可以修改rd的值了。
引用和指针的区别
1、在概念上,引用定义一个变量的别名,指针存储一个变量的地址
2、引用在定义时必须初始化,指针没有要求
3、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4、没有NULL引用,但有NULL指针
5、在sizeof中含义不同:引用的结果为引用类型的大小,而指针始终是地址空间所占字节个数(32位平台下占4个字节)
6、引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7、有多级指针,但没有多级引用
8、访问实体的方式不同,指针需要显式解引用,引用编译器自己处理
9、引用比指针使用起来相对安全
引用做参数和返回值
假如是一般的参数传值和返回传值,都会产生一个拷贝变量,这样会影响效率。那么就诞生了引用
引用作为参数和返回值最大的一个作用就是提高效率。
int add1(int a, int b)
{
int c = a + b;
return c; // const int& ret = add1(a,b)
}
int& add2(int& a, int& b)
{
int c = a + b;
return c;
}
int& add3(int& a, int& b)
{
static int c = a + b; // 只运行一次
return c;
}
对于add1来说,在传递参数时,会在函数的栈帧创建两个空间a,b. 在最后的返回的时候也会创建一个临时变量,该临时变量具有常性。所以假如在使用引用接收这个返回值的时候需要使用常引用。
对于add2来说,传引用作为参数,不需要开辟空间,但是返回的c,是在栈中创建了空间。返回引用也不需要创建临时变量,用一个引用接收返回值,但是但函数结束的时候,会对该函数的栈帧进行销毁,那么这个c变量的空间就被销毁了,假如再调用另一个函数的时候,该空间会被覆盖。具有风险。
对于add3来说,在add2的基础上给变量c定义为全局变量,扩大生命周期,变量c存储在静态区,那么在函数的栈帧销毁时,对它不产生影响。(注意:static int c = a + b, 这行代码是全局变量的定义,在整个程序的运行过程中,该行只运行一次)
总结:当一个函数要使用引用返回时,需要判断这个返回变量出了这个函数的作用域是否还存在,如果还存在就可以使用引用返回,否则就不安全。
函数使用引用的好处:少创建拷贝一个临时对象,提高效率。