4. 函数重载
函数重载就是同一个函数名可以重复被定义,即允许定义相同函数名的函数。但是相同名字的函数怎么在使用的时候进行区分呢?所以同一个函数名的函数之间肯定是要存在不同点的,除了函数名外,还有返回类型和参数两部分可以视为函数的特征。
以返回类型来区别,因为在函数调用时不能显式地指出函数返回类型,所以难以使用返回类型来区别。因此只剩下了参数来区分,我们可以总结出以下三种常见的函数重载的情况:
①参数个数不相同:
void func(int a, int b)
{
cout << a << b << endl;
}
void func(int a)
{
cout << a << endl;
}
int main()
{
func(1, 2);
func(1);
return 0;
}
②参数类型不同:
void func(int a, char b)
{
cout << a << b << endl;
}
void func(int a, int b)
{
cout << a << b << endl;
}
int main()
{
func(1, 'a');
func(1, 2);
return 0;
}
③参数类型顺序不同:
void func(int a, char b)
{
cout << a << b << endl;
}
void func(char b, int a)
{
cout << b << a << endl;
}
int main()
{
func(1, 'a');
func('a', 1);
return 0;
}
我们不禁发问,为什么C++支持函数重载,而C语言不支持呢?我以Visual Studio 2022为例,不同编译器的规定方式不同。我们通过观察链接报错,发现C语言中外部符号仅仅是函数名,这就意味着函数在C语言编译中的符号就是函数名,那么相同函数名肯定会产生冲突。
C++就不一样了,可以发现C++给函数赋予的符号要更加复杂。通过观察规律,我们会发现C++在处理时加入了参数类型(H表示int,D表示char)。因此虽然函数名相同,但是参数不同,编译器依然可以顺利区分,达到函数重载的功能。
5. 引用
5.1 引用简介
引用是C++相较于C语言创新的地方。引用可以理解为替一个已经存在的变量起一个别名,这就意味着引用一旦定义,那么可以认为引用变量就是原变量,它们是同一个东西。因此定义引用变量时不开辟空间,引用变量和原变量使用同一块空间。
int main()
{
int a = 1;
int& ra = a; //定义引用变量
cout << a << ' ' << ra << endl;
a++;
cout << a << ' ' << ra << endl;
ra++;
cout << a << ' ' << ra << endl;
return 0;
}
定义引用类型的时候,必须和引用实体是同种类型的。
5.2 引用的特性
①引用在定义时必须初始化。之所以这么规定与第三条特定有关,因为一旦创建了引用就无法再改变其引用的实体了,所以如果不进行初始化,那就无法再和需要的引用实体进行关联了。即引用变量和引用实体的配对一定发生且只发生在初始化时。
int main()
{
//int& ra; //error 没有初始化
int b;
int& rb = b;
return 0;
}
②一个变量可以存在多个引用。
int main()
{
int a = 1;
int& ra = a;
int& rra = a;
return 0;
}
③引用一旦引用一个实体,再不能引用其他实体。这句话就是引用实体不可以发生改变,这是因为会产生歧义。
int main()
{
int a = 3;
int b = 8;
int& ra = a;
ra = b;//歧义:①将b的值赋值给ra;②将b作为引用变量ra的实体
return 0;
}
5.3 常引用
常引用顾名思义,就是对一个常量的引用,使用const进行修饰限制。值得注意的是,常引用的引用实体并非都是常量,而是一些具有常量性质的值(字面量、常量、常变量、临时变量等)。常引用自从初始化后便不会再改变,换言之不可以通过赋值等手段修改常引用的值(此时的常引用就可以被视为一个常量)。
需要格外强调的两点:
①定义引用时,权限可以缩小:非常量值作为常引用的引用实体;权限不可以放大:常量值作为非常量引用的引用实体。
②在之前的非常量引用定义时类型不同,不可以初始化的原因是发生了隐式类型转换,使得实际上是将具有常性的临时变量赋给非常量引用,因此错误原因在于权限放大。根据这一点我们就可以知道,常引用尽管类型不同,但是因为隐式类型转换所以依旧可以正常定义。
int main()
{
const int a = 10;
//int& ra = a; //error
const int& ra = a; //correct
//int& rb = 10; //error
const int& rb = 10; //correct
double d = 3.14;
//int& rd = d; //error
const int& rd = d; //correct,rd==3 d首先隐式类型转换为int,产生一个临时变量,临时变量具有常性所以可以赋给常引用
d = 4.48;
cout << d << ' ' << rd << endl; //4.48 3
return 0;
}
5.4 函数指针的引用
函数指针的引用注意点在代码注释中给出。
void func(int a)
{
cout << a << endl;
}
typedef void(**fpp)(int);
typedef void(*fp)(int);
typedef void(f)(int);
int main()
{
//对于定义函数指针引用:
//1.注意定义的引用的类型和引用实体的类型相同。
// ①函数名有两种解释方式,函数名具有右值属性。以func为例:void(*)(int)和void()(int)
// ②注意辨别不同级函数指针。
void(*pf)(int) = func;
void(**ppf)(int) = &pf;
//2.注意区别左值右值,左值只能使用非常量引用,右值只能使用常量引用
// 区分左右值的方式:判断能否位于赋值符号左边。如*p可以就是左值,&func不可以就是右值
// 注意:函数名是右值,所以在定义类似于void()(int)类型的引用(不是指针的函数类型)时,一定是常量引用
//3.注意定义函数指针的引用可以存在两种方式:①使用typedef;②不使用typedef。
/*f func_;
f& rfunc_ = func;
rfunc_ = func;*/ //void()(int)类型的引用一定是常量引用
//非常量引用的定义方式
//1.使用typedef
fp& r2 = pf;
//r2 = func;
fpp& r3 = ppf;
//r3 = &r2;
//2.不使用typedef
void(*&rr2)(int) = pf;
//rr2 = func;
void(**&rr3)(int) = ppf;
//rr3 = &rr2;
//常量引用的定义方式
//f& r1 = func;
//void(&rr1)(int) = func;
//以上两种方式虽然可行不报错,但是体现不出常引用,所以不建议使用
//r1 = func;
//rr1 = func;
//1.使用typedef
const f& r1_c = func;
//r1_c = func;
const fp& r2_c = pf;
//r2_c = func;
const fpp& r3_c = ppf;
//r3_c = &r2;
//2.不使用typedef
void(const &rr1_c)(int) = func;
//rr1_c = func;
void(*const &rr2_c)(int) = pf;
//rr2_c = pf;
void(**const &rr3_c)(int) = ppf;
//rr3_c = ppf;
const fp& r_a = func;
const fp& r_b = &func;
void(* const& r_c)(int) = func;
void(* const& r_d)(int) = &func;
// 因为函数名有两种解释方式,所以上述的定义方式也是正确的
void(*&r_e)(int) = *ppf;
//void(*&r_e)(int) = &func;
//void(*&r_e)(int) = func;
//因为*ppf是左值,所以没有问题;而&func和func是右值,所以会报错
return 0;
}
5.5 引用的应用
5.5.1 传递参数(输出型参数)
C语言中输出型参数采取传指针的方式,C++中可以使用引用来传递,效果相同但是代码更加简洁。
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 1;
int y = 2;
swap(x, y);
cout << x << ' ' << y << endl;
return 0;
}
5.5.2 传递参数(大空间对象传参,减少拷贝)
在传递如结构体等大空间对象时为了减少拷贝提高效率,可以使用引用来传参。
struct Node
{
struct Node* next;
int val;
};
void func(struct Node& node)
{}
int main()
{
struct Node n1;
func(n1);
}
5.5.3 做返回值
这里给出一个引用做返回值的正确例子。我们在下文详细分析引用作返回值的特性。
int& Add(int a,int b)
{
//static int x = a + b; //这样写因为static只定义一次,所以会导致再次调用函数时不会执行这一步。
static int x;
x = a + b;
return x;
}
int main()
{
int& a = Add(2,4);
cout << a << endl;
a = Add(8, 9);
cout << a << endl;
return 0;
}
那么让引用成为返回值有什么好处与注意事项呢?我们对比着返回值的情况来分析。
① 返回引用可以省去拷贝,返回时内存中不产生副本。值返回首先将值交给寄存器,然后释放栈帧,再然后将寄存器的值mov到需要的位置。在此期间就存在拷贝副本的产生,相当于寄存器充当了临时变量的作用。引用返回则是直接返回变量,省去了中间变量的拷贝开销。如果是使用变量接收则相当于拷贝赋值,如果使用引用接收则是相当于定义引用。所以引用返回值效率更高。
②注意引用返回的变量不可以是局部非静态变量,因为局部非静态变量在栈帧中,当函数结束栈帧释放后,返回值就变得不可控了。
int cal1(int a)
{
int b = a * 2;
return b;
}
int& cal2(int a)
{
static int b;
b = a * 2;
return b;
}
int main()
{
int num1 = cal1(1); //case1:返回方式:返回值。
//在栈中开辟空间的b在函数结束后,将值交给临时变量(寄存器),再由临时变量交给num1,内存中产生了返回值的副本(临时变量)
//int& num2 = cal1(2); //case2:报错,因为函数返回的值是临时变量,具有常性
int num3 = cal2(3); //case3:返回方式:返回引用。
//在函数结束后,因为是返回引用,所以实际上是返回b变量的别名。而num3的值则是直接从b变量处拷贝而来,避免了副本的产生。
//因为b变量是一个静态变量,所以不会随着栈帧被释放,才能在栈帧释放后进行拷贝。
int& num4 = cal2(4); //case4:返回方式:返回引用。
//在函数结束后,返回了b变量且不产生副本,而引用声明num4使用返回值初始化,所以num4就成为了b变量的引用、别名。
//因为b变量是一个静态变量,所以不会随着栈帧被释放,所以引用依旧有效。
}
5.6 引用和指针
①引用相当于是一个别名不占空间。但在底层实现上各个编译器有所不同,一般情况下,引用实际上就是一个指针实现的。所以在实际来说引用还是占空间的。
②在我看来引用是指针的更简单的表达形式。引用变量实际就是指向引用实体的指针,而使用引用变量时默认会进行解引用。
③没有空引用与多级引用但是有空指针和多级指针。
④引用必须初始化并且不可以改变引用实体。指针则可以不受这些限制。
通过以上叙述发现引用虽然是指针的简练表达,但是仅仅限于某些特定情况下。指针还是通用的方法,所以引用只能是极大简化指针的使用,但是无法代替指针。