目录
1、函数重载
学过C语言的,都知道C语言当中,同名的函数只能定义一个,一但定义两个名字相同的函数都会报重定义的错误。但是此时,有这么一种需求,不同类型的参数都要进行同一份动作时,C语言的难受地方就出来了。如:
int add_int(int x, int y)
{
return x + y;
}
double add_double(double x, double y)
{
return x + y;
}
int main()
{
int a = 1, b = 4;
add_int(a, b);// 两个整数可以调用add_int函数相加
double x = 6.6, y = 9.9;
add_int(x, y);// 两个浮点数虽然也可以,但最后的结果不如愿啊,因为它主要是计算整型数据的啊,计算其他类型的数据的话,那该怎么办?
// 只能重新定义一个与add_int函数不冲突的函数了
add_double(x, y);
//明明都是两个数相加,可是就是不能使用一个名为 add 的函数计算完所有类型的数据,这就很难受了
//C++的函数重载就很好的为我们解决这个烦恼
return 0;
}
如果 int类型、long long类型、float类型、double类型、char类型等等类型都需要相加时,那对各个类型函数起名又是一件烧脑的事情了,即使忽我们略掉这些起名复杂的问题,但是作为一个程序员,每天都在面临着巨大的代码量,调用函数起来难免不会发生调用错误,发生一些不可避免的问题。这个时候C++就诞生出了函数重载。
函数重载是函数的一种特殊情况,C++允许在同一作用域中定义多个功能类似的同名函数,这些函数之间的区别就是形参列表的参数个数、参数类型或参数顺序不同(本质就是要类型不同)。我们来看C++是怎么应对上面那种情况的:
#include<iostream>
using std::cout;//使用 [using 命名空间名::成员名] 将命名空间中的某一成员释放,上一遍讲了
using std::endl;
int add(int x, int y)
{
return x + y;
}
double add(double x, double y)
{
return x + y;
}
int main()
{
int a = 1, b = 4;
double x = 6.6, y = 9.9;
cout << add(a, b) << endl;
cout << add(x, y) << endl;
return 0;
}
这样一看,是不是比第一种情况好了?
下面介绍如何设计函数实现函数重载:
1、保证参数的类型不同
// 参数的类型不同,可以构成重载
void function(int x, int y)
{
cout << "function(int,int)" << endl;
}
void function(char x, char y)
{
cout << "function(char,char)" << endl;
}
void function(int x, char y)
{
cout << "function(int,char)" << endl;
}
2、保证参数的顺序不同
// 保证参数的个数不同,可以构成重载
void function(int x, int y)
{
cout << "function(int,int)" << endl;
}
void function(int x)
{
cout << "function(int)" << endl;
}
void function()
{
cout << "function()" << endl;
}
3、保证参数的顺序不同
// 参数的顺序不同,可以构成重载
// 不过一定要注意,是类型的顺序不同!
void function(int x, double y, char z)
{
cout << "function(int,double,char)" << endl;
}
void function(double x, int y, char z)
{
cout << "function(double,int,char)" << endl;
}
只要牢记以上这三条规则,函数重载也就是基本掌握了。这三条规则实际上就是要保证重载函数之间的类型互不相同。在函数调用时,编译器会根据传入的实参来确定调用哪个函数。
使用函数重载的同时,我们需要注意函数调用的二义性。
#include <iostream>
using namespace std;
// 两个重载test函数符合函数重载定义,他们是没有问题的
void test(int x = 3, int y = 4)
{
cout << "x = " << x << " y = " << y << endl;
}
void test(const char* str = "hello world")
{
cout << str << endl;
}
int main()
{
//char str[] = "hello C++";
//test(str);
test();// 但是调用时存在二义性
return 0;
}

这个时候就有人提出问题了,C++为什么支持函数重载?而为什么C语言不支持呢?这是因为C/C++程序在编译阶段完成后形成汇编代码,调用函数的语句转化为汇编指令。".c"文件和".cpp"文件中编译后可以得到不同的汇编代码,成为了C++能支持函数重载,C语言不支持的关键,至于更深入的实践,感兴趣的可以去看看两者在编译阶段完成后形成汇编代码的区别就明白了。
还有一个至关重要的问题,为什么函数的返回类型不能够确定重载?
原因是我们在调用函数的时候,是体现不出函数的返回类型的。调用函数的方法都是[函数名(参数)]这种格式,即使我们使用一个变量接收它的返回值,但变量接收返回值的部分是不属于函数调用的部分。所以函数的返回类型不能够确定重载的。
2、引用
引用是给一个已存在的变量取一个别名。编译器不会为引用变量开辟内存空间,引用变量与被引用的变量共用同一块内存空间。在C++中,对引用变量操作就是对被引用的变量操作。如:
#include <iostream>
using namespace std;
int main()
{
int value = 3;
int& rvalue = value;// 引用变量引用已经存在的变量value
rvalue++;// 对引用变量操作就是对被引用的变量操作
cout << "value = " << value << " rvalue = " << rvalue << endl;
return 0;
}

由此可见,引用的语法就是[类型名& 引用变量名(对象名) = 被引用的实体],这里需要注意的一点是引用变量的类型和被引用的变量的类型必须是同种类型,哪怕它们能互相发生隐式类型转换:
int main()
{
int x = 3;
float& rx = x;// 这样的写法是错误的,即使int与float能够相互转换
return 0;
}
对于引用,我们可以如下进行理解。

我们经常说的 " 变量 " 实际上指的是内存空间,例如上图使用 int 类型开辟出来的4字节空间;而 " 变量名 " 指的是我们为这块空间所取的名字,例如上图的"x"。
2.1 引用的特性
在使用引用时,需要注意以下特性:
1、引用在定义时必须初始化:
int main()
{
int& t;// 错误,引用一旦被定义,它必须被初始化
int x = 3;
int& rx = x;//正确用法
return 0;
}
2、一个变量可以有多个引用,也可以发生连续引用:
int main()
{
int v = 3;
// 一个变量可以被引用多次
int& rv1 = x;
int& rv2 = x;
// 引用之间可以连续引用
int& rv3 = rv1;
int& rv4 = rv3;
return 0;
}
3、引用一旦引用了一个实体,他便不能再去引用其他实体(我们的本意是引用另一个实体,实际上发生的是赋值):
#include <iostream>
using namespace std;
int main()
{
int x = 6;
int& rx = x;
int y = 9;
rx = y;// 我们的本意是改变rx的引用实体,但实际上发生的是赋值
cout << x << endl;
return 0;
}

2.2 引用的使用场景
1.引用做参数:在没有接触引用之前,我们写的交换函数swap的两个参数都是指针类型。学会了引用,我们可以这样写:
#include <iostream>
using namespace std;
void swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
int main()
{
int x = 3;
int y = 6;
swap(x, y);
cout << "x = " << x << ",y = " << y << endl;
return 0;
}

对该代码的分析如下:

这样的传参方式,称为引用传参。与传值传参不同的是,传值传参会发生一次拷贝,而引用不发生拷贝,也就是说,引用传参能够提高一些程序效率。
2.引用做返回值:直接上例子,通过例子再对引用进行讲解。
int func()
{
static int x = 0;
++x;
return x;
}
int main()
{
int ret = func();
return 0;
}
在这个例子中,func函数定义了一个静态变量,其名为x,在执行"return"语句的时候,x的值由0递增到1。这个时候我们需要注意了,"return x"并不是把x变量返回,而是func函数的返回值类型为int,会生成一个临时变量,x变量把值拷贝到这个临时变量当中,外部的ret变量接收func函数的返回值,实际上是临时变量再次把里面的值拷贝到ret变量当中:

那如果我们以引用做返回值,那么中间的过程会与传值返回有所区别:
int& func()
{
static int x = 0;
++x;
return x;
}
int main()
{
int ret = func();
return 0;
}
代码分析如下图:

我们将接收func函数返回值的整形变量换成引用变量,那么就会减少一次拷贝的过程。
从上面的使用场景可以看出,引用无论是做参数还是做返回值,中间过程都能减少空间开辟、拷贝所带来的消耗,从而提高工作效率。但是,当我们对上面的程序稍做修改,就会产生一个小错误:
#include <iostream>
using namespace std;
int& func()
{
int x = 0;
++x;
return x;
}
int main()
{
int& ret = func();
cout << ret << endl;
return 0;
}
在这个例子中,当主函数调用func函数时,会创建func函数对应的函数栈,此时x变量不再存储在数据段,而是存放在函数栈中,也就是说,当func函数完成工作之后,其申请的函数栈会被"销毁",x变量也会随之"销毁"。注意我们的"销毁"打了双引号,我想说的是,这个"销毁"并不是内存直接被销毁,而是函数退出之后,其原先申请的内存便不属于我们了。而我们接收func函数返回值的变量是一个引用变量,也就是说,ret引用了一块不属于我们的内存,但是当前程序依然能够正确输出结果,其实只是一个巧合,其中原因是编译器、操作系统没有初始化、占用这块空间,而是保留了原来的数据。那么ret刚好引用了"正确"的值。

但是这并不意味我们的程序没有错误,如果我们将程序稍微的修改一下,就不会有这种效果了。
#include <iostream>
using namespace std;
int& func()
{
int x = 0;
++x;
return x;
}
int main()
{
int& ret = func();
cout << ret << endl;
cout << ret << endl;
return 0;
}

看见没,就是一个道理。看到这里,应该明白这些细节了吧,写代码往往就是很多细节没有处理好,到处很多bug出来。二次输出就会变成一个随机值。下面就对这两种输出结果进行分析。
1.正常输出1的原因:当主函数调用func函数,func函数结束时,主函数当中ret引用变量引用了一块不属于我们的空间。我们能看到正常输出1仅仅是一个巧合,说明编译器、操作系统没有初始化、占用这块空间,而是保留了原来的数据。那么ret引用了"正确"的值,它本身被当作参数传递给cout(暂时这么理解,反正输出语句是个函数),那么在屏幕上正常输出1就可以理解了。
2.输出一个随机值的原因:因为函数栈的某些原因,这块空间被随机值覆盖了,ret引用的那块空间被随机值覆盖了,此时ret引用的那块空间的值就发生了改变。然后再将ret作为参数传递给第二条输出语句,就在屏幕上打印随机值了。
通过这个错误程序,我想说的是:内存空间的"销毁"并不意味着内存空间不存在了,它是一直存在的,只不过它不受任何保护,可以被任何数据修改,我们依然能够访问那块空间,但是访问到的数据是不确定的,例如上面的那段错误程序,我们可能访问到正确的值,也可能访问到一个随机值,也可能访问到一个属于其他函数的变量值。
再其次强调一下引用的用法:上面的那段错误程序的复杂程度是很低的,如果我们在以后的开发过程中滥用引用会造成难以排查的"BUG",也是一种基础不扎实的表现,所以要把引用当作返回值的开发场景当中,一定要确保引用的对象出了函数作用域不销毁。
2.3 引用传参和引用返回对效率的影响
引用传参和引用返回都能减少临时变量的开辟、数据拷贝的次数,从而在一定程度上提升代码的运行效率。但是在现代的计算机硬件体系当中,这些细微的效率差距我们是体会不出来的,所以下面的代码尽可能复现引用对效率的影响:
#include <iostream>
using namespace std;
#include <time.h>
// 这个结构体就有 4w字节
struct A
{
int arr[10000];
};
struct A a;// 定义一个全局变量
struct A func1()// 传值返回
{
return a;
}
struct A& func2()// 引用返回
{
return a;
}
int main()
{
// 计算传值返回的时间差
size_t beign1 = clock();
for (int i = 0; i < 100000; i++)// 调用10W次传值返回的函数
{
func1();
}
size_t end1 = clock();
// 计算引用返回的时间差
size_t begin2 = clock();
for (int i = 0; i < 100000; i++)
{
func2();
}
size_t end2 = clock();
cout << "struct A func1():" << end1 - beign1 << endl;
cout << "struct A& func2():" << end2 - begin2 << endl;
return 0;
}

从输出结果来看,传值返回的函数调用10万次用时187ms,引用返回的函数调用10万次用时1ms,可见在这个场景当中引用比传值的效率高出了100多倍。
其实引用做不做参数、做不做返回值无关紧要,重要的是当引用做参数时,它可以做输出型参数;外部用引用接受函数的返回值时,返回值可以被修改(关于这部分的用法在往后的内容会被频繁使用)。
2.4 常引用
看一段代码,判断是否正确?
int main()
{
const int y = 7;
int& ry = y;
return 0;
}
这段程序是错误的。原因在于y变量被定义的本意就是让它只能被初始化,不能被赋值,也就是说我们的本意是让y变量在以后的场景当中保持它的初值,我们对y的权限只能读,不能写。但是紧跟着的ry引用变量却违背了这个初衷,ry引用的变量我们认为能够对其进行读写操作,而这段代码当中ry引用了一个只能读不能写的变量,此时就会产生一个冲突。所以编译器严厉制止这样的行为,这种情况我们称为权限放大。在C++中,权限只能被平移或缩小不能被放大。我们对上面的代码做出修改:
int main()
{
const int y = 7;
const int& ry = y;//权限平移
int z = 3;
const int& rz = z;//权限缩小
return 0;
}
常引用的权限虽然只能读,但它不会影响被引用的变量的权限:
int main()
{
int x = 3;
const int& rx = x;// x被常引用引用
x++;// 但是不会修改其权限
// x++语句执行完后,x的值为4,rx的值也为4
return 0;
}
接着我们上面讲的"引用变量的类型和被引用的变量的类型必须是同种类型,哪怕它们能互相发生隐式类型转换",但是使用常引用可以使得两边类型不相同(能够相互发生类型转换的类型):
#include<iostream>
using std::cout;
using std::endl;
int main()
{
double x = 3.14;
const int& rx = x;
cout << x << endl;
cout << rx << endl;
return 0;
}

这个时候我们必须探究一下这种现象是为什么。首先,发生类型转换的变量不会引起它自身的变化,而是生成一个类型转换后的临时变量,再将变量的值拷贝到临时变量当中。我们以一个程序以及画图来说明这个问题:
#include <iostream>
using namespace std;
int main()
{
double x = 3.14;
cout << (int)x << endl;
cout << x << endl;// 上一条语句x已经发生类型转换,但是它本身不变
return 0;
}

分析图如下:

那么这段代码我们就可以解释了:
#include<iostream>
using std::cout;
using std::endl;
int main()
{
double x = 3.14;
const int& rx = x;
cout << x << endl;
cout << rx << endl;
return 0;
}
在这段代码当中,rx引用变量并没有引用x变量,而是引用了double类型变量x向int类型转换过程中产生的临时变量。也就是说,临时变量具有常性,临时变量的生命周期仅限于程序的当前行。那么我们需要注意,函数的参数(传值传参的参数,也叫形参)并不是临时变量,而是函数在创建函数栈时预先开辟好的栈空间,也就是说,形参的生命周期跟随函数(函数栈销毁形参也销毁)。
请看下面看一段代码:
#include <iostream>
using namespace std;
int func1()// 注意,这里是传值返回
{
int x = 0;
++x;
return x;
}
int func2()// 这里也是传值返回
{
int x = 100;
return x;
}
int main()
{
const int& ret = func1();
cout << ret << endl;
cout << ret << endl;
func2();
cout << ret << endl;
return 0;
}
func1函数以传值返回的方式返回,外部使用常引用接收其返回值,这是正确的做法。这段程序没有问题的,下面对程序进行分析,通过画图分析一下函数的返回值存放在哪里:

图(1)描述了函数返回返回值、函数外部接收返回值的过程;图(2)描述了一个函数要调用某一函数之前,编译器会根据需要确定被调用函数的返回值类型,在调用被调用函数的函数栈中开辟足够的空间用来存放返回值,也就是说ret引用变量引用了跟自己生命周期一样的一块空间,这就没有违背"不要去引用不属于自己的空间"的原则,又因为这块空间并不是用户主动开辟的,所以它具有常属性。所以在随后调用func2函数时会发生同样的事,所以就不存在空间覆盖问题。
注意: 在使用引用传参的时候也需要注意函数调用的二义性(当有函数重载时):
// 下面两个Add函数构成重载,这是没有问题的
int Add(int x, int y)
{
return x + y;
}
int Add(int& x, int& y)
{
return x + y;
}
int main()
{
Add(1, 2);//匹配第一个Add,因为第二Add的参数是普通引用,引用不了常量
int x = 3, y = 4;
Add(x, y);// 这里就有问题了,x、y都是变量,调用任何一个Add函数都可以,所以存在二义性
return 0;
}

2.5 引用与指针的区别
引用能做到的事指针也能做到,但在C++中引用不能够完全代替指针,这是因为C++中给引用的定义就与其他语言不一样,下面列举引用与指针的不同点:
1.引用变量实质上是为一个已存在的变量取一个新的名字,它本身不开辟空间;而指针变量能够存储空间的地址,它具有空间大小。
2.引用变量在定义时必须被初始化,而指针可以不初始化。
3.引用变量在引用了一个实体之后便不能再引用其他实体(如果引用另一个实体,实际上发生的是赋值),而指针变量可以在任何时候改变指向的实体(改变存储的空间地址)。
4.没有空引用,但有空指针。
5.在sizeof运算符中的含义不同。sizeof(引用变量)的计算结果为被引用变量的大小(对引用的操作就是对被引用对象的操作),但sizeof(指针变量)计算的是指针变量的大小。
6.对引用的++操作会递增被引用变量的值,而指针++而是按照类型向后偏移对应的字节数。
7.有多级指针,但不存在多级引用。
8.访问实体的方式不同。指针访问实体需要解引用,而引用由编译器自动处理。
9.引用使用起来比指针更加安全。
以上列举的都是语言层面的不同,实际上在底层,它们两个都一样。也就是说,引用在底层实际上会开辟空间,引用就是由指针设计而来。我们观察下面这段程序的汇编代码:
int main()
{
int a = 10;
int& ra = a;
int b = 3;
int* pb = &b;
return 0;
}

3、auto关键字
auto关键字是C++11的一个关键字,在这之前,auto是用来声明局部变量的。我们在".c"文件中执行下面这段代码:
int main()
{
auto int x = 3;
int y = 6;
return 0;
}
那么到了C++11,标准委员会发现没人会像上面这样使用auto,于是彻底将以前的功能删除,取而代之的新功能是自动类型推导。需要自动类型推导的场景常常发生在类型难于拼写、类型含义不明确而导致的出错,使用auto可以解决这些问题。
在我们定义某一变量时,auto使用变量的初始化数据的类型来确定该变量的类型。 也就是说使用auto定义变量时必须对其进行初始化,编译器会在编译阶段根据初始化表达式(等号右边的变量类型)来推导auto的实际类型,所以auto并不是一种具体类型,它实际上是一个类型声明的占位符(告诉编译器这里有一个类型,不知道是什么,等待编译器推导)。
#include <iostream>
using namespace std;
int main()
{
auto a = 10;// 10为int类型,所以a为int类型
auto b = a;// a为int类型,所以b为int类型
auto c = (double)b;// b类型转换后为double类型,所以c为double类型
double& rc = c;
auto cc = rc;//cc为double类型,并不是double&类型
auto p1 = &a;//&a为int*类型,所以p1为int*类型
auto* p2 = &a;// 与上面等价
// 输出各变量类型的名称
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(cc).name() << endl;
cout << typeid(p1).name() << endl;
cout << typeid(p2).name() << endl;
return 0;
}
auto"类型"的变量不能不初始化:
int main()
{
auto x;//错误,auto修饰的变量必须被初始化
auto y = 3;// 正确用法
return 0;
}
使用auto在一行定义多个变量时,需要保证这些变量的类型是相同的:
int main()
{
auto a = 1, b = 2, c = 3;
auto x = 1.1, y = 3.14, z = 'c';//错误
return 0;
}
下面两个场景当中就不能使用auto:
1.auto不能做函数参数
void func(auto i = 10)
{
cout << typeid(i).name() << endl;
}
在编译阶段,主要的工作就是检查语法,然后生成汇编代码,这个阶段并没有发生函数调用(函数栈可以说是编译器创建的,具体体现在汇编代码当中,CPU执行这些指令就创建函数栈了,所以函数调用发生在程序运行时),这就意味着函数没有收到外部实参,没有收到实参就意味着参数的值不确定,值不确定就意味着类型不确定,即使我们上面的func函数写了缺省值,但缺省值是在调用函数时不给实参的情况下才起作用的。所以编译器在编译阶段无法确定auto要推导的参数的类型。
2.auto不能用来直接声明数组
int main()
{
int a1[] = { 1, 2, 3, 4, 5 };
auto a2[] = { 2, 3, 4, 5, 6 };//错误用法
return 0;
}
在C/C++中,数组算不上很严格的数据类型,我们平常用C/C++编程时不会说这是个"数组类型"。以下面一段代码就可以说明这个问题:
#include <iostream>
using namespace std;
int main()
{
int arr[5] = {1,2,3,4,5};
cout << sizeof(arr) << endl;// 输出20,很合理
//int arr2[5] = arr;// 这样的赋值是错的
int* parr = arr;// 这样才是对的
cout << sizeof(parr) << endl;//32位平台
return 0;
}

其实在C++11当中,"{}"已经是一个类型了(initializer_list<T>),也就是说auto不给声明数组:
#include <initializer_list>
#include <iostream>
using namespace std;
int main()
{
//auto il[] = { 1, 2, 3, 4, 5 };//C++不给这么玩
auto il = { 1, 2, 3, 4, 5 };//我们这么玩
cout << typeid(il).name() << endl;
return 0;
}

注意:auto声明数组是错的,但是声明initializer_list是正确的;
文章详细介绍了C++中的函数重载概念,包括如何实现和设计重载函数,以及函数重载的重要性。接着讨论了引用,包括其特性、使用场景,特别提到了引用做参数和返回值对效率的影响。此外,还讲解了常引用的概念,指出引用与指针的区别。最后,文章提到了C++11引入的auto关键字,用于自动类型推导,简化了代码并提高了可读性。
448

被折叠的 条评论
为什么被折叠?



