一、函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这
些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型
不同的问题。
简单来说:C语言中是不允许同名函数出现的,而在C++中,如果这些函数的参数不同(包括参数个数、参数的顺序、参数的类型),就允许同名函数的出现。在编译过程中,编译器就通过参数进行自动识别,并且调用你想使用的函数。
1、参数类型不同
int Add(int left, int right)
double Add(double left, double right)
// 2、参数个数不同
void f()
void f(int a)
// 3、参数类型顺序不同
void f(int a, char b)
void f(char b, int a)
参数不同的意义是让编译器能够通过参数准确的识别出要调用的函数,因此我们不能使其出现歧义。
void f(int a,int b)
void f(int left, int right)
这个函数,看似参数不同,但实际上都是int类型。当我们调用它们,如f(1,1),编译器是识别不出来我们具体想要调用哪个的,即会出现歧义,导致错误。
void f()
void f(int a=10)
这个函数比较特殊,因为参数不同,所以构成函数重载。但是由于缺省参数的存在,当我们调用f()时,会产生歧义,无法区分。
函数名修饰规则:
名字修饰本质上来说就是编译器识别不同参数的手段。
在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
这样一来,虽然我们写代码时函数名字相同,但是在编译器看来,实际上是几个不同的函数,对应着不同的地址。
注意:参数相同,返回值不同时不构成重载。因为我们调用的时候,一定会传入参数,而不会传入返回值,编译器就无法区分了。因此编译器的名字修饰,采用的都是对参数的判断。
编译的时候,test.s中,只有头文件,即函数的声明,和调用函数的代码。而函数的定义是在Stack.s中的,要在链接阶段才能找到地址。
编译进行符号汇总(全局符号、函数),汇编生成符号表,在链接时,通过符号表去找地址,完成符号表的重定位。
反汇编中,两个函数的地址不同。
g++编译器,利用参数,将函数名进行区分。
执行程序时,call调用就能清楚区分不同函数了。
二、引用
1、引用的概念:
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。
2、引用的特点:
1. 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
int a=10;
int& b=a;//引用初始化
int& c =b//多个引用
//此时b c 均为a的别名,改变一个其它2个都改变
int x=100;
b=x;//此时为赋值
3、引用作参数(类似于指针)
引用做参数 与指针的某些功能类似。
1、输出型参数
可以通过形参直接改变实参。
如LeetCode中的returnsize,指针时可以利用解引用操作符,*returnsize=x。
当returnsize参数为引用时,直接通过returnsize=x,可以直接修改外面的returnsize
void Swap(int& left,int& right)
{
int tmp = left;
left = right;
right = tmp;
}
Swap函数交换值时,用引用作为参数,可以直接修改外面的left和right。
2、提高效率
大对象/深浅拷贝
我们知道,在外面传入参数后,在函数内部往往需要拷贝一份,作为临时变量/形参。
当拷贝的对象内存较大或者是深浅拷贝时,拷贝的操作就有一定的消耗了。
使用指针时,我们可以传入地址,通过解引用来访问,只需要拷贝一个指针即可,消耗很小。
使用引用时,我们直接用相同类型的引用作为形参,直接用别名来访问,消耗很小,提高了效率。
总结:引用作为参数时,没有使用的限制条件,可以用来作为输出型参数,还可以有效提高效率,理解上与指针和取地址、解引用操作类似。
4、引用作返回值
1、提高效率
int Count1()
{
static int n = 0;
n++;
return n;
}
int Count3()
{
int n = 0;
n++;
return n;
}
对于这两个函数,无论变量是否用static修饰(本质上是存储在栈区还是静态区),返回值都是int类型的。因为使用函数时会涉及到函数栈帧的创建与销毁,对于除了引用外的其它类型(C类型)编译器会统一创建一个临时变量用来存储返回值(不区分是否是static修饰),小对象可能是寄存器register,大对象就是创建变量了。
创建临时变量的过程额外需要拷贝一次,从而降低了效率。
使用指针时,返回地址,通过解引用找到函数中创建的那个变量,然后进行后续操作。
还可以用引用作为返回值,引用是别名,可以直接通过别名找到函数中创建的变量,不用额外拷贝一次,提高了效率。
2、越界风险
int& Count2()
{
static int n = 0;
n++;
return n;
}
int& Count4()
{
int n = 0;
n++;
return n;
}
int& Count5(int x)
{
int n = x;
n++;
return n;
}
对于Count2函数,创建的n是被static修饰的,在静态区创建的,无论Count2的函数栈帧是否销毁或清理,它都是存在的,也可以正常访问。
先用一个ret1,简单接收一下Count2返回值,也就是一个简单的赋值操作,将n的别名赋值给它。
再调用一次Count2函数,静态区的n的值变为2,返回别名,再创建一个别名ret2来接收,此时ret2也就是n的别名,其值也为2。
因为保存在静态区,无论栈帧是否清理,均不变,且安全不越界。
对于Count4函数
Count4中的n是创建在栈区的,是局部的,我们用一个ret2作为别名,可以对其访问。
第一次打印ret2,其栈帧还未被清理,因此还为1.
调用test函数清理栈帧后,再次调用ret2,就变成随机值了。
并且由于Count4栈帧已经销毁,此时我们对ret2的访问实际上已经越界了,只是编译器没有报错。
对于Count5函数
第一次调用ret的值为11,然后栈帧销毁。第二次调用,由于函数相同,栈帧也相同,参数变为20,ret作为别名的位置相同,最终结果为21。
总结:引用返回局部对象,结果不确定,会越界访问(与返回栈空间地址类似)。在堆区、静态区使用引用作返回值是安全的。是否出现随机值,与编译器有关,是否会在销毁栈帧后立马清理栈帧。(与指针返回地址类似,可能返回的地址的权限已经还给操作系统了)
3、简化读写操作
对于C语言的顺序表,修改和得到值需要不同的函数。使用引用操作,可以得到要操作那个位置数据的别名,然后可以对这个别名进行 读取/打印 写入/修改 操作
5、常引用
1、变量之间
权限可以缩小或平移,但是不能放大
const int a = 10;//int a=10;
int& ra1 = a; // 该语句编译时会出错,a为常量,权限放大了,ra可以修改a
const int& ra2 = a;//权限的平移,若a仅为int,则为缩小
引用过程可以看作本体和分身的关系。本体的权限一定是>=分身的。
若本体a没有const修饰,它可以随意修改,若有一个引用的分身ra1,也没有const修饰,此时它们权限相同。若另一个引用的分身ra2被const修饰了,那么它不能修改,即不能进行类似ra2++的操作。这个const,缩小的是ra2作为分身/别名的权限,而不是本体/本身a的权限。
2、对常量引用
常量不能被修改,别名必须用const来修饰。
3、涉及类型转换的引用
普通的类型转换
这个临时变量具有常属性
当将i给别名ri初始化时,没有类型转换,不会报错。
当用d给rri初始化时,先进行类型转换,生成一个值为6,具有常属性的临时变量,临时变量都可看作右值,包括之前的Count1、Count3函数,此时若想赋值,rri前必须要加上const修饰。
总结:引用的知识从以下几点考虑。
引用作为参数,引用作为返回值,返回值的对象的栈帧是否被销毁/清理,是否存在。
是否用引用来接收,接收的值是否具有常属性,是否需要加const来修饰。
引用与指针的相似之处,引用可能导致的越界问题,引用对于数据读写在代码层面上的优化。
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全