C++(2)--- 引用,缺省参数,函数重载,inline,nullptr

一、缺省参数

1.1什么是缺省参数

在C语言里,函数参数就两种形态,一种无参,一种有参;数在C++中新引入了能给有参函数的参数一个默认值的情况,这就是缺省参数。
示例代码如下:

#include<iostream>
using namespace std;

int ADD(int x = 100,int y = 200)         //使用的就是给定参数的默认值
{
	return x + y;
}

int main()
{
	//1.缺省参数
	int sum1 = ADD();       //这里没有传递参数 ---- 此时输出300
	cout << "sum1= " << sum1 << endl;

	int sum2 = ADD(10, 20); //这里给定参数 ---- 此时输出30
	cout << "sum2= " << sum2 << endl;
	return 0;
}

输出结果:
在这里插入图片描述
上述代码中 ADD(int x = 100,int y = 200) ,这种形式的就是缺省参数。

1.2为什么有缺省参数

有人就会有疑问了,既然函数参数我们可以自己给定,为何还要给其设定默认值呢?
举个例子:
在学习栈这种数据结构时,我们定义了一个初始化方法,其中一个参数传递的是起始数据的个数,若我们一开始就知道要插入100个数据时,可以直接在实参处指定100即可,相反一开始不知道要插入多少个数据时,就使用缺省参数(默认值)初始化即可,要是自己输入,没准要么空间小了(频繁扩容),要么空间多了(空间浪费),随说使用默认值,后续也可能会开辟新的空间,但比起自己输入来说更加省心一点。

1.3怎么使用缺省参数

缺省参数有两种状态,全缺省和半缺省。

1.3.1全缺省

当函数参数全部指定默认值时,即为全缺省状态,像上述ADD函数这种就是全缺省状态。
示例代码如下:

//全缺省状态
void Func(int a = 10, int b = 20, int c = 30)   
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

1.3.2半缺省

当函数参数只指定了几个参数默认值时,即为半缺省状态。
示例代码如下:

//半缺省状态
void Func(int a, int b = 20, int c = 30)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

1.3.3缺省的注意事项

1)C++规定在给定函数参数缺省值时,必须从右向左缺省,不能跳跃缺省,否则会报错。
示例错误代码如下:

//形参b没有给定缺省值
void Func(int a = 10, int b, int c = 30)    
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

报错如下:
在这里插入图片描述

2)在函数给定实参时,必须从左向右给定实参,也不能跳跃给定,否则报错。
示例错误代码如下:

//这里本意是实参10给形参a,让b使用缺省值,实参100给形参c --- 会报错
int main()
{
	Func(10,,100);  
	return 0;
}

报错如下:
在这里插入图片描述
直接成为一个语法错误。

3)函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
例如对栈的初始化,给定的缺省值只能在声明时给定,否则报错。
示例代码如下:

//Stack.h
void STInit(ST* ps, int n = 4);

//Stack.cpp
#include"Stack.h"
// 缺省参数不能声明和定义同时给
void STInit(ST* ps, int n)      //假设在这里也给定缺省值int n=4
{
	assert(ps && n > 0);
	ps->a = (STDataType*)malloc(n * sizeof(STDataType));
	ps->top = 0;
	ps->capacity = n;
}

报错如下:
在这里插入图片描述

二、函数重载

2.1什么是函数重载

函数重载就是函数可以同名,用函数参数来区分。
示例代码如下:

//交换函数重载 --- 函数名相同,
void Swap(int* x,int* y)
{
	int temp = *x;
	*x = *y;
	*y = temp;
}

void Swap(double* x, double* y)
{
	double temp = *x;
	*x = *y;
	*y = temp;
}

2.2为什么有函数重载

在C语言里,函数是无法同名的,就有人有疑问了,函数不能同名不是理所应当的嘛,调用一个函数如果同名,那还怎么分得清调用的是哪一个函数啊?
我来举个例子:
假设程序需要Swap交换数据函数,按照上述说法,如果每种数据类型都需要交换函数,那岂不是要写很多种的交换函数,就算函数重载这种形式下写也要写这么多的交换函数,若在导入第三方库的情况下,每个程序员对于函数命名的方式都不一样,比如对于整型变量的交换,我自己写是Swapint,那其他程序员可能写成SwapInt ,要是这样调用就会很麻烦,所以在C++里就引入了函数重载。

2.3怎么使用函数重载

函数重载的区分在于函数参数。

2.3.1函数参数类型不同

像上述Swap函数就是参数类型不同。

2.3.2函数参数个数不同

示例代码如下:

//函数参数个数不同
void f()
{
	cout << "f()" << endl;
}

void f(int a)
{
	cout << "f(int a)" << endl;
}

//main函数内:
	//函数参数个数不同
	f2();
	f2(10);

输出结果:
在这里插入图片描述

2.3.3函数参数顺序不同

示例代码如下:

在这里插入代码片
//函数参数顺序不同
void f(int x,double y)
{
	cout << "(int x,double y)" << endl;
}

void f(double x,int y)
{
	cout << "(double x,int y)" << endl;
}

//main函数内:
	//函数参数顺序不同
	f3(10,1.0);
	f3(1.0, 10);

输出结果:
在这里插入图片描述

2.3.4函数重载不能以返回值的不同来界定

示例错误代码如下:

//试验 --- 函数返回值不同能否成为重载函数
void f4(int a)
{
	cout << "f(void返回类型)" << endl;
}

int f4(int a)
{
	cout << "f(整型返回类型)" << endl;
	return 0;
}

错误结果:
在这里插入图片描述
所以不能以返回值来定义重载函数。

2.3.5函数重载的错误写法

示例代码如下:

void F()
{
	cout << " ! " << endl;
}

void F(int x = 10)
{
	cout << " ? " << endl;
}

int main()
{
	F();
	return 0;
}

建议不要这样写函数重载,会产生歧义,函数不知道要去调用哪一个函数。
错误结果:
在这里插入图片描述

三、引用

3.1什么是引用

在C++中引入了一个叫引用的概念,引用就是取别名,对一个变量取一个新的名称,用来代指此变量。
示例代码如下:

//引用 --- 取别名
#include<iostream>
using namespace std;

int main()
{
	int x = 10;
	int& y = x;      //这里的y就是x的别名
	return 0;
}

在C++中引用使用&号表示。
定义方式:类型& 引⽤别名 = 引⽤对象

比如说:水浒传里的李逵,有个外号叫“黑旋风”,又被宋江称为“铁牛”,这里“黑旋风”和“铁牛”都代指的李逵,也就是他的别名。
此时既然别名也是代指的此变量,那么就说明改变别名也就是改变代指的变量。
示例代码如下:

//继续上面的代码:
	y = 20;
	cout << "x= " << x << endl;
	return 0;

输出结果:
在这里插入图片描述
为什么改变了别名就将代指的变量也给改变了?
探究原因:打印它们的地址看看
示例代码如下:
在这里插入图片描述
你会发现这两的地址是一模一样的,那不就跟指针改变值是类似的嘛,指针改变它指向的变量的值,引用直接改变代指变量的值。

3.2为什么有引用

仔细观察上述代码就会发现,这个引用怎么和指针的表示方法一样啊,只是少了一个取地址操作。
其实就是这样的,有一个形象的表述:引用和指针是一对“亲兄弟”,指针是“哥哥”,引用是“弟弟”。
众所周知,C语言的指针既复杂又难理解的,我们的本贾尼博士也考虑到了这点,所以设计出了引用,用来在特定场景下代替指针进行操作,用来简化代码,提高程序的可读性。

3.3怎么使用引用

3.3.1引用传参代替指针传参 ---- 这个场景和指针用法是重叠的

引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
示例代码如下:
场景1:

//引用传参代替指针传参
#include<iostream>
using namespace std;

//修改形参,影响实参
void Swap(int& a, int& b)      //使用引用代替指针
{
	int temp = a;
	a = b;
	b = temp;
}

int main()
{
	int x = 10, y = 20;

	Swap(x, y);

	cout << "x= " << x << " y= " << y << endl;
	return 0;
}

输出结果:
在这里插入图片描述
上述功能就是引用修改形参,影响实参。

场景2:

//减少拷贝时间,提高效率
struct Sqlist
{
	//当数据过大的时候,此时传值传参的效率就很低,此时就可以使用引用或者指针
	int a[100];  
	int size;
	//……
};

void func(struct Sqlist* b)
{}

void func(struct Sqlist& b)
{}

int main()
{
	struct Sqlist a;
	func(&a);
	func(a);
}

上述功能就是减少拷贝时间,提高效率。

场景3:
示例代码如下:

//交换指针变量 --- 引用代替二级指针
void Swap(int*& p1,int*& p2)
{
	int* temp = p1;
	p1 = p2;
	p2 = temp;
}

int main()
{
	int x = 10, y = 20;
	int* p1 = &x;
	int* p2 = &y;

	cout << p1 << endl;
	cout << p2 << endl;

	Swap(p1,p2);

	cout << p1 << endl;
	cout << p2 << endl;
	return 0;
}

输出结果:
在这里插入图片描述
上述功能就是引用代替二级指针。
这个场景也能引申到数据结构链表的学习,像我们要影响一个链表的结果那么必然就需要使用到传址调用,链表中又有非常多二级指针的使用,所以学习了引用在这里就能代替二级指针,让代码更好理解,更简洁。

3.3.2传引⽤返回

学习过C语言就知道传值返回的结果并不是直接返回其结果,返回的是结果的临时拷贝,是一个临时变量,而临时变量又具有常性,所以想要直接对其返回结果进行某种操作的话,会报一个左操作数必须为左值这样一个错误。
举例说明:
创建一个场景,实现一个顺序表进行查找下标为i的元素,并将其修改的方法。
示例代码如下:

//引用返回值
//创建顺序表结构 --- 动态内存开辟
typedef int SLDataType;          //int重命名
typedef struct SeqList           //结构体重命名
{
	SLDataType* arr;             //指向顺序表的指针
	int size;                    //有效数据个数
	int capacity;                //空间容量
}SL;


//初始化
void SLInit(SL& ps,int n = 4)
{
	ps.arr = (SLDataType*)malloc(sizeof(SLDataType) * n);      
	ps.size = 0;
	ps.capacity = n;   
}

//尾插
void SLPush_Back(SL& ps, SLDataType x)
{   
	//空间不足的时候 --- 先增容
	//……
	//空间充足的时候 --- 插入
	ps.arr[ps.size++] = x;
}

//找到下标为i的元素
SLDataType SLAT(SL& ps,int i)
{
	assert(i < ps.size);
	return ps.arr[i];
}
int main()
{
	SL s;
	SLInit(s);
	SLPush_Back(s, 1);
	SLPush_Back(s, 2);
	SLPush_Back(s, 3);
	SLPush_Back(s, 4);

	for (int i = 0; i < s.size; i++)
	{
		SLAT(s, i) += 1;  //这里会报错
	}
	
	for (int i = 0; i < s.size; i++)
	{
		//返回第i下标的元素
		cout << SLAT(s, 3) << endl;
	}

	return 0;
}

将找到下标为i的元素的方法,并且修改其i位置上的值,拿出来做一个图解:
在这里插入图片描述
所以要想达到直接修改其i位置上的值,这里就要用到传引用返回:
示例代码如下:

//找到下标为i的元素
SLDataType& SLAT(SL& ps,int i)    //只是多了一个引用符
{
	assert(i < ps.size);
	return ps.arr[i];
}

输出结果:
在这里插入图片描述

没有这个取别名,那么其函数返回值是一个临时变量,具有常性,是一个不可修改的状态。
这里取了别名,取的这个变量是数组i位置上的元素(此元素在堆上,没有free则一直存在),既然修改别名就是修改代指的变量,所以这里就直接绕过了创建临时变量这一步,直接“直捣黄龙”,成功修改。

当然引用做返回值类型同3.3.1中场景2里面处理非常多的数据时,同样也可以减少拷贝时间,提高效率。

3.3.3错误的引⽤

示例代码如下:

//错误的引用
int& f()
{
	int temp;     //局部变量
	//……
	return temp;  //出函数就被销毁了
}

int main()
{
	f();
	return 0;
}

这里对一个被销毁对象进行引用,就好比指针里面的野指针,这是一种类似于野引用的操作,
这种操作是绝对不行的。

3.4const引用

在C语言阶段,const就是使一个变量具有常量不可改变的属性,但本质还是一个变量,也就称其为常变量。
这里引入一个概念叫做权限的放大,平移,缩小。比如说const就是使一个变量具有常量不可改变的属性,这就是权限的缩小。

权限平移的示例代码如下:

	//权限平移 --- a可修改,b也是可修改
	int a = 10;
	int& b = a;

	//权限平移 --- x不可修改,y也不可修改
	const int x = 10;
	const int& y = x;

权限缩小的示例代码如下:

	//权限缩小 --- m可修改,n不可修改
	int m = 10;
	const int& n = m;

权限放大的示例代码如下:

	//权限放大 --- i不可修改,j可修改   ,这种是错误的
	const int i = 10;
	int& j = i;    //这里有报错

报错如下:
在这里插入图片描述
const引用的特殊用法:
1)能对常量取const别名
2)对类型转换(比如double赋值给int)的结果取const别名
3)对运算表达式取const别名
示例代码如下:

	//1)能对常量取const别名
	const int& k = 10;

	//2)对类型转换(比如double赋值给int)的结果取const别名
	double x = 1.1;
	//int y = x;
	const int& y = x;

	//3)对运算表达式取const别名
	int m = 1;
	const int& p = m * 10;

对于1),常量不可修改,自然const引用就可以对它取别名。
对于2),这里的赋值并非是将x的整数部分直接赋值给y,而是中间会创建一个临时变量,这里面存储x的整数部分,再将其赋值给y,在上文说过,临时变量具有常性,所以这里要引用,前面需要加上const。
对于3),道理同2),m * 10的结果也不是直接赋值的,而是中间创建一个临时变量,将临时变量里存储的值赋值给目标对象,而临时变量具有常性,所以这里要引用,同样前面需要加上const。

3.5引用的特性

1)一个变量可以有多个引用,可以给引用再引用
示例代码如下:

int main()
{
	int a = 0;

	//一个变量可以有多个引用
	int& x = a;
	int& y = a;
	int& z = a;

	//也可以给引用再引用
	int& m = x;
	int& n = y;
	int& k = z;
	
	return 0;
}

2)引⽤在定义时必须初始化
示例代码如下:

//2)引⽤在定义时必须初始化
int main()
{
	int b = 0;
	int& x;        //这里会报错
	int& y = b;    //这样才正确
	return 0;
}

报错如下:
在这里插入图片描述
3)引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
示例代码如下:

//3)引用一旦引用一个实体,再不能引用其他实体
int main()
{
	int a = 0;
	int& b = a;

	int c = 10;
	b = c;          //这里并不是让b去引用c,而是赋值

	cout << "b= " << b << endl;   //b=10
	return 0;
}

所以有这个特性,再有些情况下引用就不能替代指针了,比如说在实现链式结构这种,要改变指针指向,这种情况下就不能使用引用,只能使用指针。

3.6引用与指针的关系和区别

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

四、inline

4.1什么是inline函数

用inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就不需要建⽴函数栈帧了,就可以提⾼效率。
这里的调⽤的地⽅展开内联函数就和C语言阶段里面的宏的替换是相似的。

4.2为什么有inline函数

这里inline函数就是用来代替C语言宏函数的。
仔细观察下列宏函数代码:

//定义正确的宏函数ADD
#define ADD(x,y) ((x)+(y))

你会发现就这么一个简单的ADD相加函数,用宏的表示怎么会如此的复杂,括号里面还要套一层括号,那有人就会有疑惑了,这看着也不复杂啊,括号套括号不就是将x,y分离嘛。是这样的没错,但是这样的括号添加都是由实际场景来推导出来的:
1)宏不能带分号
在C语言阶段接触过宏就会发现宏末尾都是不带分号的,你知道这是为什么嘛,是因为宏的本质是替换,当替换进如下代码时就知道为何宏要不带分号了:
示例代码如下:

int main()
{
	if (ADD(1, 2))    //这里是将((x)+(y));这一坨代换
	{                 //这里什么都不是吧,所以宏不能带分号

	}
	return 0;
}

2)最外层的括号
直接代入场景,示例代码如下:

	int num = ADD(1, 2) * 5;     //结果为11,并不是15
	cout << num << endl;

最外层的括号就是保证使表达式先进行ADD加法,再进行乘法。
3)内层的括号
直接代入场景,示例代码如下:

	int x = 10, y = 20;
	int num = ADD(x | y, x & y);  //这里运用代换就成为:num = x | y + x & y

你会发现上述代码符号的优先级不同,先算的是y+x,再&,再|。
经过上述推导,会发现一个小小的宏函数背后竟然有这样奥秘。所以宏函数的定义是挺麻烦的。
宏函数不仅仅有定义复杂这一个缺点,还有(宏常量)类型安全的检查,不能调试这些缺点,
但是也是有优点的,最大的优点就是宏在预处理阶段就(替换)展开了,不用建立栈帧,本质是一种提效。
为了解决这些缺点,C++就新引入了一个inline函数,并且也继承其优点。

4.3怎么用inline函数

C++提供一个关键字inline,放在所需函数的函数名之前,即可成为内联函数。
示例代码如下:

inline int Add(int x, int y)
{
	int ret = x + y;
	return ret;
}

如何观察一个函数是否成为内联函数了呢?
此时就要使用到汇编层。
没有展开,也就是不是内联函数时:
在这里插入图片描述

会观察到有一行有一个call的东西(不理解不重要),此句最后跟的那一串地址就是函数的地址,当有call时,就代表没有展开。
展开,是内联函数时:
补充:vs编译器默认是不展开的,所以我们需要手动设置一下。
先进入属性页面,按照下图设置方式点击即可。
在这里插入图片描述
在这里插入图片描述
此时再来观察刚刚的inline函数:
在这里插入图片描述
会发现没有call这一句,所以他展开了。

4.3.1inline函数的建议:

1)虽然inline函数是不用建立函数栈帧的,但是这只适用于短小和频繁调用的函数,假设你给所有的函数(比如函数递归,代码特别多的函数)都加上inline,编译器会取代你这种“乱用”的作为,加上inline也会被编译器忽略掉(inline对于编译器来说只是一个建议)。
举例上述Add场景多加几句代码,示例代码如下:

inline int Add(int x, int y)
{
	int ret = x + y;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	return ret;
}

同样的环境,同样的配置,如果inline代码过多,编译器会选择忽略的:
在这里插入图片描述
变成了没有展开的样子。
2)inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
所以需要把inline定义到同一个头文件(.h)下,因为inline函数不需要链接,不用进入符号表,和static相似。

五、nullptr

在C语言里NULL可以看作空指针,也可以看做为0,但在C++环境下,使用NULL会有如下的歧义场景:
在这里插入图片描述
会发现明明第二个函数参数给的NULL,可还是执行的是第一个函数,这就是C++里NULL的歧义场景,为了解决这一歧义,C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。
使用nullptr场景如下:
在这里插入图片描述
这样就不会对NULL产生歧义了。

六、总结

回顾上一篇和本片写的内容,其实都是C++对于C语言的一系列补全,比如namespace是为了解决命名冲突的问题;cin,cout解决C语言不能输入输出任意类型数据;引用是为了在某些特定场景下代替复杂的指针使用;inline解决C语言宏函数的问题;以及nullptr,解决NULL在C++某些场景下有歧义的问题;其他的也是对于C语言的补充和优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值