1. 函数重载
1.1 概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
//参数的类型不同
int Add(int a, int b)
{
return a + b;
}
int Add(double a, double b)
{
return a + b;
}
//参数的个数不同
int Add(int a, int b)
{
return a + b;
}
int Add(int a, int b, int c)
{
return a + b + c;
}
//参数的顺序不同
int Add(int a, char b)
{
return a + b;
}
int Add(char b, int a)
{
return a + b;
}
注:
- 函数的返回参数不同构成不了重载函数
- 传参的时候注意类型,发生数据转换的时候可能会出错
int Add(int a, double b)
{
return a + b;
}
int Add(double a, int b)
{
return a + b;
}
int main()
{
Add(20, 20);
return 0;
}
注意看这里错误的原因不是参数类型传错了,是编译器判断不了用哪个函数。
1.2 原理
C++支持重载函数的原理是名字修饰(name Mangling),要理解该原理需要知道C语言讲的一个程序运行起来需要步骤。
一个程序需要运行起来需要经过:预处理、编译、汇编、链接。
一个实际的项目中通常包含多个的头文件和源文件,由我们前面的知识知道,当a.cpp调用b.cpp中定义的函数的时候,汇编形成的a.o文件是没有该函数的地址的,该函数在b.o中,所以后面的链接步骤是用来解决该问题的,链接器看到a.o调用函数,但是没有函数的地址,就会到b.o的符号表中找函数的地址,然后链接到一起,那么链接是根据哪个名字去找到该函数的呢?每个编译器都有自己不同的函数名修饰规则。
这里介绍一下Linux中g++编译器的修饰名规则,在采用C语言编译器的时候,对函数名没有修饰,所以C不支持重载函数,而在采用C++语言编译器的时候,函数名修饰之后变成【_Z+函数长度+函数名+类型首字母】
所以C++可以支持重载,因为参数的不同修饰出来的函数名就不同。
这里在给大家看看Windos下函数修饰名规则
2. 引用
2.1 概念
引用不是定义一个新的变量,而是给一个已知的变量取一个别名,和那个变量使用同一块空间,编译器不会新分配空间。就好像我们每个人都有外号,无论叫本名还是叫你外号都是叫你这一个人。
语法格式是:变量类型+&+取的别名 = 引用的实体
int main()
{
int a = 10;
int& b = a;
cout << &a << endl << &b << endl;
return 0;
}
2.2 特性
- 引用在定义时必须初始化
int main()
{
int& a;
return 0;
}
- 一个实体可以有很多的引用(就像一个人可以有很多的外号),引用也可以有引用
int main()
{
int a = 10;
int& b = a;
int& c = a;
int& d = c;
cout << &a << endl << &b << endl << &c << endl << &d << endl;
return 0;
}
- 但是一个引用只能有一个实体
int main()
{
int a = 10;
int b = 20;
int& c = a;
//这里是赋值,不能理解成变成b的引用
c = b;
cout << &a << endl << &b << endl << &c << endl;
return 0;
}
2.3常引用
int main()
{
const int a = 10;
int& b = a;
return 0;
}
这种用法是错误的,因为权限放大了,a被const修饰不能改变,但是b确可以改变,这就冲突了,所以修改的方法就是把b的权限给缩小。
int main()
{
const int a = 10;
const int& b = a;
return 0;
}
注:权限缩小是可以的
int main()
{
int a = 10;
const int& b = a;
return 0;
}
当我们引用的类型和引用对象类型不同时,编译器报错也是因为这个原因
int main()
{
double a = 10;
int& b = a;
return 0;
}
可能很多人不理解这跟上面权限有什么关系,这里涉及到两个知识点,(1)在值传递、类型转换使会生成临时变量 (2)临时变量具有常性。
所以我们可以解释上面的代码了,a转换成int型使生成的临时变量,而临时变量具有常性,但是b确没有,所以这里权限放大了,发生了错误,我只需限制b就行。
int main()
{
double a = 10;
const int& b = a;
return 0;
}
2.4 应用场景
- 做参数
void Swap(int& a, int& b)
{
int t = a;
a = b;
b = t;
}
int main()
{
int a = 3;
int b = 4;
cout << "a = " << a << endl << "b = " << b << endl;
Swap(a, b);
cout << "交换后" << endl;
cout << "a = " << a << endl << "b = " << b << endl;
return 0;
}
- 做返回值
int& fun()
{
static int a = 1;
a++;
return a;
}
int main()
{
cout << fun() << endl;
}
在你使用引用做返回值的时候,你要清楚,在出函数作用域的时候,你引用的对象有没有被系统回收,如果回收了就必须使用值返回了。
/*这种做法就是错误的,在调用完fun函数后变量a会被系统回收,所以a中的值是随机值,
可能有的人运行该段代码得到正确结果,这是因为你所使用的编译器没有清除a中的值*/
int& fun()
{
int a = 1;
a++;
return a;
}
int main()
{
cout << fun() << endl;
}
2.5 传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
struct A {
int a[10000];
};
void TestFunc1(A a){
}
void TestFunc2(A& a) {
}
int main()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A) - time:" << end1 - begin1 <<endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 <<endl;
return 0;
}
2.6 引用和指针的区别
注:引用在语法上是没有独立空间的,和引用对象使用同一块空间,但是在底层实现上实际是有空间的,是按照指针的方式来实现的。
区别:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全