首页 > 编程语言 >【C++】穿越编程岁月,细品C++进化轨迹,深化入门基石(续章)——揭秘函数缺省参数的魅力、函数重载的艺术、引用的奥秘与内联函数的效率

【C++】穿越编程岁月,细品C++进化轨迹,深化入门基石(续章)——揭秘函数缺省参数的魅力、函数重载的艺术、引用的奥秘与内联函数的效率

时间:2025-01-10 20:57:46浏览次数:3  
标签:函数 int 缺省 C++ 引用 指针

在这里插入图片描述

文章目录

一、函数缺省参数

   缺省参数其实就是默认参数,它是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数(有些地⽅把缺省参数也叫默认参数)

   为了更好理解缺省参数,我们直接来演示一下它的用法,案例就是我们之前写过的顺序表初始化函数,如下:

void SLInit(SL* ps, int n = 10)
{
	assert(ps);
	//C++也有自己的一套内存开辟方法,后面会讲到
	//现在暂时使用malloc
	SLDataType* tmp = (SLDataType*)malloc(sizeof(n));
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}
	ps->arr = tmp;
	ps->size = 0;
	ps->capacity = n;
}

   在这个初始化函数中,我们多增加了一个参数,这个参数就是缺省参数,用户在调用这个函数时可以不传第二个参数,这个时候就会使用缺省参数10,给顺序表开辟好10个元素大小的空间

   如果用户在使用前就大致知道了自己要使用多大空间,就可以直接自己传第二个参数,如果用户传了参数,之前的默认参数就失效了,函数会按照用户传的num来给顺序表开辟默认大小

   这样做的好处就是可以避免频繁向堆区申请空间,可以提高性能,从这个例子我们也可以看出缺省参数确实很有用

   接着我们来介绍一下缺省参数的分类,全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值,注意不是一半的参数给缺省值,而是“部分”,同时C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值,比如:

//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
 {
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl << endl;
 }
 
//半缺省,半不是一半的意思,是“部分”
//半缺省要从右往左给默认值
void Func2(int a, int b = 10, int c = 20)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl << endl;
}

   在上面的半缺省函数中,要从右往左给默认值,跟赋值的顺序一致,如果我们我们可以把需要自己传的参数写在前面,后面就可以交给默认参数,当然也可以自己传

   在传参时,C++规定必须从左到右依次给实参,不能跳跃给实参,比如当我们给Func1函数传参时,可以只给a传,可以只给a,b传,也可以都传,但是不能跳过a给b传,因为这样编译器分不出来到底要传参给谁

   函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值,因为如果声明和定义的缺省参数不同的话,编译器也不知道到底采用哪一个缺省值,所以干脆语法上直接规定在函数声明给缺省值

二、函数重载

   函数重载就是C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同,但是返回值不同不能构成函数重载,这个我们稍后举例解释

   这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活,C语⾔是不⽀持同⼀作⽤域中出现同名函数的,比如相加函数Add,如果我们要整型相加就要写一个整型Add,如果要浮点型相加就要写一个浮点型相加,并且两个函数名字不能都为Add

   那么我们要实现相加函数还要想办法取不同的名字来避免名称冲突,所以C++引入了函数重载,只要参数不同,那么函数名就可以相同,所以我们可以实现多个Add来支持多种类型的相加,如下:

#include <iostream>
using namespace std;

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

double Add(double x, double y)
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 20;
	double c = 10.1;
	double d = 20.5;
	cout << Add(a, b) << endl;
	cout << Add(c, d) << endl;
	return 0;
}

   我们这两个Add函数的参数不同,构成了函数重载,编译器会根据类型自动匹配对应的函数,比如上面传a和b的函数会调用参数为int的Add函数,传c和d的函数会调用参数为double的Add函数,如图:

在这里插入图片描述

   那么我们现在来测试一下,如果两个同名函数只有返回值不同,能不能构成函数重载,如下:

int Func()
{
	return 1;
}

double Func()
{
	return 1.1;
}

int main()
{
	cout << Func << endl;
	return 0;
}

   我们来看看代码运行后会发生什么:

在这里插入图片描述

   可以看到编译器报错了,告诉我们这两个函数无法构成重载,因为它们仅仅只有返回值不同,在调用Func函数时,编译器也不知道要调用哪个函数,有了冲突,所以两个函数只有返回值不同是无法构成函数重载的

三、引用

1.引用的概念和定义

   引用是C++提出的新概念,它不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,从语法角度来说,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间,就像我们小时候的乳名一样,叫现在的名字是我们,叫乳名也是我们

   引用的大致使用格式如下:

类型& 别名 = 引⽤对象

   类型后面跟的就是跟取地址一样的符号,但是要注意区分,那并不是取地址操作符,而是引用操作符,那么为什么这个符号都已经用作取地址了,还要拿来给引用使用呢?

   这是因为C++为了避免引入过多的运算符,所以把一些符号复用了,比如这里的引用和之前的流插入<<和流提取>>(C语言中也用作位运算符),我们要特别注意区分一下这些符号

   接下来我们就简单演示一下引用的使用,后面还会具体讲到引用的使用场景:

int main()
{
	int a = 10;	
	cout << a << endl;
	//b就是a的别名
	//从语法角度上别名不占空间
	int& b = a;
	b = 8;
	cout << a << endl;
	return 0;
}

   这里b就是a的别名,如果我们修改b的话,a会不会跟着被修改呢,我们来看看代码运行结果:

在这里插入图片描述

   可以看到我们修改a的别名b,也会把a跟着修改了,因为从概念上来说b是a的别名,和a共用同一块空间,修改了b自然也就修改了a

2.引用的特性

   (1)引用在定义时必须初始化,这一点和指针不同,指针在定义时可以不初始化,而引用不初始化化就会报错
   (2)一个变量可以有多个引用,也就是一个变量可以取多个别名,并且取别名后再对别名引用,这个引用还是原变量的引用
   (3)一个引用一旦确定,就不能再更改为其它变量的引用

//演示第(2)条特性
int main()
{
	int a = 10;
	//b是a的别名
	int& b = a;
	//c是b的别名
	int& c = b;

	c = 20;
	cout << a << "  " << b << endl;
	return 0;
}

   在上面的代码中,b是a的别名,c是b的别名,那么按理来说c也是a的别名,因为b和a都共用同一块空间,c和b共用一块空间,自然a和c就共用一块空间了,那么我们来看看代码运行结果:

在这里插入图片描述

   接下来我们来演示第(3)条特性

int main()
{
	int a = 10;
	int& b = a;
	int c = 20;
	b = c;
	return 0;
}

   这里b是a的引用,当我们写出b = c之后,不会改变b对a的引用,让它变成c的引用,而是一个赋值操作,将c的值赋值给b,同时也是赋值给a

   这也是引用无法完全替代指针的原因,虽然后面使用指针的大部分情况都可以使用引用替换,但是在数据结构上引用不能代替指针,比如链表,当我们删除节点后,一个引用不能改变指向引用到下一个节点

   所以大部分情况下指针和引用相辅相成,大部分情况下可以使用引用代替指针,一些特殊情况下还是要使用指针

3.引用的使用

   (1)引⽤在实践中主要是于引⽤函数传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象,因为引用只是原变量的别名,从语法角度上并没有开辟新的空间,更加有效率

   (2)引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些,比如我们使用引用来写一个Swap函数,如下:

void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

   由于x和y是对应变量的引用,所以我们在使用时不必再解引用了,写起来非常方便

   (3)至于引用作为返回值我们就要格外小心,因为有时候我们会返回局部变量的引用,当局部变量出函数的作用域后会销毁,那么引用也就变成了野引用,如下:

int& Func()
{
	int a = 10;
	return a;
}

   当a出了函数Func的作用域之后,a就被销毁了,那么我们返回a的别名,就变成了野引用,所以我们要保证我们返回的引用不是一个局部变量,而是一个即使函数栈帧被销毁了也依然存在的变量

   (4)引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代, C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向

   (5)⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但实际上引用和指针的结合使用反而更容易让学生混淆了,况且C教材使用C++语法本身就不是很合理

4.const引用

   引用是可以引用一个const对象的,但是这个引用必须是const引⽤,const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩和平移,但是不能放⼤,为了更好地理解这句话,我们举一个例子:

int main()
{
	const int a = 20;
	//a和b都被const修饰不能更改
	//权限一致,所以叫权限的平移
	const int& b = a;
	
	//c没有被const修饰,可以更改
	//但是a被const修饰不能更改
	//所以这属于权限放大,是不被允许的
	int& c = a;

	int x = 10;
	//原本x可以更改
	//但是y被const修饰,不能更改
	//所以这属于权限的缩小
	const int& y = x;
	return 0;
}

   根据上面代码的分析,我们大致知道了权限的缩小、放大和平移,其中权限的放大是不被允许的,所以我们要谨慎使用,但是我们也要注意的一些我们可能会误会的情况,我们来看这样一个问题,如下:

//这里的b属于权限放大吗?
int main()
{
	const int a = 20;
	int b = a;
	return 0;
}

   很明显这是不属于权限的放大的,因为这里根本没有涉及到引用,这里只是把a的值赋值给了b,没有发生有关权限的操作,所以不要搞混了,一定要注意看有没有引用

5.指针和引用的关系

   1. C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代
   2. 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间(实际在汇编层引用的本质就是使用了指针,但是语法层面上引用没有开空间)
   3. 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的
   4. 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象
   5. 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象
   6. sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
   7. 指针很容易出现空指针和野指针的问题,引⽤很少出现野引用,引⽤使⽤起来相对更安全⼀些

四、inline内联函数和nullptr

1.inline

   当函数最前面加上inline后,我们称它为内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就不需要建⽴栈帧了,就可以提⾼效率
它就和我们C语言里面的宏函数差不多的作用,但是要比宏要更加稳定和可控,并且宏还不方便调试,C++中的内联函数就是为了替代宏函数

   不过我们要注意的是inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个

   因此inline内联函数更适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略

   同时inline不建议声明和定义分离到两个⽂件,分离会导致链接错误,因为inline被展开,直接替换成了对应的语句,类似于宏展开,也就没有函数地址,那么链接时会出现报错,所以我们可以直接把它定义到头文件中

   那么接下来我们简单地举一个例子:

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

   在这个Add函数中,它的逻辑十分简单,甚至只有一条语句,所以为了避免要开辟函数栈帧造成浪费,我们将它定义为内联函数,使它在调用的地方直接展开,而不需要开辟它的函数栈帧

2.nullptr

   nullptr也的出现是为了解决C++和C当中空指针的坑,我们首先来看看C++和C是如何定义空指针NULL的,方法就是:在main函数中写出NULL,然后ctrl + 单击即可跳转到NULL的定义中,如图:

在这里插入图片描述

   在C++和C中,NULL其实是一个宏,这个条件编译指令我们在C语言预处理阶段已经学习过了,如果看不懂参考:【C语言】预处理(预编译)详解(下)(C语言最终篇)

   那么现在我们直接翻译上面的宏语句,首先第一句就是,如果没有定义NULL那么就进入下面的判断,同时这里嵌套了一个ifdef

   因为如果是C++文件,在文件前面会定义一个标志_ _cplusplus,这里就是判断前面有没有定义这个标志,如果定义了,说明是C++文件,然后C++文件中的NULL就被定义成了0,否则不是C++文件的话,NULL就被定义为了(void*)0

   这两种定义其实都各有不足,我们来举一个例子:

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

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

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

   在上面代码中,有两个函数f,它们构成函数重载,我们期望f(0)调用第一个函数,f(NULL)调用第二个函数,那么事实如我们所愿吗?我们来看看代码运行结果:

在这里插入图片描述

   我们发现两次调用都调用的是第一个函数,原本NULL应该当作指针处理呀?原因就是前面我们说过的,在C++文件中,NULL被定义为了0,这是一个大坑,那么我们能否使用C语言的定义形式呢?
   在C++中,这也是不被允许的,不能随意把整型转成void*的指针,所以会报错,如图:

在这里插入图片描述

   所以C++为了弥补这个错误,C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型
   使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型,所以如果我们在C++中想要使用空指针就直接使用nullptr即可,如图:

在这里插入图片描述

   如上图所示,当我们使用nullptr中就可以正确匹配到对应重载的函数,所以我们要及时在学习的初阶就把习惯养成,用nullptr表示空指针,而非NULL

   那么今天C++入门基础的知识(下)就分享到这里啦,欢迎点赞三连,有什么问题欢迎给我私信,我会尽快回复
   bye~

标签:函数,int,缺省,C++,引用,指针
From: https://blog.csdn.net/hdxbufcbjuhg/article/details/143952674

相关文章

  • 【C++】揭开C++类与对象的神秘面纱(首卷)(类的基础操作详解、实例化艺术及this指针的深
    文章目录一、类的定义1.类定义格式2.类访问限定符3.类域二、类的实例化1.实例化概念2.对象的大小三、隐藏的this指针与相关练习1.this指针的引入与介绍练习1练习2练习3一、类的定义1.类定义格式   在讲解类的作用之前,我们来看看类是如何定义的,在C++中,class......
  • cursor创建微信小程序+云函数+数据库
    111111111111111111111111111111111111111111   我想要创建一个微信小程序,请帮我创建完整的微信小程序的目录和所有必需的文件,保证其可以运行。  我想要做一个基于音标来背单词的小程序,首页有三个功能,第一个功能是背单词,第二个功能是,查看我背过的单词,第三个......
  • Qt C++学习笔记1.7
    1.7Qt入门:实现一个图片查看软件需要用到的控件:QLabelQLineEditQPushButton需要实现的功能:打开目录选择图片显示图片的名字显示图片QLabel基本用法设置文本voidsetText(constQString&);获取文本QStringtext()const;设置图片voidsetPixmap(constQPixm......
  • C++并发编程之基于锁的数据结构的适用场合与需要考虑和注意的问题
    在C++多线程编程中,锁是一种常用的同步机制,用于保护共享数据,防止多个线程同时访问和修改,从而避免数据不一致或其他并发问题。基于锁的数据结构适用于多种并发编程场合,但同时也需要注意一些关键问题。1. 适用的并发编程场合锁在以下几种场合特别有用:1.1 保护共享数据当多个......
  • 详解 C++ 防御性编程声明一个类型 int *(*(*foo)(int))[5];
    C++中有一些语法由于灵活性和强大功能显得非常复杂。例如,复杂声明是许多人在学习C++时遇到的难题之一。下面以一条常被称为“C++最难的声明”为例,逐步拆解它的含义。声明:int*(*(*foo)(int))[5];这是一个看似复杂的C++声明。让我们逐步分析它的含义。1.阅读......
  • c++ 赋值运算符的定义
    1.赋值运算符的定义赋值运算符是用于修改已有对象的内容,而不是用于创建新对象。其典型的定义如下:Person&Person::operator=(constPerson&other);Person&Person::operator=(Person&&other);左侧对象(*this):表示已经存在的目标对象。右侧对象(other):表示要从中复制或转......
  • C/C++ 数据结构与算法【排序】 常见7大排序详细解析【日常学习,考研必备】带图+详细代
    常见7种排序算法冒泡排序(BubbleSort)选择排序(SelectionSort)插入排序(InsertionSort)希尔排序(ShellSort)归并排序(MergeSort)快速排序(QuickSort)堆排序(HeapSort)计数排序(CountingSort)算法复杂度1、冒泡排序冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比......
  • C++ —— 构造函数和析构函数
    C++——构造函数和析构函数引言构造函数析构函数注意事项引言构造函数和析构函数是class和C++的struct专属的功能(C的struct没有),用于管理对象的生命周期。构造函数:在创建对象时,自动的进行初始化工作。析构函数:在销毁对象前,自动的完成清理工作。构造函数访问权限......
  • C++:爬楼梯问题,设有阶台阶需要攀登,每次只能上1阶或2阶,问共有多少种上台阶方案。程序输
    代码如下:#include<iostream>usingnamespacestd;intlou(intx){ if(x==1||x==2) returnx; else returnlou(x-1)+lou(x-2);}intmain(){ intn; cout<<"请输入台阶数:"; cin>>n; cout<<"上台阶方案总数为&quo......
  • python学opencv|读取图像(三十)使用cv2.getAffineTransform()函数倾斜拉伸图像
    【1】引言前序已经学习了如何平移和旋转缩放图像,相关文章链接为:python学opencv|读取图像(二十七)使用cv2.warpAffine()函数平移图像-CSDN博客python学opencv|读取图像(二十八)使用cv2.getRotationMatrix2D()函数旋转缩放图像-CSDN博客在此基础上,我们尝试倾斜拉伸图【2】核心代码......