目录
一、函数重载
1.函数重载概念
函数重载:是函数的一种特殊情况,C++允许在 同一作用域中 声明几个 功能类似的同名函数 ,这些同名函数的 形参列表(参数个数 或 类型 或 类型顺序)不同 ,常用来处理实现功能类似数据类型不同的问题。
- 参数个数不同:
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
- 参数类型不同:
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
- 类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
- 注:单单改变函数的返回类型是不能构成重载的。
2.函数重载的原理
- 实际项目通常是由多个头文件和多个源文件构成。首先,我们假设有a.cpp、b.cpp两个文件,在b.cpp中定义了Add函数,a.cpp调用了b.cpp中定义的Add函数。 通过C语言编译链接的知识,我们可以知道,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。
- 链接阶段就会专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
- 那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。
- 由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使用了g++演示了这个修饰后的名字。
- 通过下面我们可以看出,C语言用gcc编译后的函数修饰名字不变。而C++用g++编译后的函数修饰名变成【_Z+函数长度+函数名+类型首字母】。
gcc编译结果:
- 结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。
g++编译结果:
- 结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
通过上文我们就理解了C语言为什么没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
同时也解释了为什么单单的改变函数返回类型不能构成重载,在调用函数时我们都是直接用函数名调用,并不会写明返回类型,所以返回值并不会作为函数信息加入到修饰名字中。
二、引用
1.引用概念
概念:引用不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量 共用同一块内存空间 。格式:类型& 引用变量名(对象名)=引用实体,如下代码。
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
这时变量ra就是变量a的别名,在内存中a和ra的地址相同,对ra修改,就等同于对a修改。
2、引用特性
引用具有三大特性:
引用在 定义时必须初始化 一个变量 可以有多个引用 引用一旦引用一个实体,再不能引用其他实体下面对他们进行解释。
引用在定义时必须初始化
可见,如果如果引用时没有初始化,还没有编译就会报错。
一个变量可以有多个引用
int main()
{
int a = 10;
int& ra = a;
int& rra = ra;
int& rrra = rra;
}
在VS调试后发现,a,ra,rra,rrra都是a的别名,都是a的引用,可见一个变量可以有多个引用。
3.常引用
在给变量取别名时,不是可以随便取的,要注意变量的权限。
引用变量的权限只能等于或小于原变量的权限范围。(权限只能缩小不能放大)
用const修饰一个变量,变量就会变成一个常变量,他的权限就从能读写变成了只能读,权限被缩小了。
权限放大会报错
如图,由于a加了const,它的权限降低为只能读,此时让b一个具有读写两种权限的变量成为a的应用显然是不合理的,编译器会发生报错。
权限统一
为了保持权限的统一,我们可以在b之前也假设const来降低b的权限,实现两者的权限相同,编译器就不会再报错。
int main()
{
const int a = 10;
//权限相同
const int& b = a;
}
权限缩小
把能读写的a,取一个只能读的别名b同样也合理。
int main()
{
int a = 10;
//权限缩小
const int& b = a;
}
拓展1:常量取引用
我们可以给常量取别名,但是要加上const。如下。
const int& c = 20;
拓展2:临时变量具有常性
如下代码很明显存在问题,会发生报错:
int main()
{
double a = 10.28;
int& b = a;//error
}
报错如下:
但是在b前面加上const就可以解决问题。对比如下图:
这里,我们就要结合C语言中学的隐式类型转换来理解了,大给小会发生截断,小给大会发生提升。在double转换为int的过程中,就发生了截断,但是这需要借助一个临时变量来实现。double类型的a会把自生的值给临时变量tmp,同时发生截断,然后编辑器会把临时变量tmp的值赋给int类型的b,最终完成赋值。
由于临时变量tmp为常量,只具有读的权限,赋值给有读写权限的int自然就会发生报错。只要在前面加上const让他们权限统一,就不会报错。
但是我们需要注意的是,此时的b不再是a的别名,而是临时变量tmp的别名。由下图可见,两者的地址并不相同,不是同一个变量。
4. 引用的使用场景
1).做参数
引用做参数就是我们经常提到的函数传指针,这就是引用做参数的典型例子。
//函数传指针
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//函数传引用
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
显而易见的是传引用的方法会更加的方便和简洁,不需要再通过指针进行繁琐的操作。
此外,引用还可以用作输出形参数。同样使代码更加的简洁。
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
//……
}
int main()
{
preorderTraversal(tree, &size);
}
int* preorderTraversal(struct TreeNode* root, int& returnSize) {
//……
}
int main()
{
preorderTraversal(tree, size);
}
总结:引用做参数的好处
- 作输出形参数,优化代码
- 减少指针的使用,简化代码
2).做返回值
根据函数栈帧的知识我们可知,当一个函数执行结束时,栈帧的使用权限会被归还给操作系统,为了防止返回值丢失,这时就需要创建一个临时变量来存储要返回的结果。如果返回类型如果较小,就会存储在寄存器中,如果较大,就会开辟新的空间来存储,这会导致程序效率降低。
证明存在中间的临时变量:
可见图中的发生了和上文提到的同样的报错,其原因就是临时变量具有常性,只能读。有读写两种权限的ret做临时变量的引用存在权限放大的问题。 (如下代码能解决这个问题)
//引用作为返回值
int& fun()
{
static int n = 0;
n++;
return n;
}
int main()
{
//用引用ret来接收
int& ret = fun();
return 0;
}
这时加上引用之后,依然会产生一个临时变量,n的值依然会赋值给这个临时变量,只是这个临时变量的类型时int*,mian函数中再通过ret来接收临时变量,所以ret和临时变量其实就是n的别名,不需要额外开空间,实现了效率的提升。
总结:
- 传值返回:会有一个拷贝
- 传引用返回:没有这个拷贝了,返回的直接就是返回变量的别名
但是有一点需要加以思考,如果把代码中的static去掉,引用作为返回值还是否正确?
答案是否定的!!!
由于传引用的返回值是函数中那个变量的别名,如果函数中的变量不像static变量一样能在函数被销毁之后保留变量的值,那么传引用就会造成非法地址的访问。
总结:
- 只有返回值是在函数销毁后依然存在的静态量时才能采用传引用。
- 一般的变量只能采用传值返回
5.引用和指针的区别
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全