欢迎来到C++
1、缺省参数(默认参数)
1.1、认识及规则介绍
缺省参数(默认参数),就是给函数的形参 赋上默认值。
c
#include<iostream>
using std::cout;
using std::endl;
void func(int a = 0)//缺省参数(默认参数)
{
cout << a << endl;
}
int main()
{
func(10);//传入参数一定有返回值
func(); //不传入参数也会有返回值
return 0;
}

缺省参数(默认参数)按形式,分为全缺省,和半缺省。
全缺省,就是函数的所有形参,都赋上默认值:
c
#include<iostream>
using std::cout;
using std::endl;
void func(int a = 1, int b = 2, int c = 3)//全缺省
{
cout << a << " ";
cout << b << " ";
cout << c << endl << endl;
}
int main()
{
func();
func(10);
func(10, 20);
func(10, 20, 30);
return 0;
}

半缺省,就是部分形参赋上默认值:
c
#include<iostream>
using std::cout;
using std::endl;
void func(int a, int b = 2, int c = 3)//半缺省
{
cout << a << " ";
cout << b << " ";
cout << c << endl << endl;
}
int main()
{
//func();
func(10);
func(10, 20);
func(10, 20, 30);
return 0;
}

对于半缺省,有两个规则:
- 规则1:缺省参数的设置,必须从右往左。与函数实参对形参值传递从左往右的规则相匹配
- 规则2:对于某些缺省参数未设置的形参,调用函数时必须对他们赋值
若缺省参数的设置不是从右往左,会报错:

调用函数时未对缺省参数未设置的形参赋值,会报错:

而对于缺省参数(默认参数),还有一个规定:
- 如果函数同时存在声明与定义,那么缺省参数必须设置在函数声明中,不能设置在在函数定义中
这可能是用来防止在不同地方设置了不同默认值,导致机器识别混淆的情况发生。
示例:

1.2、应用
缺省参数(默认参数)的使用,为我们在一些问题的处理上提供了一些便利。
比如我们正在对一个顺序表进行操作。
1.2.1、规避性能的浪费
在不使用缺省参数(默认参数)的情况下,我们创建一个顺序表并初始化,大概是这样的:

我们在之前的学习中知道,向顺序表中传入数据,如果顺序表空间不够,需要扩容 。那么问题来了,当我们想一次性传入1000个(整型)数据,如果使用realloc扩容 按照之前从4*4个字节开始,每次扩容一倍,那么需要扩容9 次;然而系统并不是直接扩容,而是先拷贝,然后申请新空间并粘贴,再释放旧空间,经过这么多次全过程,势必有性能的浪费。
这时我们使用缺省参数,即一开始规定需要扩容的空间。

如果没有额外的要求,默认值可以向上面一样设置为4;而如果有额外的要求,比如1000,那么我们就可以输入参数1000。这样一来,就可以规避性能的浪费。
1.2.2、解决一些难以解决的问题
当我们使用顺序表的查找函数 时,对于一个有多个相同数据的顺序表,我们只能查找这个相同数据从左往右第一次出现的位置,想要寻找其他的位置非常困难。
这时我们使用缺省参数,规定开始查找的位置 :

这时,我们就可以查找到任意位置的相同数据。想找第一个,参数就设置成0 ,想找第二个,参数就设置成参数为0的函数的返回值......
当然,我们也可以设计一个函数,查找所有相同数据的位置:
c
void test2(SL& sl, int val)
{
//先确定有没有
int pos = SLFind(sl, 5);
if (pos == -1)
{
cout << "没找到。" << endl;
return;
}
//找出所有相同值的下标
pos = -1;
while (pos < sl.size)
{
pos = SLFind(sl, val, pos+1);
if (pos == -1)
{
break;
}
cout << pos << " ";
}
}
比如传入数据:1、1、3、4、5,规定找"1"的位置,我们就能得到下标:

当然,也可以使用for循环实现。
2、函数重载
2.1、函数重载的认识
C语言中,不能存在同名函数。而在C++中,同名函数可以存在 ,但是需要函数的形参不同:
c
#include<iostream>
using std::cout;
using std::endl;
int Add(int x, int y)//整型加法
{
cout << "int Add(int x, int y)" << " " << endl;
return x + y;
}
double Add(double x, double y)//双精度浮点型加法
{
cout << "double Add(double x, double y)" << " " << endl;
return x + y;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.23, 2.34) << endl;
return 0;
}

函数重载,可以有三种不同形式:
- 函数形参类型不同
- 函数形参个数不同
- 函数形参位置不同(可以理解为类型不同)
下面来看看不构成函数重载的情况:
2.2、不构成函数重载的情况
第一种:函数的参数相同,但类型不同。
其实,这种形式从函数重载的定义上来看,就不会构成(函数重载),因为参数就已经相同了,导致编译器无法准确找到对应的函数:
c
#include<iostream>
using std::cout;
using std::endl;
int f()
{
cout << "int f()" << endl;
}
void f()
{
cout << "void f()" << endl;
}
int main()
{
f();
return 0;
}

所以设置不同的形参,可以解决这一点:
c
#include<iostream>
using std::cout;
using std::endl;
int f(int x)
{
cout << "int f()" << endl;
return;
}
void f()
{
cout << "void f()" << endl;
}
int main()
{
f();
f(1);
return 0;
}

第二种:两个函数(或多个函数),其中一个无参数,另一个全缺省。
我们在缺省参数(默认参数)的学习中得知,对于一个全缺省的函数,调用时是可以不传入任何参数的。此时又出现了一个同名的没有参数的函数,那么当我们调用这个函数而不传入参数的时候,编译器就会分不清,调用的是没有参数的函数,还是全缺省的函数:
c
#include<iostream>
using std::cout;
using std::endl;
int f1()//无参数
{
cout << "int f1()" << endl;
}
int f1(int x = 10)//全缺省
{
cout << "int f1(int x = 10)" << endl;
}
int main()
{
f1();
return 0;
}

3、引用
引用,相当于我们给一个对象取别名 。一个对象的所有引用,与这个对象共用空间。
3.1、引用的使用方法及规则
引用的使用方法是:类型 + &
c
#include<iostream>
using std::cout;
using std::endl;
int main()
{
int i = 10;
int& j = i;//引用
cout << i << endl;
cout << j << endl;
cout << &i << endl;//取地址
cout << &j << endl;
return 0;
}

引用的使用有三个规则:
规则1 :引用必须初始化。
所以这样的写法会报错:

规则2 :一个对象可以被多次引用。
c
#include<iostream>
using std::cout;
using std::endl;
int main()
{
int i = 10;
int& j = i;//j引用i
int& k = j;//k引用i
cout << i << endl;
cout << j << endl;
cout << k << endl;
cout << "-----------------------------------------------" << endl;
cout << &i << endl;//共用空间,地址相同
cout << &j << endl;
cout << &k << endl;
cout << "-----------------------------------------------" << endl;
j = 20;//改变对象和引用中的任意一个,其他所有跟着变
cout << i << endl;
cout << j << endl;
cout << k << endl;
return 0;
}

规则3 :一个引用有了实体之后,就不能再去做另一个实体的引用。
c
#include<iostream>
using std::cout;
using std::endl;
int main()
{
int i = 10;
int& j = i;
cout << "-----------------------------------------------" << endl;
cout << i << endl;
cout << j << endl;
int k = 20;
j = k;//这里不是使j引用i,而是将k的值赋值给j(i)
cout << "-----------------------------------------------" << endl;
cout << i << endl;
cout << j << endl;
cout << k << endl;
cout << "-----------------------------------------------" << endl;
cout << &i << endl;
cout << &j << endl;
cout << &k << endl;//地址不同
return 0;
}

k 与 j 的地址不同,佐证了这一规则。
3.2、不能使用引用的场景
在讲引用的实际使用场景之前,先简单说说不能使用引用的场景。
诸如链表、树中定义节点的地方,不能使用引用。因为这些节点往往会改变指向,而引用一旦对应了某一实体,就不能再对应另外的实体。
3.3、引用的实际使用
引用的使用,可以:
- 减少拷贝提高效率
- 改变引用对象的同时改变被引用的对象
引用的使用,可以在大部分场景下,代替指针。
引用的使用主要有两个地方:引用传参 、引用做返回值。
3.3.1、引用传递参数
3.3.1.1、值交换函数
比如我们之前学过的值交换函数,我们知道值交换函数要用到传址调用:
c
#include<iostream>
using std::cout;
using std::endl;
//值交换函数
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int x = 1, y = 2;
cout << "交换前:" << x << " " << y << endl;
Swap(&x, &y);
cout << "交换后:" << x << " " << y << endl;
return 0;
}

在传址调用中,我们必须传入指针。如果直接使用传值调用,由于此时函数的形参只是实参的一份临时拷贝,改变形参不能改变实参,就达不到值交换的目的。
然而,我们使用引用,就不需要传入指针,因为引用与对象共用空间:
c
#include<iostream>
using std::cout;
using std::endl;
//值交换函数
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 1, y = 2;
cout << "交换前:" << x << " " << y << endl;
Swap(x, y);
cout << "交换后:" << x << " " << y << endl;
return 0;
}

3.3.1.2、单链表传入参数的简化
我们在单链表创建的学习中,知道单链表节点是动态申请 的;而传值调用中,改变形参不会改变实参。所以我们当时使用了二级指针。
但当我们学会了引用的使用,就不用那么麻烦了:


3.3.2、引用做返回值
3.3.2.1、错误的使用:返回局部变量的时候使用引用
首先我们要明白,编译器给一个函数开辟的栈帧中,如果创建了一个局部变量,并且最后返回了这个变量值,那么对于这个,过程底层的逻辑应该是:编译器首先使用另一个临时变量(寄存器)存储变量值,然后将这个临时变量返回,最后释放为函数开辟的栈帧。
这个过程,进行了一次临时拷贝。
cpp
#include<iostream>
using namespace std;
int Swap()
{
int ret = 0;
//TODO...
return ret;
}
int main()
{
int x = Swap();
cout << x << endl;
return 0;
}

这种临时变量是不可以被修改的:
这时,我们将函数改成引用做返回值:
cpp
#include<iostream>
using namespace std;
int& Swap()
{
int ret = 0;
//TODO...
return ret;
}
int main()
{
int x = Swap();
cout << x << endl;
return 0;
}

我们会发现,打印的之还是0。但是编译器报了警告:

在这里,调用的函数Swap(),成了ret的引用(别名)。但是函数Swap()的栈帧已经释放,通过别名去访问ret,是一种危险的行为。
不是所有的出错情况,编译器都会报错。
比如数组越界,越界读不会报错:
这里我们看到退出码为0。但是对于越界读行为,还是报了警告。
而越界写,就会报错:
引用的底层实现,与指针是一样的。
所以,我们不能在返回局部变量的时候使用引用。
3.3.2.2、正确使用
既然我们不能在返回局部变量的时候使用引用,我们就不要使用全局变量。
比如:
c
#include<iostream>
using namespace std;
int& Func()
{
static int ret = 0;//使用static修饰,使ret变为全局变量
//TODO...
return ret;
}
int main()
{
int x = Func();
cout << x << endl;
return 0;
}
引用的一个很好的运用案例,是运用到顺序表中的修改值函数 :


由于SLat()可以被看作SLat()这个函数的返回值的引用,所以我们修改引用,就修改了对应实体本身,非常简单地达到了修改值的目的:



