个人主页:小年糕是糕手

个人专栏:《C++》《数据结构(初阶)》《C/C++刷题集》《C语言》

你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来!


​​


目录

一、函数重载

1.1、参数类型不同

1.2、参数个数不同

1.3、参数类型顺序不同

二、引用

2.1、引用的概念和定义

2.2、引用的特性

2.3、引用的使用

2.4、const引用

2.4.1、part 1

2.4.2、part 2

2.5、指针和引用的关系


一、函数重载

函数重载我们通俗解释就是:一个函数名有多个意思但是会自动匹配

C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。
但是C语言是不支持同一作用域中出现同名函数的:

//C不支持同名函数存在
int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}
double Add(double left, double right)
{
	cout << "double Add(double left, double right)" << endl;
	return left + right;
}

下面我们分为不同情况来依次解释:

1.1、参数类型不同
//函数重载 -- 一个函数名有多个意思但是会自动匹配
//C++可以通过参数匹配去自动识别
//这里需要注意返回值不同不可以构成函数重载 -- 因为无法区分
//无参和函数缺省可以构成函数重载
//但是无参和全缺省虽然构成函数重载,但是如果我们不传参数调用时容易产生歧意
//也就是说构成但是不推荐使用
#include
using namespace std;
int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}
double Add(double left, double right)
{
	cout << "double Add(double left, double right)" << endl;
	return left + right;
}
int main()
{
	cout << Add(1, 2) << endl;
	cout << Add(1.1, 2.2) << endl;
	return 0;
}

我们来看这段代码:编译器在调用函数的时候会进行自动匹配,所以在C++中这段代码可以正常进行。

1.2、参数个数不同
void f()
{
	cout << "f()" << endl;
}
void f(int a)
{
	cout << "f(int a)" << endl;
}
1.3、参数类型顺序不同
void f(int a, char b)
{
	cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
	cout << "f(char b, int a)" << endl;
}

上述的三种情况均可以构成函数重载,但是也有一种情况不符合函数重载:

// 返回值不同不能作为重载条件,因为调用时也无法区分
//void fxx()
//{}
//
//int fxx()
//{
// return 0;
//}
// 下⾯两个函数构成重载
// f()但是调用时,会报错,存在歧义,编译器不知道调用谁
void f1()
{
	cout << "f()" << endl;
}
void f1(int a = 10)
{
	cout << "f(int a)" << endl;
}

二、引用

2.1、引用的概念和定义

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量共用同一块内存空间。
比如:水浒传中李逵,江湖上人称"黑旋风";林冲,外号豹子头。

类型& 引用别名 = 引用对象

C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的 >,这里引用也和取地址使用了同一个符号&,大家注意使用方法角度区分就可以。

#include
using namespace std;
int main()
{
	//C语言中&就是取地址
	int i = 1;
	cout << &i << endl;
	//C++中&就是引用
	//j是i的引用(我们给i取了一个别名)
	int& j = i;
	return 0;
}
#include
using namespace std;
int main()
{
	int a = 0;
	// 引⽤:b和c是a的别名
	int& b = a;
	int& c = a;
	// 也可以给别名b取别名,d相当于还是a的别名
	int& d = b;
	++d;
	// 这⾥取地址我们看到是⼀样的
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &d << endl;
	return 0;
}

我们来看运行结果发现是一模一样的,我们就相当于给一块空间取了一个名字和三个别名,就类似于家人给我们取的小名一样,他们喊大名或者小名本质上就是在喊我们:

​​ 

2.2、引用的特性
  • 引用在定义时必须初始化
  • 一个变量可以有多个引用
  • 引用一旦引用一个实体,再不能引用其他实体
#include
using namespace std;
int main()
{
	int i = 1;
	int& j = i;
	cout << &i << endl;
	cout << &j << endl;
	++j;//i和i本质上就是同一个变量,所以这里他俩都会++
	//一个变量可以有多个引用
	int& k = j;
	k = 10;
	//i j k就是同一空间的不同名字而已
	//引用在定义时必须初始化
	/*int& x;
	x = i;*/
	//这样就会报错
	//引用一旦引用一个实体,再不能引用其他实体
	int m = 20;
	k = m;//这里就是赋值
	//将m的值赋给k,也相当于赋给i,j
	return 0;
}

我们再来看另一串代码加深印象:

#include
using namespace std;
int main()
{
	int a = 10;
	// 编译报错:“ra”: 必须初始化引⽤
	//int& ra;
	int& b = a;
	int c = 20;
	// 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向,
	// 这⾥是⼀个赋值
	b = c;
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	return 0;
}

我们来逐步分析:这里首先我们定义了一个变量a,并且给a赋值为10,下面我们给a取了一个别名叫b(b和a本质上是一块空间),然后我们又定义了一个变量c,并且给c赋值到20,我们将c的值赋给b(实际上也就是a),这时候a,b,c三个值应该是一致的,但是地址不一样(a和b相同,c不同于a、b)

运行结果如下:

​​ 

2.3、引用的使用
  • 引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。
  • 引用传参跟指针传参功能是类似的,引用传参相对更方便一些。
  • 引用返回值的场景相对比较复杂,我们在这里简单讲了一下场景,还有一些内容后续类和对象章节中会继续深入讲解。
  • 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++ 的引用跟其他语言的引用 (如 Java) 是有很大的区别的,除了用法,最大的点,C++ 引用定义后不能改变指向,Java 的引用可以改变指向。
  • 一些主要用 C 代码实现版本数据结构教材中,使用 C++ 引用替代指针传参,目的是简化程序,避开复杂的指针。
#include
using namespace std;
//指针
//引用
//大部分场景去替代指针,部分场景还是离不开指针
void Swap1(int* rx, int* ry)
{
	int tmp = *rx;
	*rx = *ry;
	*ry = tmp;
}
void Swap2(int& rx, int& ry)
{
	int tmp = rx;
	rx = ry;
	ry = tmp;
	//rx 和 ry 是 x 和 y 的别名
	//本质上还是x和y
}
int main()
{
	int x = 0, y = 1;
	/*cout << x << " " << y << endl;
	Swap1(&x, &y);
	cout << x << " " << y << endl;*/
	cout << x << " " << y << endl;
	Swap2(x, y);
	cout << x << " " << y << endl;
	return 0;
}

引用帮助我们轻松实现了交换函数,这里我们会发现引用会比指针更便于我们去理解。

如果我们要交换指针的地址,之前我们只能使用二级指针,而现在我们就可以去使用引用解决问题:

//解决更复杂的场景
#include
using namespace std;
void Swap1(int** pp1, int** pp2)
{
	int* tmp = *pp1;
	*pp1 = *pp2;
	*pp2 = tmp;
}
void Swap2(int*& rp1, int*& rp2)
{
	int* tmp = rp1;
	rp1 = rp2;
	rp2 = tmp;
}
int main()
{
	int x = 0, y = 1;
	int* p1 = &x, * p2 = &y;
	//俩种交换方式,一种采用二级指针,一种采用引用
	return 0;
}

这里大家在有些教材中可能看到以下的一些写法:

typedef struct SListNode
{
	struct SListNode* next;
	int val;
}SLTNode, * PSLTNode;
//等价于
typedef struct SListNode SLTNode;//结构体的变量
typedef struct SListNode* PListNode;//结构体的指针

大家第一次看可能会一头雾水,在这里为大家点出来希望可以帮助到大家。

//引用不可以完全取代指针
//在链表、树结点定义位置,就只能使用指针
//原因:C++的引用无法改变指针,结点一定存在改变指向场景,所以这里无法引用
#include
using namespace std;
//传引用返回 -- 本质相当于返回ret的别名
//int& func() 表示函数 func 的返回值是一个int 类型的引用,
//即返回的是某个 int 变量的 “别名”。
int& func()
{
	int ret = 0;
	//...
	return ret;
}
int main()
{
	int x = func();
	cout << x << endl;
	//这个x可能是0可能是随机值,不确定
	//这里越界了,但是越界不一定会报错,越界写才会报错
	int& y = func();
	cout << y << endl;
	//相当于y是ret的别名
	return 0;
}
2.4、const引用
  • 可以引用一个 const 对象,但是必须用 const 引用。const 引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
  • 不需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样一些场景下 a*3 的和结果保存在一个临时对象中,int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值,也就是时,rb 和 rd 引用的都是临时对象,而 C++ 规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。
  • 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++ 中把这个未命名对象叫做临时对象。
2.4.1、part 1
#include
using namespace std;
int main()
{
	const int a = 0;
	//权限放大
	//int& b = a;
	//如果还想使用就需要让他们权限相同
	const int& b = a;
	int c = 0;
	//这是可以的,这是权限缩小
	//c自身可以修改,但是d作为c的别名的时候是不可以修改的
	const int& d = c;
	//总结一下:使用引用的时候权限可以缩小或相同,但是不可以放大权限
	//这里不是权限放大,这里是个拷贝
	int e = a;
	const int& b = a;
	return 0;
}
权限的放大和缩小只存在const指针和&引用,权限只可以缩小或者不动,不可以放大!

const引用也是可以引用常量的

#include
using namespace std;
int main()
{
	//const引用是可以引用常量的
	const int& a = 10;
	return 0;
}

如果我们以后写代码不需要使用形参去改变实参,尽量去使用const:

#include
using namespace std;
void func(const int& x)
{
	//...
}
int main()
{
	///const int& a = 10;
	int y = 10;
	func(y);
	const int z = 1;
	func(z);
	func(2);
	return 0;
}
2.4.2、part 2

我们来看下面一段代码

#include
using namespace std;
int main()
{
	int i = 1;
	double d = i;
	int p = (int)&i;
	const double& rd = i;
	const int& rp = (int)&i;
	return 0;
}

分析:

这里我们首先创建了一个变量i,接着我们想要将i的值赋给b,但是由于他们所处的类型不同不可以直接赋值,实际上他会先产生一个临时变量(我们之前说的传值返回也会先创建一个临时变量),也就是说i不是直接给给d的,i是先给给一个临时变量(按照浮点数的类型去去存储),临时变量再给给d;上述的就是隐式类型转换,下面我们将i的地址强制类型转换为int类型的数值赋给p,这里是显式类型转换,依旧会先产生一块临时空间然后将i的地址强制类型转换成一个数值放到临时变量里,然后再将临时变量的值赋给p,我们要知道,临时空间具有常属性,就像被const修饰了一样,接着分析下一句:rd引用的是临时变量,所以我们也要加上const,否则会造成权限放大,最后一句与上述类型,也要加上const。

2.5、指针和引用的关系

C++ 中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。

  • 语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。
  • 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
  • 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
  • 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
  • sizeof 中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数 (32 位平台下占 4 个字节,64 位下是 8byte)
  • 指针很容易出现空指针和野指针的问题,引用很少出现(不代表不会出现,例如返回局部引用就会出现野引用),引用使用起来相对更安全一些。

引用的底层是用指针实现的