现在我们讲完了数据结构初阶部分的内容,数据结构剩下的内容会在C++语言讲解的差不多的时候加入。
1. 什么是C++
C语言是结构化模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了OPP(object oriented programming:面对对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩大了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此,C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
可能我们还见过C#语言,值得注意的是这个根C/C++没有关系,它是微软公司搞出来对抗Java语言的,所以它对标的是Java。
2. C++的发展史
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为 C with classes
C++不断发展的道路上出现了众多的版本,目前最新的版本发展到了 C++23 ,但是目前主流的版本还是C++98和C++11,所以大家也不用一味的追求最新
3. 命名空间
在C/C++中变量、函数和后面学到的类都是大量存在的,这些变量、函数、和类的名称将都存在于全局作用域中,可能会导致很多冲突,使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字的污染,namespace关键字的出现就是针对这类型的问题的
比如我们现在定义一个全局变量rand,并打印它,目前程序没有任何问题
此时我们引入stdlib.h头文件,现在rand变量和rand()函数发生了冲突,程序报错,其原因就是出现了命名冲突的问题
那么C++中的命名空间就可以很好的解决这一问题,在此之前我们先说一个操作符
我们直到在同时定义了全局变量和局部变量的时候,程序会使用局部变量,那么此时如果我们还是想用全局变量的时候就要用到域作用限定符 :: ,当域作用限定符前面为空时默认是访问全局域
那么在使用命名空间的时候我们也可以用这一方法避免变量名的冲突:
当我们使用一个变量、函数时编译器默认的查找顺序是:先在当前局部域找,再到全局域找,值得注意的是默认情况下不会去命名空间去找。可以说如果我们用了域作用限定符,那么就会直接到对应命名空间查找,如果不用就不会访问命名空间
命名空间中除了可以定义变量还可以定义函数和类型,这样可以避免和全局域,或者他人的命名空间的内容冲突
同时命名空间还可以嵌套:
值得注意的是结构体类型在使用的时候命名空间的名称写在struct之后
在同一项目的不同文件中可以使用同名的命名空间,若如此做,编译器会自动将几个文件的相同命名空间合并
像这样我们就将add函数和sub函数的声明也放进了命名空间a当中,但是我们注意到这两个函数现在的状态是未找到定义,再运行一下也会发现有如下报错
这提醒了我们,当把这两个函数声明放到命名空间a中后,它们的名字就应该变成了 a::add ,但是我们在定义它们的时候没有写 a:: 这样的结果就是没有 a::add 的定义,只有 add 的定义。
解决方法有两种,第一种就是在前面加上域作用限定,但是这种方案无疑太过繁琐,在函数很多的情况下就太麻烦了。第二种解决方案就是将函数的定义也放进命名空间中,这样声明和定义都在一个空间中,自然就能定义上了
那么如果我们嫌弃每次使用命名空间的时候都要在前面指定命名空间太麻烦怎么办,此时我们可以使用展开命名空间的方式避免。
展开命名空间,可以理解成将命名空间中的所有内容粘贴到 using 语句的所在位置,和#include的功能相似。但不完全是这样,那些同名的变量或者函数的身份还是在不同的域中,也就是说,只要我们不使用这些变量或者函数,只是将它们各自的作用域展开,是不会造成因引用歧义而导致的报错的。
就像这样,即使我们没有在前面限定命名空间,但是第二句代码还是正确的执行了,但是我们要注意的是在展开命名空间的时候不要有其他重名的内容,无论是在全局变量还是其他被展开的命名空间中,否则还是会造成访问冲突。当然,如果有重名的局部变量,就按局部变量走了,不会造成访问冲突。
展开命名空间还有一种单独展开命名空间中的某个变量的玩法
我画红直线的语句就是单独展开了a命名空间中的 rand 变量,但是 rand1 没有被展开,所以不能直接使用,因此报错了。可以注意一下,展开命名空间和展开命名空间中的一个变量的语句是有区别的,具体来说就是有无namespace
4. C++输入输出
C++的输入输出方式放在<iostream> (in out stream) 流式输入输出库当中,其输入函数是cin,输出函数是cout,c指的是conslo就是控制台的意思,在输入输出的时候需要用到 >> 和 << 流提取、流插入,这个东西与C语言不同的是它可以自动识别类型,而且你可以随便写,不用注重格式什么的,输出的最后换行用 \n 或者 endl (end line) 都可以他俩是等价的。
C++的标准库为了防止库里的内容和我们自己定义的东西冲突,所以将库里的内容包到了std命名空间中,因此我们想使用C++标准库里的东西的时候要记得展开一下这个库的命名空间,当然你可以完全展开,也可以只展开 cin cout
当然,因为C++对于C的兼容性,你也可以在iostream头文件下使用printf和scanf函数进行格式化输入输出,格式化的一个最基本好处就是方便控浮点数的精度。当然流式输入输出也能控制精度,就是麻烦一点
5. 缺省参数
缺省参数是指在声明或定义一个函数时,为函数的参数指定一个缺省值。在调用该函数的时候,如果没有指定实参则采用该缺省值。
5.1 全缺省参数
顾名思义,就是将函数的所有参数进行缺省预设。
但是要注意在为函数传参的时候必须按从左向右的顺序传参,不能跳着传
5.2 半缺省参数
这个就是可以只给后几个预设置缺省值,同样要注意非缺省参数只能在前面,否则将引起歧义。同时半缺省参数也不能跳着设置,非缺省参数必须从左向右连续排列,剩下后半段全都是缺省参数。
那么像这种半缺省的函数就不能啥参数都不传了,至少要把那些没有预设值的参数填上
这里还要注意一个点,就是缺省参数不能在函数声明和定义中同时给,因为怕你两个值给的不一样呗,那到底该听谁的。所以缺省参数只能给在一个上面,当然一般是给在声明上面为主。
6. 函数重载
函数重载是函数的一种特殊情况,C++允许在同一作用域下声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题
这段代码中俩个Swap函数符合同一作用域,不同形参列表的要求,因此它们满足函数重载要求,并且事实上,它们确实重载了。编译器根据传过来的参数类型,智能的选择了对应参数类型的函数。这个功能还是相当实用的,看起来用的就是一个函数名,但是可以实现不同类型参数的相似函数功能,回想一下上次在C语言中我们实现类似功能的时候用的还是宏的时候有一个##运算符,emmm……好像也不是太像啦
C语言·预处理详解_c语言 复杂预处理应用-CSDN博客文章浏览阅读1.2k次,点赞24次,收藏29次。本节讲解了预定义符号的含义。#define定义常量或定义宏的用法和注意事项,关键是其暴力替换的原理可能产生的问题以及解决办法。在定义宏中#和##的意义。#undef取消宏定义。命令行定义不确定大小的参数。条件编译的用法。头文件不同包含方式的意义原理,以及头文件重复包含的危害和解决办法_c语言 复杂预处理应用https://blog.csdn.net/atlanteep/article/details/135648080?spm=1001.2014.3001.5501
在不同命名空间中的同名函数,在两个命名空间同时展开在同一域之中的情况下,这俩个函数也会产生函数重载,此时再加上缺省参数就会变得更加好玩了
现在可以思考一个问题,如果只给a2中的函数搞上缺省参数,不给a1中的搞了会发生什么,程序会报错吗?还能不能不传参数?还能不能只传一个参数?如果用函数重载的方式还能不能访问到a1中的函数,如果不能,我们该用什么方案使用a1中的函数?
这些问题最终都导向一点,就是在重载函数的时候不要搞出引起歧义的东西。
C语言不支持函数重载但是C++支持,究其本源是因为在编译链接的过程中,C程序中需要调用函数的时候会直接用函数名去查找,而C++程序需要调用函数的时候会用修饰后的函数名去查找。修饰的意思可以简单理解成,在汇编的过程中会将该函数的形参信息与函数名放在一起,形成一个修饰后的函数名,这个修饰后的函数名我们可以在汇编代码中见到。
7. 引用
7.1 引用的概念
引用不是定义一个新的变量,而是给已经存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块空间
类型& 引用变量名(对象名) = 引用实体
引用特性:
1. 应用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
通过这段代码我们可以感受到一个变量可以被多次引用、引用可以被引用、所有引用和它对应的实体共用一块空间,随便改变其中一个引用,其他引用和实体也会改变。
7.2 常引用
引用不能引用一个常量,因为这造成了权限放大,也就是说,如果能引用的话,难道我们可以容忍通过引用来改变它的实体吗?
说到底常引用的问题就是一个权限放大(不能容忍),权限平移(可以容忍),权限缩小(可以容忍)
观察上面这段代码,权限放大和平移就不讲了。我们观察权限缩小了之后,k不能改变了,但是我们可以通过它的本体r间接的改变k,,甚至还可以用其他有权限的r的别名间接改变r和k,这就是权限缩小。
我们观察下面一段代码
我们先看第一组,将一个double类型的变量赋值给一个int类型的变量,在赋值的过程当中发生了隐式类型转换,毋庸置疑这样做是没有问题的。
第二组,将double类型的变量引用给int类型报错了,但是引用给const int类型变量就行。这是因为在变量赋值时同时发生了类型转换,那么编译器就会先将赋值的结果先放到一个临时变量中去,在这里就是3会先被放到一个临时变量里面去,这个临时变量是具有常性的,就相当于是被const修饰过的,之后再把这个临时变量的值再给给别名。这样就解释了为什么p1报错了,因为发生了权限放大。无论是隐式类型转换还是强制类型转换都一样会产生临时变量。
第三组报错的原理和第二组一样,加法计算完后的结果也会先放到一个具有常性的临时变量中去。如果别名不用const修饰,那么就会发生权限放大。只要是表达式运算就会产生临时变量。
7.3 使用场景
做参数
之前我们想要交换两个变量需要传递他俩的指针,但是我们可以用将形参变成实参的别名简化这一操作,这样我们改变函数形参就能影响到实参了。
7.4 引用和指针的区别
1. 引用概念上定义了一个变量的别名,指针存储一个变量的地址
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4. 没有NULL引用,但又NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
8. 内联函数
以 inline 修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数可以提升程序的运行效率,其效果和宏函数是一样的,但是宏函数要写那么多括号很容易出错。
上面这段代码就是利用了内联函数和引用写出来的一个交换函数,像我们在写排序那节中的代码的时候,可能要执行成百上千次的交换函数,如果每次调用都要建立函数栈帧,那消耗未免太大,所以我们可以用内联函数的方式提升代码运行效率。同时我们发现内联函数的写法很简单,就是在函数前面加上个inline,比宏函数简单实用太多了。
如果你想通过观察汇编代码看内联函数的执行方案的话不要用debug模式,要用release模式,然后如果有call Swap就说明call这个函数了码,就是建立栈帧了。
如果我们尝试把内联函数中的代码量加大的话就会发现,汇编代码中还是call这个函数了,这是正常的,因为inline对于编译器就是个建议,将函数规模较小、不是递归且频繁调用的函数采用inline修饰,否则编译器会忽略inline的特性。
如果我们给的内联函数比较大,比如100行,如果我们要调用1W次。用内联函数把函数中的操作粘贴到指定位置的方式我们的汇编代码中需要100*1W行。那么如果用函数调用的方式就只用100+1W行就够了,因为每次调用只用写一行call这个函数就行。那么汇编代码最终就能影响到生成可执行程序的大小,汇编代码量越小自然可执行程序越小。所以说编译器不将inline的权限完全放给我们,最终还是让它自己决定是否将这个函数搞成内联的。
最后内联函数不能声明和定义分离,就是说不能像之前那样在头文件中写它的声明,在对应源文件中写它的定义,这样操作会导致链接错误,因为内联函数不需要call所以在符号表中不会生成它的地址,此时我们从主函数中调用就相当于通过头文件中的声明去call它的地址,但是符号表中压根就没有它的地址,所以就会链接错误。那么正确的做法就是把声明定义都放到头文件中就好了,这样include头文件就直接把内联函数整体都粘过去了,就没有问题了。
9. auto关键字
auto作为一个新的类型类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
这里的 typeid(变量).name() 就是取这个变量的类型的函数,我们可以看出来,auto自动识别出来的结果正是我们想要得到的结果,并且auto还能识别函数返回值的类型。
注意:使用auto定义变量的时候必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译的期会将auto替换为变量实际的类型
9.1 auto的使用细则
用auto声明指针类型时,用 auto 和 auto* 没有任何区别,但是用auto声明引用类型时则必须加&
在同一行定义多个变量时,这些变量必须是相同的类型,否则编译器会报错。因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
auto不能推导的场景:1. auto不能作为函数的参数 2. auto不能直接用来声明数组
10. 范围for循环
如果用C我们写一个循环是这样的
但是在C++中我们可以使用范围for来遍历一个数组
for循环后的括号由冒号 : 分成了两个部分,第一部分是范围内用于迭代的变量,第二部分是被迭代的范围,这个范围是定死的就是从数组的开始到结束的一个正序遍历。遍历的过程就是拿出arr中的一个值赋值给e然后进行大括号中的操作,范围for会自动进行初始化,结束判定,和++
11. 指针空指针nullptr (C++11)
NULL其实是一个宏,在传统的C头文件(stddef.h)中,可以看到如下定义
可以看到NULL可能被定义为字面上的常量0,也有可能被定义成无类型指针(void*)常量。无论采用那种定义方案,在使用空指针时,都可能遇到一些麻烦
可以看到第二行的f函数在重载的时候,因为NULL被定义成0,所以结果偏离了我们的预想。
所以说我们之后可以将NULL都换成nullptr关键字代替。
注意:
1. 因为nullptr被引入称为了关键字,所以说在使用的时候不用包含头文件
2. 在C++11中,sizeof(nullptr) 和 sizeof((void*)0) 所占字节数相同,我们可以直接理解成他俩等价。
3. 为了提高代码的健全性,在后续表示空指针的时候都建议使用nullptr