当前位置: 首页 > news >正文

C++入门(内含命名空间、IO、缺省参数、函数重载、引用、内联函数、auto关键字、新式范围for循环、关键字nullptr的超全详细讲解!)

C++入门

  • 前言
  • 一、C++与C语言的渊源
  • 二、C++弥补的C语言缺点有哪些
        • 1.命名空间的提出
            • 1.命名空间的访问
            • 2.命名空间的嵌套
        • 2.C++的输入输出
        • 3.缺省参数
        • 4.函数重载
        • 5.引用
            • 1.传引用相比传值效率更高,开销更小
            • 2.传引用相比传值可以直接更改数据本身
            • 3.引用和指针的区别
        • 6.内联函数
        • 7.auto关键字
        • 8.基于范围的for循环
        • 9.关键字指针空值nullptr
        • 附调试代码:


前言

本篇为C++入门文章,主要讲解C++基础语法


一、C++与C语言的渊源

C++,也可称作c plus plus,可以认为是C语言的升级版本,最早于1979年由本贾尼·斯特劳斯特卢普在AT&T贝尔工作室研发。(本贾尼可以尊称一声我们C++的祖师爷了)

C++是由C语言编写的一门语言,它可以兼容C语言,也就是说,在C++文件中,我们的C语言代码一样可以编译通过并执行

同时,C++与JAVA等语言一样,都是面向对象的语言,而C语言则是面向过程的语言(我们后面的文章会对面向对象和面向过程的区别进行详细解释)

可以说,C++是在C语言的基础上发展而来的,而JAVA等大部分语言是对C++进行抄作业发展而来的

众所周知,当一个物品需要更新换代时,往往是旧的版本无法适应我们的需求,而我们C++创作的初衷,便是为了弥补C语言的不足,比如作用域方面、IO方面、宏方面、指针方面、函数方面等等

二、C++弥补的C语言缺点有哪些

1.命名空间的提出

在C++中,首次提出了命名空间这个概念,命名空间,就是单独开辟一块空间,将你的部分代码的索引(暂且称为索引,即被调用时使用的代码,比如函数名)存储进去,使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,而这块空间的作用域,就被称为命名空间域,在命名空间中,也不允许出现重复的成员名。我们现阶段分有全局域,局部域,命名空间域以及类域,其他的域后续会讲到

这么做的原因就是为了弥补C语言可能出现的命名冲突的问题

命名冲突又分为自己与自己冲突、与库冲突两个方面

我们举个例子
在这里插入图片描述

我们可以看出,当我们定义同名函数和同名变量的时候,编译器会报错,这就是自己与自己冲突

同时,如果我们取的变量名字和关键字冲突时,编译器也会报错,如下图中,rand函数是取随机数,当我们定义它为变量时,就会报错,这就是自己与库冲突,并且C语言没有解决这种情况的办法
在这里插入图片描述

而我们在实际工作中,往往需要几十万甚至几百万行的代码,这么大的工作量不可能只有一个人完成,往往是由一个小组负责,小组中每个人负责一部分代码,写完之后再将代码合并

而一千个读者有一千个哈姆雷特,每个人都有自己的想法,并且面对几十万行的代码量,我们不可能事无巨细每个细节都处理到位,如果我们使用C语言去写,那么编写代码的时候,我可能觉得Sort这个函数名很不错,我就使用了这个函数名,而你也觉得这个函数名很好,你也使用了这个函数名

那么我们在各自调试代码的时候,功能没有任何问题,但是一旦代码合并,就会出现命名冲突,导致编译不通过,而合并之后往往有几十万行的代码量,我们挨个审查去修改,会非常麻烦,耗费人力物力,这时候祖师爷本贾尼就和其他开发人员相想出了一个办法,创建一个命名空间的概念,相当于为名字建造了一堵墙,我们使用的时候可以将墙拆掉,直接访问,也可以在墙里开一个小门,指定访问

而使用命名空间,需要namespace这个关键字

我们先讲清楚命名空间和命名空间域的区别

命名空间,顾名思义,就是单独开辟一块空间,相当于一个容器,用于封装一组标识符(变量、函数、类等),里面的成员的可以是变量,也可以是函数
在这里插入图片描述

命名空间域,则是命名空间所定义的作用域范围,即命名空间中成员的作用范围

1.命名空间的访问
namespace xx
{int a = 1;
}

如上图中,在命名空间xx中的成员变量a,它的作用域就是xx这个命名空间域,仅可使用xx::a去指定访问(除非我们使用using关键字将该命名空间展开)

总结就是,命名空间相当于一个语法结构,类似一个容器,而命名空间域就是该结构所划定的作用域范围,比如可以在命名空间中使用该成员,如果命名空间在某个作用域展开,就可以在该作用域使用该命名空间中的成员

定义命名空间的模板为:namespace后跟命名空间的名字,这个名字可以随意,后接{},{}中编写命名空间的成员,如下图所示(此时我们要把全局域中对rand的定义删掉,因为它是与库中的rand函数冲突的)
在这里插入图片描述
我们使用的是指定访问,即random::rand的格式,这代表我们要去命名空间域中寻找rand
如果我们什么都不加,就代表我们会沿着顺序去寻找rand,局部域->全局域

而此时,我们的命名空间random是展开的

using namespace random;

代表此时命名空间中的random,被注入到了当前空间域(此时为全局域),此时random中的rand与全局域中的rand函数是同级的,那么当我们直接访问rand时,编译器无法分清我们究竟是要访问谁
在这里插入图片描述
如图,当我们局部域中也定义一个rand时,我们可以看到:

当直接访问rand时,访问的是局部域中的rand

当访问::rand时,访问的是全局域中的函数rand,我们打印地址,打印出的就是函数rand的地址

当访问random::rand时,访问的就是命名空间域中的rand

在这里插入图片描述
值得一提的是,我们展开命名空间,只是相当于把命名空间中的成员注入到了当前空间域中(当前空间域不一定是全局域,我们可以在某个函数中展开命名空间,此时相当于把命名空间中的成员注入到了局部域中),注入到其中后,命名空间中的成员和全局域中的成员是同级的,但是命名空间中的成员并不属于全局域,依然是属于命名空间域的,只是它的作用域扩大了,就比如你有一把伞,这把伞是属于你的,只有你能用,有人借用了你这把伞,那么别人也可以使用这把伞,但是此时这把伞依然是属于你的,并不属于借用的那个人

所以,当访问命名空间和全局域中重名且局部域无定义的成员时,编译器并不知道该访问哪个变量,因此会报错;

如果当全局域中无定义,命名空间在全局域中展开,并且局部域中也没有定义,那么访问a会直接访问到命名空间中的a
在这里插入图片描述

而在部分编译器中,当我们使用域作用限定域::去访问时,比如::rand,此时是指定访问全局域中的成员,并不会访问注入到全局域中的命名空间的成员,如果全局域中没有rand,此时就会报错

但是有的编译器,当我们展开命名空间后,访问::a,会优先访问全局域,如果全局域无a的定义,会转而搜索命名空间中的a,如果命名空间中也没有a,才会报错(无论局部域中有没有a的定义)

如图:
在这里插入图片描述
在这里插入图片描述
我们可以看到,当我们在全局域中展开命名空间时,如果全局域中没有定义a,那么::a会直接访问命名空间中的a
在这里插入图片描述

在这里插入图片描述

我们同时也可以看出,当命名空间没有展开时,我们不会主动去命名空间中搜索a进行访问,直接访问a和访问全局域的::a都会报错,仅可通过指定访问xx::a的方式去访问

因此,我们编译器在编译的时候,

直接访问a

printf("%d",a);

会先搜索局部域中的a,如果局部域没有,会转而搜索全局域中的a如果此时命名空间在全局域中展开并且命名空间中有a的定义,会报错,因为两个a都是同级的,所以编译器并不知道该访问谁

如果命名空间没有在全局域展开,会访问全局域中原本存在的a

如果全局域中没有a的定义,并且命名空间在全局域展开才会访问命名空间中的a

访问全局域a

printf("%d",::a);

此时局部域有无a的定义并不对其造成影响,编译器会直接去全局域中搜索a,且无论命名空间是否在全局域中展开,只要全局域中有a的定义,就会率先访问全局域中定义的a(注:不要出现命名冲突,否则会报错,比如定义变量名为rand,它就会和函数rand发生冲突)

如果全局域中原先没有a的定义,命名空间在全局域中展开了,那么我们会直接访问命名空间中的a

如果全局域中原先没有a的定义,命名空间也没有在全局域中展开,那么程序会直接报错,因为在全局域中搜索不到指定的变量

指定访问命名空间中的a

namespace xx
{int a = 1;
}
printf("%d",xx::a)

我们相当于把命名空间这个容器开了一个门,直接进去访问,这个门的钥匙就是命名空间名+域作用限定符,比如xx::

如果命名空间中没有定义,那么编译器会报错

综上,命名空间的访问分为两种,一种是命名空间的展开访问,此时并不安全,比如命名空间在全局域中展开后,相当于把容器的外壳给拆掉了,用于防止命名冲突的墙被拆掉了,此时极有可能与全局域中的变量发生命名冲突,因此我们一般都只使用指定访问

2.命名空间的嵌套

我们前面讲到,命名空间在当前空间域展开后,命名空间中的成员会被注入到当前空间域,相当于将命名空间中成员的作用域扩大了,而当前空间域可以是任何作用域,比如全局域、局部域,甚至是命名空间域也可以,那么这就可以引出我们的嵌套

一个命名空间是可以嵌套其他命名空间的,比如:
在这里插入图片描述
并且我们可以看到,嵌套的命名空间qq中的成员是可以和命名空间xx中的成员重名的,此时不会发生命名冲突,但存在一种特殊情况
在这里插入图片描述
如图,当局部域与全局域中没有a的定义时,我们直接访问a,此时命名空间xx在全局域中展开,xx中的成员被注入到了全局域中,而命名空间qq又在xx的命名空间域中展开,那么qq的成员就被注入到了xx的命名空间域中,根据传递性,那么qq中的成员就相当于被注入到了全局域中,此时命名空间qq和xx中的成员变量a的关系,就好似前面全局域中的a和在全局域中展开的命名空间xx中的a的关系

此时我们访问a,编译器发现局部域中没有,就去全局域搜索,全局域中原先没有a的定义,搜索不到a,此时编译器发现命名空间xx在全局域中展开,就会去命名空间中搜索a,有a的定义,此时又发现命名空间qq在xx的命名空间域中展开,相当于qq中的成员被传递性地在全局域中展开,那么也会搜索命名空间qq,发现qq中也有a的定义,此时两个a是同级的,编译器并不知道该访问谁,便会报错

同样,如果我们不在xx中展开qq,此时编译器便不会报错
在这里插入图片描述
而如果我们想访问命名空间qq中的成员,分为多种情形,我们假定全局域与局部域中均没有重名定义

第一种就是无论xx和qq展不展开、无论在哪里展开,我们都可以通过指定访问xx::qq::a去进行访问,此时指定访问的就是qq命名空间中的a,其他地方有无a与其无关

在这里插入图片描述
但是,如果我们不通过xx::的限定去指定访问,仅通过qq::a去指定访问,是不可以的,因为在qq的外面有着一层围墙xx,而xx并未展开,说明有一堵墙围住了qq这块空间,就像我们进学校找在教学楼里上课的学生一样,我们连学校的大门都进不去,又怎么进入教学楼找到学生呢?
在这里插入图片描述

第二种便是我们刚刚在全局域中展开xx,在xx命名空间域中展开qq的情形: 如果xx中没有定义a,那么直接访问a便可以访问到qq中的a

在这里插入图片描述

如图,我们直接访问a和b,可以访问到命名空间qq中的成员a和b
同时,通过::a访问全局域中的a,通过xx::a、qq::a和xx::qq::a指定访问a都可以访问成功

在这里插入图片描述
值得一提的是,如果xx有a的定义,此时我们访问::a,如果全局域中不存在a的定义,会优先访问xx中的a,如果xx中没有才会访问qq中的a,因为qq中的a是根据传递性才与xx中的a同级的,本质上它还是属于命名空间xx的内层成员

第三种,当xx在全局域中展开,而qq没有展开时:

在这里插入图片描述
此时我们可以看到,当我们直接访问a和b,通过::a访问全局域中的a,抑或是仅通过xx::a指定访问xx中的a时,编译器都会报错,因为我们只是在全局域中展开了命名空间xx,此时并未在xx命名空间域中展开qq,这相当于我们去学校找正在上课的学生,只是采取粗暴的方式把学校的围墙给拆掉了,使得学校中的教学楼暴露了出来,但是学生仍然在教学楼中,被教学楼包围、保护着,我们必须将其展开或是打开教学楼的门才能进去,所以我们必须通过指定访问来访问xx的内层成员,即必须通过qq这个中间层去访问

xx::qq::a是必然可以访问到的,而qq::a之所以可以访问到,是因为xx在全局域中展开了,我们无需指定访问xx中的成员,就像学校在装修一样,围墙倒了还没有修好新的,我们可以直接进入学校,通过指定教学楼进入找学生,即通过qq::指定进入qq中寻找成员

第四种,当xx没有展开,而qq在xx命名空间作用域中展开时,

在这里插入图片描述
我们可以看到,此时直接访问a和b、通过全局域搜索::a访问、指定qq::a访问编译器都会报错,这是因为xx并未展开,而qq仅在xx命名空间域中展开,我们要访问的是是qq的内部成员,即xx的内层成员,那么我们就要指定xx,但不必指定qq

如图,xx::a和xx::qq::a都可以访问

同时,还有两个点需要注意,当我们将xx和qq均在全局域中展开,但是qq不在xx命名空间域中展开时
在这里插入图片描述
此处我们可以看到,当直接访问a、b,全局搜索::a访问、qq::a和xx::qq::a指定访问时,都可以访问到,只有指定xx::a访问时编译器会报错,这是因为qq并未在xx中展开,只是在全局域中展开了

这相当于qq确实是嵌套在xx里的,但是qq没有通过传递性注入到全局域,而是自己注入到了全局域中,可以通过全局域搜索访问到,它可以通过全局域直接找到,但是它在命名空间xx中还有一堵墙保护,无法被找到

相当于你结婚后你的表弟表妹暂住在你的家中,从全国系统中可以找到你包括你表弟表妹的信息,但是在你的户口本上找不到你表弟表妹的信息,在你表弟表妹家的户口本上也找不到你的信息

还有一点需要注意的是,当我们不展开xx,仅在全局域中展开qq时,编译器会直接报错,因为xx没有展开,说明qq外面围着一堵墙,就像古代的对外开放,qq是下属州,xx是朝廷,朝廷不允许对外开放,下属州想要对外通商,肯定是不被允许的,必须等朝廷下了命令,即xx宣布展开后,下属州qq才能宣布对外开放通商
在这里插入图片描述
当然,xx也可以自己的空间内展开,但这并没有什么意义,如

namespace xx
{int a = 1;using namespace xx;
}

同时,如果在命名空间中定义一个未被赋值的变量,那么当我们访问它时,会被默认赋值0

如果我们再定义一个包含a定义的命名空间pp并在全局域中展开,那么直接访问a或者::a会直接报错,此时只能通过命名空间名+::去进行指定访问,同时如果我们在新的命名空间域中展开了命名空间,那么也可以通过该命名空间名+::去指定访问被展开的命名空间中的成员,比如pp::b
在这里插入图片描述
讲到此处,我们可以知道,解决c语言中的命名冲突的问题,可以使用namespace命名空间来解决,通过指定访问,我们可以完美的避免命名冲突的问题(尽量避免直接展开命名空间,要通过命名空间名+::去进行指定访问)

此时我们的using namespace std;这条语句由来便可以弄清楚了,同时命名空间也可以部分展开比如只展开命名空间中的某个成员

namespace xx
{int a = 1;
}
using xx::a;
2.C++的输入输出

我们在学习C语言时,用到的输入输出分别是scanf和printf函数,我们每次使用时,都要标明输入输出数据的类型,较为麻烦,而C++中解决了这个问题,C++中使用cout和cin进行输入输出,cout,即输出的关键字,cin,即输入的关键字,使用这两个关键字我们无需注意输入输出数据的类型,这十分方便,因为cin和cout会自动识别数据类型

使用cin和cout时,必须包含头文件,否则无法编译通过

进行输入输出时,需要用到<<流插入运算符和>>流提取运算符,比如:
在这里插入图片描述
其中endl的意思代表换行
在这里插入图片描述
cin和cout都包含于命名空间std中,std是C++中的一个标准库,前面我们讲过,将命名空间展开后,容易发生命名冲突的问题,因为我们一般不会将它展开,而是指定进行访问
在这里插入图片描述
但是我们发现,每次使用都要加一次std::,每次使用都要加,这会让代码量急剧增大,当我们开发一个产品时,代码量大了,那么这个产品在我们手机上所占用的内存就会大,内存大了肯定会有些用户不愿意使用,如果说全部是有用的代码,那也能接受,但是如果有很多重复的std::,显得有些得不偿失,此时我们就可以使用我们在讲命名空间时用到的部分展开
在这里插入图片描述
在这里插入图片描述

如图,当我们仅将cout和endl展开后,代码就变得十分简洁,没有那么臃肿了

而且C++的输入输出有个优点,就是可以直接输出字符串或者是数组的所有数据,比如

int main() {Point p = {1, 2};cout << p;  // 直接输出 (1,2),scanf/printf 无法做到return 0;
}

同时,printf和scanf并非没有好处,它们在处理大量数据时性能是较为优越的,而且它可以更加方便地控制精准度,比如prtinf("%.2f")保留两位小数,而使用C++的输入输出就会比较麻烦

但是使用cout和cin就不必考虑数据类型,C++后期有的数据类型会非常长,使用cout就会十分方便

3.缺省参数

缺省参数,指的是在函数的参数列表中定义一个固定的值,即给形参赋值,这个值就称作形参的缺省值

当我们调用这个函数但是没有指定实参的时候,该函数默认使用形参的缺省值作为参数,否则使用指定的实参
在这里插入图片描述
如图,我们定义缺省值为1,当我们调用Func函数而不传递实参时,输出的是1,即缺省值

当我们调用Func函数并传递实参10时,输出的则是10,即指定的实参

缺省参数的好处就是可以减少我们代码的重复输入以及空间开销

在这里插入图片描述
如图所示,当我们计算三个数相加时,我们给Func1的三个形参都赋上缺省值,计算的时候随时改变就可以

而如果我们突然想要计算两个数的和,按照传统方式,我们需要再写一个函数Func2,来计算两数相加之和,但是当我们使用缺省参数后,我们直接将形参c的缺省值设置为0,那么我们进行相加的时候只需要传递a、b两个形参的值即可进行计算,此时我们就可以减少函数定义的数量

在这里插入图片描述

值得一提的是,我们在赋缺省值的时候,必须在参数列表中按照从右往左的顺序进行赋值,比如
在这里插入图片描述
当我们没有给某个形参赋缺省值时,代表我们传参的时候必须给那个形参传递指定的实参,而如果我们不按照从右往左缺省的顺序,比如按照从左往右的顺序赋缺省值时,那么就会报错

因为某个形参没有赋缺省值时,代表我们传参的时候必须给那个形参传递指定的实参,但是我们传参的顺序是对照形参列表一一赋值
在这里插入图片描述
如上图,我们只给a赋缺省值时,b和c就都要有传递指定的实参,如果我们只传递1,此时是赋值给了a,b、c并没有被赋值,会报错

传递1和2时,此时是给a和b赋值,c并没有收到实参的值,还是会报错

这代表我们必须传递三个值才能通过编译,但是如果传三个数,这就违背了我们使用缺省参数的初衷,用和不用并没有什么区别,还容易造成误解发生错误,因为我们缺省参数的规定是必须按照从右往左的顺序为为形参赋缺省值,如果有某个参数没有缺省值,此时称为半缺省参数

同时**,缺省参数不能在函数声明和定义中同时出现**,因为这容易造成混淆,比如在声明的时候,我赋值缺省值为10,在函数定义的时候,我赋值形参的缺省值为20,此时编译器并不知道该使用哪个缺省值,就会报错,因为我们通常默认在函数声明时指定缺省值,函数定义时则不再赋缺省值

并且,缺省参数默认值不能是局部变量(局部变量生命周期有限,可能导致未定义行为,比如函数执行完后销毁栈帧),只能是常量或者局部变量

同样,在我们学习栈等数据结构的时候,我们通常需要对该结构进行初始化,初始化时一般都要用malloc进行扩容,而我们通常扩容都是默认扩容4个或是用宏定义去默认扩容多少个空间,但是如果我们正好要扩容6个呢,那么默认扩容4个就会扩容两次,并且还会造成空间浪费,而宏定义去扩容呢,可能也会造成浪费,但是有同学说,那我们改宏定义呢,是不是就可以适配了,如果针对单个栈,那确实可以,但如果我们同时需要多个栈呢?

比如我们需要一个空间大小为4的栈,一个空间大小为8的栈,这时候该怎么办?难道我们要多写一个函数?抑或是多扩容一次?

都不需要!我们可以使用缺省参数,默认为4,一个不传参,一个传参8,这样就可以完美解决,即不用给空间大的栈多扩容一次,也不用担心出现空间浪费的情况

如下列函数声明:

void StackInit(struct Stack* pst, int defaultCapacity = 4 );

综上,缺省参数解决的问题就是避免大量重复输入与编写大量用于实现重复功能的函数

4.函数重载

函数重载,即功能类似的同名函数,常用于处理功能相似但是类型数据不同的情况,避免功能相似但函数名字太多容易造成混淆,比如要求计算并返回两个浮点数之和,计算并返回两个整数之和,这时候就可以用到函数重载,而这些同名函数,它们与原函数的区别就是参数列表不同,比如形参类型排列顺序不同、形参的类型不同、形参的数量不同,与你的返回值类型和函数内容无关,比如:

形参类型不同:

在这里插入图片描述

形参类型排序顺序不同:

在这里插入图片描述

注意,此处的排列顺序不同指的是类型的排列顺序不同,与你的形参叫什么名字无关,并且如果形参类型均相同的话,是无法通过类型排列顺序不同构成函数重载的,如:
在这里插入图片描述
我们可以看到,两个形参类型都是int的Func1函数仅改变了形参名字的排列顺序,这样是无法构成函数重载的,因为形参的名字是由自己控制的,随意即可,两者本质上并没有改变,是同一个函数

形参个数不同:

在这里插入图片描述
注意,当形参数量不同构成函数重载时,尽量不要给形参设置缺省值,因为设置缺省值之后,如果你想要使用这个缺省值,必然会空出一个参数不进行传递,此时你传递参数数量可能会与某个函数相同,编译器会不知道访问哪个函数,如下图:
在这里插入图片描述
这样看来,函数重载是十分好用的,但是它是C++给C语言打的补丁,C语言是不可以使用的,为什么呢?

众所周知,C或C++中,想要执行一个代码文件,编译器一定会经过预处理、编译、汇编、链接四个阶段预处理主要是宏替换、头文件、注释清除等操作编译则是将代码转为汇编代码,并进行语法分析是否有错误,同时进行语义分析、符号汇总,在汇编阶段则是将汇编代码转为二进制代码,同时生成符号表,符号表中存储函数名、各类符号以及地址等等数据链接则是合并符号表,并对符号表重定位

当我们使用两个文件去执行代码时,比如一个是test1文件,一个test2文件,链接就是将test1和test2的符号表合并,并将重复的函数重定位在一个位置去调用。而如果我们要调用test2中的Add函数,编译器在test1中找不到Add函数的地址,那么就会去test2的符号表中去找,然后链接到一起,而我们说过,符号表一般都是有函数名、各类符号和地址的,那么我们去test2中寻找Add函数地址时,要以什么样的名字去找呢?

在C语言中,我们会直接以Add的名字去寻找,C语言中的符号表,函数名就是原本的函数名,并没有多加修饰

但是在C++中,我们去寻找时,是以修饰过的名字去寻找的,我们在Linux的g++规则下进行测试,因为windows上vs的修饰规则太过于麻烦
例如:
在这里插入图片描述
这便是Windows中函数名的修饰规则,太过于麻烦
在这里插入图片描述
我们以图中的Add函数和func函数举例
在这里插入图片描述
如图,当我们调用Add函数和func函数时**,在编译后,C语言是直接使用的原函数名,也就是说,如果我们使用函数重载,那么会有许多函数名相同的函数,此时C语言的编译器就不知道该去调用哪个函数了,便会报错**
在这里插入图片描述
而在C++中,我们可以看到,函数名是经过修饰的原函数名,以func举例,_Z代表代表前缀,_Z4中的4代表的是原函数名的长度,比如func,长度为4,后面加上原函数名,即func,后面的idPi代表形参的函数类型,func的形参类型分别为int、double、int,那么i就是int,d就是double,而Pi就是int**

在C++中,无论是Windows还是Linux,在编译后都有一套对函数名的修饰规则,因此当构成函数重载时,并不会出现编译后函数名相同的情况,编译器可以分清楚什么时候该访问哪一个函数

5.引用

引用,其实就类似于指针,不同的是它并不是新的变量,它相当于给一个变量取别名,并不会开辟一块新的内存空间,而是与原变量共用同一块空间
它的使用规则是:引用变量类型& 引用变量名(对象名) = 引用实体

例如:

int a = 10;
int& b = a;

此时我们就可以说b是a的引用

此处的&并不是取地址符,而是引用符号,代表这是一个引用

而我们对一个变量取引用后,打印该引用输出的值与被引用变量的值相同,并且两者会同时改变

在这里插入图片描述

如图,对a取引用b,我们可以发现a和b的值是相同的,并且a和b的地址也是相同的

当b++后,a与b均为11

当a++后,a与b均为12

这些都说明,a和别名b是共用同一块空间的

并且,别名可以被赋值,别名被改变后引用实体也随之改变,如下:
在这里插入图片描述
我们对a取别名b,将x赋值给b,注意,此处是赋值,并不是给别名b再次取别名,b被赋值后,a也随之改变

示意图如下:
在这里插入图片描述
引用的概念很好理解,映射到现实其实就类似于取外号,比如我们给小A取了个外号叫做钢蛋,那么到新学期了,钢蛋评上了奖学金,我们能说小A没有评上奖学金吗?又比如水浒传中的豹子头林冲,林冲火烧草料场,那豹子头自然也是火烧草料场了呀

在JAVA中,引用几乎可以完全代替指针,但是在C++中,引用只能在某些地方代替指针,并不能完全代替

但是引用还是十分好用的,比如我们在之前学习链表的时候,如果我们用C语言去写,在某个操作比如尾插数据时,我们就需要传参二级指针才能生效,但是我们如果使用引用的话,只需要形参写为一级指针的引用即可

typedef struct ListNode
{int val;struct ListNode* next;
}LTNode, *PLTNode;//void ListPushBack(struct ListNode*& phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PLTNode& phead, int x)
{// ...// phead = newnode;// ...
}

如代码所示,当使用引用时,我们便不需要通过二级指针来进行尾插

同时,如果我们用typedef将结构体的名称改为*PLTNode,那么PLTNode存储的就是struct ListNode的地址,那么我们可以直接写PLTNode& phead作为形参

同时解决的,还有我们的Swap交换函数
在这里插入图片描述
如图,我们之前通常用传地址的方式实现两数交换,而使用引用后,则不需要使用指针进行地址传递了

我们进行指针情况的Swap交换,可以看到m变为10,n变为1,实现了两数交换

之后进行引用情况的Swap交换,可以看到m变为1,n变为10,也实现了两数交换

同时,引用可以再次被引用,即给一个引用去别名,比如:
在这里插入图片描述
我们可以给a取别名b,再给别名b取别名c,再给别名c取别名d,最后我们输出a、b、c、d,都是输出的1
这说明给别名取别名,别名的别名和别名也是共用同一块空间的,此时相当于一个实体可以被多次引用,对于引用,我们要分清是给引用取别名,还是给引用赋值

同时,引用不能自绑定,因为同一作用域下,变量名是不可以重复的,这个我们之前讲过,比如说int a = 1; int& a = a;是错误的

并且,引用的数据类型必须与被引用的变量数据类型相同,比如
在这里插入图片描述
我们给整型的a取浮点型的别名b,此时编译器会报错

引用被创建之后,必须实例化,因为引用和指针不同,引用相当于是一个实体的别名,一个人的外号,它是和实体共用同一块空间的,只有当实体存在的时候,它才有意义,而指针不同,指针是可以指向空,指向NULL的,即使不指向任何地方,也是可以的,只是存在野指针的风险,如图
在这里插入图片描述

还有一点,我们知道指针是可以改变指向的,因为指针是开辟了空间存储指向空间地址的,它是可以随意改变指向的,只要改变它存储的地址即可,但是引用不同,引用并没有开辟空间,除了与原变量共用同一块空间之外,它也没有任何与原变量相关的地方,它仅仅是一个别名,一个外号而已,这是已经规定好的,比如下图:
在这里插入图片描述
可以看到,当我们先给a取别名c,再给b也取别名c后,编译器会报错,显示c重定义,多次初始化

这说明,引用一旦引用一个实体,便不能再引用其他实体

引用还有一个概念,叫做常引用

引用的权限允许平移、缩小,但是不允许放大,
在这里插入图片描述
如图,我们使用const关键字修饰,表示引用b是一个常量,此时可以编译通过,如果不修饰编译器就会报错

const代表的是常量,相当于把一个变量修饰后,将其变为了常量,该变量无法被改变

数字10是一个常量,当被引用后,因为引用是可以改变的,常量不可变,此时相当于权限的放大,因此编译器会报错

其他例子如下:
在这里插入图片描述

我们对a取别名b,此时b是实体a的引用,两者同时变化

我们对b取别名c,并用const关键字修饰,此时代表c无法改变,而别名如果不被const修饰的话,它的值是可以进行改变的,因此这相当于权限的缩小,权限由可变转为不可变

我们对a取别名d,并用const修饰,此时与c相同,也代表d无法改变,引用d原本可以改变,被const修饰后无法改变,这也属于权限的缩小

我们对c取别名e,由于c本身就是不可变,e也不可变,此时相当于权限的平移,因此不会报错

而当我们a++后,发现a、b、c、d、e全部变为了11,这是为什么?不是说常量无法改变吗?

我们注意看,我们的const修饰符,修饰的是c、d、e,但并没有修饰a和b,而b、c、d、e其实都相当于是实体a的引用,只是c、d、e无法被改变而已

但是,b、c、d、e仍然是与a共用同一块空间的,实体a改变了,难道它们这些引用就不改变了吗?不能变,只是说不能它自己变,但是通过实体或其他别名变是可以的,例如,水浒传中,李逵在世人眼中被称作黑旋风,但是在梁山上,大伙们都是兄弟,称呼自然不会过于生分,于是都称他为铁牛

而李逵喜爱喝酒,有一次宋江让李逵下山办事,怕他喝酒误事,便告诉他,你下了梁山,就不能喝酒,但是等你回来,到了梁山,可以喝酒。

那么李逵下了山,它的外号就是黑旋风,黑旋风是不能喝酒的

但是等李逵办完事回到梁山了呢?李逵就是铁牛,铁牛是允许喝酒的啊,那铁牛喝酒了,黑旋风没有喝酒吗?难道铁牛和黑旋风是两个人吗?

被const修饰的引用,它的原理就是如此,它自己不能改变,但是可以通过其他方式去改变

因此可以改变的a和b,就相当于铁牛,a改变,铁牛喝酒,黑旋风自然也是喝了酒,b、c、d、e自然也跟着改变

在这里插入图片描述
如图,这也是权限放大的一种,权限放大,指的是引用的权限相对于实体的权限放大了,该图中的a是被const关键字修饰的,是常量,不可以改变,而引用b,是可以改变的,这就是引用的权限相对于实体的权限放大了

在这里插入图片描述
而如果我们给b加上一个const修饰符,此时编译就不会报错了,因为a和b都是常量,这就属于权限的平移

而引用的作用远不止于此,上面我们讲到的将引用用于Swap函数以及链表尾插使用二级指针上,就是引用作参数,除此之外,引用还可以用作返回值

在这里插入图片描述
如图,Func1函数的返回值类型就是引用类型,但是这种方式是不对的,原因是Func1函数中局部变量a的定义

在某些最新的编译器中,这种情形可能会通过编译、执行并输出正确结果,但是有的编译器,程序可能会崩溃,或者是输出错误的结果
在这里插入图片描述
这是vs2022输出的结果,如果我们用dev或小熊猫,或许会试出不一样的结果(如果程序依然输出正确结果,我们可以在输出b之前调用一些其他函数)

我们看Func1函数中的内容,里面直接定义了一个a,这标明a是Func1函数中的一个局部变量,当Func1函数结束后,函数栈帧可能会被销毁,亦或者是被后续调用的函数覆盖,一旦Func1的函数栈帧被销毁或是覆盖,那么局部变量a的定义自然也不复存在,而我们的返回值是引用,返回的是a,这代表我们返回的是a的引用,也就是说,变量b接收到的值是a的别名的值,我们知道,实体a存在,它的引用才存在,那么a如果被销毁,引用自然也不再存在,此时b接收到的就是一个随机值,这是有风险的

同时,如果说你的结果是正确的,这说明函数栈帧侥幸没有被清理、销毁掉,但这不能说明是正确的,只能说明函数栈帧开辟的空间足够大,没有覆盖到Func1函数的栈帧,一旦被覆盖到,就会输出随机值

我们函数栈帧的创建,是一层一层往上叠的,并且当某个函数执行完后,该函数创建的函数栈帧会被销毁,销毁并不是销毁这片空间,而是将这块空间的使用权还给了操作系统,不再拥有这块空间的使用权,这块本质上还是存在的,并且使用权回归后可以再分配给其他成员使用

为了避免这种情况,我们通常使用static关键字,static关键字的作用是将其变为静态变量,并且如果用于局部变量身上,它的生命周期将会与整个程序相同,相当于在生命周期上变为了全局变量,函数结束后不会销毁,整个程序结束后才会销毁该变量

在这里插入图片描述如图,这种情况是没有风险的,可能有的同学说,这也没变啊,如果栈帧没被销毁,不加static也是对的啊,这种情况确实是有可能发生,存在潜在风险,不过还有一种情况,是百分百会出现风险的

当我们用引用接收返回值时,此时相当于为Func1函数中的a起了两个别名
在这里插入图片描述
如图,当我们使用引用接收返回值并打印,输出的结果是正确的

但是一旦我们再调用该函数传参不同的话,此时就会有风险
在这里插入图片描述
如图,当我们传参10并用引用b接收返回值时,此时输出a和b均为12,也就是说,传参10后,a的值从3变成了12,这不是简单的a++,这是a本身加了9
同理,传参10时,b是12,当我们传参20并用c接收时,此时c和b输出都是22,也就是说b的值从12变成了22,和a一样,变得幅度非常大,这不符合我们设计程序的初衷,并且有着极大的风险
而当我们用static修饰之后,不管你传参多少,都只会完全按照函数的逻辑进行变化了

在这里插入图片描述
如图,当用static修饰a之后,代表static int a = b + 1这条语句仅在第一次调用时触发,后续只执行a++和return a这两条语句,那么就是说,我们每执行一次,先前接收返回值的变量都会+1(因为它们都是静态变量a的别名,而此处因为Func1中的a只是生命周期和全局变量相同,但它的作用域并没有改变,还是在Func1的局部域,而main函数中的变量a的作用域是main函数的局部域,所以两者之间并不会发生命名冲突),并不会发生很大的变化,不会随传参改变而改变

相反,如果我们不使用传引用返回,而使用传值返回并且使用引用接收返回值的,此时也是存在风险的,如图
在这里插入图片描述
当我们使用引用接收传值返回时,此时会报错

这是因为我们函数返回值时,不会直接把函数中的局部变量赋值给接收返回值的变量,我们以图中代码为例,Func函数是传值返回,当我们Func函数返回a + 1时,并不会直接将a + 1赋值给main函数中的a,而是先生成一个临时变量,将a + 1的值赋值给临时变量,之后再将临时变量赋值给main函数中的a,而此处的临时变量,是一个常量值,相当于一份a + 1的临时拷贝,并且是常量,是不可改变的
而我们知道,引用的常引用权限是不允许放大的,我们使用引用去接收常量,那代表这个常量可以通过引用改变,能被改变的常量还是常量吗?此处就是引用权限的放大,是错误的

在这里插入图片描述
如图,当我们使用const关键字修饰后,代表引用变量a是一个常量,无法被修改,此时相当于权限的平移,不会报错

我们在传值返回时,是通过创建一个临时拷贝,再赋值给变量的,同样,传递实参的时候,也是如此,创建一个实参的临时拷贝,然后传递给对应的函数

注:这么做的原因是为了保护原数据不被破坏,同时避免访问函数结束后被销毁的栈帧里的临时变量,比如我们使用Swap函数进行交换两数时,如果不使用引用,就要传址调用Swap函数去交换两数,而不是传值调用,因为临时拷贝会让传值调用无法实现交换的效果

而我们发现,当使用传引用返回时,我们可以使用引用接收返回值,这是因为当我们使用传引用返回时,函数不会再创建临时拷贝

同时,我们的传引用调用也不会再创建临时拷贝,因为传引用调用和传引用返回都是传递的该变量的别名,实际上传递的就是原变量,更改会对原变量生效,类似于传指针

通过上面的讲解我们可以知道,传引用调用和传值调用,传引用返回和传值返回,都有着区别,而传引用自然是更好用一些

1.传引用相比传值效率更高,开销更小

我们讲到,无论是传值调用还是传值返回,它们都需要定义一个临时拷贝

而临时拷贝,自然是要占据空间的,无论你申请的空间有多小,终归是有开销,比起传引用,开销自然是大的,那么效率也会低,特别是当数据量非常大的时候,这个影响会更加明显

下面我们用运行时间来对比一下
在这里插入图片描述
在这里插入图片描述

我们可以看到,同数据量级下,无论是传参还是返回值,传引用都要比传值效率高,开销也小

2.传引用相比传值可以直接更改数据本身

传引用相比传值,更适合输出型函数,比如我们要在某个函数中更改某个值并在原函数中使用,也适合输出型参数

举个例子,数据结构中的栈,我们之前采取的都是传值返回,如果我们想判断栈顶元素是否大于10,大于就加1,小于等于10就减1,那么我们就需要调用两个函数,一个是获取栈顶元素,先对其进行判断是否大于10,之后调用修改栈里某个位置的值的函数,对其进行更改

而如果我们使用的是传引用返回呢?

我们先获取栈顶元素,使用引用接收,此时接收变量便是栈顶元素的别名,那么根据引用的概念,我们直接对引用变量进行改变,那引用实体(即栈顶元素)不就被改变了吗?此时我们只需要调用一个函数即可,即用于获取栈顶元素的函数

在这里插入图片描述
在C++中,struct升级为了类,类中可以包括函数

如图,我们使用at函数获取到栈顶元素,将其赋值为0,之后再加1,这表明我们可以直接对其进行更改,无需调用多个函数

operator是一个用于重载运算符的关键字,我们后续会讲到,我们此处的s[1]就是s.operator[1]

3.引用和指针的区别

1.引用必须初始化指向一个实体,指针可以指向空,也可以不初始化只定义(有野指针的风险),不做要求

2.引用定义不占用空间,是一个实体的别名,指针占用空间,是一个独立的变量

3.引用指向一个实体后,不能再指向其他实体,不可以改变指向,但指针可以随意改变同类型数据的指向

4.引用绑定的是实体的值,指针存储的是指向变量的地址

5.没有NULL引用,但是有NULL指针

6.在sizeof中占用大小不同,引用是自己引用实体的数据类型的大小,指针始终是地址空间所占字节个数(32位平台下通常为四个字节)

7.引用自加即引用实体的值加1,指针自加相当于指针向后偏移一个类型的大小

8.引用比如指针更加安全

9.有多级指针,没有多级引用

10.访问实体方式不同,指针需要显示解引用,引用则由编译器自己处理

6.内联函数

用inline关键字修饰的函数叫做内联函数,内联函数执行的时候不会被调用,而是直接将函数代码嵌入到调用的地方,取代调用指令,直接执行该函数。在编译阶段,编译器会尝试将内联函数的代码直接嵌入到调用它的地方(称为 “内联展开”),而不是像普通函数那样生成 “函数调用指令”。此时,该函数的执行不会建立栈帧,这就降低了消耗

但这种方法是一种用空间换时间的方法,因为在普通函数调用时,只会生成一行调用该函数的指令,而内联函数则是将整个函数体嵌套进去,一条语句一条语句地挨个往下执行,如果函数体过大或者是执行次数过多,就会导致最后链接生成的文件过为庞大,占用内存空间太多。不过它省去了函数栈帧的创建,在某些情况下会提高程序运行的效率

并且内联函数只是向编译器发送一个申请,并不一定执行,需要编译器检查该函数变为内联函数后开销是否过大,过大自然是不会通过,而检查通过后才能生效,才算的上的内联函数内联机制适用于优化规模较小、流程直接、频繁调用的函数,但75行的函数体就不一定能通过内联函数了,而且内联递归函数大部分编译器都不支持

同时,内联函数的定义和声明最好不要分开,要放在同一个文件里,因为内联函数省去了符号表中函数调用的指令,函数调用的指令里有该函数的地址,如果定义和声明分离,那么链接的时候就找不到函数的地址,此时就会报错

inline void f(int i)
{cout << i << endl;
}

在调试的时候,debug模式下内联函数会失效

与内联函数相似的,还有宏函数

宏函数也不需要建立函数栈帧,它也省去了这一步骤,因为它在编译的预处理阶段就被文本替换为宏体代码,并且宏函数通过文本替换直接使用调用处的参数,不需要专门的参数传递和返回值处理流程,进一步减少了开销

但是宏函数并不安全,极易出错,安全性极低

比如写一个宏函数,功能是实现两数相加,只有这种方式才能保证不会出错

#define Add(x, y) ((x)+(y))

需要注意的地方太多,并且没有检查机制,也不方便调试,因此我们一般使用内联函数来代替宏函数

7.auto关键字

auto是一个类型指示符,它相当于各种数据类型,可以像C++里的输入输出一样,自动识别类型,比如auto a = 1,此时auto会自动识别变量a为int类型

在后面我们使用auto会十分方便,因为后面的类型会非常长,都是由各种类型拼凑而来

同时,使用auto替代具体数据类型的变量,必须被初始化,因为auto是通过编译器在编译阶段根据初始化的表达式来推导该变量的类型,也就是说这个变量必须初始化,编译器会根据初始化的内容自行推导变量的数据类型,并将auto替换为对应的数据类型

auto可以与引用一起用,也可以和指针一起使用

auto和指针一起用时,auto和auto*并没有区别,但是和引用一起用时,必须写成auto&的形式,不能省略&,如图
在这里插入图片描述

auto在同一行定义多个变量时,类型必须相同,否则会报错
在这里插入图片描述
同时,auto不能作为数组定义时的数据类型和函数的形参类型,并且auto在部分编译器中可以作函数返回值类型,但有些编译器里存在一些限制

auto适合与C++中的新式for循环搭配,以及一些其他表达式

8.基于范围的for循环

C++中引入了新式for循环,由类型+变量名+:+范围组成,如:

int arr[] = {1,2,3};
for(int i : arr)
{cout << i << endl;
}

这就相当于一次遍历,前面的类型+变量名是范围内用于迭代的变量,后面的范围则是被迭代的范围,两者之间用:隔开,该循环与普通循环类似,也可以用continue和break跳出或结束循环

同时,该循环可以使用auto关键字进行遍历,并且该循环遍历时,对变量i的改变不会影响到原数组,因为这相当于是把原数组里的值生成了一份临时拷贝,赋值给了临时变量i,而对临时变量i的改变,自然不会影响到原数组的值,如果想通过新式for循环改变原数组的值,就要用到引用,如下是引用和auto的同时用法:
在这里插入图片描述
如图,当我们使用引用之后,对i的改变才会体现在原数组上

其次,. for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定

void TestFor(int array[])
{for(auto& e : array){cout << e << endl;}
}
9.关键字指针空值nullptr

对于没有指向的指针我们通常会给它初始化为NULL,如

int* a = NULL;
int* b = 0;

但是在C的头文件中,我们可以发现NULL其实是一个宏,它可以是NULL,表示空,也可以是常量0,也可能是无类型指针的常量(void*)在这里插入图片描述
那么在实际中,它就有可能出错,比如
在这里插入图片描述
我们可以看到,图中的两个f函数构成了函数重载,而当传0和传NULL时,都会调用第一个函数,这是将NULL宏替换为了字面常量0,当我们使用(int)进行类型强转时,才会调用第二个函数,因为默认情况下编译器会将其当作字面常量0,只有强转后,NULL才是无类型的指针(void*)常量(当然,也可以强转为其他类型,比如上面的整型指针int*)

为了解决这种情况,C++引入了新的NULL,称作nullptr,它专用于表示指针空值

同时,在使用nullptr时,不需要引入头文件,因为它是一个关键字

在C++11中,sizeof(nullptr) 与 sizeof((void)0)所占的字节数相同*

在后面,为了避免出现混淆,我们一般使用nullptr代替NULL

综上,C++弥补了C语言的多项缺点,这九项新特性就是主要补丁

附调试代码:
#include<stdio.h>
#include<iostream>
//using namespace std;//int Func()
//{
//	return 1;
//}
//
//int Func()
//{
//	return 2;
//}//namespace random
//{
//	int rand = 1;
//}
//using namespace random;
//int main()
//{
//	int rand = 2;
//	printf("%d\n",rand);
//	printf("%p\n", ::rand);
//	printf("%d\n", random::rand);
//	return 0;
//}//::域作用限定符//namespace xx
//{
//	int a = 2;
//}
////int a = 3;
////using namespace xx;
//int main()
//{
//	/*int a = 3;*/
//	//printf("%d\n", a);
//	int a = 4;
//	//printf("%d\n", ::a);
//	printf("%d\n", xx::a);
//	return 0;
//}//namespace xx
//{
//	int a = 10;
//	int Add(int a, int b)
//	{
//		return a + b;
//	}
//	using namespace xx;
//}
//using namespace xx;
//int a = 1;
//int main()
//{
//	//int a = 2;
//	//printf("%d\n", a);
//	printf("%d\n", ::a);
//	printf("%d\n", xx::a);
//	return 0;
//}//namespace xx
//{
//	//int a = 10;
//
//	namespace qq
//	{
//		int a = 3;
//		int b = 2;
//		int c;
//	}
//	using namespace qq;
//}
//namespace pp
//{
//	int a = 1;
//	using namespace xx;
//	using namespace qq;
//}
//using xx::a;
//using namespace xx;
//using namespace pp;
////using namespace qq;
////int a = 1;
//int main()
//{
//	printf("%d\n", a);
//	printf("%d\n", pp::b);
//	printf("%d\n", ::a);
//	printf("%d\n", xx::a);
//	printf("%d\n", qq::a);
//	printf("%d\n", xx::qq::a);
//
//	//printf("%d\n", qq::a);
//	//printf("%d\n", xx::qq::a);
//	//printf("%d",::a);
//	//int a = 2;
//	return 0;
//}//#include<iostream>
//
//using std::cout;
//using std::endl;
//using std::cin;
//int main()
//{
//	int a = 1;
//	cin >> a;
//	cout << a + 10 << endl;//std::cout << "C++ ";//cout << "helloc" << endl;//cout << "helloc" << endl;//cout << "helloc" << endl;//cout << "helloc" << endl;//cout << "helloc" << endl;//cout << "helloc" << endl;/*std::cout << "hello" << std::endl;std::cout << "hello" << std::endl;std::cout << "hello" << std::endl;std::cout << "hello" << std::endl;std::cout << "hello" << std::endl;std::cout << "hello" << std::endl;std::cout << "hello" << std::endl;*//*cout << a << endl;int b = 10;double x = 11.11;cout << b << " " << x << endl;*/
//	return 0;
//}//#include<iostream>
//using std::cout;
//using std::endl;
//using std::cin;
//void Func(int a = 1)
//{
//	cout << a << endl;
//}
////void Func1(int a = 10, int b, int c)
////{
////	cout << a + b + c << endl;
////}
//void Func1(int a = 10, int b = 20, int c)
//{
//	cout << a + b + c << endl;
//}
//int main()
//{
//	//Func1(1);
//	//Func1(1,2);
//	return 0;
//}//#include<iostream>
//using std::cout;
//using std::endl;
////void Func1(int b, int a)
////{
////	cout << a + b <<endl;
////}
//void Func1(int a, int b)
//{
//	cout << a + b << endl;
//}
//void Func1(double a, double b)
//{
//	cout << a + b << endl;
//}
//void Func1(int a, char b)
//{
//	cout << "Func1(int a, char b)" << endl;
//}
//void Func1(char b, int a)
//{
//	cout << "Func1(char b,int a)" << endl;
//}
//void Func1(int a, int b, int c)
//{
//	cout << a + b + c << endl;
//}
////void Func1(int a, int b, int c = 10)
////{
////	cout << a + b + c << endl;
////}
//void Func2(int a, int b)
//{
//	cout << a + b << endl;
//}
//int Func2(int a, int b)
//{
//	cout << a + b << endl;
//	return 0;
//}
//int main()
//{
//	Func2(1, 1);
//	Func2(2, 2);
//	Func1(1, 2);
//	Func1(1.1, 2.2);
//	Func1(1,'a');
//	Func1('a', 1);
//	Func1(1, 2, 3);
//	return 0;
//}//#include<iostream>
//using std::cout;
//using std::endl;
//using std::cin;
//void Swap(int* a, int* b)
//{
//	int tmp = *a;
//	*a = *b;
//	*b = tmp;
//}
//void Swap(int& a, int& b)
//{
//	int c = a;
//	a = b;
//	b = c;
//}
//int main()
//{
//	/*int m = 1;
//	int n = 10;
//	Swap(&m, &n);
//	cout << m << " " << n << endl;
//
//	Swap(m, n);
//	cout << m << " " << n << endl;*/
//	/*int a = 10;
//	cout << a << endl;
//	int& b = a;
//	cout << &b << endl;
//	cout << &a << endl;
//
//	b++;
//	cout << a << endl;
//	cout << b << endl;
//	a++;
//	cout << a << endl;
//	cout << b << endl;*/
//	//int a = 1;
//	//int& b = a;
//	//int& c = b;
//	//int& d = c;
//	//cout << a << endl << b << endl << c << endl << d << endl;
//	/*int a = 1;
//	double& b = a;*/
//	/*int& c;
//	int a = 1;
//	int& b = a;*/
//
//	/*int a = 1;
//	int b = 2;
//	int& c = a;
//	cout << c << endl;
//	int& c = b;
//	cout << c << endl;*/
//	/*int& a = 10;
//	const int& b = 10;*/
//
//	/*int a = 1;
//	int& b = a;
//	cout << b << endl;
//	int x = 10;
//	b = x;
//	cout << a << endl << b << endl;*/
//	/*int a = 10;
//	int& b = a;
//	const int& c = b;
//	const int& d = a;
//	const int& e = c;
//	a++;
//	cout << a << endl << b << endl << c << endl << d << endl << e << endl;*/
//
//	const int a = 1;
//	const int& b = a;
//
//	return 0;
//}//#include<iostream>
//using std::cout;
//using std::cin;
//using std::endl;
//int Func()
//{
//	int a = 1;
//	return a + 1;
//}
//int& Func1(int b)
//{
//	static int a = b + 1;
//	//a++;
//	return a;
//}
//int& Func2()
//{
//	static int a = 1;
//	return a;
//}
//int main()
//{
//	int& b = Func2();
//	cout << b << endl;
//	b++;
//	int c = Func2();
//	cout << c << endl;
//	/*const int& a = Func();
//	cout << a << endl;*/
//	
//	/*int& a = Func1(1);
//	cout << a << endl;
//	int& b = Func1(10);
//	cout << a << endl << b << endl;
//	int c = Func1(20);
//	cout << b << endl << c << endl;*/
//	return 0;
//}//#include <time.h>
//using std::cout;
//using std::endl;
//struct A { int a[10000]; };
//void TestFunc1(A a) {}
//void TestFunc2(A& a) {}
//void TestRefAndValue()
//{
//	A a;
//	// 以值作为函数参数
//	size_t begin1 = clock();
//	for (size_t i = 0; i < 10000; ++i)
//		TestFunc1(a);
//	size_t end1 = clock();
//	// 以引用作为函数参数
//	size_t begin2 = clock();
//	for (size_t i = 0; i < 10000; ++i)
//		TestFunc2(a);
//	size_t end2 = clock();
//	// 分别计算两个函数运行结束后的时间
//	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
//	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
//}
//#include <time.h>
//using std::cout;
//using std::endl;
//struct A { int a[10000]; };
//A a;
//// 值返回
//A TestFunc1() { return a; }
//// 引用返回
//A& TestFunc2() { return a; }
//void TestReturnByRefOrValue()
//{
//	// 以值作为函数的返回值类型
//	size_t begin1 = clock();
//	for (size_t i = 0; i < 100000; ++i)
//		TestFunc1();
//	size_t end1 = clock();
//	// 以引用作为函数的返回值类型
//	size_t begin2 = clock();
//	for (size_t i = 0; i < 100000; ++i)
//		TestFunc2();
//	size_t end2 = clock();
//	// 计算两个函数运算完成之后的时间
//	cout << "TestFunc1 time:" << end1 - begin1 << endl;
//	cout << "TestFunc2 time:" << end2 - begin2 << endl;
//}
//int main()
//{
//	TestReturnByRefOrValue();
//	return 0;
//}
//int main()
//{
//	TestRefAndValue();
//	return 0;
//}//#include<assert.h>
//#include<iostream>
//using std::cout;
//using std::endl;
//struct SeqList
//{
//	int a[100];
//	size_t size;
//
//	int& at(int pos)
//	{
//		assert(pos >= 0 && pos < 100);
//		return a[pos];
//	}
//
//	int& operator[](int pos)
//	{
//		assert(pos >= 0 && pos < 100);
//		return a[pos];
//	}
//};
//int main()
//{
//	SeqList s;
//	s.at(0) = 0;
//	s.at(0)++;
//	cout << s.at(0) << endl;
//	s[1] = 10;
//	s[1]++;
//	cout << s[1] << endl;
//	return 0;
//}
#include<iostream>
using std::endl;
using std::cout;
void f(int)
{cout << "f(int)" << endl;
}
void f(int*)
{cout << "f(int*)" << endl;
}
int main()
{f(0);f(NULL);f((int*)NULL);return 0;
}//int main()
//{
//	int array[] = {1,2,3,4,5};
//	for (auto i : array)
//	{
//		cout << i << " ";
//	}
//	cout << "\n" << endl;
//	for (auto i : array)
//	{
//		i *= 2;
//	}
//	for (auto i : array)
//	{
//		cout << i << " ";
//	}
//	cout << "\n" << endl;
//	for (auto& i : array)
//	{
//		i *= 2;
//	}
//	for (auto i : array)
//	{
//		cout << i << " ";
//	}
//	return 0;
//}
//int main()
//{
//	auto a = 1;
//	auto* b = &a;
//	auto c = &a;
//	auto d = a;
//	auto& e = a;
//	cout << typeid(a).name() << endl;
//	cout << typeid(b).name() << endl;
//	cout << typeid(c).name() << endl;
//	cout << typeid(d).name() << endl;
//	cout << typeid(e).name() << endl;
//
//	auto a = 1, b = 2;
//	auto c = 1.0, d = 35;
//
//	return 0;
//}
http://www.dtcms.com/a/394613.html

相关文章:

  • 红黑树的介绍
  • NumPy 系列(六):numpy 数组函数
  • 手写链路追踪-日志追踪性能分析
  • 数据库自增字段归零(id)从1开始累加
  • 轻量级本地化解决方案:实现填空题识别与答案分离的自动化流程
  • P1104 生日-普及-
  • CMake如何添加.C.H文件
  • 实时数据如何实现同步?一文讲清数据同步方式
  • 六、Java框架
  • 施耐德 M340 M580 数据移动指令 EXTRACT
  • 4. 引用的本质
  • 专业历史知识智能体系统设计与实现
  • 算法基础篇(4)枚举
  • 【C++】二叉搜索树及其模拟实现
  • 第二十一讲:C++异常
  • 2025年9月第2周AI资讯
  • 从 UNet 到 UCTransNet:一次分割项目中 BCE Loss 失效的踩坑记录
  • leetcode刷题记录2(java)
  • JAVA八股文——方法区
  • 链表操作与反转
  • AI编程 -- 学习笔记
  • 动态规划问题 -- 子数组模型(乘积最大数组)
  • 【AIGC】大模型面试高频考点18-大模型压力测试指标
  • Cannot find a valid baseurl for repo: base/7/x86_64
  • Lowpoly建模练习集
  • 六、kubernetes 1.29 之 Pod 控制器02
  • OpenCV:人脸检测,Haar 级联分类器原理
  • 类和对象 (上)
  • FreeRTOS 队列集(Queue Set)机制详解
  • 【论文速递】2025年第20周(May-11-17)(Robotics/Embodied AI/LLM)