序言
今天是2021年12月31日,也是2021年的最后一天,时间过得很快,也很慢,回想年初在江边玩耍的时候定下学习目标,仿佛就是昨天的事,细想来从定下目标的那一天至今我已完成了很多课程的学习,复盘今年,好像我走哪都只有学习的那种感觉给我留下的印象最深刻,我都怀疑如果不是学习,也许我现在根本想不起今年都去到了哪里,干了些什么。今年出了两趟远门,去了趟上海出差,在酒店的桌子上学习链表。去了趟河南的丈母娘家,在书桌上学习动静态库的引用和作用域、链接属性等。年初时周末为了不被家里小孩打扰,在弟弟的新房里学习,那段时间学的是裸机,再后来周末带娃去恒大售房部的游乐元园玩,我在旁边的会客桌学的是uboot移植。再后来,孩子放暑假送回了老家,开始过二人世界,炎热的夏天,二人在卧式开着空调,学习到12点,那段时间学了linux驱动,再后来每个周末都在公司学习,开始过上了全年无休的生活,还是公司呆的时间比较长,学的知识也比较多,多到我没办法把它们都一一列出来。
有理想且为自己的理想奋斗的过程是幸福的,这一年我虽然有过迷茫,有过工作低谷,即使现在的我也没有完全从迷茫和低谷中走出来,但是我心里也有些许的踏实,这种踏实是学习给我带来的。今年部门人员变动很大,该走的不该走的都走了,我再次夯实了老油条这个称号,看着别人的离开我的内心也很动荡,要不是我有自己的计划,我现在也应该已经离职了。
很庆幸我年初为自己制定了学习目标,很感激一路坚持学习的自己,至少学习让我内心动荡的期间,有一个精神支柱,使我内心的桥梁没有崩塌,没有在迷茫的时候愁白头。
学完linux核心课程,紧接着从11月1日踏上了这条注定只有开始没有结束的c++征途,为期2个月的时间把c++学了个七七八八,学习过程中,越学越发现c++的博大精深,以前觉得c语言是门大学问,现在想来比起c++它简直是容易死了,到现在所使用的c还是98版的标准,而c++去年才更新了,而且后续还会有更新,所以不敢说把c++学完了,只是把现有的课程学习完了,也不敢说精通c++,只能说c++略知一二吧。至此c++的学习暂告于段落,接下来要做的就是通过学习qt去应用c++,在应用中去领悟c++。
今日写下这篇总结目的,一是借此将这2个月以来的c++课程进行一个回顾。二是对所学知识进行梳理将零散的知识进行串联,也算是对2021年的一个总结吧。
截止2021年10月31日,完成了嵌入式核心课程的学习,我本应该多做些项目去巩固课程所学,但是我厌倦了工作中的各种开发流程,所以要快刀斩乱麻,快速的学完c++和qt后转向做软件开发,因为软件开发的流程简单很多,我不必花很多的时间来应付那些看似合规实则没有意义的事情。这一遍的学习我并没有准备去和每一个知识点死磕到底,通过之前学习c语言的过程中我发现,技能这个东西需要循序渐进,先有一个知识架构,再在使用中去慢慢研究,必要时还会再回来重复看一看视频教程。好了废话不多说,开始我的主题。
c++概述
c++的由来
c++由c发展而来,早期有一种叫法叫做带类的c,后来随着要解决的问题越来越复杂,c++增加了很多新的语法和特性,导致今天的c++已看不到太多当初c的身影,虽然变复杂了,但是还是完全兼容c的,所以他们还是好兄弟。c++应该这么来看,它并不是一门更好c,而另外的一门语言,c有少量的库,而c++的库多了很多,特别是强大的STL库,c之所以成功,是因为他有指针,c++之所以成功,我想他除了面向对象外,还有很多可以高效率的完成工作有关,c之所以没被淘汰,是因为c的用户多,c的效率比c++更高,所以在linux底层都是使用的c或效率更高的汇编,只是汇编有点费人。到目前为止c在单片机领域仍然处于霸主地位,因为单片机业务相对简单些,所以对语言的要求也简单些,c适合用来做小规模的程序,适用处理性能一般的cpu,而c++则是凌驾于系统之上,有着更高级的语法特性,来解决更复杂业务逻辑。
c++到底是什么
c++是一门半面向对象的语言,之所以说是半,是因为他为了完全兼容c,也多了很多的包袱。c++中的面向对象的语法特性的权重相当于指针在c中的地位,c和c++都是编译型语言,最终的可执行文件的运行,与运行平台有关,c++文件的扩展名典型的是.cpp。记得课程上老师用了一个难度守恒定律来描述c++,就是说语言简单了,那么程序员需要考虑更多,语言难度增加了,那么解决业务问题的编程就变简单了。因为c++难,所以更适合编写大程序,编写一些架构等。
c++是面向对象语言之一,面向对象并不是c++独有的,很多高级语言都有面向对象,况且c++并非纯面向对象,他有很多非面向对象语言,c++能够经久不衰,是因为他是面向对象语言中效率最高的语言,他的效率高主要归功于他的多态、封装、继承、面向对象的设计模式。使用c也能实现面向对象只是要实现面向对象要程序员做很多额外工作,额在面向对象的语言中,这些额外的工作已经被语言本身实现,程序员的工作就变少了。
非面向对象、面向对象、框架设计、设计模式是一件顺利成章的事。
要学习面向对象应该先学习面向过程。明白实现过程的步骤再去体会面向对象的解决问题的思路这样的学习来得更结实,更透彻。
面向对象是相对面向过程来说的,编程由面向过程转向关注对象,面向对象其实是一种编程的架构组织方式,实际上干活的还是那些代码,干的还是那些活,只是编写思路发生了变化。面向对象是一种分装数据和看待问题的更高层次和视角。面向对象是应对复杂问题更有效的方法,语言由面向过程到面向对象,是一个自然的成长之路。和人的成长之路一样。
面向对象编程的工作主要分我两大块,一个是编写类库,另一个是使用类库来完成具体的业务,这两大块通常由两拨人来分工完成,大部分人通常是应用类库实现业务。
c++该怎么学
c++相较于c来说要无论是关键字还是其内容的抽象度和语法细节都要复杂很多,关系不止2倍,面向对象是其核心,有了面向对象的思维,也就会了c++,模板和泛型是精髓,只是刚入门一般用得很少,只有大神在结构、框架编写中才会遍地使用,老师曾不止一次提到,不要出去吹自己精通c++,这门学问太大,精通容易被打脸。
c++的学习不要试图去记住所有,根本不现实,要以理解为主,通过写代码去吸收语法的特性,形成自己的理解,达到在解决问题时大概知道用声明语法,即使你连关键字都不会拼写,语法细节也记不住了,完全可以通过查阅资料,来编程。写多了自然也就记住了。
c++基本保持在3-5年更新一次,每次更新都是增加一些关键字,其实这些增加,也有很多是借鉴了其它类型的编程语言的一些优秀特性。
c++的学习分为4重境界,
第一层:语法层面,对语法比较熟悉,会使用c++的语法来建模编程。
第二层:能使用c++解决具体业务问题,大部分人停留在这一步。
第三层:编写类库供别人使用,出了问题能快速解决,具有一定的框架思维。
第四层:理解c++设计背后的原因,有自己独立思考的能力,能品出c++的魅力,思考问题的方式和c++的设计者具有一定的同步思想,把c++上升到哲学境界。
c++的应用场景
c++就是用来干业务复杂的开发的,类似于qt、opencv等软件虽然应用编程是使用c++实际上这些软件本身也是使用c++开发而来,c++更适合用来做后台业务逻辑,而前台的开发并不是c++的强项,在当下c++最大的优势就是在视觉领域做AI开发,游戏和图像引擎、网络服务引擎等开发。
c++程序员的发展前景
c++的学习难度大,通过率低,只要是使用c++编程的人无论是做什么方向,待遇都不会差,相较于其他语言,c++的程序员的辉煌周期更长,天花板也更高。c++对内功的要求比较高,很多人根本都入不了门就被淘汰,当然c++的岗位需求也比java、python要少,但整个行业的c++的大神非常稀缺。
总的来说,语言没有好坏之分,只有适合与不适合。c适合资源拮据的平台,如单片机,而c++适合资源中产的平台,如手机、中控屏等,像java、python就适合资源富裕云端,只在乎开发效率,不在乎资源的平台。
c++的语言特性
语言特性来源于实际需求,因为需求变的得越来越复杂,所以语言必须跟着变复杂,每一种语法都是为了解决实际需求,都对程序员有帮助,语法特性越多,某种程度上也可以说明这门语言也更厉害,同时学习难度也更大,本质上语法特性是靠编译工具链的支持,我们使用的各种语法,其实都是在按照规则,调用编译器的各种功能,所以高级语言的背后真正强大的是编译器。语言的本身变迁无非是关键字新增或变更一些语法特性,学习过程中的重点就是掌握这些语法,而真正解决问题还是要靠编程思想。
c的源文件扩展名.c 头文件扩展名.h。c++的源文件扩展名有.cpp、.cxx、.cc、.c、.c++ 头文件扩展名有.hpp、hxx、.h
在c++中完全兼容c,在c++中包含头文件一般没有扩展名,如”#include<iostream>” 当然也支持c中的.h的写法,其实c++为了支持c的.h写法,c++的编译器为了兼容c在背后做了很多工作,这些工作是对高效的一种破坏,所以我们在c++的程序中应尽可能的避免c中的一些特殊语法。
cpluspluse前后双下划线,是c++编译器提供的一个环境变量,在程序中我们无需定义可以直接使用,该变量内部记录了c++的版本,而这个变量在c的编译环境下木有,利用这个特点,很多时候我们使用一个宏来判断该变量是否存在,如果不存在就按照c的方法来处理,存在就按c++的方法来处理。
c与c++混合编程
c有很多优秀的代码和库在c++中重写没有必要,丢了也可惜,另外就是一些底层实现时比较注重效率,所以通常会出现在c++中引用c的静态库或者c++的静态库在c源码中引用。c和c++的程序最终都是被编译成.o文件,所以在.o文件将不同类型的源代码链接在一起理论上完全是可行的,然鹅还是有一些细节的东西有区别,如c++支持函数重载,所谓函数重载就是允许函数名相同参数类型不同的函数存在,编译器可以通过对参数对别仍然可识别程序员的意图,编译器底层其实是对重载函数的名称做了更改,添加了一些关于传参类型的标识,来实现函数名重载,然而c不支持函数重载,自然也不会修改名称,所以如果在c涉及调用c++的函数,就无法找到函数,因为编译器已经把函数名进行了修改。导致无法链接成功。解决方法就是让c++安装c的方式来命名。实现方法就是extern “C” { },括号内的元素使用c规则编译,extern是c++中才有的关键字,在c编译器下无法编译,为了通用,我们又添加了#ifdef _cpluspuls,来对extern“c”进行选择编译,__cpluspulus是c++编译器自带的环境变量,用来存储c++的版本,利用对该变量的判断我们即可知道当前编译器是c还是c++。
#ifdef __cplusplus
c++源码引用c静态库混编
这种情况通常出现在供应商给我的文件时.a库,由于编译器编译时会把源文件中的函数名进行修改,所以我们调用的函数名肯定是错误的,处理办法就是加上extern “C”,按照c规则不改变函数名称。
c源码与c++静态库混编
该混编的问题也出在我们不知道c++编译的函数名,解决思路就是我们另外建立一个c++的函数,函数中去引用原来 .hpp中给出的函数,而我们自己写的函数再添加extern”c”来实现名称不改变,编译成另外一个库,其原理就是内部调用名称被改变的函数,对外的封装函数名称没有改变。
gcc 1xxx -c -o 2xxx.o //将1xxx只编译不链接,命名为2xxx.o
objdump -d 2xxx.o > 3xxx.i //将2xxx.o文件反编译成.i文件。
.o文件内部全部是二进制,本身也带有符号信息,只是我们看不懂二进制,所以要使用反编译器将其转换成我们能看懂的格式。
gcc 1xxx -c -o 2xxx.o //将1xxx只编译不链接,后文件名命名为2xxx.o
ar -r lib1xx.a 2xxx.o //ar是工具链,-r静态库,lib1xxx.a库名,lib开头。
g++ main.cpp -l1xxx -L. //-l指定库名,-L指定当前路径
c++编译器版本指定 -std
在linux的gcc编译器中集成了多个c++的版本,不同版本的linux默认使用的c++的版本也不同,但是我们可以使用-std来指定。示例如下
g++ aa.cpp -std=c++11 //指定c++11版本来编译aa.cpp源程序
C++语法库
valgrind工具查看内存泄漏
之所以c++难,其实主要是我们要对内存操心,当我们程序太大后内存申请后容易忘记释放,导致程序吃内存,所以检测程序是否有内存泄漏比较重要,在此介绍一个工具来做内存检测valgrind。
valgrind工具介绍
Memcheck是valgrind应用最广泛的内存检查器,能够发现开发中绝大多数内存错误使用情况,除了内存检查还包了以下功能:
Callgrind—用于检查程序中函数调用过程中出现的问题。
Cachegrind—用于检查程序中缓存使用出现的问题。
Helgrind—用于检查多线程程序中出现的竞争问题。
Massif—用于检查程序中堆栈使用中出现的问题。
命令:sudo apt-get install valgrind
memcheck使用
编译:重新编译要检查的源码,编译时添加-g生成dbug版本目标文件。
g++ person.cpp main.cpp -g -o apptest
检查内存:实际上该工具就是对代码运行过程的内存申请和释放统计。./后面跟被检查的可执行文件,
valgrind --tool=memcheck --leak-check=full --show-reachable=yes --trace-children=yes ./app
命名空间就是用来解决变量、函数、结构体、枚举等重名的问题,在一个庞大的程序中,重名很难避免,所以很多高级语言都有命名空间。在c中我们解决重名主要有几个方面,文件内的重名由程序员自己解决,需要做全局变量的变量名通常会以模块或开发团队缩写字母来做前缀来区分,文件内的函数则使用static来将作用域限定在本文件内,而枚举在c中压根就没能解决重名的问题,无论在c中用了什么方法来解决重名的问题,总的来说都是程序员自己定义的一些潜规则,然而很多人也根本不遵守这些规则,语言本身并没有解决重名问题。
在c++中解决重名使用了命名空间来对任何元素统一做链接属性的限定,包括宏、枚举、结构体、联合体等。命名空间的定义使用namespace,紧跟xxx来表示空间名称,以{ }大括号来限定范围,括号内可以有函数、变量、枚举等任何东西。在括号内大家可以相互访问,而括号外要访问括号内必须使用“空间名::变量名”方式来访问,这种方式看起来也是一种前缀。
关键在于关键字namespace示例如下:
namespace people
{
int a;
void func();
}
- 方式一
外部引用命名空间内的成员,使用::示例如下
people :: a=10;
people :: func();
- 方式二
使用using关键字,将空间内的成员进行声明,声明后的访问就不在需要加空间名,函数声明时不需要使用括号,调用时需要使用括号,示例如下:
using func;
using a;
func();
a=10;
- 方式三
使用using将整个命名空间的所有成员都进行声明,相当于把指定空间内的所有成员都拿出来共享。示例如下:
using namespace pople;
a=10;
func();
没有特意指定命名空间的函数或变量会被编译器统一归类为一个命名空间,这个空间就是默认命名空间,如我们的man函数。默认命名空间我们在访问时也不需要指定空间,直接调用即可,典型的就是我们的c程序使用c++编译器编译。
匿名也就是没有名字,在c中我们想要把一个函数的作用域限定在文件内,在函数前加static,但是static不能用来修饰enum、struct,而匿名命名空间就是用来解决这个问题的。默认命名空间没有名称,自然无法访问内部成员,典型使用就是用来做拒绝跨文件引用。只能在空间内部即文件内部访问。实现方法就是在定义时不用写命名空间的名称,示例如下
namespace
{
int a;
func();
}
命名空间也可以嵌套,即一个命名空间中包含另一个命名空间,在外部访问时需要使用“::”一层一层的向内剥离,如下示例
namespace aa
{
int a;
namespace bb
{
int b=10;
a=b;
}
}
aa::bb::b=10;
嵌套的命名空间内部可以直接访问上层所有成员,但上层不能访问下层成员,如果两空间成员名称相同,那么在命名空间内部优先使用同级命名空间成员,类似于全局变量与局部变量同名时优先使用同级别的成员。
c++标准库的使用
在学习c的时候我们用到了一些库,如stdio.h,c的库少的可怜,往往越是高级的语言库就越多,如java学习完基础的语法才算是刚刚入门,而真正的进阶是库的学习。c的头文件在c++中做了一些优化,其名称只是在前面添加了一个c,而引用时也不在是原来的方式,而是没有了.h如 “#include<cstdio>”,c++为了兼容c,其实是牺牲效率换来的成果,如我们在c++中包含头文件使用“#include<stdio.h>”实际上在背后会被转换成“#include<cstdio>”,所以为了效率我们应该尽可能的使用c++的语法。
iostream引用
iostream是c++非常常用的库之一,和c中的stdio.h一个重量级,为了防止重名,该库使用了命名空间,空间名称为std,在访问iostream内部成员时必须在前面添加空间名称和域操作符,由于使用得太过频繁,所以我们通常会直接对命名空间的所有元素统一进行声明,常用的方式如下:
using namespace std;
在c中标准输出使用printf在c++中使用cout,cout属于std命名空间中的成员,标准输出的典型格式为
cout << “hudaizhou” <<endl;
其中“<<”是流操作符,可以多个元素的接续输出,输入输出涉及ostream和istream两个类,他们都继承了iostream,cout本质上是osteam的一个对象。<<则是在ostream中对<<进行的运算符重载。可以简单的把<<看成一个箭头,cout关键字看成一个显示设备,我们将字符移动到设备就是输出,endl也是osteam中的一个对象,用来做换行操作,等效于\n\r,
cin是c++中的标准输入,他是isteram的一个对象,定义于std命名空间。示例如下
cin>>a>>b;
其中箭头的方向与cout相反,我们同样可以将cin看成一个输入设备“键盘”,箭头指向的对象就是用来接收输入的内容,他同样可以级联,如上例,输入第一个存放在a中第二个放在b中,输入内容之间以空格为分割。如果输入的元素超出接收的变量,那么后面的会被丢弃。
在c找那个文件读写使用了open()、read、write等在c++中同样也有这一套,只不过把名称进行了修改。
open函数
将名为 filename 的文件打开并与文件流关联。
示例:fstream fs; //创建一个文件流指针
fs.open(pach); //打开pach文件
is_open函数
检查文件流是否有关联文件,检查是否打开成功。
if(fs.is_open()==true)//打开成功
close函数
功能:关闭打开的文件
示例:fs.close(); //关闭文件
read函数
功能:读取文件
示例:char read_buff[20]={0};
fs.read(read_buff,sizeof(read_buff));
write函数
功能:写文件
示例:fs.write(write_buff,sizeof(write_buff));
fs << "hello word" << endl;
skeep函数
功能:移动文件指针
示例:fs.seekp(0,fs.beg);//移动到文件头
C++字符串
在c中没有字符串,在c++中的STL库定义了一个字符串类,我们创建一个字符串变量就是在创建类的对象,在该类中实现了对字符串的各种操作和运算符重载,我们可以通过对象来调用内部成员。
定义常规的方法为string a,或者string a(“hudaizhou”),初始化时,括号中不能有常量,如数字,也不能有单个字符。
二次赋值的时候直接使用a=”hudaizhou”即可,内部还实现了一些成员函数,如length()用来获取文件字符串长度,siaze()是占用空间。而在内部还实现了对“+”运算符重载,来实现两个字符串直接相加组成一个新的字符串变量。且可以直接使用“==”“!=”来对两个字符串变量进行比较,也是使用的运算符重载,默认是按照字典序比较。
c++关键字
c++的关键字,这是一个大话题,c++的关键字非常多,主要变化分为四类:新增关键字、关键字新增语义、关键字语义变化、无变化关键字。本文不提c中无变化的关键字。
bool类型虽然我们在c中也有使用,但是他并不是c的关键字,是我们使用typedef对其它类型进行的重命名,而在c++中收编了bool作为关键字,他实际上是一个enum,结果只能是true、false,当然true和false也是c++中的关键字。bool一般占用1个字节,具体与编译器有关。
字符类型,一般暂用1个字节,c和c++标准没有指定为1个字节,是编译器指定的,所以具有不确定性。如果给char变量赋值一个数字在使用cout输出时并不会输出数字,而是对应的asci码,char用来存储字符或unicode,char的符号类型取决于平台,在arm平台使用的是unsigned与,在x64平台时signed,所以在对char的符号类型有要求的场合建议进行指定。
wchar是宽字符类型,应用于超出一个字节的字符unicode,如汉字。总的来说与char的应用非常相似,输入输出需要使用配套的cin、cout,字符串对应的有wstring。
正因为前面的字节长度和符号类型不确定性,所以c++发明了char_8t、char_16t、char_32t,其它方面和char相似,只是明确了占用字节数,且c++明确规定为无符号类型,单片机中使用的u8其实就是char_8t,对应的字符串也有u8string、u16string、u32string。
运算符的重名,&&为and,||为or、!为not,没啥用,主要是给新手不适应符号语法而准备的。
看上去与“与&”一样,实际没没有任何关系,主要用来做传址引用,简单说是一个弱化版指针,指针威力太大,而普通变量做函数传参时又是传值调用,所以有了引用。引用用于函数传参时传址调用。另外一种用法就是给一个变量创建一个符号链接,如int a& = b;后面凡是操作a就是在操作b。引用定义时必须给初值。
引用前的const其实和函数传参前加const很类似,函数前加const的目的是不允许函数中去改变const修饰的变量。
sizeof与引用
sizeof一个引用时,获得的不是引用本身的大小,而是引用的目标的大小,定义一个struct,内部含有一个引用,当我们没有实例化结构体时,sizeof发现结果是int大小,所以不能单纯的说引用变量不占内存。
引用的底层仍然使用了指针来实现,否则不可能实现传址调用,只是添加了const使指针不能被改变。等效实现如下:
int &a=b等效于int *const a = &b;
c中的enum的链接属性是全局外链接,容与宏或其他enum重名和成员类型不能被指定,所以c++为其设定了命名空间,和允许指定类型。c++中的enum的访问需要在前面加上空间名,使用如下:
enum aa{a, b, c, d} //定义一个枚举类型
aa b //定义一个aa类型的变量
b = aa::a;//访问需使用命名空间访问。
inline是一种用于实现的关键字,不是用于声明的关键字。所以inline必须与函数结合使用,函数声明可以没有inline。这个和c中差不多,只是给编译器的一种建议,编译器并不一定会听。但是函数实体写在类的内部,就会被inline处理。函数外的主要函数看函数定义时是否有inline。
c中的NULL是一个宏NULL=(void *)0;c++中的NULL是int型的0;且c++中也不允许指针赋值为void类型,NULL是一个int型数字,那么在做函数重载时就无法将指针类型和int类型分开,所以就有了nullptr,nullptr本质是一个模板类,内部实现了”=”运算符重载,会根据接收nullptr的变量类型返回一个同类型的指针类型0;使用方法if(nullptr != p)
谁又知道assert实际上是c中都有的关键字,他其实是一个宏,作用是对后面的表达式执行结果进行判断,是false,则向标准输出打印一条输出信息后,结束程序运行。为true时不干任何事,继续向下执行。
assert是运行时的一种运行结果判断,算是动态断言,而static_assert是用在编译过程中,在编译时如果判断结果为假,那么就停止编译,
static_assert(sizeof(int)!=4,"system bit 32"); //检查当前系统位数
其实跟多的时候是用在模板类型检测上,类型不满足要求就停止编译。
和sizeof用法一样,只不过是用来检测类或结构体对齐单位。
用来指定类或结构体对齐单位,只能向上指定,不能向下指定。
typeid可以用来获取一个变量的类型,然后在去定义一个新的变量,他可以把是否带const都区分出来。
隐式类型转换,用于告诉编译器,这种转换是我故意的,产生的后果由我自己负责,你不用管,当然这种转换必须是类型兼容的,背后还是有编译器监控,不能说把一个类转换成基础类型。示例如下:
double a=22.2;
int a=static<int> (a);
void *p=&a;
int *p2 = static_cast<int *>(p);
将原变量所占有的空间的解析方法按新制定的规则从新解释,如将int型的空间按double类型解释,示例如下:
unsigned *p1=reinterpret_cast<unsigned char *>(0x1234)
用来实现const与非const转换,多用于将const转换为非const,在c++中变量被const修饰后,其实编译器就把这个变量与一个初值绑定了,以后看到这个变量赋值就直接调用绑定的值,不会去考虑是否在中途被改变。应用的比较多的还是在函数传参中
const int a=5,
func(int *b)
func(const_cast<int *>(&a))
动态类型确定,数据类型由运行时确定。
自动类型推导,主要是根据赋的初值来确定数据类型。
作用是判断一个表达式的类型,再定义一个新的类型。示例如下
int a
decltype(a) b;
auto类型推导时,会忽略const修饰,而decltype不会。
class用来创建一个类,与struct很类似,只是多了权限管理public公开的、private私有的、protected受保护的。这3个也是c++的关键字。
c中的malloc在c++中使用new来代替,而free用delete来代替。其作用都是用来申请和释放内存空间。
除了c中定义静态变量、修饰函数作用域为文件内,在c++中还新增了一个特性,当static用在类中,可将其成员修饰为静态成员。
是一个指针,用来描述对象自己的指针,在对象的函数中需要使用成员变量时,可以通过this来访问,有时候还需要将当前操作的指针作为一个返回值,传给其它函数。
翻译为虚拟的,用来修饰一个函数为虚函数,一般使用在类中,将基类的一些函数修饰为虚函数。有时候也专用来制作接口类。
重写,主要用在基类中对函数声明做修饰,告诉编译器,该函数实体在派生类中去完成,此处没有实体。
继承终止,当一个类不想被继承或类中的成员不想被继承,即可在前面添加final,在后面就不会被继承。
朋友,用于在类中声明友元函数,一个外部函数想要访问类的内部成员变量时,在类中将函数声明为友元函数,那么函数既可以访问类中成员。
const的用法比较多,当我们声明一个变量为const表示该变量不能被修改,当修饰一个指针的时候会出现cosnt int *p不允许修改指针指向的对象,而int const *p不允许修改指针。出现在函数形参中时,来表示该函数不会修改传入的变量,特别是引用混合。
而在类中,在成员方法后面跟const来修饰在该方法中不会修改任何成员变量。
mutable是类中的const的一个特列行为,当成员方法后跟const来声明不会修改任何成员变量,但是被mutable声明的变量例外。
作用是用来指定代码在编译过程运行,将其运行结果返回,是一种用来提升效率的利器,然而很多编译器并不支持。
template用来定一个模板类,而typename用来指定模板类名,这是实现泛型的一个主要手段,他可根据传参类型来推导在函数内部或类的内部的变量类型。
export的功能和extern有些类似,都是用来在头文件中做外部链接属性的声明的,只不过extern通常用来声明变量,而export用来声明一个模板的名称。
用来做异常处理,c中没有异常处理机制,一旦某段程序出错,整个程序都有可能跟着崩溃,在高级语言中都有这种错误处理机制。示例代码如下:
try //括号内的代码被监控
{
if(b==0)
{
throw -1; //满足设定的错误条件,抛出int型-1
}
return (a/b); //上部被抛出,本代码得不到运行
}
catch(int) //抛出的是int型,执行本代码
{
cout<< "input error"<<endl;
}
try加一对大括号,表示该括号内的代码被监控,throw作用是抛出错误,catch用来捕获错误。如上例中在try中throw抛出一个int型变量。catch捕获到int类型的抛出的错误就执行括号内的代码。在我们去调用很多库时如果内部代码执行错误,都会抛出一个错误,而这个抛出的错误如果我们没有去catch那么这个错误会继续一层层向外抛出,直到被catch,通常最后会被操作系统catch。
c++的内存管理
内存管理庞大而复杂,内存的管理由操作系统完成,操作系统对外预留了内存申请与释放接口,而这些接口被c++再次封装后预留出接口,给程序员使用。
c++的可用内存
- 栈:放局部变量。
- 全局数据区:用于存放全局变量。该区域还包含了const变量区。
- 代码段:用来存放代码,在操作系统的监管下使用,是一片只读区域。
- 堆,malloc申请free释放,实际上在该区域还有一片自由存储区域,有new申请,delete或delete[ ]释放。
new和malloc的区别
new底层其实还是使用了malloc来实现,只不过做了些附加工作,例如在new一个对象时,会去调用构造函数,这样一个比喻malloc返回的是一片空地,而new是返回的一个基础设施完备的园区,具体对比见下表。
malloc | new |
c库函数 | 运算符、关键字 |
分配空间由传参决定 | 大小由数据类型决定,编译器自动计算 |
返回值void * | 明确的类型,申请啥返回啥 |
free释放 | delete、delet[ ]释放 |
申请内存不初始化 | 可以隐式和显示初始化 |
无构造函数 | 执行构造函数 |
申请失败返回NULL | 申请失败返回bad_alloc异常 |
创建一个普通对象只需要“new 类 对象”,有时候我们在给一个函数传参是需要传入一个临时对象,可在函数传参列表中以格式“类()”,类后面写个括号,括号可以传参也可以不传参。示例如下:
class people //定义一个类
构造与析构函数是类中必备的成员函数,构造函数用来初始化对象,申请对象中所需要的的内存,析构则是用来销毁一个对象,当我们要删除一个对象时,由析构函数将对象中所申请的内存释放。
构造函数是一个没有返回值,函数名与类名相同的函数,构造函数也不例外的支持函数重载,可以分别提供带参和不带参的构造函数,在我们创建一个对象时,即使我们没有提供构造与析构函数,编译器也会为我们提供一个空的构造与析构函数,来保证编译不出错。而析构函数则是在类名前面添加一个~符号。
通常我们在编程时会将类写在头文件中,类中包含了成员方法的声明,当然包括构造与析构成员方法,而将成员方法(成员函数)实体写在源文件.cpp中。
析构函数一般都是空的,因为在析构中需要做的事情一般都是去释放构造函数中申请的动态内存,如果在构造函数或整个对象使用过程中没有产生内存的申请,那么析构函数就啥都不用做。
构造函数的一大功能就是初始化对象。编译器提供的即默认的构造函数是无参的。
如果我们编写了带参的构造函数,那么编译器就不会再为我们提供默认的无参构造函数。当我们把对象定义在栈上时,不传参那么对象名后面不能有括号,要传参时才使用括号,如果对象定义在堆上则可以有括号,这一点有些奇葩,不吐槽,接收他,遵循规则。
我们对象初始化时可以写在构造函数的大括号内,而另外一种快捷的方式就是使用参数列表来初始化,示例如下
person::person(string n,int a,bool s):name(n),age(a),sex(s)
特点是函数括号后面添加括号,多个参数初始化时使用逗号隔开,实际上冒号后面是一种语法支持,还可以用来干其他事情,如继承,又或继承与参数列表同时存在,同样可以使用逗号隔开。
c++支持在函数定义的形参时给出初值,如果调用函数有传参,那么就使用传入的参数,如果没有传参,则使用给定的默认值。这个特点当然也适用于类的构造函数。注意的是设置默认参数不能只有前面没有后面,因为传参时是从前向后匹配的,所以默认值就变得没有意义。
函数声明带默认初值容易使函数重载时出现歧义。如下示例,当我们调用person 时无论传入几个参数,都符合示例一的条件,都会被匹配到示例一,所以当涉及到函数重载时,最好不要使用参数默认值。
示例一:person(string n="lilei",int a=6,bool s=1);//默认全带参构造函数
示例二:person( );
person(“han” );
person(“han”,10 );
person(“han”,10,flas );
继承是c++的面向对象特性之一,c中的结构体包含结构体其实就是一种继承关系,被包含的结构体就是类中的父类(基类),包含结构体的类叫子类(派生类),继承本质上是一种代码重用的解决方案,示例如下:
class A:继承权限 基类1,继承权限 基类2
我们把事物的共同点抽象成基类,再针对独有的特性创建成派生类,派生类当然也具有基类的共同特性,所以派生类是将抽象程度精确化,就如同基类是人类(2只手,2条腿,一个头),派生类是男人(能吃、能睡、能玩)。
继承的权限控有public、private、protected这些权限与类中的成员权限管理相似,使用相应的权限来继承基类,那么基类的元素就被列在子类的某一权限列表下,类中的private不能被继承,虽然成员被拷贝,但是不能访问,protected的特性是内部可以访问,外部不能访问,但是可以被继承和在子类中访问。
- public继承:父类的public、protected成员在子类中可以访问,父类的private不可以访问。对外保持父类原来的访问权限。
- private继承:符类继承而来的全部成员在基类中变为private。仅内部访问,外部不能访问。
- protected继承:父类的public、protected基于访问,继承来的所有成员变为protected。
class person //定义一个类person
{
public:
string name;
private:
int sex;
protected:
int hige;
};
class man:protected person //定义一个man类,同时继承person类
{
public:
void print (void);
};
子类继承父类其实就是将父类拷贝了一份在子类中,仅此而已,原来的那个被拷贝的基类与现在新建的子类之间没有任何关系。是完全独立的两个类,类中的private权限下的成员很奇特,虽然被拷贝到子类中了,但是子类不能访问,要访问,得使用从父类哪里继承过来的成员方法来访问。实际上并不是所有的东西都进行的继承,基类的构造和析构函数就不会被拷贝。因为子类必须要有自己的构造与析构,本质上子类还是一个普通类,所以类的所有操作都可以使用,虽然我们没有把构造和析构函数拷贝过来,但是我们在继承的哪一刻,子类就会去调用基类的构造函数,如果传参就调用传参的构造,没有传参就调用无参的默认构造函数。虽然我们调用了基类的构造函数,但是初始化的是子类中包含的那一部分向基类继承而来的成员。虽然我们可以在子类的构造函数去初始化继承而来的成员,但是不推荐这么做,因为这样很不清晰,在初始化时,别人看了会莫名其妙,需要经过推敲后才能发现初始化的是基类的元素。通常我们初始化基类成员会去借用基类的构造函数。
当我们子类与父类存在同名同参的函数时,编译器并不会提示函数重名,而是悄悄的把父类的函数隐藏起来,看起来是将父类的函数进行了重写,实际上在两份函数都在,只是处于不同类域中,我们仍然可以通过指定父类来放问到父类中的函数。函数如此,变量亦是如此。
在c/c++中,long int 兼容int兼容short int,而int与float虽然内存大小相同,但是他们完全不兼容,这就是类型兼容规则。
派生类中包含了基类,所以派生类是兼容基类的,使用cast可以将基类从派生类剥离出来,裁剪掉派生类中多余的部分,而直接定义一个派生类的指针,是可以指向且操作基类的对象。
在派生类继承基类的时候,基类的有很多东西并不是派生类需要的、甚至是错误的,这种现象被称为不良继承,其典型的案例就是椭圆不是圆的问题。在数学理论中圆是特殊的椭圆,如果将椭圆抽象成基类,圆抽象成一个椭圆的派生类,用来描述椭圆时有长轴和短轴,而圆只有直径,所以这种继承参数完全是无用的,所以是不良继承。
组合是c++中的一个概念,也是日常生活中的一个概念,将多个同类东西放在一起形成一个新的集体,被称为组合,在c++中将多个类集成在一起就是组合,当然组合实际上可以通过继承来实现。不管什么手段,最终都是组合。继承是组合的一个分支。
但是组合与继承还是有差异,继承讲究谁属于谁,不具有传递性,不对称性。而组合是包含关系,继承是一种白盒复用,在继承中可以对基类覆盖(重定义),这一特点其实是破坏了类的封装特性,因为父类的实现细节暴露给了子类。而组合刚好相反,内部谁也不干预谁,相互保密,但是在组合时被组合的元素必须是有对象的,不能直接将类进行组合,推荐优先使用组合。
二义性,通俗的说就是有歧义,通常出现在子类继承了两个内部含有同名成员的的基类,在访问时编译器不知道我们到底想访问哪一个对象中的成员。另外一种产生二义性的问题就是菱形继承,就是两个类继承了同一个基类,而这两个类又被继承到同一个派生类中,导致最后的派生类包含了两个最顶层的基类,如下图。解决二义性问题就是使用重定义(隐藏)技术,另外就是使用虚继承、多态等手段,同一可以解决二义性问题。
虚继承使用很简单,就是在继承的时候添加关键字virtual,如以下的菱形继承时在class B1:virtual public A和class B2:virtual public A,即可对A进行虚继承,这种的效果与防重包含类似,当检测到已经有了一份,就不在继续包含。虚继承他底层实际上B2/B1两个都没有拷贝到子类中,而是将其拷贝到一个临时地址,再用指针去指向它,之所以叫做虚继承,原因就在这里,他本质上根本没有被拷贝,只是多了个指针。所以是虚的。
多态就是在基类中的成员函数前添加virtual,将函数修饰为虚函数,编译时是不直接与某函数绑定,而是根据运行时的条件来确定具体调用哪一个函数,这就是多态。实际上多态在c中就已经有用到,例如我们的函数指针,根据运行时传入的函数,选择运行,当子类与父类有同名同参的函数时,使用基类的指针去访问子类,会访问到子类中继承而来基类的函数,将基类的函数修饰为虚函数,那么在调用时就会被调用到子类的函数,和重定义类似,其特点就是添加了virtual就是多态,否则就是重定义。
- 重载—同名不同参的函数,编译器可以准确调用,被称为函数重载。
- 重定义(覆盖)—子类中的函数覆盖掉父类中同名同参函数。
- 多态—在基类的函数前添加virtual修饰为虚函数,根据运行条件,选择性调用函数。
虚析构函数,就是在在析构函数前面添加virtual关键字,与普通的同名函数一样,当我们使用基类的指针指向派生类对象,结束时会被调用到基类的析构函数,而不执行子类(即定义的对象)的析构函数,所以我们要将析构函数定义为虚函数,就可以去执行子类(对象)的析构函数了。
纯虚函数就是在对虚函数赋初值0,纯虚函数不暂用内存,一旦类中有了纯虚函数,就不能定义对象,只能用来被继承。而这个类就变成了抽象类,所谓抽象类其特点就是不能实例化对象,且必须在子类中去重定义父类的虚函数,否则子类也跟着变成抽象类。一个类中全是虚函数,只能被继承,且必须在子类中去重定义函数,这种方法常用来在父类中明确子类的一些必须的方法,如果一个类中全是纯虚函数那么这个就被称我接口。
using修改访问权限
父类的非private在使用了private权限继承后,父类的所有成员在子类中就变成了private属性,外部就不能访问了,如果我们想要对个别的修改为允许外部访问,就可以使用using来修改访问权限,示例如下:
public:
using 基类::成员
注意,在指定成员方法时不写返回值、形参、括号,只写函数名。
运算符就是我们平时常见的一些符号如+、-、×、/ <<等等,重载则是将这些符号进行重定义,见到这些符号时去做一些指定的操作,为什么需要这个了,是因为我们自定义的类与类之间不能进行运算,试问一个对象加一个对象应该等于什么了?这个答案可以有千万种,具体哪一种由程序员说了算。
运算符重载的关键字operator,在该关键字后面跟什么符号就是对什么符号进行函数重载。当我们写了一个对象A和对象B,当对象C=A+B,解析后是C=A::operator+(B),运算符重载使得同样的符号有不同意义,这也体现出多态。
编译器会为每一个对象免费提供一个operator=来实现同类型的对象可以直接做赋值操作,这是c中的结构体没有的。
C=A::operator+(B)
operator+(B)
{
return this->value+B.value;
}
this是对象A的指针,return返回值是一个值传递,涉及拷贝效率偏低,所以我们完全可以将其修改为一个返回值为引用的函数。
如class a = b,当我们定义一个对象,并同时赋初值,那么调用的是拷贝构造函数,而我们直接写a=b时调用的是a中的=运算符重载函数,为了防止出现a=a的这种自赋值的情况,通常会在运算符重载函数中首先判断两者不相等。
当我们的函数在栈定义的变量做了返回值,且是以引用方式传址,那么接收这个地址的函数形参必须添加一个const,因为栈的资源在函数结束时就会被释放,所以我们只能引用值不能引用到地址,对地址的修改是毫无意义的,所以需要加const。但是当我们的变量是在new出来的对象就不同了。
例如a=func();a是一个对象,而函数func中包含有return x来赋值给a,这个背后其实分了2次拷贝,首相return x要建立一个临时变量,并将return时的x拷贝到临时变量,再将临时变量拷贝到a,看这多麻烦,效率好低,所以最好的办法就是直接返回一个引用。
此时我联想到了将引用换成指针,本质上是完全可以实现的,但是指针的赋值需要使用&符号,如果没有我们会觉得很别扭,所以最优解还是使用引用。
++运算符重载
++涉及到前置还是后置的问题,如i++还是++i两者的结果是不一样的,当然进行运算符重载时也应该不同,由于都是符号++并不好区分,所以c++给了一个固定格式,即i++重载函数需写成operator++(int x);int x是固定格式,虽然没有进行传参,但是是一个识别的格式,而++i要写成operator++(void),运算符重载函数和普通成员函数一样,可以写在类中,也可以外置,只是在类中做声明。
即是将运算符重载以友元的方式写在外部,因为涉及到类的成员访问,所以需要声明为友元,
其实将其写成外部函数更利于理解和阅读,因为写在外部就不存在this指针的问题。另外就是a和b的位置交换对结果没有影响,都是调用同一个函数,而写在内部就不同了,完全看a和b的运算符重载函数是否相同。
(因为没必要,所以支持):
. | 成员访问运算符 |
.* ->* | 成员指针访问运算符 |
:: | 域作用符 |
sizeof | 长度运算符 |
? : | 条件运算符 |
# | 预处理符号 |
运算符重载=、->、[ ] 、( )不能使用友元方式实现,如=,编译器为我们默认提供的有一份,当再进行符号重载匹配时,编译器并不会识别到友元函数,只会检查类的内部是否有,如果没有就会调用默认提供的函数。
默认值运算符重载原理
c=a+7;
c和a都是一个对象,而7是一个非对称的数字,编译器会构建一个与a和c相同类型的对象,并将7作为创建对象的初值进行赋值,然后在调用a的operater+函数对临时创建的对象与a相加后再赋值给c。
静态成员就是在类的成员中添加static将成员修饰成类的本身,static在c中已经有了定义静态局部变量和限定函数链接属性为文件内部的功能,在c++中还给了一个新语义就是在类中用来定义静态成员,如下示例,静态成员在类进行初始化时不能在类中或构造函数对其进行初始化,所以需要在类的外部去独立初始化,如果修饰了一个成员函数,函数实体本身就是一种初始化,则不需要额外进行初始化。
struct person //定义一个person类
{
static int a; //声明一个静态成员变量a
};
int person::a=23; //定义静态成员变量并初始化
静态成员的访问与普通的成员变量一样,可以使用域来访问,也可以通过对象来访问。他的访问遵循类的权限管理。静态成员函数只能在类中的声明前添加static不能再函数实体前添加static因为那样会与普通的使用staic的函数有歧义。
静态成员的调用可以有以下方式:
p1.a=23; //通过对象访问
person::a=23; //通过类名访问
this->a=23; //通过对象指针访问
推荐第二种,这种可读性最好,因为所有的普通成员都不允许通过类名访问,所以一看这种以类名的方式访问就知道是静态成员。静态方法只能访问静态成员,不能访问非静态成员。
普通成员随对象的建立而建立,对象的消亡而消亡和局部变量一样,放在栈上或malloc空间,而静态成员和函数内的静态变量一样,程序运行时就已经分配空间,整个程序结束时才消亡。在对象中不占内存,只是一个声明。
静态成员是一种破坏面向对象的行为,看起来在对象中,但是可以通过类名访问。静态成员其实就是一个全局变量,只是封装到了类中而已。
类中的成员全部是静态类型,那么类就成了静态类,静态类由于不含有具体成员,所以不能实例化对象,不能被继承,不能有构造函数,但是可以有静态构造函数,利用这些特性来实现密封
静态成员和静态局部变量一样,完全可以使用全局变量来完成,但是全局变量具有可移植性差,打破了封装的特性,为了解决这些问题,所以就有了静态成员。
友元函数主要用来访问类中的私有成员,友元函数是一个普通函数,只是在类中做了friend声明,因为有该声明所以可以访问类的私有成员。友元函数声明可以是在类的public也可在private或protected对其不做限制。友元函数不带this指针,所以在友元函数内访问变量时必须使用域操作符或使用函数传参的方式访问。友元函数类似在类的封装上打了个洞,所以这是一种对类的封装性的一种破坏。
虽然友元破坏了封装特性,但是实现了数据共享,减少了数据交换的开销,提高了效率。当利大于弊时我们就可以考虑使用友元,例如运算符重载使用友元可以使语义更清晰,更符合编程习惯。
友元成员值再一个类中什么为友元的成员时另外一个类中的成员。
友元类和普通友元成员没啥两样,只是将一个类在另外一个类中以友元方式进行了声明,那么被声明的类就可以访问另外一个类的全部程艳。友元类是一个批量制造友元函数的策略,所以是一次再类的封装特性上打了多个洞,这种破坏更严重,所以我们应该少使用这种方式。
互为友元就是两个类都在对方相互为友元,那么两个类之间就变成了共享成员了。
友元函数没有this指针,毕竟只是朋友,不是自家人,友元函数不能被继承,就像父亲的朋友不是儿子的朋友,友元函数不具有传递性,a和b是友元b和c是友元,并不代表a和c是友元。
指同一个函数在两个或多个类中声明了友元,那么这个函数就拥有了同时访问多个类的权限,在函数内部可以对两个类进行赋值,这种方式间接的实现了两个类的数据交换或共享,很像一根管道打通了2个桶。
嵌套类就是类中嵌套了一个类,外部的类叫外围类,内部的加内部类,两者的友元不共享,成员都有自己的权限,只是内部类的访问需要从外部类一层层向内访问,嵌套类可以用来隐藏类名,减少全局标识的同时强调两个类之间的注重关系。
局部类就是写在函数内部的类,和局部变量类似,都可以减少全局标识,通常局部类不需要权限管理,因为本身函数都不大,没必要再做权限管理,只是单纯的对数据进行封装。
数值与对象互转
数值类型就是简单的原生类型,如int、float、double等,数值转换为对象实际上是构造了一个新的对象,将值以对象初值进行赋值,而对象转换为数值涉及到数据的丢失,编译器不能为我们提供这种方法,只能我们自己在内部做一个带有返回值的函数,将对象内部的成员获取后传出。
对象数组就是建立一个数组,数组的每个元素都是一个对象,使用需要结合数组与对象来访问并没有什么特殊,只是在释放对象的时候需要使用delete[ ]来释放。
模板template是c++实现多态和泛型的关键,主要是用来解决写类库的人不知道将来使用类库的人会传入什么类型的数据,而将类型进行的一个抽象,该抽象在实际使用时根据传入的数据来倒推出数据类型后根据该类型来定义内部预留的变量的类型。泛型抽象通常用一个标识符来代替类型,该标识符由程序员使用typename指定,这其实是一种延迟类型确定的手法,模板的类型确定最终还是在编译阶段完成,只是确定的时间进行了推迟,一个简单的模板函数示例如下:
template<typename T> void myswp(T &a,T &b) //交换数据
{
T temp;
temp=a;
}
也是一种多态,类中的“动态多态”是在运行时确定,而模板类的多态是在编译时实现,运行过程是不可变的,所以也称为静态多态。
模板在c++中的重量级类似指针在c中的地位,模板能让我们编写出万能类型的函数,模板时一种更高抽象的思维,非常适合用来写库,模板相对比较复杂,他是拿复杂度来换取工作量。模板的定义通常使用teplate也可以使用class,两者使用的都比较多,看心情吧,模板的类型可以有多个,如template <typename T,typename A>。模板分为模板类和模板函数,上例中定义的就是模板函数。
类模板就是和函数模板类似,都是将内部成员的类型延迟确定,类模板实例如下
template <typename T> class people //模板类前添加模板类型声明
{
public:
people(T a); //类型使用模板虚拟类型//是people<T>(T a)简写
private:
T age;
};
如下示例,类模板的成员外置时,需要注意一些语法细节。
template定义一个模板类型的作用域仅在标识的大括号内。
外部涉及类名的引用时需要在类名后面添加<T>来标识该类是一个模板类。
类成员写在外部时还需要对T进行重新定义,需添加template <typename T>。
模板类在定义对象的时候必须进行指定,people<int> p;
template <typename T> people<T>::people(T a) //外置带参构造函数
{
age=a;
}
int main(void)
{
people<int> p(5); //使用类须指定模板的类型,定义一个对象
}
当一模板类中被定义了一个友元函数,且该友元函数也是一格模板类型,则声明时函数名需要重新定义一个模板,且该模板类型的标识符与类的不能相同,示例如下, 友元函数名后必须加<T1>
template <typename T> class people //定义类
{
public:
T age;
template <typename T1> friend void print<T1>( people<T1> &p);
};
template <typename T> void print(const people<T> &p)// 函数名后不能加<T>
{
cout<<"age= "<<p.age<<endl;
}
模板运算符重载没有什么新的知识点,基本就是前面的运算符重载与模板类的知识结合。
template<typename T> class people
{
private:
T age;
public:
people<T> operator+(people<T> &p); //运算符重载申明
};
template<typename T> people<T>::operator+(people<T> &p)
{
people<T> temp;
temp = age+p.age;
return temp;
}
类模板继承类模板,就是在继承过程中将模板的类进行传递,传递时与函数形参传参类似,都是按排列顺序对应传递,与具体标识符无关,在子类构造函数调用父类构造函数时要指定父类的名且添加模板<T>,模板中的构造函数其实应该写成带<T>的,只是编译器允许我们不带。如下示例:
template<typename T> class people
{
int x;
people(T a):x(a){}; //该构造函数原型是people<T>(T a):x(a)
};
template<typename T> class man:public people<T>
{
int y;
man(){};
man(T a,T b):y(a),people<T>::people(b){ };
};
模板库不能被单独编译,因为模板在编译时需要根据传入的参数来倒推类型,所以模板库的开发和发布只能是以源码的方式进行。
STL全称stand template libriy 标准模板库,是c++提供的一套由算法和数据组成的库,分为容器和算法库,容器库就是一些数据结构,如数组、链表、队列等,而算法库就是一些算法。STL的核心即是容器类,其它的即使都离不开容器类,STL的本质就是一套模板技术泛化类型的c++基本数据结构和算法库,学习STL就是学会每种容器的使用,根据容器的特点,知道适用在什么情况下,遇到需求后知道适用那种容器类比较合适。
STL容器
STL容器大致可分为序列容器、排序容器和哈希容器。序列容器中的元素的位置与值本身无关,即不排序,类似于数组、链表等只负责存数据,数据存放无规律。排序容器就是数据的位置与值本身有关系,他们按一定顺序排列。
迭代器是一种对容器类的数据结构进行遍历的一个工具,我们接触得最早的迭代器就是数组的遍历,因为数组的类型相同所以我们使用指针进行加减操作时编译器知道应该移动多远,当我们的内容是结构体时,结构体内部的变量是不同类型的,指针就不知道该移动多远,就无法进行简单的遍历,只有些容器类的人知道,因此所有的容器库都必须对外提供遍历的接口,这个接口就是迭代器,正向移动的叫正向迭代器,而逆向移动的就是逆向迭代器。迭代器分为开始begin()和结束end();如果只是只读使用cbegin()和结束cend()。利用这两个,我们只需要基于beging++就可以访问到下一个元素,end—就可以访问到上一个元素。每个迭代器还包括了一个迭代器指针iterator,使用方法是
array<int, 5> :: iterator iter;
利用迭代器变量一个容器如下示例
for(iter=a.begin(); iter != a.end();iter++) //判断不用小于,使用!=
{
cout<<iter<<" = " << *iter <<endl; //读
}
遍历时实际上我们并不会去定义一个迭代器指针,都是直接使用auto来推导类型,示例如下。
array<int,5> aa={1,2,3,4,5};
for(auto iter : aa)
{
cout << iter<<endl;
}
array容器类
array容器类其实是c++使用template技术对数组进行的二次封装,具有数组定长、元素类型相同、在内存中连续排布的特点,定义一个array的方法有多典型使用
array<int, 2> a; //定义但不初始化,可定义1个或多个
array<int, 4> a2={1,2,3,4}; //调用带参构造函数聚合初始化
operator[ ]方式: a[0]=1或者int b=a[0]; //[ ]运算符重载
vector容器
vector是动态数组,可以按需扩展和收缩数组大小,实际上vector在c中就已经有了,只是我们没用过而已,是一个可以扩展大小的数组,他的伸缩是自动的我们无法干预。定义方式为
vector<类型> 对象
list容器
list容器内部是一个链表,具有链表的任何时间任何位置插入和删除节点。优点是支持内存不连续的访问,缺点是不支持快速访问,必须进行遍历。通常使用双向链表。定义方式
list<类型> 对象
STL泛型算法
软件无非就是数据和算法,STL容器是对数据的封装,STL的泛型算法就是对容器内的数据进行各种计算,虽然容器内本身自带了一些算法,但功能都非常单一,所以STL提供了一系列算法库,这个算法库具有通用性,所以叫泛型,泛型算法具有更高的抽象度,实现起来难度更大,是STL的核心技术,使用STL算法库排序示例如下
vector<int> bb{4,2,1,3,5};
sort( bb.begin(), bb.end(), greater<int>() ); //传入迭代器和谓词,使其降序排列
for(auto x : bb)
{
cout << x <<" ";
}
谓词就是可以做谓语的词,语义上含有动作,其实函数就是一个典型的谓词,STL中的谓词可以是函数、函数对象、lamada表达式,其形式上为
bool func(T &a) //一元谓词
bool func(T &a, T &b) //二元谓词
其特点是返回值都是bool,形参输入都是引用。STL算法库提供了算法,但是一些条件依靠谓词来完成。如sort排序,sort完成的工作时排序,至于是按照什么规则来排序,由谓词提供,sort将数据遍历传给谓词,再接收谓词的返回值,
函数对象也叫仿函数,是一个长得像函数的对象,实际上是一个类,在类中对“( )”进行了运算符重载,使得在调用形式像模板函数,运算符重载operator()( )其实也是一个函数,所以底层还是调用了函数。
template( typename T ) struct greater
{
T operator()(T a) //()运算符重载
{
if(a>0) return 1; return 0;
}
};
greater<T> func; //定义一个对象
int b = func<T>(5); //调用类中运算符重载函数。
lambda表达式
lambda表达式是一种函数对象的简写,也被称为闭包,lambda表达式是一个匿名函数对象,既然是匿名的,自然就无法直接二次使用或被调用,当我们写了一个lambda表达式时,其实编译器会将其翻译成一个匿名的函数对象,示例如下:
[ ](int &i){return i%2==0;};
bool operator()(int &i)
{
reutrn i%2==0;
}
虽然无法直接调用,但是如果我们在表达式前面使用auto自动推导类型来创建一个同类型指针,是可以实现二次访问。示例如下
auto func =[ ](int &i){return i%2==0;};
lambda表达式格式
[ 参数捕获] (操作符重载函形参) mutable或exception声明 -> 返回值 {函数体;};
由5部分组成,其中“声明”和返回值可以省略,其余部分即使内容为空,也要使用符号占位,
lambda表达式逐渐变形如下
[ ]( ){ }(); //空lambda表达式调用
[ ]( ){cout <<hello << endl; }(); //带函数体的lambda表达式
[ ](int i){cout << i << endl;}(5); //带传参的lambda表达式
auto func = [ ](int i){cout << i << endl;}; //定义一个lambda表达式,并func指向
auto func = [ ](int i) ->int {return i;}; //带返回值
lambda表达式是一个闭包所以不能访问到其他变量(传参除外),但是有时候我么就是需要去访问一些变量所以就设计了参数捕获功能,将要访问的变量写在“[ ]”中,就可以实现在表达式的函数体内访问,访问的方式可以是引用的方式(变量前加&),也可以是普通方式,参数捕获必须是在同一个代码块内(大括号内)。
lambda表达式仅是一个定义,就像写一个函数一样,在没有被调用的时候是不会被执行的,如果我们想要在定义的同时执行可以在定义后面添加一个()。这一点也适用于类的匿名对象对象。
[ ] 空:完全不捕获,只占位。
[=] 传值,捕获代码块所有变量和this指针,传值自动变cosnt内部不能改。
[&] 引用方式,捕获代码块内所有的变量,包含this指针。内部可修改捕获变量。
[this] 当前指针捕获,主要出现在class中,用来捕获当前class的this。
[a] 传值方式,指定访问a变量。
[&a] 引用方式,指定访问a变量。
[a,&b] 传值访问a,引用访问b;
[=,&a] 引用访问a,其余的传值访问。
[&, a] 传值访问a,其余的引用访问。
函数适配器
函数适配器是一个中间函数,主要是用来对形参数量进行适配,他的存在主要是为了解决我们调用STL算法库时缺少参数的的情况。如下示例,func1的实现需要回调可以接收1个参数的函数,但是func2有2个形参,不能直接回调,我们再写1个func3并在内部调用func2的时候填充一个变量,这个就是函数的适配原理。简单说,函数适配器就是在不同传参个数的函数间进行适配的技术。
func1(viod (*func) (int a) ) //该函数需要传入有2个int形参的函数
func2(int a, int b) //定义1个形参的函数
func3(int a)
{
func2(a, 5);
}
适配器bind
bind是一个STL提供给我们的适配器,如下示例,placeholders是占位关键字,后面跟的数字就是去占位函数的第几个参数,bind会生成一个匿名函数,可以使用auto自动推导,向bind的匿名函数传参时仍然按位置匹配,但是bind将参数转交时是按照占位的位置匹配,如下列,字符“A”最终会赋值给变量b,5赋值给变量a。
void print(int a,char b,double c) //定义一个函数需要传3个参
{
}
auto func = bind(print,placeholders::_2, placeholders::_1, 33.1);
func('A',5);
all_of
STL的算法库all_of用来检查传入的数据判定全为true则返回true,否则返回flase,判定标准由谓词决定,all_of将容器内的数据依次输入给谓词经过谓词判定返回true或false,如果谓词返回的结果全部是true那么all_off则输出true。示例如下:
class mult //定义一个函数对象
{
mult(int i):base(i){}; //构造函数
bool operator()(int &i) //运算符重载
{
return i % base == 0 ;
}
int base;
};
array<int, 5> table={2,2,2,2,2}; //定义并初始化一个array容器。
bool c = all_of(table.begin(),table.end(),mult(2));
cout << c << endl;
less释义更少的,是一个函数对象,用来大小比较,返回值为bool的()运算符重载,使用如下,比较方式类似于在传入的两个数据之间加一个小于符号“<”,成立返回true,否则false
less<int>()(10,20) //10<20,返回true
bind1st与bind2nd
bind1st和bind2nd只能用来适配2个形参的函数,如bind1st(less<int>(),10); 就是将10绑定在“<”符号的左边,bind2nd(less<int>(),10)就是将10绑定在<符号的右边,返回只有1个形参的匿名函数。使用示例如下:
array<int,5> aa{11,12,9,8,7};
auto func = bind1st(less<int>(),10);
for(auto x:aa)
{
cout << func(x)<<endl;
}
用来对容器做遍历,是STL算法,传入起始和结束迭代器,依次遍历容器元素,并逐个传给f,示例如下:
vector<int> aa{5,4,6,7,8,10,15};
auto print = [](int &tige){cout<<" "<<tige;};
for_each(aa.begin(),aa.end(),print);
相似的还有for_each_n,功能类似,只不过是传入起始迭代器和往后的数量。
用来查找给入容器中与设定值相同的数量,示例如下
vector<int> aa{5,4,6,7,8,10,15,5};
int i = count( aa.begin(), aa.end(), n); //n表示要比较的值
查找给如容器中的元素,符合要求的数量,示例如下
i= count_if(aa.begin(),aa.end(),[](int &data){return data%2==0;});
模板函数将数据类型进行了抽象,函数根据传入数据的类型来自动适配,如
template<typename T1, typename T2> add(T1 a,T2 b),在内部进行两个变量相加,该模板具有通用性,无法满足一些个例的自定义需求,如我们想在传入两个字符串变量相加的时候中间加入一个空格,就可以写一个sting的特例版本,当传入的参数为sting类型时,不去调用通用版本,而是调用特殊版本。
模板特化实现在编译阶段,所以效率高,实现原理和函数重载类似。其语法是特化模板template<>的括号空,当同时出现同名同参的普通函数、特化模板、模板函数,优先级是先匹配普通函数再特换模板在模板函数。当特化一部分参数被称为半特化,全部参数特化时叫全特化。版特化除了支持类型,还支持指针、cosnt。
其实模板特化时我们人为的帮助编译器做了些事情,告诉他不用去推导类型,按我给出的类型做。
模板特化指特换类型是另外一种模板对象,如下示例:
template<typename T> class peple<vector<T>>
模板函数重载指有两个特化版本,名称相同,只是形参类型不同,如下,模板函数仅支持全特化。
template<typename T1> void func(T1 a)
template<typename T1> void func(T1 *a)
编译器重载规则
第①步:匹配非模板函数,即普通函数,如果匹配到就执行,匹配不到进行下一步;
第②步:匹配基础泛化版本函数,匹配不到就报错,匹配到了再进入下一步。
第③步:匹配全特化函数,如果有就执行,无就执行上一步的基础泛化版本。
细节:模板函数的特化版本,不参与函数重载,即可以有完全特化版本的函数与普通函数同名、同参,在调用的时候,普通调用就是func(a);而泛化版本的调用时func<int>(a);
模板特化和模板实例化两个概念注意区分,模板实例化指的是具体确定模板参数的类型T,模板特化就是前面一直在学的给出具体类型,本质上模板全特化就是一种实例化,而偏特化则不是,偏特化只是把模板类型的可能性压缩了,如模板偏特化成了指针,那么后面匹配的时候就只能是一个指针类型,因为模板函数并不是实例,普通函数是一个实例,所以他们不会冲突。
类型萃取可以理解为类型获取,典型的应用是原生类型与自定义类型的区分,因为原生类型和自定义类型涉及的算法是不同的,所以需要区分。
用来判断传入的值是否为POD类型,原型是一个模板类。
cout << is_pod<int>::value << endl;
他的内部的实现可能是“特化+静态成员”其实就是把基础类型全部列为特化模板,在内部使用静态变量value初值为true,而泛化版本的初值为false。
还有可能是特化+普通成员实现,核心还是特化,只是使用构造函数赋初值给私有成员value,使用了一种之前没提到的语法,就是在没有对象的情况下,使用括号“()”建立匿名对象,来引用类成员my_is_pod<int>().get_value()。
prtdiff_t本质上是一个long int 类型变量,该变量被定义在c++标准库中,将其拆分为prt(指针)diff(差距),该变量就是用来存放数组之间的地址差,可理解为步距。
size_t本质上是一个unsined int变量,通常用来表示数组元素个数。
和前面一样,都是库定义的一个变量,是signed类型,通常用来表示迭代器的差距。即两个迭代器的差。如vector<int>::difference_type = iter1-iter2。
STL算法库需要根据容器的各种类型做不同的算法,为了实现STL泛型算法与容器完全独立, 在容器中专门为算法课提供了如下类型查询接口。
value_type : 迭代器所指对象类型
difference_type : 表示迭代器距离的类型
reference_type : 迭代器解引用操作结果的类型
point_type : 迭代器->操作结果的类型
iterator_category : 迭代器类型(由所支持操作决定)
这些接口实际上就是使用了typedef对内部变量类型进行的重命名。访问示例:
arra<int,5>::value_type aa = 54; //等效于int aa = 54;
容器adapter,容器适配器是对已有容器的二次封装,构建成新的容器,如我们可以对数组array容器封装成一个栈,容器适配器有stack(栈)、queue(队列)、priority(优先级队列)。
stack:栈,先进后出,通常最关心压栈和弹栈操作。
queue:队列,FIFO,通常关心入队和出队操作。
priority_queue:优先级队列,进入按自排序式,按优先级出,通常也是关心入队和出队操作,例如我们指定从大到小排序,那么我们每入一个数据都会被排序,取的时候就会从大到小取出。
前面学的一些基本容器如array、vector、list都是独立的,偏向于底层的内存管理,而容器适配器是依托底层的基本容器的二次封装,是上层应用,栈和队列等是数据结构型容器,偏向于数据操作,主要是控制数据如何进入和输出,他没有迭代器,因为不需要,也不允许访问内部的底层的基础容器的迭代器。
STL其它容器
stack容器适配器
基于deque实现的一个栈,
初始化
stack<int> aa; //无参构造函数
deque<int> deq{1,2,3,4,5};
元素访问
元素访问只有aa.top(); 因为栈只允许从顶部弹栈,所以只有这一个读取接口。
size() int v = aa.size(); //查看压栈数量
empty() bool v = aa.empty(); //aa为空时返回true
push() aa.push(5);//将数字5插入到aa栈(顶部);
pop() aa.pop();//弹栈,删除栈顶内容;
queue容器适配器
队列容器适配器,是基于deque序列容器的一次封装,实现一个队列FIFO,队列的特点就是先进先出,如下示图。
访问元素时,用front()访问队首,back()访问队尾。
push只能到back,pop只能到front
初始化
queue<int> aa; //无参构造函数
deque<int> deq{1,2,3,4,5}; //建立一个队列容器,1在前,5在后
元素访问
aa.front(); //读队首元素
aa.back(); //读队尾元素
修改器
push() aa.push(5);//将数字5加入到aa队尾;
pop() aa.pop();//从队头读出元素;
priority_queue是一个优先级队列,当我们每push进一个数据,都会与队列中原来的数据进行比较,默认是小于比较,大的在队首,小的在队尾,我们也可以传入stl的其它比较算法,来改变大小关系。如果传入大于比较,那么队列的数据就是小的在首,大的在尾,无论是大于还是小于比较,都是以队首为目标。
初始化
priority_queue<int> aa; //无参构造函数
vector<int> deq{1,2,3,4,5}; //建立一个队列容器,1在前,5在后
元素访问
aa.top(); //读队首元素,优先级最高元素。不弹栈
修改器
push() aa.push(5);//将数字5加入到aa队尾;
pop() aa.pop();//从队头读出元素;
set容器
set容器是有序关联容器,内部基于红黑树实现,每次插入数据的时候都会比较后再排序。
set<string> aa; //创建一个空的set
aa.insert("hu"); //重复插入不理会。实际上会被创建一个string对象来传入。
遍历
for(auto x:aa)
{
cout<<x<<" ";
}
multiset是set的升级,set的key不能重复,如果出现重复的插入直接不理会,但是有时候我们就是有重名的需求,如一个班级有同名的学生等,multiset就是用来存储可以重名的key的有序容器。multiset和set非常相似,其相关的使用方法一样,简单的示例如下:
multiset<string> aa;
aa.insert("hu");
aa.insert("dai");
aa.insert("zhou");
aa.insert("hu");
aa.insert("dai");
aa.insert("zhou");
map释义“映射”,和set类似,是一个典型的有序关联容器,不同的是map是成对的(key,value)且一对一映射,一个key对应一个value,key不能重名,但value是不做限制的,而set只有key,map和set用法很类似,map中的元素由pair构成。
构造函数
map<string,int> map1; // 默认构造函数
map<string,int> map2(map1.begin(),map1.end()); //范围构造函数
map<int,string> map5{ {1,"hu"}, {2,"dai"}, {3,"zhou"} };
元素访问
at() auto x=map5.at(1); //访问第一个元素value为“hu”
operator[ ] auto x = map5[3]; //访问key为3的value,为“zhou”
迭代器
迭代器和array的相同,不同是内部元素有2个,访问时要指定具体的哪一个
begin() & cbegin() cout<< map5.begin()-> second <<endl;//第一个元素
end() & cend() cout<< map5.end()-> second <<endl; //最后一个元素
修改器
insert(): map5.insert({1,"suai"}); //插入必须{ },key重复不理会。
insert_or_assign map5.insert_or_assign(1,"suai"); //不需要{ },替换value值。
multimap是map的升级,map的key不能重复,如果出现重复的插入直接不理会,但是有时候我们就是有重名的需求,如一个班级有同名的学生等,multimapt就是用来存储可以重名的key的有序关联容器。multimap和map非常相似,其相关的使用方法一样,简单的示例如下:
multimap<int,string> bb{ {1,"hu"}, {1,"dai"}, {1,"zhou"} };
for(auto x:bb)
{
cout<<x.first<<" "<<x.second<<endl;
}
cout<<endl;
智能指针是为了解决内存泄漏问题,与普通指针相比,他能够自动释放malloc或者new的内存的空间。智能指针本质上的实现是函数连带自动释放,利用了函数执行结束时会自动释放栈上局部变量内存,而我们将对象定义在了栈上(在函数内部定义的局部对象),所以函数结束时不例外的会把对象释放,但是不会管对象在堆上申请空间,在销毁对象时会调用对象的析构函数,利用这个特点,我们把delete堆内存放在析构函数中,则会被自动调用,从而实现自动delete堆内存的目的。
auto_prt已过时,在c++17版弃用,因此在新开发程序中不要使用,但是对我们学习智能指针的使用还是有帮助。auto_ptr是一个类模板,在使用的时候需要指定类型。
头文件:<memory>
构造函数
auto_ptr<people> p1(new people("hu")); //创建智能指针并指向新建的对象;
auto_ptr<people> p2=p1; //P1转移到P2,再访问P1会崩溃。
观察器
get() people *p3 = p2.get(); //获得智能指针p2管理的对象的指针。
operator-> cout<<p3->name; //通过→访问管理的对象内的元素
operator* cout<<(*p3).name; //通过*访问管理的对象内的元素,( )优先级。
修改器
reset() p2.reset(new people("dai")); //替换,创建新对象,释放原对象,绑定新对象。
release() people *p4 = p2.release(); //释放管理,释放后,不能再访问p2管理的对象。
弊端
弊端不是错误,是规则设计不合理。虽然auto_prt能解决自动释放内存的问题,但是又引入了新问题,导致使用的时候反而要更小心,所以在17版就被废止。
auto_ptr<people> p2=p1;
所有权转移,P1所有权转移P2后,如果再操作P1,就会出错。
func(p1);
函数调用传值转移所有权,函数传参时,值传递,导致智能指针所有权被转移。
p2.release();
没有接收返回值,对象指针就会永久丢失,泄漏内存。
由于auot_ptr存在弊端被遗弃,所以c++又发明的新的智能指针来替代, unique_ptr就是其中之一,与auto_ptr不同的是在unique_ptr对象被销毁时,会先去调用与其绑定的删除器(一个对象),该删除器由用户给入,并要在内部实现构造函数、拷贝构造函数、移动构造函数和最重要的operator()(people *p)运算符重载,在销毁前会将管理的对象的指针传入到删除器的operator(),在这里要去删除申请的空间,同时调用管理的对象的析构。典型的删除器如下:
struct Dlete
{
Dlete(){cout<<"Dlete()"<<endl;}; //默认构造函数
Dlete(int a):a(a){cout<<"Dlete(int a)"<<endl;}
Dlete(const Dlete&){cout<<"Dlete(const Dlete&)"<<endl;} //拷贝构造函数
Dlete(Dlete&){cout<<"Dlete(Dlete&)"<<endl;} //移动构造函数
Dlete(Dlete&&){cout<<"Dlete(Dlete&&)"<<endl;} //移动构造函数
void operator()(people *p){
cout<<"oprator(people *p)"<<endl;delete p;} //()运算符重载
} del; //定义一个对象
构造函数
unique_ptr<people> a;
unique_ptr<people,Dlete> a(new people,del);//默认构造people对象
修改器
release() people *ptr = aa.release(); //解绑aa管理的对象,并返回对象指针
reset() bb.reset(new people(6)); //替换bb管理的对象
unique_prt与auto_prt对比
auto_prt允许两个对象用赋值语句赋值(a=b), 而unique_prt不允许,包括函数传参和return返回值,因为涉及管理权限转移,容易出错,unique_ptr要直接赋值必须使用a= move(b)。
都支持匿名对象赋值auto_ptr<people> aa = auto_ptr<people>(new int),也包括函数传参和return返回值。
unique释义为独一无二的,强调的就是他只能管理一个对象,当他管理一个新对象时,原来的对象就会被释放。同样也不能多个智能指针同时管理一个对象。会导致在释放的时候出错。
当unique_ptr本身被释放时,他会调用“删除器”,在删除器中去调用被管理的对象的析构,然后在析构自己。
在使用智能指针的时候,尽量不要与普通指针混合使用,因为c++语法并没有规避两个智能指针去管理两个普通指针。如下列:
int *p(new int);
unique_ptr<int> kk(p);
unique_ptr<int> yy(p);
shared_ptr共享指针,意思是同一个对象可以有多个智能指针管理,auto_ptr和unique_ptr是独占式智能指针,语法上只能一个对象一个智能指针,即任何时候对象与智能指针一对一对应。而shared_ptr是基于引用计数式的设计,因此可以多个智能指针绑定1个对象,使得更加接近普通指针,shared的基本使用与前前面两个智能指针类似。同样可以使用构造函数初始化,也可以使用make_shared来创建。
shared_ptr<people> aa(new people);
auto bb = make_shared<people>(10);
shared_ptr是引用计数来记录同一个对象被几个智能指针所管理,因为内部有计数机制,所以最后在析构的时候才能避免重复去析构而出错,为了避免重复释放出错的问题,auto_ptr会对管理权限进行转移,来保证析构不出错,而unique_ptr是干脆就不允许左值赋值语句。
shared_ptr<people> aa(new people);
shared_ptr<people> bb = aa;
use_count() int cnt = bb.use_count();
//获取bb所管理的对象有几个智能指针。unique() if(aa.unique()); //判断aa是否独享对象
reset() aa.reset(new people) //对aa指向的对象更新,bb指针计数值减1;
weak_ptr是一个非独立的智能指针,用来给shared_ptr打辅助的,shared_ptr算是一个近乎完美的智能指针,但是他还是有一个由计数而引起缺陷,而这个缺陷则需要使用weak_ptr来配合使用来避免。
weak_ptr本身也是一个模板类,但是不能直接用来创建智能指针对象,只能用来接收shared_ptr智能指针对象,且不会引shared_ptr智能指针计数。weak_ptr能使用的成员少得可怜,甚至连operator*和operator->都没有,不同的是他比其他只能指针多了lock和expired。
weak_ptr中只有函数lock和expired两个函数比较重要,因为它本身不会增加引用计数,所以它指向的对象可能在它用的时候已经被释放了,所以在用之前需要使用expired函数来检测是否过期,然后使用lock函数来获取其对应的shared_ptr对象,然后进行后续操作。
构造函数
shared_ptr<people> sptr(new people(12));
weak_ptr<people> wptr=sptr;
观察器
expired() if(!wptr.expired());//管理的对象被删除返回true,否则false
lock() cout<< wptr.lock()->value<<endl; //lock用来获取shared_ptr管理的对象,所以lock返回的是一个对象指针。通常用expired先判断数据是否被销毁,在通过lock来间接访问,
STL中迭代器分类有输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器等,这些迭代器是STL中标准迭代器,很多时候我们遍历容器的场景比已提供的不同。所以就有了迭代器适配器,迭代器适配器也是一个模板类,是基于前面提到的基础迭代器而实现的,仍属于迭代器,可以理解为是“升级版”迭代器。
反向迭代器(reverse_iterator),又称“逆向迭代器”,其内部重新定义了递增运算符(++)和递减运算符(--),专门用来实现对容器的逆序遍历。
list<int> aa{1,2,3,4,5}; //定义一个list
reverse_iterator<list<int>::iterator> begin = aa.rbegin(); //读逆序迭代器作为begin
reverse_iterator<list<int>::iterator> end = aa.rend();//读逆序迭代器作为begin
while(begin != end)
{
cout<<*begin<<" ";
++begin;
}
安插型迭代器(inserter或者insert_iterator),用于在容器的任何位置添加新的元素,需要注意的是,此类迭代器不能被运用到元素个数固定的容器(比如 array)上。
流迭代器
流迭代器(istream_iterator / ostream_iterator)流缓冲区迭代器(istreambuf_iterator / ostreambuf_iterator),输入流迭代器用于从文件或者键盘读取数据;相反,输出流迭代器用于将数据输出到文件或者屏幕上。
输入流缓冲区迭代器用于从输入缓冲区中逐个读取数据;输出流缓冲区迭代器用于将数据逐个写入输出流缓冲区。
移动迭代器(move_iterator),此类型迭代器是 C++ 11 标准中新添加的,可以将某个范围的类对象移动到目标范围,而不需要通过拷贝去移动。
function函数包装器
在c++中有多种可调用的对象,如函数、函数指针、lambda表达式、bind创建的对象、函数对象。对这些东西的调用各有差异,而函数包装器就是来实现将这些东西放在一起,并统一接口后对外预留同样的调用方法。即不同类型的可调用对象共享同一种调用方法。他的底层其实是使用了map来实现,map的格式是map<key,value>,我们把key用来做运算符标识,value写成函数,即实现了函数的包装,示例如下
int add(int a,int b){return a+b;}; //定义一个普通函数
auto sub = [](int a,int b){return a-b;}; // 定义一个lambda表达式
struct div //函数对象
{
int operator()(int a,int b)
{
return a / b;
}
};
map<string,function<int(int,int)>> bing
{
{"+",add},
{"-",sub},
{"/",div}
{"*",[](int a,int b){return a*b;}},
};
cout<< "2+3= "<< bing["+"](3,2)<<endl;
cout<< "2-3= "<< bing["-"](3,2)<<endl;
cout<< "2*3= "<< bing["*"](3,2)<<endl;
cout<< "6/3= "<< bing["/"](6,3)<<endl;