C++基础讲解,用于C语言向C++的衔接
命名空间
如果你以前看到过C++的程序,那么你大概率会看到这样一行代码
这行代码就用到了命名空间的知识,using就是使用的意思,namespace就是命名空间,std就是这个命名空间的名字。
C++的命名空间,顾名思义,就是在某一空间内对一些类型(变量,常量,结构体,函数等)进行命名。
在这里我进行示范,写下一个命名空间
注意这里命名空间内的两个变量是全局变量,而不是局部变量。这里我就不得不提一下,只有定义在函数内的变量才是局部变量,为什么?因为局部变量存储在栈区,而函数的调用会创建栈帧,函数结束时栈帧会销毁,并且局部变量随之销毁。
而命名空间内的变量是全局变量,它们存储在静态区。并且我顺便提一下,局部变量存储在栈区,全局变量存储在静态区,动态内存开辟的空间存储在堆区。
还有就是我们头文件里的变量属于什么?在预处理时头文件就被展开了,所以头文件内的变量其实也属于全局变量。
并且编译器默认的查找规则是:先在局部找,再到全局找。并且如果有了命名空间,直接去指定的命名空间内找,当命名空间内也找不到这个这个名字的话,就会报错,而不会继续去别的地方找。
命名空间如何使用?
这时候我们发现我们用不了a,刚才不是说a是全局变量吗,这里为什么用不了?原因是命名空间就像无形的一堵墙一样,它把它内部的变量全都关住了,别人根本就找不到它。
如果要想使用,就必须加上名称空间标示符。
但是,如果我们要大量地使用a这个变量的话,每次都加上名称空间标识符岂不是很麻烦?
所以我们也有其他的方法
如果我们不只是频繁地使用这个命名空间中的一个变量,而是频繁地使用其他变量的话,也可以这样表示。
我们明白了这个之后,再解释解释经常看到的那个命名空间。
std 是C++官方库内定义的命名空间。C++官方库内有许多文件,每个文件内的命名空间都为std,这样并不会导致冲突,因为不同位置的同名命名空间会合并为同一个命名空间。
上面那条语句的作用,相当于把std展开,把它里面所有的东西都暴露出来,这样使用就很方面了。
但是这样又有一些不足,我们在日常写一些小程序的时候这样写没什么问题,但是项目里不要这么写,容易引起和自己定义的变量引起冲突。
我们在项目里面可以进行这样使用。
把常见的名字这样单独拿出来,在自己起名字的时候避开这些常见的就可以了。
另外,命名空间也是可以嵌套的,比如:
总结:通常来说,命名空间是唯一识别的一套文字,这样当相同的名字却来自不同的地方的时候就不会含糊不清了。也就是说,命名空间的使用可以避免命名的冲突。同时,命名空间的展开又可以分为局部展开,全部展开,也可以不展开,在使用的时候加上标识符即可。
C++输入输出
C++的输入输出我们作为新手不需要过多的了解,只要会使用即可。
cin相当于C语言的输入函数,cout相当于C语言的输出函数。与C语言中输入输出函数不同的是,cin和cout可以自动识别变量的类型。
缺省参数
定义:缺省参数是在函数声明或者定义时为函数的参数指定一个缺省值,在函数调用时如果没有指定的实参则形参的值为缺省值,否则就为指定的实参。
当函数的参数中有多个参数具有缺省值时,则有不同的传参方法。
并且,传参是连续的,必须从左到右传参,不能只给c传参而不给a和b传参。
注意,函数的声明和定义中不能同时出现缺省参数,防止出现声明和定义中缺省参数不一样的尴尬局面。如果函数同时有声明和定义的话,要把缺省参数放在声明中。
函数重载
定义:函数重载是函数的一种特殊情况,C++允许在同一空间内定义功能类似且名字相同的函数,这些函数的参数(参数类型,参数个数,参数顺序)不同,常用来处理功能类似但数据类型不同的问题。
就像这样,我们定义两个add函数,实现函数重载,就不用在意我们要相加的两个数是整数还是浮点数了。
但是,函数重载不能与缺省参数结合起来使用,就像这样。
你不传参进去,编译器不知道你想调用哪一个函数。
为什么C语言不支持函数重载,而C++可以?C++存在名字修饰!
C语言编译之后,函数名不会发生变化。而C++编译之后,编译器用根据函数的参数类型对编译器进行修饰,这就是名字修饰。
为什么同名函数的参数相同而只有返回值不同不会进行函数重载?
这并不是因为名字修饰时不会带上返回值的类型,而是因为在调用函数的时候具有二义性,编译器不知道函数要返回什么类型的值。
引用
引用就相当于给变量取别名,使得变量在同一空间内有多个名字。
你对引用操作,和对引用的实体操作的是一样的。
并且这和指针不一样,指针是指向a这个变量的地址。而引用就是引用的实体,它们两个是一样的。
对引用和引用的实体取地址你就明白了。
引用的特性:
- 引用必须进行初始化
- 一个变量可以有多个引用
- 一个引用一旦引用了一个实体,就不能引用其他实体了
Java和Python的引用是可以改的,但C++不可以。
从这方面看,C++的引用不可能会代替指针。就比如在写一个链表的时候,如果要遍历一个链表,我们会用到一个链表节点的指针,并且不断改变这个指针的指向,如果用引用的话,就不能实现这个功能了。
引用的使用场景:
-
作参数
这里也可以用指针,但是用引用效率会更高一些。 -
作返回值
引用是可以作为返回值的,并且效率比较高,但只适用于特定的情况下
要明白这个道理,首先我我们要明白传值返回和传引用返回的区别
这样是可以的,为什么可以?int类型的变量用static修饰之后会存储在静态区,而不会存储在栈区,这样在函数的栈帧销毁之后n这个变量就不会随之销毁了,这个时候n还是存在的,我们把它的值拷贝给ret当然是可以的。
这样也是可以,虽然在函数栈帧销毁之后存储在函数栈帧的n也随之销毁,但是在销毁之前会把n的值保存起来,具体保存在哪里需要看变量的大小,如果变量较小就会保存在寄存器中,如果变量较大就会保存在上一层函数的栈帧中。所以,这是可以的,虽然n不存在了,但是n的值已经被保存下来了,我们可以将保存的这个值拷贝给ret。
这样呢?我们将引用作为返回值,返回的就不是n了,而是n的引用。此时和第一种情况一样,n仍然是存在的,所以n的引用的值和n是相同的,我们用ret是可以得到n的值了。
这样就会出现问题了,编译时虽然可以通过,并且ret的值也可能是我们想要的,但是存在一些风险。
在返回引用时可就不会像第二种情况临时拷贝一份n的值并保存起来了,而是直接返回,不关心n是否被销毁。这样的话,n已经被销毁,再返回n的引用的,我们可以根据引用来找到n,但是得到的值是不确定的。
首先我们应该好好想想,n被销毁了,也就是说保存n的空间被销毁了。
空间被销毁了,空间还存在吗?存在!只是没有了这个空间的使用权了而已。
空间被销毁了,我们还能访问那?可以!只是访问得到的值不确定了,这块空间上的值,可能还是原来的值,也可能值是随机的,也可能空间被别人使用了,从而值被修改了。
如果你不明白,我给你打一个比方?
空间的申请和释放就像住酒店,你申请一块空间就像开一个房间,你释放这块空间就像退房。假如你退房之后,悄悄地赔了一把这个房间的钥匙,然后你仍然可以进入这个房间,虽然这是非法的。但是,如果你原来在房间内放了一个苹果,你再次进入这个房间之后,这个苹果的情况你确定吗?它可能还是原来的苹果,也可能被别人咬了一口,还有可能这个苹果被换成了梨。
那你空间释放之后,你再访问这个空间的变量,就像你再次非法进入了这个房间,虽然你能访问(虽然你能进去),但是得到的值可能是原来的值(苹果还是原来的苹果),也可能值不确定了(苹果被咬了一口),还有可能这个值被其他的值替换了(苹果被换成了梨)
你也可以看下这种情况,如果你返回引用,并且用ret这个引用来接收,那么ret就是n的引用,我们打印ret的值,ret可能是原来的值,但是之后调用test2这个函数之后,原来n的空间存的1就被100覆盖了,这个时候ret的值也就变为100了。
说了这么多,我就是要说明传引用返回在一些情况下是不适用的。
只有变量在被static修饰的时候传引用返回才是可以的,这个时候我们就相当于是把苹果放在了酒店的前台,不管我们上面时候去拿这个苹果,它也都是原来的苹果。
并且,当大量调用这个函数时,用传引用返回是最好的,因为它的效率比较高。
那么,关于传引用返回的一些细节就介绍到这里。
关于引用,也有一些奇妙的东西,下面我们一起来看一下。
引用和指针一样,在赋值时,权限可以缩小,但不能放大。
当变量是只读时,引用的权限也只是只读。
很多时候,用引用作参数都将引用设为了只读,可以避免权限的放大。
但是,有些要对变量进行修改的函数就不能作为const了,比如swap。
并且用const修饰的实体可以为常量。
再来看一些好玩的东西。
我们知道,类型转换的时候会产生临时变量
不管是加括号的强制类型转换,还是隐形转换,都是产生了一个转换后的临时变量,然后将这个临时变量赋值给其他变量。
并且临时变量具有常性,是不可以修改的。如果用要引用的实体和引用不同,那么引用的实体也会产生一个转换后的临时变量,然后将它赋值给引用,同时因为临时变量不可修改,引用必须要加const来修饰。
同时,如果要用引用接收函数的返回值也要加const修饰。刚才我们提到,返回值是临时拷贝了一份,然后再赋值给接收的变量,刚才我们又说临时变量是不可修改的,所以用来接收返回值的引用也要加const修饰。
引用占空间吗?引用在语法上面,它只是一个名字,是不占空间的。但是在底层实现上,引用是用指针实现的,它是占有一定空间的。
引用和指针的对比:
- 引用在概念上是一个变量的别名,而指针是存储变量的地址。
- 引用必须初识化,而指针不用
- 引用在引用一个实体之后,就不能引用其他实体,但指针可以改变指向。
- 没有NULL引用,但有NULL指针
- 使用sizeof,括号内是引用得到引用的实体的大小,括号内是指针得到存储地址的大小。
- 引用自加则实体也会自加,而指针自加表示则向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 引用的解引用编译器会处理,而指针的解引用需要程序员来处理
- 引用比指针更加安全
内联函数
定义:以inline关键字修饰的函数是内联函数,在如果调用该内联函数,编译时会直接将函数体展开在程序中,而不会建立栈帧,从而提升了程序运行的效率,是一种以空间换时间的做法
若要大量使用某个函数,同时又不想建立栈帧,C语言可以使用宏来进行处理。
但是宏是有很多确定的:
- 宏不可以进行调试
- 宏没有类型安全的检查
- 宏会因为优先级的问题而产生许多不可预料的结果
基于这个方面,C++的方案是内联函数。
这样的话,就既有了函数的优点,又具有了宏的优点。
不是所有的函数都适合内联,比如递归函数和比较长的函数就不适合,一般内联函数都是在十行以内。
关于内联函数,我们需要知道以下几点:
- inline是一种以时间换空间的做法,若使用内联函数,在编译阶段就会将该调用内联函数的地方替换为函数体,就不需要进行函数栈帧的创建和销毁了。这样做的缺点是会使目标文件变大,优点是不需要进行函数栈帧的创建和销毁,大大提高了程序的运行效率
- 内联函数的使用,是向编译器发出了一个使用的建议,而具体使用不使用还是取决于编译器,如果内联函数规模较大(就是比较长,具体标准是多少取决于编译器),或者用到了递归,又或者是频繁地调用,编译器就会忽略这个内联的特性。为什么会这样?防止代码膨胀!
- 如果使用内联函数,定义和声明不能分离在不同文件中,分离会导致链接错误。因为在编译阶段内联函数就被展开了,地址也就不存在了,链接也就找不到了。
auto关键字
使用auto声明指针类型时,auto和auto*没有区别,但是使用auto声明引用类型必须使用auto&。
auto不能作为函数参数,并且auto也不能声明数组。
基于范围for循环
它就是用来更方便地遍历一个数组
这样写没有效果,不能真的将数组元素乘2,为什么?
因为x只是数组元素的临时拷贝!
正确的写法是这样的
并且这里我们不能用指针,因为我们每次取到的是每个元素的值,而不是每个元素的地址
就比如你在这个函数内对arr进行sizeof,得到的只是arr这个数组首元素地址的大小而已。
指针空值 – nullptr
在C++中,空指针有两种表示方式。
并且NULL的值就是0
这是因为
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
这样就存在二义性了,程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
语言的更新有一个原则,就是不变动原来的语法规则(随意变动原来的语法规则会导致原来的许多程序崩溃),而是加入新的特性。
所以,在C++11中,我们引入nullptr这个关键字。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
好啦,看到这里,你已经走出了C++的新手村,继续向后学习吧!