C++探险之旅--能有效解决C语言某些问题而出现的C语法

C++探险之旅–能有效解决C语言某些问题而出现的C++语法

命名空间

​ 了解过C++的道友肯定见过这几行代码:

#include <iostream>
using namespace std;

int main()
{
    printf("Hello World\n");
    
    return 0;
}

​ 没错,这就是C++版本的Hello World程序,是不是感到十分亲切?但是是不是又对其中的using namespace std等语句感到奇怪。不用担心,后面我们就会介绍。现在,我们先转移战场,先来看看另一个程序的运行结果。

#include <stdio.h>

int rand = 0;
int main()
{
    printf("%d",rand);
    
    return 0;
}

​ 学习过C语言,大家一定很轻松就能看懂该程序,大家猜猜这段程序的运行结果是什么。没错就是打印出0,可以正常运行。我们可以在VS上查看运行结果。

在这里插入图片描述

现在,我们再对这个程序进行修改。

#include <stdio.h>
#include <stdlib.h>

int rand = 0;
int main()
{
    printf("%d",rand);
    
    return 0;
}

​ 我们在程序1的基础上添加了1行代码,即引入了<stdlib.h>这个头文件,大家不妨再猜一猜这个程序的运行结果😁。这次,我相信很多对细节不是很了解的道友会犯错。对于这个程序,编译器会报错,程序无法运行,我们不妨来看一看这个错误信息。

在这里插入图片描述

​ 错误信息大家一定不默认–重定义,编译器的意思是说我们重复定义了rand。有些道友现在肯定很迷,这程序就这几行代码,哪来的重定义?再看下警告信息,它说rand有了后面这个类型,学过C语言的一看就知道,这是个函数指针,参数是一个void类型的变量,返回值为int,这意思是说rand是个函数?看到这,不知道大家是否有想起来rand是什么。没错,rand是C语言中用来生成随机数的一个函数。现在大家知道了rand是什么。但是又可能有另一个问题了,为啥程序1就没问题,程序2就重定义了?后者相较于前者就只是引入了一个头文件,为啥就导致了这个问题?大家不妨看一下这个头文件,试着想一想它和作为函数的rand的关系。是的,在使用rand这个函数时,我们需要包含<stdlib.h>这个头文件,为什么?因为rand的函数声明存在于这个头文件中。现在我们不妨再回忆下头文件引入的意思:

C语言标准委员会为了给C语言使用者提供那些需要经常使用的函数,将这些函数的声明包含在不同的头文件中,在需要使用对应的函数时,我们引入对应的头文件即可。而引入头文件,就是在编译器在预处理阶段在引入头文件的对象位置,将其替换为头文件的内容。

​ 现在,我们再来梳理下这个程序的问题:我们引入了<stdlib.h>,相当于加入了rand函数的声明,然后又定义了一个同名的全局变量rand,我们不妨来模拟一下这个场景:

#include <stdio.h>

int rand(void);
int rand = 0;

int main()
{
    printf("%d",rand);
    
    return 0;
}

​ 现在,我们再来回忆一下编译器的查找规则(假设查找rand):

首先,编译器会先看对应的局部域内有没有叫rand的变量(C语言中函数都是定义在全局的或者其他源文件中的)找到了就使用;如果没有,再去全局域中查找同名的变量或者函数,找到了就使用,没找到就报错。

​ 而在这个程序中,编译器先在调用printf函数的main函数所在的局部域查找rand,没有找到,然后去全局域中查找,然后就找到了两个rand(在按名字查找时,编译器无法识别是函数名还是变量名,除非传参等一些操作),于是,就产生了这个错误。

​ 到此,我们终于理清了这个程序的问题,其主要原因是因为我们引入了<stdilb.h>这个头文件。怎么样?各位道友有没有感到很无语,最开始程序没什么问题,但就因为我们引入了一个头文件,突然就报了一个错误。现在思考一个问题:对于头文件,我们没有办法将所有的头文件的内容的内容全部获悉,有时难免我们会定义和某个头文件中名字相同的变量或者函数,然后,就报错了。。。。。。是不是很无语,关键是在C语言中还没有很好的办法来解决这个问题。不止如此,还有另一个更加常见的易报错场景:在开发大型项目是,通常采用分工合作的方式,现在不妨想,你在开发某个功能时,定义了一个叫做max的变量,并多次使用。而同时,另一个同事在他实现的功能中也定义了max这个变量。你们的代码分开编译时都没有问题。然而你们在将代码合并的时候,却在多处报了"重定义/max不明确"这个错误,这时,你们就必须牺牲一人的max,换成其他的名字,哈哈,想着就难受。你绝对不能说这种情况不可能出现,因为你们不可能事先协商好此类的所有问题,同时max这个变量也经常定义。

​ 在这样的场景下,祖师爷创造性地增加了一个语法–命名空间域,关键字namespace,我们可以使用namespace关键字,创建多个命名空间域,这些空间相互独立,我们可以在不同的命名空间域内部定义同名的变量或者函数,它们同样互不影响(当然,这是在你不同时展开这两个作用域或者你不指定命名空间时,后续会解释)。现在,我们来用命名空间来修正上面的程序。

#include <stdio.h>
#include <stdlib.h>

namespace tmp //创建一个命名空间域的格式 namespace +空间名+{}
{             //{}中是一个独立的空间,可以定义变量,也可以定义函数
    int rand = 10;
    
    //int Add(int a,int b) 可以在命名空间中定义函数,与外部函数无关,可以同名同参
    //{
    //   return a+b;
    //}
}

int main()
{
    printf("%d",rand);
    
    return 0;
}

​ 这时我们运行该程序,不会再报错,会正常运行,打印的是rand的地址转换为十进制后的值。可以再用%x(16进制)打印一下,我们观察一下。

在这里插入图片描述

​ 还有一点需要注意,我们可以定义名字相同的命名空间域,编译器会自动将名字相同的命名空间域合并在一起。

​ 接下来,我们进入下一个与命名空间域密切相关的主体,作用于限定符。

作用域限定符::

​ 在解决了这这个问题之后,随之而来与之密切相关的是另一个问题:我们如何访问命名空间中的变量/函数,如果有一个局部变量tmp,还有一个全局变量tmp,我们如何在局部变量所在的作用域内访问全局的tmp,使用代码实现一个这个场景:

#include <stdio.h>

int tmp = 20;

int main()
{
    int tmp = 30;
    printf("%d\n",tmp);
    
    return 0;
}

​ 我们运行该程序,来观察一下结果:

在这里插入图片描述

​ 可以观察到,如果使用printf的局部域内有名为tmp的局部变量,printf会默认打印该局部变量的值;同理,如果使用tmp作为右值给变量赋值,也会默认使用局部的tmp。在C语言中,如果我们想使用全局的tmp,是没有很好的办法的。所以在C++中,为了解决这个问题,引入了域作用限定符**:😗*,既可以和命名空间域相配合使用,也能很好地解决这里的问题。下面我们用代码来展示一下作用域限定符的使用方法和效果:

#include <stdio.h>

int tmp = 20;

namespace test
{
    int tmp = 10;
}
int main()
{
    int tmp = 30;
    printf("%d\n",tmp);   //如果局部有tmp,默认使用局部的tmp,打印30
    
    printf("%d\n",::tmp); //使用::如果该作用域限定符前面是空格(可省略)
                          //则使用全局的tmp,没有则报错
    
    printf("%d\n",test::tmp); //namespace内部也是一个域,使用::,前面加空间名,则指定访问对应空间                               // 的变量和函数,没有则报错。
    
   return 0;
}

​ 编译运行该程序,大家可以有个很直观的认识(注意,源文件后缀为.cpp,否则编译该程序报错,因为namespace是C++相较于C语言特有的关键字,只有C++的编译器才能识别):
在这里插入图片描述

​ 还有两个地方需要注意:

  1. 前面说过,我们可以在namesapce定义的空间域内定义函数,所以我们可以在全局域和不同的命名空间域内定义名字相同的函数,它们互不影响,也可以通过::显示调用不同域内功能相同或不同的同名函数。
  2. 对于命名空间内部的变量和函数,编译器是不会主动去内部查找使用的,如果需要让编译器使用某个命名空间域内部的东西,我们需要显示用::指定命名空间域或者使用using关键字展开命名空间域,关于using的使用方法,接下来就会介绍。我们可以这样理解命名空间,刚创建它时,它是封闭的,我们没有进入其中的权利;只有我们使用::跟它进行沟通,才能获得访问的权利;或者我们可以用using关键字让它打开这个空间的大门,这时我们才能进入访问。

​ 这个问题到此结束,我们开始探究下一个问题,using的使用。

using

​ 现在,我们回到我们最开始的那个C++版本的"Hello World"。

#include <iostream>
using namespace std;

int main()
{
    cout<<"Hello World"<<endl;
    
    return 0;
}

​ 先说第一行代码:

#include

​ 使用C语言的都会觉得这行代码很眼熟,和C语言中包含头文件很相似,只不过不是.h文件了罢了。其实这本来就是沿袭的C语言的风格,只是为了和C语言区分,所以C++省去了后面的.h,当然,这其中还有一些原因,比较显著的是不加.h的头文件中含有C++特有的内容,比如说namespace定义的命名空间,C++标准库的内容就定义在std这个命名空间中,但是大家现在不必关心。而这个头文件包含了C++中有关输入,输出的函数。由于我们在后面的代码中使用了其中的输出流函数cout,所以需要包含这个头文件。当然,我们也可以在.cpp文件中包含类似于<stdio.h>这样的C式的头文件,这样不会有任何问题–之前说过,C++是兼容C语言的。

​ 接下来,我们先解释cout这行代码:

cout<<“Hello World”<<endl;

​ 前面说过,cout是C++特有的流输出函数。关于流的概念,大家可以想象成水。与cout配套的是<<这个操作符。

在C语言中,这个操作符被称为左移操作符,作用是对整型数据的二进制形式数据进行移位,空的位置补0。比如一个数的二进制位是00000011那么对这个数<<2后,这个数就变成了00001100。与之类似还有一个右移操作符>>,作用与<<操作符相反,但是还有算术右移和逻辑右移的区别,这里是介绍C++,就不再赘述,感兴趣的道友可以自行去了解。

​ 而在C++中,对这两个操作符进行了操作符重载,至于有关操作符重载的有关内容,我们后续在进行介绍。在C++中,当<<和cout一起使用时,<<被称为流插入操作符,<<右边的内容,就好像流水一样,依次流入cout,而cout其实就相当于我们C语言中的命令行窗口(黑框框),所以右边的内容就会依次出现在命令行窗口中。比如说cout<<“Hello World “<<”你好,世界“<<endl;这时,<<右边的内容会像水一样流入cout,先流入”Hello World “,再流入"你好,世界”,所以命令行窗口中会打印出"Hello World 你好,世界”。而关于>>,一般是和cin配套使用的,>>又名流提取操作符。cin的作用就类似于C语言中的scanf,它会从命令行窗口(我们输入的内容)提取内容(字符/字符串以空格作为分割,其他内置类型一般以换行为分割。当然,我们也可以自己对其进行运算符重载,来满足我们自己的需求。)流入>>的右边的变量。比如cin>>a>>b;其中a,b均为整型变量。我们输入1 2回车,它会将1流入a,2流入b,关于这两个操作符的更多细节,大家可以实践获得,这里不做重点介绍。还有endl,其实他就相当于换行,没啥说的。但是还有一点需要注意,在使用cout和cin时,%d,%s之类的格式化字符串形式是不能使用的。如果确实需要对内容进行格式化处理,大家可以继续使用printf,scanf之类的C氏函数。

​ 现在我们再次回到第二行代码:

using namespace std;

​ 首先,我们可以清晰地看到namespace这个我们之前讲解过的C++中的关键字,还记得它的作用吗?定义命名空间域;至于std,之前也提到过,有关C++标准库的内容是放在std这个命名空间域内的;至于using,这个单词大家都不会陌生–使用。我们为什么需要加上这行代码呢?前面提到过,类似cin,cout,之类的函数是定义在std这个命名空间的,而之前也说过,命名空间内部的内容我们是无法直接进行访问的,也就是说,如果我们需要使用cout这个函数,应该这样使用:std::cout。同理,cin和endl应该这样使用:std::cin,std::endl。大家知道,在一个程序中,输入和输出操作往往是很多的,如果一个程序中有上千处地方使用了这几个函数,我们就需要在每个地方加上std::这个限定操作,这会降低我们的开发效率。所以为了应对这个问题,祖师爷创造了using这个关键字,我们可以使用using关键字指定获取访问某个命名空间域的能力。比如说在这里我们使用using namespace std;告诉编译器编译器可以去std这个命名空间域内部进行查找。这样在使用cou之类的std这个命名空间中的函数时,虽然全局域内没有该函数的实现,但是由于std这个命名空间域内有该函数的实现,就会使用该命名空间域内的cout等函数。但是需要注意的是,其实打开某个命名空间域的方式其实并不好,因为打开某个命名空间域就相当于将这个命名空间域的变量与函数放在了全局,而对于std这样的有别人定义的命名空间域,其内部细节我们是不易全部获悉的,我们很有可能定义与其内部某个函数同名同参数的函数或者同名的变量,这个时候编译器就会报错,显示某某某不明确的错误。如果这个变量或者函数我们使用的次数少还好说,但是如果其使用过于频繁,我们就会对出大量的用于指定命名空间域的改错时间,降低开发成本。所以说using还有另一个用法,指定访问某一个变量或者函数,也就是只将某个变量或者函数暴露到全局。使用方法如下:using std::cin;其他的类似,这是一个良好的风格。当然,我们平时日常使用可以直接打开std这个空间域,因为我们平时的程序一般比较短,易于修改。

​ 我们再观察一下上面说到的某某某不明确的问题:

在这里插入图片描述

​ 至此,有关using的内容就到此结束了。我们就如下一个话题–内联函数。

内联函数(inline)

​ 经常使用C语言的道友们都知道,函数调用会导致压栈,而压栈会消耗时间,如果频繁压栈且不出栈,就会导致栈溢出(有名的stack overflow),当然,一般情况下这个错误不会出现,除非在使用递归函数而函数没有设置返回条件时才会出现。但是,如果有些较为短小的函数被频繁地调用,就会频繁压栈,出栈,会大量的浪费时间,降低效率。而在C语言中,为了一定程度上解决这种有短小函数的频繁调用而导致的时间浪费,引入了宏函数来解决这个问题,宏函数类似于用#define定义的常量,会在其使用的地方在预处理阶段直接替换其内容,不会调用函数,产生压栈,出栈(即对应的汇编代码中不会用call指令)。坏处就是无法调试,同时书写困难,容易导致一些无法调试的错误。我们这里以一个实现加法功能的宏函数为例:

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

​ 可能有些不怎么使用宏函数的道友对这个宏函数感到疑惑–为什么要用这么多的括号?我们以一个例子来理解一下原因。

#define ADD(x,y) (x+y)  //假如我们这样定义该宏函数

ADD(1,2);      //这不会有问题。预处理器会将其直接替换为 (1+2)

ADD(1<<2,3+4)  //出现错误。编译器会将其替换为(1<<2+3+4),由于<<的优先级比+低,该式会先执行+操作
    		  //即1<<(2+3+4) -> 1<<9=512 (<操作符相当于做操作数乘以2的右操作数次幂)
    		  //而我们的本意是(1<<2) + (3+4) = 11  所以应该写成#define ADD(x,y) (x)+(y)
#define ADD(x,y) (x)+(y)  //假如我们这样定义该宏函数

int a = 10*ADD(1,2)    //出现错误,编译器会将其替换为10*(1)+(2) == 12
    				 //而我们的本意是10*((1)+(2)) == 30
    				 //所以我们应该写成#define ADD(x,y) ((x)+(y)) 

​ 到此,我们说明了为什么该宏函数应该是这样,也只能是这样才能规避掉其他错误。我们在编译器上观察一下我们上面的例子的执行结果。
在这里插入图片描述
在这里插入图片描述

​ 定义宏函数时,除了以上错误,还有一种对于那些不经常使用宏函数的道友来说更加常见的错误–在定义宏函数时,末尾多写了一个分号。这个错误可以说十分常见了,至于为什么错误相信大家可以理解。

​ 总而言之,C语言定义宏函数虽然一定程度上解决了短小函数的频繁压栈,出栈而导致的时间消耗,但是也导致了更多的不易发现的错误,对于程序员的要求反而有所提高。

​ 在这种现象下,祖师爷为了解决这个问题,创造除了内联函数,极大地改善了这个问题。内联函数的创建十分简单,我们只需要在声明好一般函数后,在函数声明的位置在其返回值前面加上inline关键字,那么这个函数就可能成为内联函数。为什么说可能?因为C++规定,只有那些短小的,内部没有循环,递归的函数才能成为内联函数,而这个“短小“由编译器决定,所以最终这个函数能否成为内联函数,由编译器决定。所以一个函数要成为内联函数,有以下条件:

  1. 函数内部的代码块短小,代码量少。
  2. 函数内部没有循环,递归

​ 在说明了一个函数如何成为内联函数后,我们再来探讨内联函数的使用。对于其使用方法,和普通函数没有什么区别,只是编译器的调用过程会有区别。在我们调用函数后,编译器会判断该函数是否是内联函数,如果是,则直接将调用部分替换为函数体内部的内容执行。如果不是内联函数,则会call该函数的地址,压栈调用该函数。我们可以用编译器观察该过程。

在这里插入图片描述
在这里插入图片描述

​ 图一是不加inline关键字的普通函数的调用结果,可以在右侧的汇编代码中明显看见一条call指令,这条指令就是调用函数的标志,表示跳转到Add函数的地址。图二是加上inline关键字后的调用结果,可以发现call指令消失了,这可以告诉我们没有函数调用的压栈,出栈操作了。

​ 内联函数还有一个地方需要注意,内联函数不能声明与定义分离。因为内联函数不会进入符号表,即编译器不会保存内联函数的地址,这也是它不能call的原因。假如我们将声明和定义分离,如果声明位置加上inline关键字,位于一个.h头文件中;而定义写在另一个.cpp源文件中,编译器是无法调用该函数的,我们可以看一下这样做的结果。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
​ 编译运行该该程序后,会产生图一的错误。因为编译器在发现我们的函数调用的,回去按照名字查找该函数的声明。发现该函数时内联函数后,会直接将调用部分转换为函数体的内容,这和宏函数类似。但是由于我们声明与定义分离了,这就和我们声明了函数后但是没有定义函数一样。和下面这种情况类似:

在这里插入图片描述
在这里插入图片描述
​ 可以看到,这个程序的运行结果和上面类似。所以,在构造内联函数时,不要将声明和定义相分离!

​ 那么内联函数有什么缺点呢?考虑这样一个场景:有一个程序,存在一个内联函数,函数的代码块部分编译后有五六行指令,但是程序中有上万处地方调用了该函数,那会产生什么后果?原本这个程序调用该程序所产生的指令应该只有call指令数(即该函数的调用次数)+该函数编译后的指令数5-6个,然而当这个函数变成内联函数后,其指令直接变为(指令数5-6)*(调用次数)。这会导致什么?这会使你程序的体积即占用的内存急剧增大。在某些内存非常珍贵的地方就会产生非常严重的问题。

​ 以上就是关于内联函数的相关内容。内联虽好,各位道友也要慎用。接下来,我们进入下一个话题,函数重载

函数重载

​ 从名字就可以看出,这个内容是和函数相关的。不知道大家在C语言中使用函数时有没有遇到这个问题:我们要构造一个函数,作用是实现两个数的加法运算,然后我们实现了这样一个函数int Add(int x,int y)。后来,我们需要实现两个浮点数的加法运算,但是由于Add这个函数名已经被int型的两个数的加法函数所使用了,所以我们只能这样构造该函数:float AddForFloat(float x,float y)。再后来,我们还需要实现一个int和一个float的加法运算,我们又需要实现float AddForIntFloat(int x,float y)。后来,我们的需求提升,还需要多个变量的加法运算…就这样,本来都是实现加法功能,但在C语言中我们却不得不为每个函数去想不同的名字,而且这个名字还往往比较长,我们会在这种无意义的事情上浪费大量的时间。

​ 所以在C++中,祖师爷就想:我们可以定义函数名相同的函数,我们给它传不同的参数,它可以自动匹配,寻找可以匹配且最合适的函数调用。所以祖师爷弄出了函数重载这种语法。我们可以定义不同的函数参数不同的同名函数,编译器会在编译器间对这些函数根据参数对其函数名进行修饰以在我们调用是根据我们传的参数来匹配对应的函数。要满足函数重载首先需要满足函数名相同,然后满足下面的其中一个条件就可以了:

  1. 函数的参数个数不同。
  2. 函数的参数类型不同,(int,float) 和 (float,int)这种也可以构成函数重载。

​ 需要注意的是,函数返回值不同是无法构成函数重载的,一定不要搞混。

​ 接下来,我们使用一个例子来验证一下我们的说法:

#include <iostream>
using namespace std;

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

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

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

int main()
{
    int i1 = 1;
    int i2 = 2;
    float f1 = 2.2;
    float f2 = 3.3;
    cout << Add(i1,i2) << endl;
    cout << Add(f1,f2) << endl;
    cout << Add(i1,f1) << endl;
    cout << Add(f1,i2) << endl;
    
    return 0;
}

​ 我们在VS上编译运行该程序,并连续按F11让程序逐条执行。图片右侧箭头表示当前执行语句,每两张表示一个函数的运行结果。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
​ 至此,关于函数重载的基础内容就到此结束了,有关它的更多内容,我们将在后面的探索中揭晓。

​ 下面,我们进入另一个话题:函数缺省值

函数缺省值

​ 在C++中,我们是可以给函数的参数一个默认值的值的。比如我们在C语言中实现一个动态顺序表时,我们需要给顺序表提前开辟空间,而这时我们就需要显示传入初始化空间的大小(当然,我们也可以在内部限定初始化空间的大小,即无论如何,都开辟同样大小的空间。但是一般我们喜欢自己定义初始化空间的大小。比如有一个场景需要向顺序表中插入大量的数据,这是已知的。如果这时我们还是提前开辟默认空间的空间,就会导致大量的扩容消耗;反之类似,如果顺序表中已知不会插入多少数据,但是我们默认开辟的初始空间却过大的话,也会导致空间的浪费。综上,我们一般喜欢按需求控制初始化空间的大小)。但是大部分情况下都不会出现极端情况,这时使用默认的初始化空间大小即可,但是在C语言中我们却不得不传入这个参数,就会显得比较麻烦。所以在C++中,支持了函数的缺省值。我们就用上面的顺序表的初始化来举个例子:

void initSeqList(seqList* l,size_t n = 10)
{
	l->key = (int*)malloc(sizeof(int)*n);   //key是顺序表中存储数据的空间
    if(l->key == nullptr)
    {
        perror("malloc fail\n");
    }
}	

​ 就这样,当我们需要指定初始化空间的大小时,我们就可以传入第二个参数;如果需要使用默认值,我们不传第二个参数也可以。当然,函数缺省值的使用不止于此,后面有具体的使用场景时再详细介绍。

​ 下面,我们进入下一个话题,nullptr和NULL

nullptr和NULL

​ 这个话题其实是C与C++的一个小的差异。可以看到,在上面顺序表的初始化中,如果开辟失败,我用的是和nullptr比较而不是NULL,这是为什么呢?C++中为什么要新弄出个nullptr呢?我们只需要知道,C++中nullptr指的是空指针,而NULL则被定义为了0。而在C语言中,NULL就指的是空指针。至于为什么要这样做,那就不得而知的。

​ 下面,我们进入下一个话题,引用

引用

​ 使用C语言的道友都知道,指针是C语言的灵魂,但同样的,它也是C语言中最复杂的东西。如果一个C程序员说他没有没有因为指针的问题而掉过头发,我大抵是不信的。祖师爷或许也是注意到了指针使用的复杂,所以他制造出了引用的语法。引用,就好像是我们给自己起了个绰号一样,它虽然和我们有不同的名字,但是本质上却是同一个人。

​ 这里举个例子:在水浒传中,李逵又叫黑旋风。那你说黑旋风改变了,李逵改变了吗?答案是肯定的。所以说同样的,对某个变量的引用进行更改操作,原变量自然也改变了。

​ 下面,我们来介绍一下如何使用引用:

int a = 10;

int& b = a;

​ 如上就是引用最基本的用法,类型+& 变量名 = 引用对象,就这么简单。有些使用C语言的道友看到&这个符号可能会认为是取地址符,但是在这里它可不是用来取地址的,只是用来标识b是一个引用而已。至于为什么要这么用,那各位只能给祖师爷打电话问他了😂。在理解上,我们认为引用是不开辟空间的,只是原变量起了个别名。引用有几点需要注意:

  1. 引用类型必须与引用对象一致,这点很好理解。
  2. 引用变量声明时必须赋初值,也就是说声明这个引用变量的时候就必须指定它是谁的别名。
  3. 引用变量在它的生命周期内只能作为一个变量的别名,不能更改。比如说上面,如果我们再定义一个变量:int c = 20;然后我们执行b = c;这个操作,这个操作是给b,即a赋值为a的值,并没有改变b的引用对象。

​ 关于引用,还有一个比较重要的只是,叫做常引用。举个例子说明:

int a = 10;

const int& b = a;

​ 注意到,我们在声明b时在b前面加了const,这个关键字相信大家不会陌生,const修饰变量时会使变量具有常属性。也就是说被const修饰的变量虽然本质上仍然是一个变量,但是它的值却不能被更改了。而在引用这里也依然类似,当const修饰引用变量时,引用变量的值就无法改变了。通俗点说,就是不能再对该引用变量进行赋值了。当然,这种常属性仅仅只会影响被const修饰的引用变量,是不会对原变量或者原变量的其他引用的。同样是上面那个例子来帮大家理解一下:李逵下山之前当家的叫他下山后作为黑旋风时不要喝酒,难道说当黑旋风在山上作为李逵时仍然不能喝酒吗?答案同样是肯定的。

​ 关于常引用,还有一个地方需要注意,一个const变量不能为它设置一个不具有常属性的引用变量。比如说下面这种错误案例:

const int a = 10;

int& b = a;

​ 如果我们把这两行代码放在程序中编译,会报下面的错误:

在这里插入图片描述

​ 这就好比是你自己本身没有喝酒的能力,难道给你取个别的名字,你就有了喝酒的能力了吗?上面那种情况(正确的常引用)就类似于你有能力但是不赋予你使用这个能力的权利,这当然是可以实现的;而下面这种错误情况就类似于你本身就没有这个能力,但是却想给你赋予使用这个能力的权利,这本身就没有什么意义。

​ 到此,有关引用的基础使用就介绍到这里。更多内容后面穿插讲解。

​ 下面,我们进入下一个主题–模板

模板

​ 上面的函数重载虽然解决了一些同名函数的问题,但是大家有没有发现:这些函数内部实现的功能都是类似的,只是函数的参数的个数或者参数的类型不同。我们虽然可以自己在需要使用某种函数时自己对其进行函数重载,但是既然这些函数的内部都是类似的,那么我们是否可以把这种重复性的动作交给编译器去做呢?面对这种情况,祖师爷设置了模版这种语法。可以这样说,模板是C++实现代码复用和泛型编程的重要组成部分。泛型编程通俗地讲就是一段代码可以供至少两种以上的类型使用。

​ C++中,模板的关键字是template,然后一个<>,<>内部写入我们参数列表,可以这样说,在我们将要实现的函数中,我们依赖于几个类型,<>内部就定义几个参数。而参数的格式是class/typename+参数名,除了某些特殊情况,class和typename是没有区别的,它们的区别后面遇到了再解释。我们可以把<>内部的参数看做类型来使用,而这个类型是由我们自己显示传入或者编译器根据我们传入的参数来隐式确定的。光说可能不好理解,我们举个例子方便大家理解,同样是上面的实现两个变量加法的函数实现:

#include <iostream>
using namespace std;

template<class T1,class T2>
//template <typename T1,tepename T2>
T1 Add(T1 x,T2 y)
{
    return x+y;
}

int main()
{
    int i1 = 1;
    int i2 = 2;
    float f1 = 2.2;
    float f2 = 3.3;
    cout << Add(i1,i2) << endl;
    cout << Add(f1,f2) << endl;
    cout << Add(i1,f1) << endl;
    cout << Add(f1,i2) << endl;
    
    return 0;
}

​ 运行该程序,出现下面的结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
​ 在VS中的调试窗口中打开调用堆栈,然后分别在进入Add函数的位置查看堆栈信息,截图如上所示,大家可以看见,我们虽然只实现了一个模版函数,但是实际上却有四个Add函数,它们的参数是不同的,而且其参数类型刚好就是我们给它传入的参数的类型,这就是编译器自己根据我们传入的参数判断了T1,和T2的类型。可以看到,由于我们模板函数返回值的位置写的是T1,所以这四个函数的返回值类型是和第一个参数的类型是一致的,这可以从打印结果看出。我们也可以显示实例化,比如:

Add<int,float>(i1,f1);

​ 实例化的方式就是这样,<>内部写入类型,分别传给T1和T2。编译器根据函数模板生成函数的操作又叫函数的模板实例化,模板的作用其实还有生成模板类,只是类我们还没有讲到,所以后面介绍类的时候再介绍。

结语

​ 在C++中,如果没有具体的应用场景,有很多知识是不好讲解的,所以有很多比较抽象的东西我只是粗略的介绍了一下,我们在后面的逐渐深入中还会反复使用这些语法,到时候大家会更好理解这些语法。现在如果造成了大家的困扰,我感到很抱歉。希望各位道友能和我一起学习,我相信在后面的学习中,我们会逐渐掌握这些细节。第一次介绍C++的内容,如果大家发现了什么问题,欢迎在评论区指出,我不胜感激。创作不易,如果大家有所收获,希望大家点个赞,点个订阅,在此感谢大家。希望大家持续进步!
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值