函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了
函数重载的概念
函数重载: 是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(形参类型)(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
但是如果是下面这种情况是不能算作函数重载的:
返回值不同无法区分,所以返回值不是重载的条件
1.参数类型不同
#include<iostream>
using namespace std;
int Add(int left , int right)
{
cout<<"int Add(int left,int right)"<<endl;
}
int Add(float left , float right)
{
cout<<"int Add(float left,float right)"<<endl;
}
2.参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a=1)
{
cout << "f(int a)" << endl;
}
但是如果是下面这种情况,编译器将会打印什么呢?
int main()
{
f();
return 0;
}
而最后是无法完成运行,因为这里出现了调用歧义,不知道该调用谁,所以我们在写代码时,需要必要由缺省参数带来的调用歧义。
3.参数类型顺序不同
void f(int a, float b)
{
cout << "f(int a,float b)" << endl;
}
void f(float b, int a)
{
cout << "f(float ,int a)" << endl;
}
输入输出的结果:
C++支持函数重载的原理-名字修饰
上面已经介绍了函数重载,那么为什么C++支持函数重载,而C语言不支持呢?这里解释相对复杂,我将在以后单独写一篇博客来介绍它的原理,那么我们现在只需要记住它的结论就行。
C语言之所以没有函数重载是因为同名的函数无法区分,函数名字的修饰没有发生变化。
C++之所以存在函数重载是因为C++可以通过函数修饰规则来进行区分,只要参数不同,修饰出来的名字就不一样。这里也更好的解释了为什么返回值不同,却不算函数重载,而是会报重定义的错误。
引用
引用的概念
引用不是新定义一个变量,而是给已存在变量取一个别名,编译器不会为引用定义的变量开辟内存空间,它和它引用的变量共用同一块空间。就好像每一个都会有自己的小名和外号,但究其都还是指向这个人,而不是其他人。
表达形式为:
类型& 引用变量名(对象名)=引用实体
void test()
{
int a=0;
int& ra=a;//<===定义引用类
printf("%p",&a);
printf("%p",&ra);
//两者最终会打印出相同的地址
}
引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,就不可以再引用其他实体
void test()
{
int a=10;
int& ra;//该句语句编译时会报错
int& ra = a;
int& rra = a;
printf("%p %p %p",&a,&ra,&rra);
//三者最后会打印出相同的地址
}
常引用
常引用的格式为const 类型& 引用变量名(对象名)=引用实体,常引用的作用是不希望对所引用的内容进行修改,但是在常引用中会经常出现权限的问题,那我们通过下面的代码,对常引用权限问题进行一下讲解:
void test()
{
int x = 0;
int& y = x;//权限的平移
const int& z = x;//不会报错,属于权限的缩小
//z++;//由于z是常引用,所以不能对值进行修改
//y++;//可以进行
const int m = 0;
int& n = m;//会出现报错,属于权限的放大
//权限的放大:
//m是只读不可写
//n只是变成了我的别名
//n的权限是可读可写
const int& n1 = m;//权限的平移
int p = m;//不会报错,只是单纯将m拷贝给了p,p不会影响m
}
上面的代码对常引用权限问题解释的较为详尽,这里稍微进行一下总结:
- 常引用可以进行权限的平移和缩小,不可以进行权限的放大;
- 如果变量为常量,那么在使用引用的时候,务必使用常引用,不然编译时会报错,造成权限的放大
其实,指针也存在权限的问题,下面我们一起来分析一下:
void test()
{
const int* p1=&m;//p1可以改,*p1不可以改
int* p2 =p1;//会报错,存在了权限的放大,*p2可以改了
int* p3=&x;
const int*p4=&p3;//不会报错,是权限的缩小
}
使用场景
1.做参数
#include<isotream>
using namespace std;
void test(int b)
{
......
}
int main()
{
int a = 0;
int& b = a;
b++;
void test(b);
}
2.做返回值
观察下面的代码,输出的结果是什么?为什么?
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is : "<< ret << endl;
return 0;
}
传值,传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
综上所述,传值和指针在作为传参以及返回值类型上效率相差很大
引用和指针的区别
- 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块土地。
int main()
{
int a = 10;
int& ra = a;
cout << &a << endl;
cout << &ra << endl;
return 0;
}
- 在底层实现上实际是有空间的,因为引用是按照指针方式来实现的
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
下面观察下引用和指针的汇编代码对比:
引用和指针的不同点:
①引用概念上定义一个变量的别名,指针存储一个变量地址。但是在语法上,引用不开辟空间,指针要开辟空间;
②引用在定义时必须初始化,指针没有要求;
③引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
④没有NULL引用,但有NULL指针(相对而言,不容易出现);
int* p1=NULL;
int& a= *p1;//没有解引用的行为
⑤在sizeof中含义不同:,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
⑥引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
⑦有多级指针,但是没有多级引用;
⑧访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
⑨引用比指针使用起来更加安全,指针存在非法访问的问题。
内联函数
对于频繁调用小函数,在C语言中,常常采用宏函数来实现,但是宏函数的使用过程中存在许多需要注意的小细节,很容易出现错误,比如一个简单的加法函数,宏函数写出来需要写成:
#define Add(a,b) ((a)+(b))
所以在c++中,提出了内联函数的概念来处理频繁调用的小函数。
概念
以inline修饰的函数叫做内联函数。它的作用是编译时 C++编译器会在调用内联函数的地方展开。没有函数调用建立函数栈帧,内联函数提升程序运行的效率。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体来替代函数调用,也就是在调用的地方进行展开,没有函数栈帧的建立。
查看方式:
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add (有call,则没有展开)
- 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化)
设置完后,如果使用inline关键字,汇编的结果则是,没有call:
特性
-
inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体代替函数调用。
缺陷:可能会使目标文件变大;优势:少了调用开销,提高程序运行效率。
这里举出一个例子:有10000个位置调用100行指令,如果展开(使用inline)— 需要开辟10000*100的空间,如果不展开 — 10000+100(1万个call+100行函数体指令)。展开后,编译出来的可执行程序变大,也就是下载的内容变多的,也就是它的缺陷。 -
inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同。
一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现),不是递归,且频繁调用的函数采用inline关键字,否则编译器会忽略inline特性。下面是某本书关于inline的建议:
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
一般来说,内联机制用于优化规模较小,流程直接,频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不太可能在调用点内联地展开。 -
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
//F.h
#include<iostream>
using namespace std;
inline void f(int i);//内联函数没有地址,单纯展开
//F.cpp
#include"F.h"
void f(int i)
{
cout<<i<<endl;
}
//test.cpp
#include"F.h"
int main()
{
f(10);//call+地址
return 0;
}
//链接错误:test.obj : error LNK2019: 无法解析的外部符号 “void _cdecl f(int)” (?f@@YAXH@Z),该符号在函数 _main 中被引用 ——有声明没有找到定义
auto关键字(C++11)
类型别名思考
随着代码的数量增多,程序越来越复杂,程序中遇到的类型也越来越多,经常表现在:
1.类型难以拼写
2.含义不明确,导致容易出错
#include<iostream>
#include<string>
#include<map>
using namespace std;
int main()
{
std::map<std::string,std::string>m{{"apple","苹果"},{"orange","橙子"},{"pear","梨子"}},
std::map<std::string,std::string>::iterator it=m.begin();
while(it!=end())
{
//....
}
return 0;
}
其中std::map< std::string,std::string >::iterator是一个类型,但是该类型特别长,容易写错,且可读性不高。那么,根据之前的内容,很容易想到用typedef该类型自定义命名,比如:
#include<string>
#include<map>
typedef std::map<std::string,std::string> Map
int main()
{
Map m{{"apple","苹果"},{"orange","橙子"},{"pear","梨子"}};
Map::iterator it=m.begin();
while(it!=end())
{
//....
}
return 0;
}
使用typedef给类型取名确实可以简化代码,但是会带来新的问题:
typedef char* pstring
int main()
{
const pstring p1;
const pstring* p2;
return 0;
}
在编译时,通常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚知道表达式的类型,然而有时候要做到这一点是不容易的,所以在C++11中给auto赋予了新的含义
auto简介
在早期C和C++中auto的含义是:使用auto修饰的变量,是具有自动储存器的局部变量,但这个时候并没有什么人使用它,原因在于:
C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量是必须由编译器在编译时期推导而得
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e;无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
【注意】
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。 因此auto并非是一种“类型”的声明而是一种类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto的使用细则
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto* 没有任何区别,但是auto声明引用类型时则必须加&
int main()
{
int x=10;
auto a=&x;
auto* b=&x;
auto& c=x;
*a=20;
*b=30;
c=40;
return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a=1,b=3;
auto c=3,d=4.0;//该行代码会编译错误,因为c和d的初始化表达式类型不同
}
auto不能推导的场景
- auto不能作为函数的参数
//此处代码编译失败,auto不能作为形参类型,因为
void Testauto(auto a)
{ }
-
auto不能用来声明数组
-
为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
-
auto在实际中最常见优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lanbda表达式等进行配合使用。
基于范围的for循环(C++11)
范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{
int array[]={1,2,3,4,5};
for(int i=0;i<sizeof(array)/sizeof(array[0]);++i)
array[i]*=2;
for(int*p=array;p<array+sizeof(array)/sizeof(array[0]);++p)
cout<<*p<<endl;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此c++11中引入了基于范围的for循环。for循环后的括号由冒号" : "分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围
从前往后遍历(从头到尾)
void TestFor()
{
int array[]={1,2,3,4,5};
for(auto& e : array)//修改array里面的值得用auto&(引用)
e*=2;
for(auto e:array)
cout<<e<<" ";//自动取数组array的值,赋值给e;自动++,自动判断结束
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。 - 迭代的对象要实现++和==的操作
指针空值nullptr(C++11)
** NULL可能被定义为字母常量0,或者被定义为无类型指针的常量**
[注意]
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。