一.函数重载
在自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重
载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个
是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
(1)函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这
些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型
不同的问题。
1.参数类型不同
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; } int main() { Add(10, 20); Add(10.1, 20.2); return 0; }
2.参数个数不同
void f() { cout << "f()" << endl; } void f(int a) { cout << "f(int a)" << endl; }
3.参数类型顺序不同
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)C++函数重载的原理
我们知道C语言不支持函数重载,为什么C语言不支持函数重载呢,当有两个函数名相同的函数时,C语言就已经无法区分了,是因为C语言仅仅就是通过函数名来区分每个函数。但是在C++里面,C++通过对函数名配合参数进行修饰,就可以通过函数名以及函数参数特点,对每个函数进行区分。
通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修
饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
如果两个函数函数名和参数都是一样的,仅仅是返回值不同是不构成重载的,因为调用时编译器没办法区分。
二.引用
(1)引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
类型& 引用变量名(对象名) = 引用实体;
看一段代码:
#include<iostream> using namespace std; int main() { int a = 10; int& ra = a; std::cout << "a=" << a << endl; std::cout << "ra=" << ra << endl; ra = 20; std::cout << "a=" << a << endl; std::cout << "ra=" << ra << endl; }
引用和对象使用同一块空间:
int main() { int a = 10; int& ra = a;//<====定义引用类型 printf("%p\n", &a); printf("%p\n", &ra); }
大家还记得这段代码吗:
void ListPushBank(ListNode**phead,int x) { ListNode* newhead = (ListNode*)malloc(sizeof(ListNode)); newhead->val = x; newhead->next = NULL; if (*phead == NULL) { *phead = newhead; } else { ListNode* Tail = *phead; while (Tail->next) { Tail = Tail->next; } Tail->next = newhead; } } int main() { ListNode* head=NULL; ListPushBank(&head,100); ListPushBank(&head, 200); return 0; }
在PushBank时,如果我们遇到第一个结点时,就需要把第一个结点作为头结点,此时需要更改函数外面的 head 指针,就必须要 head 的地址,在函数里面就得使用二级指针。
现在我们有了引用就可以很方便的解决这个问题:
void ListPushBank(ListNode*& phead, int x) { ListNode* newhead = (ListNode*)malloc(sizeof(ListNode)); newhead->val = x; newhead->next = NULL; if (phead == NULL) { phead = newhead; } else { ListNode* Tail = phead; while (Tail->next) { Tail = Tail->next; } Tail->next = newhead; } } int main() { ListNode* head = NULL; ListPushBank(head, 100); ListPushBank(head, 200); return 0; }
(2)引用特征
引用有一些很重要的特征需要我们注意:
1.引用在定义时必须初始化
这里不难可能出语法直接时报错的。
2.一个变量可以有多个引用
nt main() { int a = 10; int& ra = a; int& rb = a; int& rc = a; printf("ra=%d,rb=%d,rc=%d\n",ra,rb,rc); return 0; }
一个变量可以有多个引用,并且多个引用都共用一个地址空间。
3.引用一旦引用一个实体,再不能引用其他实体
int main() { int a = 10; int b = 30; int& ra = a; ra = b;//仅仅只是把b的值赋值给了ra std::cout << &a << endl; std::cout << &ra << endl; std::cout << &b << endl; std::cout << "a=" << a << " b=" << b << endl; return 0; }
a与ra 仍然公用一块空间,ra = b,并没有改变ra的引用实体,实际上也改变不了ra的引用实体。
(3)常引用
变量权限可缩小或平移,但不可放大
场景一:
因为常量只具有可读属性,如果引用成功的话,就可以通过引用来实现对变量的修改,变量权限就是放大了,这样也是说不通的。
但是也不是没有办法使得引用的实体是常量。
变量是只可读的,变量的引用对象也只能是可读的,这也就是变量权限平移.
上述的结论,不仅针对引用,对指针也是有着同样的效果:
场景二:
- 1.这里的报错,大家是不是认为是类型的原因导致的,但是往后看,加上一个const就没有报错了,而且仍然是int类型的引用。
- 2.首先来了解类型转的原理:类型转换是有一个中间变量的,而引用的对象的就是那个临时变量,因为临时变量具有常属性,所以加上一个 const 就可以消除报错。
(4) 使用场景
1.做参数
我们之前使用指针,实现函数来交换两个变量的值:
void Swap(int* pleft, int* pright) { int tmp = *pleft; *pleft = *pright; *pright = tmp; }
原理我就不多解释了,想必大家都已经熟记于心了,今天我们用引用也可以实现:
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } int main() { int a = 10; int b = 20; std::cout << "交换前a="<< a << ",b=" << b << endl; Swap(a, b); std::cout << "交换后a=" << a << ",b=" << b << endl; return 0; }
2.作为返回值
int& add(int a, int b) { int sum = a + b; return sum; } int main() { int& ret = add(1, 2); std::cout << ret << endl; add(3., 4); std::cout << ret << endl; return 0; }
问题来了,我们只对ret进行了一次赋值,但是两次输出却是两个值。这又是为什么呢?
怎样可以返回引用呢?其实只要出了函数作用域,函数的返回值还在,就可以返回引用。
例如:static 修饰的变量存储在静态区,不会因为函数栈帧的销毁而销毁。
int& add(int a, int b) { static int sum = a + b; return sum; } int main() { int& ret = add(1, 2); std::cout << ret << endl; add(3., 4); std::cout << ret << endl; return 0; }
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
(5)传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直
接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 1000000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 1000000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; } int main() { TestRefAndValue(); return 0; }
struct A { int a[10000]; }; A a; // 值返回 A TestFunc1() { return a; } // 引用返回 A& TestFunc2() { return a; } void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 1000000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 1000000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; } int main() { TestReturnByRefOrValue(); return 0; }
通过上述代码的比较,发现传值和传引用在作为传参以及返回值类型上效率相差很大。
(6)引用和指针的区别
- 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
- 在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2.引用在定义时必须初始化,指针没有要求。
3.引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
4.没有NULL引用,但有NULL指针。
5.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)。
6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
7.有多级指针,但是没有多级引用。
8.访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
9.引用比指针使用起来相对更安全。