- 一、c++基础
- 二、类的初识
- 1、类的定义:
- 2、类的使用:类名+变量名
- 3、作用域:
- 4、类成员的访问:
- 5、函数重载:
- 6、extern c浅析:
- 7、类的封装
- 8、构造函数和析构函数
- 9、构造函数的分类以及调用:
- 10、拷贝函数的调用时机:
- 11、构造函数的调用规则:
- 12、深拷贝与浅拷贝的问题:
- 13、初始化链表
- 14、类做其他类的成员变量
- 15、explicit 关键字:
- 16、动态对象创建:
- 17、静态成员
- 18、类内定义类外实现的方法:
- 三、单例模式
- 四、C++面向对象模型初探
- 五、重载
- 六、继承和派生
- 七、多态
- 八、C++模板
- 九、类型转换
- 十、异常
- 十一、C++输入输出流
- 十二、STL的基本概念和容器
- 1、STL概论
- 2、STL初识;
- vector容器:
- 3、string容器
- 4、Vector容器:
- 5、deque容器:
- 6、stack(栈)容器:
- 7、queue(队列)容器:
- 8、list(链表)容器:
- list是链表结构,它不是一个连续的空间,list对于空间的运用有绝对的精准,插入和删除更效率。List和vector是最常被使用的容器。List是一个双向循环链表。因为链表比vector多了指针域所以对空间的消耗较大,它的空间又不是连续的空间,所以遍历的时候时间消耗较大。
- 结构图:
- 迭代器:
- List常用API:
- 构造函数
- 元素插入和删除
- Push_back(elem);在容器尾部加入一个元素elem
- Pop_back();删除容器中最后一个元素
- Push_front(elem);在容器开头插入一个元素elem
- Pop_front();从容器开头移除一个元素
- Insert(pos,elem);在pos位置插入elem元素,返回新数据的位置
- Insert(pos,n,elem);在pos位置插入n个elem元素,无返回值
- Insert(pos,beg,end);在pos位置插入[beg,end)区间的数据,无返回值
- clear();移除容器的所有数据
- erase(beg,end);删除[beg,end)区间的数据,返回下一个数据的位置
- erase(pos);删除pos位置的数据,返回下一个数据的位置
- remove(elem);删除容器中所有与elem值匹配的数据。如果是自定义类型需要重载自定义类型中的=号。
- 大小操作
- 赋值操作
- 数据存取
- 反转排序
- 9、set\multiset容器:‘
- 10、map/multimap容器:
- 12、要点知识:
- 十三、STL常用的算法
- 1、函数对象:
- 2、谓词:
- 3、内建函数对象
- 4、函数对象适配器
- 5、算法概述:
- 6、遍历算法:
- 7、查找算法:
- 8、排序算法
- 9、拷贝和替换算法
- 10、算数生成算法:
- 11、集合算法:
一、c++基础
1.C++简介
- C是对C的扩展,因此c是c语言的超集,这意味着任何有效的c程序都是有效的c程序。c程序可以使用已有的c程序库。
- C再c语言的基础上增加了面向对象和泛型编程的支持,c继承了c语言的高效、简介、快速和可移植的传统。
2、C++初识:
写简单的"hello world"
需要包含 iostream 头文件 标准输入输出流Using namespace std:
使用标准的命名空间Cout:
标准输出流<<
在c下有了新的寓意,在cout
后用于拼接输出的内容endl;(end line)
刷新缓冲区 并且换行。
头文件:在C中头文件都不带.h。例如c语言中的time.h
在c++中写成ctime,C语言中的math.h
写成cmath
面向对象的三大特性:封装、继承、多态。3、双冒号作用域运算符
::
代表作用域,如果前面什么都不添加代表全局作用域。4、命名空间namespace
命名空间可以解决名称冲突
命名空间下可以放变量、函数、结构体、类…
命名空间必须要声明在全局作用域下
命名空间可以嵌套命名空间
命名空间是开放的,可以随时给命名空间添加新的成员。
命名空间可以是匿名的
命名空间可以起别名
namespace 新名字=原来的名字。
命名空间的使用:
namespace a
{
int a = 10, piy();
struct bbb {};
class ccc {};
}
访问里面的元素a::a;a::piy(),a::struct bbb, a::ccc
5、using声明及using编译指令
Using声明:Using kingGlory::sunwukongid
当using声明与就近原则变量同名会出错,尽量避免。
Using编译指令Using namespace kingGlory
当using编译指令与就近原则同时出现,优先就近原则
当using编译指令有多个时,同名参数需要加作用域区分
6、C++对C的增强
全局变量检测增强int a; int a=10;
这个代码C下可以,C能检测出来不可以通过
函数检测增强
函数的返回值、形参类型、函数调用参数的个数,在C下任何地方少写、写错都能检测出来,C不行
类型转换增强
C下必须等号左右一致类型,如果要转换必须加强制转换
Struct结构体增强
C可以在结构体中放函数
创建结构体变量时C可以省略struct
Bool数据类型扩展
C加入新的数据类型bool类型,代表真true和假false,bool类型占1个字节。所有非0值都会被转化成1代表真,0代表假
三目运算符增强:
C下返回的是变量,c语言下返回的是值
例如int a=10;int b=20;(a>b?a:b)=100;
//c语言下不能这样写,c下这样写代表如果a>b为真那么返回a并让a=100,如果为假就让b=100
7、Const增强:
C语言下:
全局const 直接修改失败,间接修改语法通过,运行失败
局部const 直接修改失败,间接可以修改。
Const修饰的变量不能做常量初始化数组
Const修饰的全局变量是外部连接属性可以在其他文件调用
C语言下:
Const在全局还是局部都不能修改
Const修饰的变量可以称为常量,可以初始化数组
Const修饰的全局变量默认是内部连接属性如果别的文件要使用需要加extern修饰
Const分配内存情况
对const变量取地址,会分配临时内存(不能修改const值是常量)
使用普通变量初始化const变量时会分配内存(可以用指针改变量的值)
例:int a=10 const b=a;
对于自定义类型const修饰后可以使用指针间接修改值
在c中尽量用const来代替#define因为宏定义的常量没有数据类型。
8、引用:
引用的本质:就是起别名
引用的方法:
类型 &别名=原名
例:int a=10;int &b=a
//表示b指向a的地址。如果b=100那么a的值也会变为100.
引用必须要初始化。引用一旦初始化就不可以引向其他的变量
数组的引用:
直接引用:Int arr[10]; int(&b)[10]=arr;
先定义出数组类型,在通过类型定义引用Typedef int(ARRAY_TYPE)[10];
ARRAY_TYPE &b=arr;
9、参数的传递方式:
10、引用的注意事项:
引用必须引一块合法的内存空间
引用做返回值时,不能返回局部变量的引用。
因为局部变量一旦函数执行完就被释放了返回他的引用相当于返回一块不存在内存的引用。
当函数的返回值时引用时,函数的调用可以做左值
例:
#include <iostream>
int& fun() {
static int a = 10;
int& b = a;
return b;
}
int main()
{
fun() = 100; //fun函数返回值是a的引用,这句话的意识是将a这块内存的值变成100
return 0;
}
11、指针引用
引用的本质是指针常量。
利用引用可以简化指针
可以直接用同级指针的引用给同级指针分配空间
12、常量的引用:
const int &ref=10
//加了const之后相当于 int temp=10;const int &ref=temp;
(注意这个值是可以用指针修改的。)
使用场景一般是作为参数防止误操作使用的。
13、内联函数:
内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销,因此应该不使用 宏,使用内联函数。
如何定义内联函数:
在函数前面加上inline关键字就称为内联函数。
注意:函数体和声明结合在一起,否则编译器将它作为普通函数对待。也就是说函数的声明和函数体都要加inline。
在类内部定义的函数自动称为内联函数。
以下情况编译器可能考虑不会将函数进行内联函数编译:
不能存在任何形式的循环语句
不能存在过多的条件判断语句
函数体不能过去庞大
不能对函数进行取地址操作。
内联仅仅是对编译器的一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联函数。
14、函数的默认参数和占位参数:
默认参数:
例
int fun(int a=10,int b=10){
return a+b;
}
fun();//参数设置的默认值为10调用时不传参的话就使用默认值,传参的话就使用传的值
默认参数的注意事项:
如果参数中有一个参数写了默认值那么从这个参数开始往右的参数都要写默认值,调用函数传参时默认从左到右
函数的声明和实现只能由一个提供默认参数不可以同时加默认参数
占位参数:
例:int fun(int a,int) //第二个参数就是占位参数只写类型不写变量名。占位参数也可以给默认值 int=10
注意:调用函数时也要给占位参数传值。
二、类的初识
1、类的定义:
2、类的使用:类名+变量名
3、作用域:
如果想让实例化的类调用类内部的成员需要声明作用域 public 全局。
4、类成员的访问:
5、函数重载:
满足条件:
同一作用域下
函数名称相同
函数参数个数、类型、顺序不同
注意:函数的返回值不可以作为重载的条件
注意事项:
加const和不加const的引用可以作为重载的条件
函数重载碰到默认参数注意避免二义性
例 fun(int a, int b=10)
fun(int a)
fun(20) //这个时候就出先了二义性。
6、extern c浅析:
用途:在C++中调用C语言文件
C++中由函数重载,会对函数名做修饰,导致调用C语言的函数连接失败
利用extern C可以解决问题
方法1:
在c++代码中加入 extern "C"
函数声明
方法2:
在C语言的头文件中加入6行代码
#ifdef __cplusplus
extern "c"{
#endif
//写函数声明
#ifdef __cplusplus
}
#endif
//这样写这6行代码中间写的函数声明在C++中包含以下头文件以后可以直接调用
7、类的封装
C语言的封装:
C++语言的封装:
将属性和行为作为一个整体,来表现生活中的事物。
将属性和行为加以权限控制
Class和struct的区别:
Class默认权限是private;
Struct默认权限是public
访问权限:
Public公共权限:类内类外都可以访问
Private私有权限:类内可以访问类外不可以访问,子类不能访问父类中的私有权限成员。
Protected保护权限:类内可以访问类外不可以访问,子类可以访问父类中的保护权限成员。
尽量将类中成员变量设置成私有
设置成私有可以控制成员变量的读写
可以对设置的内容加有效的验证。
8、构造函数和析构函数
在对象创建时调用构造函数,在对象被释放时调用析构函数。
构造函数:不能有返回值也不用写void,函数名与类名相同,可以写参数,可以发生重载。对象被创建时调用一次。
析构函数:不能有返回值也不用写void,函数名与类名相同,但是前面要加 1旁边的波浪号 ~,不可以写参数。对象被销毁时调用一次。
9、构造函数的分类以及调用:
构造函数分类:
按照参数分类:
无参构造函数(默认构造函数)和有参构造函数
按照类型分类:
普通构造函数:
拷贝构造函数:
参数传一个代拷贝对象的引用,使用const修饰
例:
person(const person &p){
age=p.age
}
person p1(10) //调用有参构造初始化对象年龄为10,person p2(p1)//调用拷贝构造初始化对象,年龄为拷贝p1对象的年龄。
构造函数的调用:
括号法:
person p1(10)
//使用有参构造初始化
person p1
//使用无参构造初始化
注意:使用无参构造时不要写成 person p1()
显示法:
person p1=person(10)
//使用有参构造初始化
person(10)
//匿名对象,执行完当前行自动释放
注意:不要使用拷贝函数初始化匿名对象。
隐式法:
person p1=10
注意:最好不要使用这种方法初始化对象。
10、拷贝函数的调用时机:
用已经创建好的对象来初始化新的对象会调用拷贝函数
值传递的方式给函数参数传值时会调用拷贝函数
以值方式返回局部对象时会调用拷贝函数
11、构造函数的调用规则:
编译器会自动给一个类最少添加3个函数
默认构造函数(空实现)
析构函数(空实现)
拷贝函数(值拷贝)
调用默认的拷贝函数会对类中所有变量的值进行拷贝
如果我们自己提供了有参构造函数编译器就不会再提供默认构造函数但是还会提供 拷贝和析构函数。
如果我们自己提供了拷贝函数编译器就不会再提供默认的构造函数和拷贝函数
12、深拷贝与浅拷贝的问题:
浅拷贝就是使用编译器提供的默认拷贝函数进行值拷贝,深拷贝就是使用自己定义的拷贝构造。
浅拷贝的问题:如果类中某一个变量是创建再堆区的,那么进行浅拷贝时就只是把这个变量再堆区的地址拷贝走了,相当于两个对象操纵的时同一个地址的变量数据,当利用析构函数释放堆区空间时就会出现重复释放的问题。
浅拷贝问题的解决:自己定义一个拷贝构造,先再堆区创建空间然后再把值拷贝到创建的堆空间中,这样两个对象操纵的就是两块堆区空间。
13、初始化链表
写法:构造函数(参数):属性(参数)
再写有参构造函数时,假设类中有三个int类型的变量a,b,c。
person():a(10), b(20), c(30) //这样写相当于给这3个变量设置了初始值,创建对象时直接使用无参构造的方式创建这3个变量就有了给定的初始值例如:person p; //这样创建以后p中3个变量的值分别为a=10,b=20,c=30。
person(int x,int y, int z):a(x),b(y),c(z) //这样写相当于把参数1的值赋给a, 把参数2的值赋给b, 把参数3的值赋给c.创建时要使用有参构造调用这个构造函数可以写成:person p(10,20,30) //这样创建以后p中3个变量的值分别为a=10,b=20,c=30。
14、类做其他类的成员变量
当其他类的对象做本类的成员变量时,初本类对象时会先初始化其他类的对象然后再初始化本类对象
例如a类中的成员变量是b类的对象,那么初始化a类对象之前会先初始化b类对象。
15、explicit 关键字:
用途:防止利用隐式转换方式构造对象
16、动态对象创建:
再C语言中再堆区申请空间时的问题:
程序员必须确定对象的长度。
malloc返回一个void 指针,c++不允许将void 赋值给其他任何指针,必须强转
malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。
用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是有编译器调用),用户有可能忘记调用初始化函数
C的动态内存分配函数太复杂,c++中推荐使用运算符new和delete
new和delete
new:在堆区开辟内存
new 数据类型 ,返回值是该数据类型的指针
例:有一个person类,person *p=new person
delete:释放开辟在堆区的内存
delete new返回的值
malloc/free和new/delete的区别:
malloc/free属于库函数,new/delete属于运算符
malloc不会调用构造函数,new会调用构造函数
malloc返回void *指针,在c++下要进行强制类型转换,new返回创建对象的指针。
注意事项:
不要用void 接受new出来的对象。利用void无法释放(无法调用析构函数)
利用new在堆区创建数组
new 类型 [i] //i代表个数
int *p=new int[10]
double *p=new double[10]
person *p=new person[10]
注意:
在堆区开辟对象数组,一定会调用默认构造函数,如果没有定义默认构造函数会报错。(默认构造函数=空参构造)
数组释放时一定要在指针变量和delete中间加一个[ ]不然程序会崩溃
在栈上开辟对象数组时可以全部创建成有参构造而不使用默认构造
17、静态成员
在类的定义中,它的成员(包括成员变量和成员函数),这些成员可以被static关键字声明为静态的,称为静态成员。不管一个类创建多少对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享。
静态成员变量:
编译阶段就分配内存,要在类内声明,类外初始化,静态成员变量所有对象都共享同一份数据,任何一个对象更改值,其他对象的值也会变。
例:
class person{
static int a;
}
int person::a=0
person p1; //p1里面的a现在是0
person p2;
p2.a=100; //现在p2和p1的a的值都是100.
//静态成员不但可以使用对象进行访问还可以用类名进行访问。
person.a;
静态成员函数:
被static修饰的成员函数叫做静态成员函数,静态成员函数被所有对象共享。可以使用对象访问,也可以使用类名访问
特点:静态成员函数只能访问静态成员变量,不能访问非静态成员变量。
18、类内定义类外实现的方法:
已知类:class person{}
构造函数:
在类中定义空参构造person(){};
在类外实现该构造:person::person(){实现体内容};
成员函数:
在类中定义成员函数:void fun(){};
在类外实现成员函数:void person::fun(){实现体内容}
成员属性:
在类中定义成员属性:int a;
在类外初始化成员属性:int person::a=10;
三、单例模式
1、单例模式的概念:
单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类,通过单例模式可以保证系统中一个类只有一个实例。简单来说单例模式能实现一个类只能被创建一个对象。
2、单例模式的实现流程:
执行流程:
创建一个类,只提供一个默认构造函数,并且是私有化,同时私有化拷贝构造函数。然后创建一个静态的成员变量,此成员变量是该类的一个指针,通过类内声明,类外初始化的方式创建和初始化该变量让该变量指向该类的唯一实例,并且将该变量也私有化,然后提供一个可以获得该成员变量的静态函数,只提供一个获取该成员变量的函数,不提供可以更改该成员变量的函数,这样设计好以后就是一个单例模式。
代码实现:
class person
{
private:
person(){}; //默认构造
person(const person&){}//拷贝构造
static person * per; //静态成员变量
public:
static person * getInstacne()
{
return per;
}
}
static person * person::per =new person; //类外初始化
//为什么在类外可以访问到私有的默认构造函数呢?
//因为加上person::作用域就相当于是类内成员,就可以访//问类内的默认构造了。
person * p = person::getInstacne();
//这样就可以获得一个指向该类唯一实例化的指针了,即使使用同样的方法再创建p2、p3、。。。。都指向的是同一个实体。
四、C++面向对象模型初探
1、成员变量和函数的储存
类中的成员变量和成员函数是分开存储的
只有非静态的成员变量会存储在类对象上
空类的对象sizeof结果为1字节。
2、this指针
this指针指向被调用的成员函数所属的对象。(谁调用指向谁)
this指针可以解决名称冲突
this指针隐式加在每个成员函数中
*this 就是调用对象的本身。
3、空指针访问成员函数
如果成员函数没有用到this指针,可以用空指针调用成员函数
如果成员函数中用到了this,那么这个this需要加判断,防止代码崩溃。
4、常函数和常对象
常函数:
成员函数声明后面加const就是常函数
例: void fun()const {};
const目的是为了修饰成员函数中的this指针,让指针指向的值不可以修改
如果某些属性想要在常函数或者常对象中可以修改,需要加入关键字mutable。
例:mutable int a;
//那么这个变量在常函数和常对象中就依然可以被修改
常对象
const person p;//在创建对象是前面加上const修饰就是常对象。
常对象也不能修改成员属性,但是可以修改mutable修饰的成员属性
常对象只能调用常函数,不能调用普通函数。
5、友元:
概念:
友元函数是一种特权函数,c++允许这个特权函数访问私有成员。
生活中你的家里有客厅和卧室,那么你的客厅是public的,所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是你也可以允许你的家人可以进去。程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元的。
语法:
全局函数做友元函数:
代码实现:
class building{
friend void gooGay(building *bud); //在类中声明外部函数为友元函数
public:
string keting; //创建公共权限客厅
private:
string woshi;//创建私有权限卧室
}
void gooGay(building *bud)
{
bud->woshi; //可以访问到私有权限的卧室
}
通过在类中写关键字friend后跟函数声明就可以将此函数设置为友元函数,设置成友元函数以后改函数就可以访问该类中私有权限的属性。
类作为友元类
如果想让A类做B类的友元类只需要在B类中写入一行代码即可:friend class A;
注意:在B类中中声明A类是友元类,但是这时在类中B类并不是友元。
成员函数做友元函数
如果想让A类中void fun()
成员函数做B类的友元函数只需要在B类中写入一行代码即可:friend void A::fun();
注意:成员函数做友元函数时访问私有权限属性有时候编译器会报错但是不会影响程序执行。
五、重载
1、运算符重载
对于内置的数据类型,编译器知道如何计算。但是对于自定义的数据类型编译器不知道怎么计算。利用运算符重载可以让符号有新的含义. operator关键字
例如:一个类中有一个成员变量想要实现该类的两个实现相加可以创建出第三个类,第三个类该成员变量的值为前两个实现的和代码如下:
#include <iostream>
class person
{
public:
person(int x) :a(x) {}; //定义有参构造
int a;
person operator+(person& p) //定义+号重载函数
{
person temp(0);
temp.a = this->a + p.a;
return temp;
}
};
int main()
{
person p1(10);
person p2(10);
person p3 = p1 + p2; //这样结果p3中a的值是20.
printf("p1.a = %d\np2.a = %d\np3.a = %d\n", p1.a, p2.a, p3.a);
}
注意:operator+这个是固定的函数名不能更改,operator后面跟什么符号就是重载什么符号,operator+就是重载+号。
重载可以定义为成员函数和全局函数
成员函数的调用本质:p1.operator+(p2)
全局函数的调用本质:operator(p1,p2)
都可以简化为 p1+p2
运算符重载也可以发生函数重载。
2、运算符重载的基本概念:
运算符重载(operator overloading)只是一种语法上的方便,也就是它只是另一种函数调用的方式。
3、左移运算符的重载
对于自定义类型,不可以直接用cout<<输出,可以通过重载左移运算符来实现。如果利用成员函数重载,只能实现p<<cout无法实现cout<<p所以使用全局函数来重载左移运算符
代码如下:
ostream & operator<<(ostream &cout,person &p1)
{
cout<<p1.a<<p1.b; //设置打印什么属性。
return cout
}
cout>>p1 //这样就可以实现自定义类型的输出了。
4、递增运算符的重载:
前置和后置的代码实现:
class person
{
person(){a=0;};
int a;
person& operator++() //前置++的实现
{
this.a++;
return *this;
}
person operator++(int)//后置++的实现
{
person temp=*this;
this.a++
return temp;
}
}
person p1;
person p2;
p1++;
++p2; //这样就可以让自定义的类型进行++操作。
从代码可以看出,在我门日常程序中应该优先使用前置++,因为后置比前置多创建了一个对象 。代码效率会更高一些。
5、指针运算符重载
通常用于创建智能指针
智能指针的用途:托管new出来的对象的释放。
设计一个智能指针的类,内部维护person 的指针,在析构时候释放堆区new出来的person对象,并重载->和来通过指针和解引用的方式来访问person的成员。
代码如下:
#include<iostream>
class person
{
public:
person(int age) :m_age(age) {};
void Show()
{
std::cout << "m_age is:" << this->m_age << std::endl;
}
~person()
{
std::cout << "person 析构函数调用" << std::endl;
}
private:
int m_age;
};
class smartPoint
{
public:
smartPoint(person* p)//传进来一个person指针
{
this->m_person = p;
}
person* operator->() //重载->
{
return this->m_person;
}
person operator*()//重载*解引用
{
return *m_person;
}
~smartPoint() //利用析构释放堆空间的对象
{
if (this->m_person)
{
delete this->m_person;
this->m_person = NULL;
}
}
private:
person* m_person;
};
int main()
{
smartPoint sp(new person(18));
sp->Show();// sp->->showAge(); 编译器优化了 写法
(*sp).Show();
}
6、赋值运算符(=)的重载:
一个类在创建以后编译器默认会给类添加4个函数
默认构造
析构函数
拷贝函数
operator=()函数
但是默认提供的=号重载只是做浅拷贝(值拷贝)
注意:如果对象是创建在栈的可以无视,但是如果是创建在堆区的要用的operator=()函数和拷贝函数就要重写拷贝函数,并且重载赋值运算符,不然释放堆区空间的时候程序会崩溃。
7、关系运算符的重载:
对于自定义数据类型,编译器不知道如何进行比较,可以使用operator后根==、<、>、<=、>= 对关系运算符进行重载。
例如已知一个类中有两个属性一个name,一个age 如何判断该类中的两个对象p1、p2是否相等.可以在类中定义一个方法代码如下
#include<iostream>
class person
{
public:
person(int sex,int age) :sex(sex),age(age)
{
std::cout << "sex = " << (sex ? "男" : "女") << std::endl;
std::cout << "age = " << age << std::endl;
};
~person()
{
std::cout << "person 析构函数调用" << std::endl;
}
bool operator==(person& p)
{
if (this->sex == p.sex && this->age == p.age)
{
return true;
}
else
{
return false;
}
}
private:
int sex; // 男1 女0
int age;
};
int main()
{
person p1(1, 20);
person p2(1, 18);
std::cout << (p1 == p2 ? "信息相同" : "信息不相同") << std::endl;
}
定义完这个函数以后再进行p1==p2 就会返回一个布尔值真或者假,如果想只对名字比较可以把年龄的判断去掉。
8、函数调用符的重载:
重载();使用时候很像函数调用,因此称为仿函数。仿函数的写法不固定很灵活。
例如在一个person类中写入:
#include <iostream>
class person
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
person p;
std::cout << p(10, 20) << std::endl;
std::cout << person()(10, 10) << std::endl; // 匿名(函数)对象
}
9、不要重载 &&、||
10、常规建议:
除了<<和>>建议通过全局函数配合友元函数进行重载。
六、继承和派生
1、继承的优点:减少重复的代码,提高代码复用性
2、继承的语法:子类 :继承方式 父类
例:class zi :public 父类名 //这就是继承的写法
子类:派生类;父类:基类。
3、继承方式:
Public:公有继承;
Private:私有继承
Protected:保护继承
此图标可以看出不管什么方式继承都不可以访问父类私有属性,可以访问public和protected权限下的属性。使用public方式继承,属性权限和父类一样;使用protected方式继承会把public权限的属性也变成protected权限,使用private方式继承会把public和protected权限的属性都变成private权限。
4、如何查看对象的模型:
vs打开Developer Command Prompt for VS 2019 跳转到程序所在文件夹输入命令:
cl d1 reportSingleClassLayout 类名 所属文件名
5、继承中的对象模型:
父类中的所有属性,子类都会继承过来,包括私有属性,私有属性只不过由编译器隐藏了,访问不到。
6、继承中的构造和析构:
执行顺序:
如果子类中由其他类作为成员属性执行顺序为:
先调用父类构造,再调用其他类构造,最后调用子类构造。
如果父类定义了有参构造那么子类如果直接定义无参构造这个时候会报错,因为默认会先取调用父类的无参构造,因为父类定义了有参构造就没有无参构造了解决代码如下:
#include <iostream>
class A
{
public:
A(int a)
{
this->m_a = a;
}
int m_a;
};
class B:public A
{
public:
B(int a) :A(a) {}; //利用初始化列表语法,调用父类中的其他构造函数
};
int main()
{
B b(100);
std::cout << b.m_a << std::endl;
}
上面子类的构造也可以写成 B(int a=100):A(a)
//给参数加上默认值。
#include <iostream>
class A
{
public:
A(int a)
{
this->m_a = a;
}
int m_a;
};
class B:public A
{
public:
B(int a=100) :A(a) {}; //利用初始化列表语法,调用父类中的其他构造函数
};
int main()
{
B b;
std::cout << b.m_a << std::endl;
}
B b; // m_a的值是100
父类中的构造、析构、拷贝构造、=重载函数是不会被子类继承的
7、继承中同名成员的处理:
属性同名:
子类对象调用同名属性时会调用子类自身的属性,如果想调用父类的同名属性需要加作用域
例如父类(A)和子类(B)都由一个 int m_ma 成员属性
#include <iostream>
class A
{
public:
A(int a)
{
this->m_a = a;
}
int m_a;
};
class B:public A
{
public:
B(int a) :A(a) {}; //利用初始化列表语法,调用父类中的其他构造函数
private:
int m_ma;
};
int main()
{
B b(100);
b.m_a = b.m_a - 20;
b.A::m_a = b.A::m_a - 30;
std::cout << b.m_a << std::endl;
std::cout << b.A::m_a << std::endl;
}
函数同名:
函数同名和属性同名调用规则是一样的,访问父类函数要加作用域。
注意:子类中如果有函数和父类中的函数同名,那么父类中该函数的所有重载都要通过作用域来访问。
8、继承中的同名静态成员
同名静态成员的调用和普通成员一样,只有使用类名调用时有点特殊,代码如下:
Bs::As::max //解释:Bs:: 表示调用这个类里面的静态成员,As::表示要调用的时父类作用域中的成员。
9、多继承
我们可以从一个类继承,也可以同时从多个类继承。相当于C类同时继承A类和B类。语法如下:
class C : public A ,public B
注意:不建议进行多继承的操作,因为同名成员过多的话容易出错。
10、菱形继承和虚继承:
菱形继承:
例如:有一个动物类,分别由一个羊类和驼类继承了动物类,而又有一个羊驼类继承了羊类和驼类。
两个类有公共的父类和公共的子类,就发生了菱形继承。
菱形继承会导致公共的子类中有两份数据,造成歧义和资源浪费。
解决方案:利用虚继承可以解决菱形继承的问题。
虚继承:
使用关键字:virtual
解决菱形继承:两个类继承公共父类时加入virtual
例如:动物类Animal,羊类sheep,驼类tuo,羊驼类sheeptuo.
class sheep :virtual public Animal
class tuo :virtual public Animal
上面就是两个虚继承。
当发生虚继承后,sheep和tuo类中继承了一个vbptr指针,虚基类指针,指向的是一个虚拟类表 vbtable。
虚拟类表中记录了偏移量,通过偏移量可以找到唯一的一个父类属性。
总结:
通过使sheep和tuo类虚继承动物类,然后让羊驼继承这两个类时可以解决菱形继承的问题,因为羊和驼中并没有数据只是有一个可以找到动物类属性的指针。
七、多态
1、静态多态和动态多态
静态多态:
动态多态:
先有继承关系,父类中有虚函数,子类重写父类中的虚函数,父类的指针或引用指向子类的对象。
#include <iostream>
class Animal
{
public:
virtual void speak() //虚函数
{
std::cout << "动物在说话" << std::endl;
}
};
class cat : public Animal //子类
{
void speak() //重写父类的虚函数可以不用加virtual关键字。
{
std::cout << "小猫在说话" << std::endl;
}
};
void speak(Animal& ani) //父类引用
{
ani.speak();
}
int main()
{
cat c; //创建子类对象
speak(c); //调用函数传入子类对象,输出结果为“小猫在说话”。
}
静态多态地址早就绑定好,静态联编。
动态多态在运行阶段绑定地址,地址晚绑定,动态联编。
2、动态多态的原理:
当父类写了虚函数后,类内部结构发生了改变,多了一个vfptr。
事实上,如果把Animal类的virtual关键字去掉,然后输出sizeof(Animal)的值,为1。实际上这不难理解,因为类的成员函数是不属于类的对象上的,一个空对象所占的内存就是1字节;当加上virtual关键字时,再次输出sizeof(Animal)的值,结果为8(本人电脑为64位操作系统),这实际上是Animal类中多了一个指针——vfptr(即virtual function pointer,虚函数指针)。
Vfptr:虚拟数表指针,该指针指向vftable虚拟数表。
虚拟数表内部记录着虚拟函数的入口地址。
当父类指针或引用指向子类对象时发生多态,调用的时候从虚拟表中找到函数入口,因为父类的指针指向的时子类,所以找到的也是子类的函数入口。
虚拟函数关键字:virtual
原理如图:
3、多态案例-计算器案例
设计抽象计算器类,分别实现加减乘计算,继承于抽象计算器类,重写虚构函数
利用多态可以调用不通过计算器
多态的好处:
代码可读性高
组织结构清晰
扩展性强
开闭原则:对扩展进行开放,对修改进行关闭
4、纯虚函数和抽象类
语法:virtual 函数返回值 函数名(形参列表)=0;
例:virtual int fun()=0;
如果一个类中包含了纯虚函数,那么这个类就无法实例化对象,这种类我门通常叫做抽象类
抽象类的子类必须重写父类中的纯虚函数,否则也是一个抽象类。
5、虚析构和纯虚析构
虚析构
如果使用多态,子类有创建在堆区的数据,调用父类指针的释放堆区命令。只会走父类的析构而不会走子类的析构,这时候把父类的析构定义为虚析构就可以走子类的析构了,定义虚析构和虚函数写法一样在析构函数前面加virtual关键字就可以了。
纯虚析构
纯虚析构语法和纯虚函数一样virtual ~类名()=0;
纯虚析构要在类内声明,类外实现。
类内声明:virtual ~Animal()=0;
类外实现:Animal :: ~Animal(){};
如果一个类中有纯虚析构那么这个类也是一个抽象类
6、向下类型转换和向上类型转换
父类转子类-向下类型转换(不安全)
子类转父类-向上类型转换(安全)
注意:如果发生多态转换永远是安全的。
7、重写、重载、重定义
重载:
同一个作用域下,函数名相同,参数个数,参数顺序,参数类型不同,和函数返回值没有关系,const也可以作为重载的条件
重定义:
重写
有继承,子类重写父类的virtual函数,函数返回值,函数名,函数参数必须和父类中的虚函数一致。
八、C++模板
1、函数模板
泛型编程->模板技术 特点:类型参数化
定义模板:
template告诉编译器后面紧跟着的函数或者类中出现T,不要报错。T是一个通用的数据类型,也可以使用template,其中class和typename效果一样。
使用:
自动类型推导:必须要推导出一致的T才可以使用。
显示指定类型:fun(a,b)
2、函数模板和普通函数的区别和调用规则
区别:
如果使用自动类型推导,是不可以发生隐式类型转换的。
普通函数:可以发生隐式类型转换
调用规则:
如果函数模板和普通函数都可以调用,那么优先使用普通函数
如果想要强制使用函数模板,可以使用空模板参数列表
例:myprint<>(a,b) //在函数名后加上<>
函数模板也可以发生重载
如果函数模板能产生更好的匹配,那么优先使用函数模板。
例:普通函数参数式int类型的,而传入的是char类型的参数这个时候就会优先调用函数模板。
3、模板的实现机制
编译器并不是把函数模板处理成能够处理任何类型的函数
函数模板通过具体类型产生不同的函数,通过函数模板产生的函数被称为模板函数。
编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
4、模板局限性:
模板并不是真实的通用,对于自定义类型,可以使用具体化技术,实现对自定义数据类型特殊的使用
语法:template<>函数返回值 函数名(形参列表)
例:template<>void fun()
注意这样写了以后就不用在函数上面写template了,参数传入的也不再是T类型而是实际的自定义类型,例如自己定义的person类,就传person类型 。
5、类模板和函数模板的区别
类模板不可以使用自动类型推导,只能使用显示指定类型,类模板中可以有默认参数
例:template
#include <iostream>
template<class NAME, class AGE, class ...>
class person
{
public:
person(NAME name, AGE age)
{
this->m_name = name;
this->m_age = age;
}
NAME m_name;
AGE m_age;
};
int main()
{
person<std::string, int>p1("张三", 18);
}
代码解释:第一行templateperson<string,int>p1("张三",18);
这句代码中的
类模板中可以有默认参数:template<class NAME,class AGE=int>
这样写后面创建实例化对象的时候就可以写成person p1("张三",18);
6、类模板中的成员函数创建时机
类模板中的成员函数并不是一开始创建的,而是在运行阶段确定出通用类型是什么类型以后才去创建的
7、类模板做函数参数
方式一:
方式二:
方式三:
如何参看数据类型:
#include <iostream>
template<class NAME, class AGE, class ...>
class person
{
public:
person(NAME name, AGE age)
{
this->m_name = name;
this->m_age = age;
}
NAME m_name;
AGE m_age;
void show()
{
std::cout << typeid(NAME).name() << std::endl;
std::cout << typeid(AGE).name() << std::endl;
}
};
int main()
{
person<std::string, int>p1("张三", 18);
p1.show();
}
8、类模板遇到继承:
写法一:
但是这样的写法已经确定了数据类型就没有体现出模板化的特点,既然使用模板就是想让该类能实现多类型的调用。例如父类中有个通用类型m_A,子类继承以后又定义一个通用类型m_B该如何解决呢?
写法二:
通过这样的方法就可以实现。写法一也可以不写int也改成这样的方式。
9、类模板中成员函数类外实现:
例:
写法和普通类成员函数的区别:
类外实现上面写上和类上面一样的模板语句。
#include <iostream>
template<class NAME, class AGE>
class person
{
public:
person(NAME name, AGE age);
NAME m_name;
AGE m_age;
void show();
};
template<class NAME, class AGE>
person<NAME, AGE>::person(NAME name, AGE age)
{
this->m_name = name;
this->m_age = age;
}
template<class NAME,class AGE>
void person<NAME, AGE>::show()
{
std::cout << typeid(NAME).name() << std::endl;
std::cout << typeid(AGE).name() << std::endl;
}
int main()
{
person<std::string, int>p1("张三", 18);
p1.show();
}
10、类模板中的成员函数分文件编写:
在分文件编写时,主程序所在文件要包含的不再是头文件而是包含具体实现的.cpp文件。
例:头文件
实现文件
主程序文件:
注意主程序不再是包含.h的头文件,而是包含.cpp的实现文件。
原因:因为类模板的成员函数是再指定完类型以后才会生成的,如果包含头文件,那么头文件中类的函数根本就没有创建也就无法调用。
这种方法虽然能解决问题但是不建议对类模板的成员函数的实现和声明进行分文件编写,把声明和实现都写在头文件中。(即使要类内声明,类外实现,也把声明 和实现都写在头文件中)头文件的名字起成XX.hpp,用于区分。
11、类模板碰到友元函数:
友元函数类内实现:
#include <iostream>
#include <winnt.rh>
template<class NAME, class AGE>
class person
{
public:
person(NAME name, AGE age);
friend void show(person<NAME,AGE> &p)
{
std::cout << p.m_name << std::endl;
std::cout << p.m_age << std::endl;
}
private:
NAME m_name;
AGE m_age;
};
template<class NAME, class AGE>
person<NAME, AGE>::person(NAME name, AGE age)
{
this->m_name = name;
this->m_age = age;
}
int main()
{
person<std::string, int>p1("张三", 18);
show(p1);
}
这样可以直接调用创建一个person<std::string,int> p1("张三",18)
对象然后调用show(p1)
就可以。
友元函数类外实现需要三步:
第一步:先在类内声明友元函数:
注意:函数名后要必须加上<>表示这是一个模板函数。
第二步:类外实现:
要写成函数模板
第三步:要在类创建之前做函数模板的声明,并在函数模板声明之前加入类模板声明。
也可以把第二步和第三步结合起来在写函数声明的时候直接写入函数实现就可以了,也就是说把函数实现写在类模板之前,但是还是需要在这个之前加入类模板声明。
#include <iostream>
#include <winnt.rh>
template<class NAME, class AGE>
class person;
template<class NAME, class AGE>
void show(person<NAME, AGE>& p);
template<class NAME, class AGE>
class person
{
public:
person(NAME name, AGE age);
friend void show<>(person<NAME, AGE>& p);
private:
NAME m_name;
AGE m_age;
};
template<class NAME, class AGE>
person<NAME, AGE>::person(NAME name, AGE age)
{
this->m_name = name;
this->m_age = age;
}
template<class NAME, class AGE>
void show(person<NAME, AGE>& p)
{
std::cout << p.m_name << std::endl;
std::cout << p.m_age << std::endl;
}
int main()
{
person<std::string, int>p1("张三", 18);
show(p1);
}
九、类型转换
1、静态类型转换:static_cast
允许内置数据类型转换
允许父子之间的指针或引用的转换
语法:static_cast<目标类型>(原变量/原对象)
例:char a=’a’;double b = static_cast<double>(a);
//内置数据类型转换
例:父类Base子类SonSon son=static_cast<Son>(base)
;//父转子Base base=static_cast<Base >(son)
//子转父
2、动态转换:dynamic_cast
dynamic_cast主要用于类层次间的上行转换和下行转换
在进行上行转换时,和static_cast的效果一样。
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
特点:
不允许内置数据类型转换
如果类型转换不安全会不允许转换。
3、常量转换:const_cast
该运算符用来修改类型的const属性。
常量指针被转化成非常量指针,并且仍然指向原来的对象
常量引用被转化成非常量引用,并且仍然指向原来的对象
注意:不能直接对非指针和非引用的变量使用const_cast操作符去直接移除它的const
语法:
1)const转非const
2)非const转const
3)引用的写法和指针类似
4、重新解释转换:reinterpret_cast
这是最不安全的一种转换机制,最有可能出问题。
主要用于将数据类型,从一种类型转换为另一种类型。它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针。(不建议使用 )
十、异常
1、基本概念
异常处理就是处理程序中的错误,所谓错误是指在程序运行的过程中发生的一些异常时间。
语法:throw关键字可以抛出一个异常,try语句包裹可能出现异常的语句,catch写在try语句后用于捕获异常,可以在catch语句中写入异常处理的代码,也可以将捕获的异常使用throw关键字继续向上抛出。如果最终程序匹配的处理未找到,则会运行函数terminate将自动被调用,其功能是让程序终止。
例:B函数调用了C函数,可以在C函数中写入判断如果出现异常就使用throw抛出该异常,在B函数调用C函数时把调用语句写在try语句中,如果检测到C函数抛出了异常,那么B函数中的try语句就会检测掉异常,在try语句后写上catch语句并写入适当的类型就可以将这个异常捕获。例如throw抛出的int类型,那么catch的参数也必须是int类型才能捕获到。也可以在catch参数中写入…三个点代表匹配所有类型。也可以写多个catch,参数类型不一样来捕获不同类型的异常。如果不向在B函数中处理异常可以在catch中继续将该异常抛出,这时如果A函数又调用了B函数,那么在调用时可以捕获该异常并处理,如果A也时继续抛出该异常并且没有程序再调用A进行捕获那么程序就会被终止。
2、栈的解旋:
概念:从try代码开始,到throw抛出异常之前,所有栈上的数据都会被释放掉,释放的顺序和创建的顺序相反(因为栈的特性先进后出),这个过程我们叫做栈的解旋。
例:
异常抛出后,在throw之前创建的p1、p2两个对象,会被释放掉。
3、异常接口声明
在函数中如果想要限定抛出异常的类型,就可以使用异常接口声明。
语法:函数()throw(指定的类型)
例:void fun()throw(int)
//表示指允许抛出int类型异常
如果写成throw(),则表示不允许抛出任何类型的异常。
注意:该特性只有在Qt和Linux下才可以实现,VS不支持。
4、异常变量的声明周期:
抛出的是throw MyException();(匿名对象) catch(MyException e) //这样会调用拷贝构造效率低。
抛出的是throw MyException();(匿名对象) catch(MyException & e) //只会调用默认构造,效率高(推荐)
抛出的是throw &MyException();(匿名对象) catch(MyException * e) //对象会提前释放,就不能再使用了
抛出的是throw new MyException();(匿名对象) catch(MyException * e) //只会调用默认构造,但是要手动释放。
总结:推荐使用抛出对象,catch接受时使用引用方式。
5、异常的多态使用
提供基类异常类class BaseException
该类中由一个纯虚函数 virtual void printError()=0;
创建两个子类空指针异常和越界异常,继承BaseException,并重写printError函数。
测试类利用父类的引用指向子类对象实现异常的多态。
6、C++标准异常库
要使用标准异常库需要添加头文件:
使用标准异常库时catch捕获的时候参数可以利用多态直接写入父类的引用catch(exception &e)
7、创建自己的异常类
自己编写的异常类要继承Exception类
必须要重写 virtual const char what()const这个函数。
如何将String类型转换成char 类型
String类型数据.c_str();
例如:string a; a.c_str();
const char*可以隐式转换成string,反之不可以。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std;
class MyException :public exception
{
public:
MyException(string ErrorInfo) {
this->myErrorInfo = ErrorInfo;
}
virtual ~MyException(){} // 虚析构函数
virtual const char* what() const {
return this->myErrorInfo.c_str();
}
string myErrorInfo;
};
class Person
{
public:
Person(string name, int age)
{
this->MyName = name;
if (age >= 150 ? 0 : 1)
this->MyAge = age;
else
throw MyException(string("年龄越界异常!"));
}
string GetMyNameInfo()
{
return MyName;
}
int GetMyAgeInfo()
{
return MyAge;
}
private:
string MyName;
int MyAge;
};
int main()
{
try
{
Person p = Person("张三", 160);
int MyAge = p.GetMyAgeInfo();
cout << p.GetMyNameInfo() << endl;
cout << MyAge<< endl;
}
catch (MyException& e)
{
cout << e.what() << endl;
cout << "异常处理结束!" << endl;
}
}
十一、C++输入输出流
1、输入输出的概念:
输入指的是从输入文件将数据传送给程序;输出指的是从 程序将数据传送给输出文件。
C++输入输出包含以下三个方面
1)、从键盘输入数据,输出到显示器屏幕,简称标准I/O
2)、从磁盘文件输入数据,数据输入到磁盘文件,简称文件I/O
3)、对内存中指定空间进行输入和输出,通常指定一个字符数组作为存储空间,这种输入和输出简称串I/O
输入输出流继承关系:
2、标准输入流:cin中的函数
**cin.get()**
:获取一个字符**cin.get(两个参数)**
:获取字符串 例:cin.get(buf,1024)
利用**cin.get**
获取字符串时,换行符遗留在缓冲区。**cin.getline()**
:获取字符串 例:cin.getline(buf,1024)
利用 **cin.getline**
获取字符串时,换行符会被直接丢掉**cin.ignore()**
;忽略,不传参数忽略一个字符,传入参数X,表示忽略X个字符。**cin.peek()**
:偷窥,只是获取一个字符,并不会拿走。**cin.putback()**
;放回,如果前面由cin.get()语句,会把那个语句拿走的字符还放回原位。
标志位:**cin.fail()**
// 0代表正常 1代表异常**cin.clear(); cin.sync()**
;清空缓冲区,重置标志位
3、标准输出流:cout
**cout.put() **
//向缓冲区写字符**cout.write(buf,num) **
//从buffer中写num个字节到当前输出流
通过流成员函数格式化输出**cout.width(20):**
指定宽度位20**cout.fill('')**
:设置使用号填充**cout.setf(ios::left)**
:左对齐**cout.unsetf(ios::dec)**
:卸载十进制**cout.setf(ios::hex)**
:安装十六进制**cout.setf(ios:showbase)**
:显示基数**cout.unsetf(ios::hex)**
:卸载十六进制**cout.setf(ios::oct)**
:安装八进制
通过控制符格式化输出:需要头文件#include <iomanip>
**cout<<setw(20)**
: 指定宽度位20**<<setfill(''):**
设置使用号填充**<<setiosflags(ios::showbase)**
: 显示基数**<<setiosflags(ios::left):**
左对齐**<<hex**
:显示十六进制
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
int main()
{
int num = 200;
cout.unsetf(ios::dec);
cout.setf(ios::hex); // 先取消原来的基标志
cout << num << endl;
cout.setf(ios_base::dec, ios_base::basefield); // 直接清除所有基信息并将基设置成八进制
cout << num << endl;
}
4、文件读写:
文件的读写操作:
头文件:
写文件:
1) 创建文件对象:有两种方式一种是使用有参构造,一种是使用无参。ofstream ofs;
//无参构造ofstream ofs("c:/1.txt",ios:out)
//有参构造,第一个参数是要写入文件的路径,第二个是以什么权限打开。2) 如果是用无参构造需要使用open函数,如果是有参构造创建的对象就不需要这一步了:ofs.open(文件路径,打开方式)
//和有参构造的参数一样。
3) 判断文件是否打开成功ofs.is_open()
//返回值时bool类型的
4) 往文件写入内容ofs<<"写入内容"<<endl;
//注意endl相当于文件中的换行。
5)关闭文件:ofs.close()
读文件
第四种时一个字符一个字符的读不推荐使用。
十二、STL的基本概念和容器
1、STL概论
STL从广义上可以分为:容器(container),算法(algorithm),迭代器(iterator).容器和算法之间通过迭代器进行无缝连接。
STL细分为六大组件:
容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据,从实现角度来看,STL容器时一种class template。
算法:各种常用的算法,如sort、find、copy、for_each。从实现的角度看,STL算法时一种function template
迭代器:扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器时一种operator*,operator->,operator++,operator—等指针相关操作予以重载的class template。
防函数:行为类似函数,可作为算法的某种策略。从实现的角度看防函数时一种重载了operator()的class或者class template。
适配器:一种用来修饰容器或者防函数或迭代器接口的东西。
空间配置器:负责空间的配置与管理,从实现角度看,配置器时一个实现了动态空间配置、空间管理、空间释放的class template
STL六大组件的交互关系,容器通过空间配置器取得数据存储空间,算法通过迭代器存储容器中的内容,防函数可以协助算法完成不同的策略的变化,适配器可以修饰防函数。
容器划分:
序列式容器:怎么输入怎么存
关联式容器:按顺序存,有个key起到引索的作用。
算法:
质变算法 :算完以后容器内数据发生变化
非质变算法:算完以后容器内数据没有发生变化
迭代器的五种类型:
2、STL初识;
vector容器:
使用方法,vector<数据类型> 对象名
例如创建一个int类型的vector容器:vector<int> v;
迭代器创建方法:
例如:vector<int>::iterator it=v.bengin()
//起始迭代器,指向容器第一个数据vector<int>::iterator itEnd=v.end()
//结束迭代器,指向容器最后一个元素的下一个位置。
使用迭代器遍历vector容器中的元素for(vector<int>::iterator itBengin=v.bengin();itbegin!=v.end;itbengin++)
使用算法遍历vector容器中的元素
需要引入头文件#include <algorithm>
for_each(v.begin,v.end(),自己定义一个打印函数)
3、string容器
String构造函数:
string();
创建一个空的字符串string(const string &str);
使用一个string对象初始化string(const char*s);
使用一个字符串s初始化string(int a,char c);
使用n个字符初始化
string基本赋值
string& operator=(const char s);
char类型字符串赋值给当前的字符串string& operator=(const string&s);
把字符串s赋值给当前的字符串string& operator=(char c);
字符赋值给当前的字符串string&assign(const char *s);
把字符串S赋值给当前字符串string&assign(const char*s ,int n);
把字符串s的前n个字符赋值给当前字符串string&assign(const string &s);
把字符串s赋值给当前字符串string&assign(int n,char c);
用那个字符C赋值给当前字符串string&assign(const string&s,int start,int n);
将S从start开始n个字符赋值给当前字符串
获取字符操作
char &operator[];
通过[]方式获取字符
如果遇到下标越界程序崩溃char & at(int n);
通过at方法获取字符
如果遇到下标越界会抛出out_of_range异常
字符串拼接操作
string & operator+=(const string &str);
string &operator+=(const char *str);
string & operator+=(const char c);
string &append(const char *s);
把字符串s连接到当前字符串尾string &append(const char*s int n);
把当前字符串s的前n个字符连接到当前字符串结尾。string &append(const string &s);
同operator+=string &append(const string &s,int pos,int n);
把字符串中从pos开始的那个字符连接到当前字符串尾部string &append(int n,char c);
在当前字符串结尾添加n个字符c
字符串查找
int find(const string& str,int pos=0) const;
查找str第一次出现位置,从pos开始查找。返回查找到的位置int find(const char *s,int pos=0) const;
查找s第一次出现位置,从pos开始查找。返回查找到的位置int find(const char *s,int pos=0,int n) const;
从pos位置查找s的前N个字符第一次位置int find(const char c,int pos=0) const;
查找字符C第一次出现的位置int rfind(const string& str,int pos=npos) const;
查找str最后一次出现位置,从pos位置查找。int rfind(const char *s,int pos=0) const;
查找s最后一次出现位置,从pos开始查找。返回查找到的位置int rfind(const char *s,int pos=0,int n) const;
从pos位置查找s的前N个字符最后一次位置int rfind(const char c,int pos=0) const;
查找字符C最后一次出现的位置
注意:find和rfind区别:find从左往右找,rfind从右往左找
找不到的话返回-1.
字符串的替换
string & replace(int pos,int n,const string & str);
替换从pos开始n个字符为字符串strstring & replace(int pos,int n,const char *s);
替换从pos开始n个字符为字符串s
字符串的比较操作
int compare(const string &s) const;
与字符串S比较int compare(const char *s )const;
与字符串S比较
字符串比较返回值:>返回1,<返回-1,==返回0.
字符串截取子串
string substr(int pos=0,int n=npos)const;
返回由pos开始的那个字符组成的字符串。
字符串的插入和删除
string & insert(int pos,const char *s);
在pos位置插入字符串sstring & insert(int pos,const string &s);
在pos位置插入字符串sstring & insert(int pos,int n,char c);
在pos位置插入n个字符cstring & erase(int pos,int n=npos);
删除从pos开始的n个字符
String和c语言-style字符串的转换
charstr 转为string
string s(str)
string str转为char
str.c_str()
char*str可以隐式转换成string,反之不可以。
字符大小写转换
toupper(char c);将字符c转换为大写
tolower(char c);将字符c转换为小写
4、Vector容器:
基本概念:
Vector相当于一个动态数组,它的操作和数组很像,区别是数组式静态的,不能改变空间大小。
所谓动态增加大小并不是在原空间之后续接空间,而是寻找一块更大的空间,然后将数据拷贝到新空间。因此,对vector的任何操作,一旦引起空间的重新配置,指向原vector的所有迭代器就都失效了,这是容易犯的错误。
Vector常用API
构造函数:
Vector(v.begin,v.end());将一个对象v[v.begin,v.end())区间中的元素拷贝给自身
Vector(n,elem)//将n个elem拷贝给自身
Vector(const vector &vec)//拷贝构造
Vector赋值:
assign(beg,end);将[beg,end)区间中的数据拷贝给本身
assign(n,elem);将n个elem拷贝给本身
vector & operator=(const vector &vec);重载等号
swap(vec);将vec与本身的元素互换。
Vector大小操作
Size();返回容器中元素的个数
Empty();判断容器是否为空,返回值bool类型,true为空
resize(int num);重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除
resize(int num,elem);重新指定容器的长度为num,若容器变长,则以elem填充新位置。如果容器变短,则末尾超出容器长度的元素被删除
Capacity();容器的容量
reserve(int len);容器预留len个元素长度,预留位置不初始化,元素不可访问。
Vector数据存取操作:
At(int idx);返回索引idx所指的数据,如果越界,抛出out_of_range异常。
Operator[];返回索引所指的数据,如果越界,直接报错。
Frount();返回容器中第一个元素
Back();返回容器中最后一个元素
Vector插入和删除:
Insert(const _iterator pos,int count,ele);迭代器指向位置pos插入count个元素ele
Push_back(ele);尾部插入元素ele
Pop_back();删除最后一个元素
erase(const iterator start,const iterator end);删除迭代器从start到end之间的元素。
erase(const _iterator pos);删除迭代器指向的元素
clear();删除容器中所有元素。
Vector的迭代器:
Vector的迭代器是随机访问迭代器,
Vector<数据类型>::iterator
Vector中函数begin()返回首元素的迭代器,end()返回末尾元素下一个地址的迭代器。
rbegin()返回第一个元素前一个地址的逆序迭代器(vector<数据类型>::reverse_iterator ;逆序迭代器)。
Rend()返回末尾元素的逆序迭代器 。
5、deque容器:
deque常用API:
构造函数:
Deque;默认构造
deque(v.begin,v.end());将一个对象v[v.begin,v.end())区间中的元素拷贝给自身
deque (n,elem)//将n个elem拷贝给自身
deque (const vector &vec)//拷贝构造
deque赋值:
assign(beg,end);将[beg,end)区间中的数据拷贝给本身
assign(n,elem);将n个elem拷贝给本身
deque& operator=(const deque &deq);重载等号
swap(deq);将deq与本身的元素互换。
deque大小操作
Size();返回容器中元素的个数
Empty();判断容器是否为空,返回值bool类型,true为空
resize(int num);重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除
resize(int num,elem);重新指定容器的长度为num,若容器变长,则以elem填充新位置。如果容器变短,则末尾超出容器长度的元素被删除
deque数据存取操作:
At(int idx);返回索引idx所指的数据,如果越界,抛出out_of_range异常。
Operator[];返回索引所指的数据,如果越界,直接报错。
Frount();返回容器中第一个元素
Back();返回容器中最后一个元素
Deque双端插入和删除操作:
Push_back(ele);尾部插入元素ele
Push_front(ele);头部插入元素ele;
Pop_back();删除尾部元素
Pop_front();删除头部元素
deque插入和删除:
insert(pos,elem);在pos位置插入一个elem元素的拷贝,返回新数据的位置
Insert(pos,n,elem);在pos位置插入n个elem数据无返回值。
Insert(pos,beg,elem);在pos位置插入[beg,end)区间的数据,无返回值。
erase(beg,end);删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);删除pos位置的数据,返回下一个数据的位置
clear();删除容器中所有元素。
6、stack(栈)容器:
基本概念:
Stack容器时一种先进后出的数据结构,只允许新增元素,一处元素,取得栈顶元素,除了最顶端外,无法访问其他元素,不允许有遍历行为,栈容器没有迭代器。
结构图:
栈容器API:
构造函数:
Stack :stack采用模板类实现,默认构造
Stack(const stack&stk);拷贝构造
Stack赋值操作
Stack &operator=(const stack &stk);重载等号
Stack数据存取操作
Push(elem);向栈顶添加元素
Pop;从栈顶移除第一个元素
Top;返回栈顶元素
Stack大小操作
empty();判断栈是否为空
size();返回栈大小
7、queue(队列)容器:
基本概念
Queue是一种先进先出的数据结构,它有两个出口,允许从一段新增元素,从另一端移除元素。不能遍历,没有迭代器
结构图:
queue常用API
构造函数:
Queue;默认构造
Queue(const queue&que);拷贝构造
queue存取、插入和删除操作
push(elem);往队尾添加元素
pop();从队头移除第一个元素
back();返回最后一个元素
front();返回第一个元素
queue赋值操作
queue &operator=(const queue &que);重载等号
queue大小操作
empty();判断队列是否为空
size();返回队列大小
8、list(链表)容器:
list是链表结构,它不是一个连续的空间,list对于空间的运用有绝对的精准,插入和删除更效率。List和vector是最常被使用的容器。List是一个双向循环链表。因为链表比vector多了指针域所以对空间的消耗较大,它的空间又不是连续的空间,所以遍历的时候时间消耗较大。
结构图:
迭代器:
List的迭代器是一个双向迭代器,只提供了递增和递减的操作,list有一个很重要的性质,插入和删除都不会造成原有list迭代器的失效,这在vector是不成立的。
List常用API:
构造函数
List ;默认构造
List(beg,end);将[beg,end)区间中的元素拷贝给本身
List(n,elem);将n个elem拷贝给本身
List(const list *lst);拷贝构造
元素插入和删除
Push_back(elem);在容器尾部加入一个元素elem
Pop_back();删除容器中最后一个元素
Push_front(elem);在容器开头插入一个元素elem
Pop_front();从容器开头移除一个元素
Insert(pos,elem);在pos位置插入elem元素,返回新数据的位置
Insert(pos,n,elem);在pos位置插入n个elem元素,无返回值
Insert(pos,beg,end);在pos位置插入[beg,end)区间的数据,无返回值
clear();移除容器的所有数据
erase(beg,end);删除[beg,end)区间的数据,返回下一个数据的位置
erase(pos);删除pos位置的数据,返回下一个数据的位置
remove(elem);删除容器中所有与elem值匹配的数据。如果是自定义类型需要重载自定义类型中的=号。
大小操作
Size();容器中元素的个数
empty();判断容器是否为空
resize(num);重新指定容器大小为num。如果容器变长,则以默认值填充新位置,如果容器变短,则末尾超出容器长度的元素被删除
resize(num,elem) 重新指定容器大小为num。如果容器变长,则以elem填充新位置,如果容器变短,则末尾超出容器长度的元素被删除
赋值操作
assign(beg,end);将[beg,end)区间中的数据拷贝赋值给本身
Assign(n,elem);将N个elem元素赋值给本身
List & operator=(const list &lst);重载等号
Swap(lst);将lst与本身的元素互换。
数据存取
front();返回第一个元素。
Back();返回最后一个元素。
反转排序
reverse();反转链表内元素的位置
sort();list排序
注意:sort()默认是从小到大排序如果想从大到小排序需要传入一个回调函数。对于自定义的数据类型必须传入回调函数,自己写一个比较函数然后把函数名作为参数传进去。
9、set\multiset容器:‘
基础知识:
Set的元素即是键又是实值。Set不允许两个元素有相同的键值。Set的迭代器不能改变set元素的值.multiset容器和set完全相同,唯一的差别在于它允许键值重复。Set和multiset底层实现是红黑树,是平衡二叉树的一种。Set和multiset都使用同一个头文件:set
Set的值默认是从小到大排序好的。如果想要set容器从大到小排序需要传入一个防函数(防函数就是重载())
注意:改变set容器的排序规制必须要在插入元素之前指定。
平衡二叉树:所有的树的左子树与右子树的高度差不能大于1
结构图:
规则小值放在左边,大值放在右边
Set常用API
构造函数:
Set;默认构造
Multiset;默认构造
Set(const set &st);拷贝构造
赋值操作:
Set &operator=(const set &st);重载=号
Swap(st);交换两个容器的元素
大小操作:
Size();容器中元素的数目
Empty();容器是否为空
插入和删除:
Insert(elem);在容器中插入元素elem
Clear();清除容器中所有元素
Erase(pos);删除pos迭代器所指的元素,返回下一个元素的迭代器(要传入迭代器)
Erase(beg,end);删除区间[beg,end)的所有元素,返回下一个元素的迭代器。(要传入迭代器)
Erase(elem);删除容器中值为elem的元素。
Set查找操作:
find(key);查找键key是否存在,若存在,返回该键的元素的迭代器,若不存在,返回set.end();
count(key);查找键key的元素个数,在set容器中只有两个返回值,存在返回1,不存在返回0
lower_bound(keyelem);返回第一个key>=keyelem元素的迭代器。
upper_bound(keyelem); 返回第一个key>keyelem元素的迭代器。
equal_range(keyelem); 返回lower_bound和upper_bound 两个结果的迭代器。(注意这个是返回两个值要用到对组pair来接受)
注意事项:
在set中存放自定义类型时必须要重载<号,或者写一个防函数(重载小括号())一般都是写一个防函数来定义排序规制
例如:
10、map/multimap容器:
基本概念:
Map所有元素都会根据元素的键值自动排序,map所有的元素都是pair对组,pair的第一个元素被视为键值,第二个元素被视为实值,map不允许两个元素有相同的键值。Map容器不可以通过迭代器来修改键值但是可以修改实值。Multimap和map操作类似,唯一的区别时multimap可以有重复的键值。底层也是以红黑树机制实现的。
常用API
构造函数:
Map;默认构造
Map(const map &mp);拷贝构造
赋值操作:
Map&operator=(const map&mp);重载等号操作符
Swap(mp);交换两个集合容器
大小操作:、
Size();容器中元素的数目
Empty();容器是否为空
插入元素操作:
Map.insert(….);往容器中插入元素,返回pair
Mapmapstu;
建议使用第二种。
删除操作:
Clear();清除容器中所有元素
Erase(pos);删除pos迭代器所指的元素,返回下一个元素的迭代器(要传入迭代器)
Erase(beg,end);删除区间[beg,end)的所有元素,返回下一个元素的迭代器。(要传入迭代器)
Erase(keyelem);删除容器中key为keyelem的对组。
map查找操作:
find(key);查找键key是否存在,若存在,返回该键的元素的迭代器,若不存在,返回set.end();
count(keyelem);查找key为keyelem的对组个数,在map容器中只有两个返回值,存在返回1,不存在返回0
lower_bound(keyelem);返回第一个key>=keyelem元素的迭代器。
upper_bound(keyelem); 返回第一个key>keyelem元素的迭代器。
equal_range(keyelem); 返回lower_bound和upper_bound 两个结果的迭代器。(注意这个是返回两个值要用到对组pair来接受)
12、要点知识:
1)迭代器分类
Iterator:普通迭代器
reverse_iterator:反转迭代器
const_iterator:只读迭代器
2)[ )的含义:包含前不包含后。
3)排序:如果容器的迭代器支持随机访问,可以使用系统提供的标准算法,如果容器的迭代器不支持随机访问,内部会提供对应的接口。
4)pair对组的创建:
创建方式一:
例:pairp(“Tom”,10)这样就创建了一个队组
p.first;访问第一个元素,p.second;访问第二个元素。
创建方式二:
例:pairp=make_pair(“Tom”,10)
5) STL容器的概括:
0
十三、STL常用的算法
1、函数对象:
基本概念:
重载了()操作符,使得类对象可以像函数那样调用,也叫防函数
总结:
函数对象通常不定义构造函数和析构函数,所以在构造和析构时不会发生任何问题,避免了函数调用的运行时问题。
函数对象超出普通函数的概念,函数对象可以有自己的状态
函数对象可内联编译,性能好,用函数指针几乎不可能。
模板函数对象使函数对象具有通用性,这也是它的优势之一。
2、谓词:
基本概念:谓词时指普通函数或重载的operator()返回值时bool类型的函数对象。如果operator接受一个参数,叫做一元谓词,接受两个参数叫做二元谓词。谓词可以作为一个判断式。
3、内建函数对象
STL内建了一些函数对象,使用内建函数对象时需要引入头文件
算数类函数对象:negate是一元,其他二元
Template T plus;加法防函数
例:plus p; p(10,20)//结果为30
Template T minus;减法防函数
Template T multiplies;乘法防函数
Template T divides;除法防函数
Template T modulus;取模防函数
Template T negate;取反防函数
例:negate n; n(10) //结果为-10
关系运算类函数对象:都是二元
Templatebool equal_to;等于
Templatebool not_equal_to;不等于
Templatebool greater;大于
例:sort函数提供的默认排序是从小到大如果想从大到小需要提供一个回调函数或防函数,可以使用这个
Sort(v.begin(),v.end(),greater());
Templatebool greater_equal;大于等于
Templatebool less;小于
例:Sort(v.begin(),v.end(),less ());//从小到大
Templatebool less_equal;小于等于
逻辑运算类函数对象:not一元,其他二元
Templatebool logical_and;逻辑与
Templatebool logical_or;逻辑或
Templatebool logical_not;逻辑非
4、函数对象适配器
函数适配器bind1st bind2nd
例:vector v;
V.push_back(10);
V.push_back(20;
V.push_back(30);
需求:每个数随机加上一个值
自己定义一个函数对象继承binary_function(参数1,参数2,返回值)
Struct myprint:public binary_function
{
void operator()(int v1,int v2)const//把函数变成常函数
{
cout<<v1+v2<<” ”;
}
}
for_ench(v.begin(),v.end(),bind2nd(myprint(),100))//这样打印出来的数据都加了100
bind1st和bind2nd的区别:
bind1st把参数1变成100
bind2nd把参数2变成100
函数对象适配器not1、not2 (取反)
Not1和not2的区别
Not1针对一元函数对象
Not2针对二元函数对象
实现步骤
1) 继承unary_function<参数,返回值>
2) 把实现的防函数变成常函数
3) 使用not1或者not2绑定适配器
例:for_ench(v.begin(),v.end(),not1(myprint());
例:内建函数对象less
Sort(v.begin(),v.end(),not2(less())) 这样就可以实现从大到小排序,但是要在release下运行,debug模式会报错。
普通函数进行适配
使用ptr_fun把普通函数变为函数对象
例如一个普通函数 fun(); ptr_fun(fun);就可以把fun函数变为函数对象。
成员函数进行适配
当容器存储的是对象,用mem_fun_ref适配它的成员函数
例如Make有一个成员函数fun(); mem_fun_ref(&make::fun);
当容器存储的是对象指针,用mem_fun适配它的成员函数
空间适配器:
容器通过空间适配器取得数据存储空间,空间适配器管理容器的空间。
如果申请的内存大小超过128,那么空间适配器就自动调用一级空间适配器,反之调用二级空间适配器。
5、算法概述:
算法通过迭代器来操作容器中的数据
算法主要由头文件 组成
常用功能涉及到比较、交换、查找、遍历、复制、修改、反转、排序、合并等…
定义了一些模板类,用以声明函数对象
包括在几个序列容器上进行简单运算的模板函数
6、遍历算法:
for_each:遍历打印
函数原型 for_each(开始迭代器,结束迭代器,函数对象)
返回值:函数对象。
transform算法:将指定容器区间的元素搬运到另一个容器中。
注意:transform不会给目标容器分配内存,所以需要我们提前分配好内存。
函数原型:
1) transform(iterator beg1, iterator end1, iterator beg2,callbakc);
beg1:原容器开始迭代器
end1:原容器结束迭代器
beng2:目标容器开始迭代器
callbakc:回调函数或者函数对象
返回值:目标容器迭代器
2)transform(iterator beg1, iterator end1, iterator beg2, iterator beg3, callbakc); 将两个容器中的元素处理后放入第三个容器中
Beg3:第三个容器开始迭代器
返回值:第三个容器的迭代器。
7、查找算法:
Find算法:查找元素
函数原型:
Find(iterator beg,iterator end,value)
Beg:容器开始迭代器
End:容器结束迭代器
Value:查找的元素
返回值:查找元素的迭代器,找不到返回v.end()
adhacent_Find(iterator beg,iterator end,callback)
函数作用:查找是否有相邻的相同元素(必须是相邻的相同元素)
Callback:回调函数或者谓词(返回bool类型的函数对象),需要两个参数。
返回值:相邻元素的第一个位置的迭代器。
注意:如果是内置数据类型不用传入callback,系统默认会传入一个内建函数对象equal_to,如果是自定义数据类型就要传入自己定义好的callback
Binary_search算法:二分查找
注意:在无序序列中不可用
函数原型1 :
Binary_search(iterator beg,iterator end,value)
返回值:bool,找到返回true,否则返回flase
函数原型2
Binary_serch(iterator beg,iterator end,value,less())
注意:自定义函数查找时需要用这个,然后需要自己重载小于号。注意这样的话容器中的数据必须时从小到大排序好的,如果是从大到小排序的需要自己传入greater(),然后重载大于号。