在 C++ 中,函数和引用是构建程序的两块重要基石。本文继续之前的基础知识往下讲,先讲缺省函数与函数重载,再深入理解引用的概念、特性与使用方式。
缺省函数
缺省函数是在函数声明或定义时,给定函数参数一个缺省值。
比如说我们写一个 Add() 函数用来实现两个整型相加的功能。
cpp
int Add(int a = 1, int b = 2)
{
int c = a + b;
return c;
}
1 和 2 就是我们给参数的缺省值,当我们调用该函数但不传参的时候,函数就会将缺省值赋给相应参数。
需要注意的是,缺省函数规定不能跳跃传参,只能连续传参,所以我们调用该函数有 3 种方式,也算使调用函数的方式变得多元化了。
cpp
int main()
{
Add();//没有传参,使用缺省值
Add(10);//只给第一个参数传参,第二个参数使用缺省值
Add(10, 20);//使用传过来的参数
return 0;
}
缺省函数分为全缺省和半缺省,全缺省就是给函数所有参数都设置缺省值,我们上述所写的缺省函数就是全缺省函数。半缺省函数是只给函数的部分参数设置缺省值,规定只能从右往左设置缺省值。
cpp
//全缺省函数
int Add(int a = 1, int b = 2)
{
int c = a + b;
return c;
}
cpp
//半缺省函数
int Add(int a, int b = 2)
{
int c = a + b;
return c;
}
对于这个半缺省函数,第一个参数就必须要传参了,因为没有设定缺省值。
关于缺省函数,最后一点要注意的是,当函数声明和定义分离时,要在声明中设定缺省值,定义中就不能写缺省值了。
cpp
//声明里设定缺省值
int Add(int a = 1, int b = 2);
//全缺省函数
int Add(int a, int b)
{
int c = a + b;
return c;
}
函数重载
在 C 语言中,我们是通过函数名来区分调用的是哪个函数,所以不允许有同名函数的存在。在 C++ 中是允许有同名函数的存在的。
当存在同名函数时,我们如何区分调用的是哪个函数呢?我们是通过参数的类型或个数来区分的。
也就是多个具有不同参数个数或参数类型的同名函数存在时,我们称之为函数重载。
比如我们之前写的整型相加的函数 Add,我们可以将参数类型换为 float、char 等类型,定义这些类型的相加函数。
cpp
//这三个函数名相同,参数类型不同,构成函数重载
//int型相加
int Add(int a, int b)
{
int c = a + b;
return c;
}
//float型相加
float Add(float a, float b)
{
float c = a + b;
return c;
}
//char型相加
char Add(char a, char b)
{
char c = a + b;
return c;
}
然后我们调用的时候,编译器就会根据参数类型来决定调用的是哪个函数。
cpp
int main()
{
int x = 10, y = 20;
Add(x, y);//整型,调用第一个函数
float c = 1.3f, d = 1.4f;
Add(c, d);//浮点型,调用第二个函数
char ch1 = 'a', ch2 = '0';
Add(ch1, ch2);//字符型,调用第三个函数
return 0;
}
我们来看一个程序:
cpp
void Func()
{
;
}
void Func(int a = 10, int b = 20)
{
;
}
参数个数不同,语法上确实构成函数重载。当我们传参调用第二个函数的时候没有问题,但如果不传参呢?调用的是哪个函数?
cpp
Func();
这时候就产生歧义了:不传参的时候这两个函数都能匹配,那究竟调用的是哪个函数,编译器也不清楚。

所以在实际运用中不要这样写函数重载,我们一般舍弃第一种写法保留第二种,因为第二个函数包含第一个函数的功能。
引用
引用的概念和定义
引用不是定义一个对象,而是给一个已经存在的对象取别名,该对象的引用和该对象共用一块内存空间。
引用的使用方式是:类型& + 别名 = 变量
比如说我们给变量 a 取别名 b,就是这样取的:
cpp
int a = 10;
//给a取别名b,b就是a的引用
int& b = a;
因为 b 和 a 共用一块内存空间,简单来说 b 就是变量 a 的另一个名字,所以修改 b 的内容,a 也会跟着修改。

引用的特性
关于引用有几个特点:
- 使用时必须初始化
- 一个变量可以有多个引用,还可以给引用取别名,但一个引用只能作为一个变量的别名
- 引用指向的对象不能修改
因为引用是给变量起别名,所以必须要初始化引用,使引用成为某个对象的别名,否则没有实际意义,语法也是不允许的。

我们还可以给一个变量起多个别名,也就是有多个引用。
cpp
//一个变量可以有多个引用
int& b = a;
int& e = b;
int& f = a;
//实际上都和a共用一块内存空间
但是不能改变引用的指向,如果我们要把 b 的指向改成 c:
我们知道 &b 就是取 b 的地址的意思,&b = c 的意思就变成了把 c 的值赋给 b 的地址,这是不允许的,或者写成 b = c,意思是把 c 的值赋给 b,所以在 C++ 语法层面上也是没有更改引用指向的办法的。
引用的使用
引用作为函数参数
引用通常可以作为函数参数,比如说实现交换两个值的函数 Swap,我们之前写的是这样的:
c
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
我们把一级指针作为函数参数,通过解引用指针的方法来交换实参中的值。
我们现在将参数改为引用类型,就会方便很多。
cpp
//引用作为函数参数
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
我们将引用作为函数参数,操作它们就相当于操作实参,直接交换两个实参的值,也同样达到了影响形参可以改变实参的目的,达到了和传指针一样的效果。
引用和指针在功能上有重叠,但各有各的特点。
比如说我们自己定义了一个结构体类型,里面的东西特别多。
c
typedef struct st
{
//n串代码
}st;
然后我们将 st 作为函数参数类型。
c
void Func(struct st a)
{
;
}
形参是实参的一份临时拷贝,所以我们在传参的时候就会产生拷贝,内容较大的时候耗时也长。
但如果我们函数参数是引用类型的,就不会产生拷贝,因为该参数相当于传过来的实参的别名,就是对该实参进行操作。
引用作为函数参数可以减少拷贝,提高效率。
引用作为返回值
当我们普通地传值传参的时候,我们返回来的值实际上是编译器创建的临时对象中存储的值。
比如说我们的 Add() 函数,返回值为 int 类型。
cpp
int Add(int a, int b)
{
int c = a + b;
return c;
}
这里是传值返回。如果我们想对返回值进行修改呢?
cpp
int a = ++Add(10, 20); // 修改返回值再赋给 a
编译器就会报这样一个错误:

为什么呢?这是因为函数在返回值的时候返回的是临时对象,而该临时对象具有常性,是不能被修改的,所以编译器会报这个错误。
函数在返回值的时候,编译器会创建一个临时对象,该临时对象把 c 的值拷贝进来,然后返回该临时对象储存的值。
如果我们返回的是引用呢?
cpp
int& Add(int a, int b)
{
int c = a + b;
return c;
}
该返回值就能被正常修改。

函数返回的是引用的时候,比如说编译器起的别名是 int& tmp = c,返回来的是 tmp,就是 c 的引用,在这过程中编译器不会创建临时变量,相当于把 c 的别名返回来,不会产生拷贝,我们还可以对返回进行操作。
所以引用作为函数返回的时候,可以减少拷贝,提高效率,还可以对返回的引用进行操作。
既然返回引用有这么多好处,我们可以把所有函数返回都改成引用类型吗?
答案当然是不行的,比如我们上面返回引用的例子:
cpp
int& Add(int a, int b)
{
int c = a + b;
return c;
}
c 是局部对象,在函数栈帧销毁时 c 的空间就被回收了,所以我们拿到 c 的引用,实际上该块内存空间已经不属于我们了,本质上就是野引用,类似于野指针,这是不安全的。
我们上面 ++Add(10, 20) 得到的恰好是 31,这只是个巧合,我们在返回一块不属于我们的内存空间的操作已经是错的了,再去修改这块内存空间的内容更是错上加错。
所以当返回对象是局部变量,在函数栈帧销毁时生命周期就结束的情况下,我们不能返回引用。