C++学习

学习路线

image

基本语法

通常头文件我们都选择使用,虽然<bits/stdc++.h>包含c++所有的头文件,但是会产生很多我们不使用的头文件,导致编译时间增长。个人认为输入输出还是使用c的函数为好(c的输入输出更快)。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>  //为输入输出流的头文件(c++)
#include <bits/stdc++.h> //该头文件包含c++中所有的头文件
#include <cstdio> //在c语言头文件的基础上加上c,去掉.h尾缀
using namespace std; //使用定义的名字空间,指定缺省的命名空间,如果没有该声明,后面的输入输出前都需加入std::

int main(){
int n;
cin>>n;//cin为c++的输入函数
cout<<n<<endl; //cout为c++的输出函数,而endl就是换行
return 0;
}

变量的定义

相比c语言,c++多了string字符串类型的定义

1
string name;

宏常量

一般在main函数的上面声明,用大写命名.

1
#define 常量名 值

命名规则

c++规定给标识符(变量、常量、函数、结构体、类等)命名时,必须遵守以下规则。

  • 在名称中只能使用字母字符、数字和下划线
  • 名称的第一个字符不能是数字
  • 区分大写字符与小写字符
  • 不能将C++关键词用作名称
  • 以下划线和大写字母打头的名称被保留给编译器机器使用的资源使用,如果违反了这一规则,会导致行为的不确定性
  • C++对名称的长度没有限制,但有些平台可能有长度限制

算术运算

注意:

  • 整数进行除法运算时,如果分母为0,程序将异常退出
  • 浮点数进行除法运算时,如果分母为0.0,将得到inf(nfinite,无穷大)
  • 两个整数进行除法运算时,将舍去小数部分,得到一个整数
  • 整数与浮点数进行除法运算时,得到的结果是浮点数
  • 在整数前面加(float)或(double)可以将整数转换为float或double类型
  • 取模运算只能用于整数(分母也不能为0)

赋值运算

注意

  • 字符串(string)只能使用等号赋值,不能使用其他的赋值运算符
  • 浮点数不能使用%=运算符

C++初始化

  1. 把值写在小括号中,等于号可以省略(C++标准)

    1
    2
    int a=(15);
    int b(20)
  2. 把值写在花括号中,等于号也可以省略(C++11标准)

    1
    2
    int a={15};
    int b{20};

    注意:在Linux平台下,编译需要加-std=c++11 参数。

C++类型转换

功能:

  • 强制转换类型

C++类型转换符

  • static_cast
  • dynamic_cast
  • reinterpret_cast
  • const_cast
  • 使用方式xx_cast<type>(expression)

C语言的风格:

1
(type)expression;

C++语言风格:

1
xx_cast<type>(expression);

const_cast转换符

  • 功能:一般用于去除const属性,将const类型转换成非const

  • 例子:

    1
    2
    const Person *p1=new Person();
    Person *p2=const_cast<Person *>(p1);

读写优化

1
2
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);

命名空间

用于解决名字冲突

使用关键字namespace,控制名称的作用域

命名空间的本质:对符号常量、变量、函数、结构、枚举、类和对象等等进行封装

命名空间语法namespace A{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace A{
int data=10;
namespace C{
int data=30;
}
}

namespace B{
int data=20;
}

namespace D{
int data=10;
}
//1000行代码
namespace D{ //如果存在就追加,不存在就新建
void func(){
cout<<"func函数"<<endl;
}
}

void test01(){
using A::a;//下方如果出现a,就拿A中的a使用
cout<<"a="<<a<<endl;
cout<<"A::data= "<<A::data<<endl;
cout<<"A::B::data= "<<A::B::data<<endl;
}

注意事项

  • 命名空间只能定义在全局中
  • 命名空间可以嵌套
  • 命名空间是开放的,可以随时将新的成员加入命名空间
  • 命名空间可以声明和实现分离(外部实现时要加作用域)
  • 无名空间(namespace{})只能在本源文件使用,里面的成员相当于静态全局成员一样
  • 命名空间起别名(namespace 别名=要取别名的空间名)
  • 使用using声明命名空间中的某几个成员可用,容易造成名字冲突(会合普通变量名字冲突)
  • using使用命名空间中的函数重载过的函数,函数重载,命名空间的所有同名函数都被声明

using编译指令

using声明整个命名空间,可以直接通过成员名使用

语法:using namespace 空间名

注意事项:如果外部有定义空间中的变量,调用时则会先找普通变量,再找命名空间的,全局范围的变量会与局部的命名空间的变量产生冲突

内存分区模型

C++程序运行时,将内存大方向划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

内存四区意义

不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程

程序运行前

在程序编译后,生成了exe可执行程序,为执行该程序前分为两个区域

代码区

​ 存放cpu执行的机器指令

​ 代码区是共享的,共享的目的是对于频繁被执行的程 序,只需要在内存中有一份代码即可

​ 代码区是只读的,使其只读的原因是防止程序意外地修 改了它的指令

全局区

​ 全局变量和静态变量存放在此

​ 全局区还包含了常量区,字符串常量和其他常量也存放 在此

该区域的数据在程序结束后由操作系统释放

总结

  • C++中在程序运行前分为全局区和代码区
  • 代码区特点是共享和只读
  • 全局区存放全局变量、静态变量、常量
  • 常量区中存放const修饰的全局常量和字符串常量

程序运行后

栈区

​ 由编译器自动分配释放,存放函数的参数值,局部变量 等

​ 注意事项:不要返回局部变量的地址,栈区开辟的数据 由编译器自动释放

堆区

​ 由程序员分配释放,若程序员不释放,程序结束时由操作 系统回收

​ 在C++中主要利用new在堆区开辟内存

总结

​ 堆区数据由程序员管理开辟和释放

​ 堆区数据利用new关键字进行开辟内存

new操作符

​ C++中利用new操作符在堆区开辟数据

​ 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用 delete

​ 语法:new 数据类型

​ 利用new创建的数据,会返回该数据对应的类型的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
using namespace std;

//new的基本语法
int *func(){
//在堆区创建整型数据
//new返回是该数据类型的指针
int *p=new int(10);
return p;
}

void test01(){
int *p=func();
cout<<*p<<endl;
cout<<*p<<endl;
cout<<*p<<endl;
//堆区的数据是由程序员管理开辟和释放
//如果想释放用关键字delete
delete p;
cout<<*p<<endl; //内存已经被释放,再次访问就是非法操作
}

//在堆区利用new开辟数组f
void test02(){
//创建10整型数据的数组,在堆区
int *arr=new int[10]; //10代表数组有10个元素
for(int i=0;i<10;i++){
arr[i]=i+100;
}
for(int i=0;i<10;i++){
cout<<arr[i]<<endl;
}
//释放堆区数组
//释放数组的时候,要加入[]
delete[] arr;
}
int main(){
test01();
test02();
return 0;
}

引用

引用的基本使用

作用:给变量起别名

语法:数据类型 &别名 = 原名

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;

int main(){
int a=10;
int &b=a;
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
return 0;
}

引用注意事项

  • 引用必须要初始化
  • 引用在初始化后,不可以改变

引用做函数参数

作用:函数传参时,可以利用引用的技术让形参修饰实参

优点:可以简化指针修改实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

//引用传递
void myswap(int &a,int &b){
int temp=a;
a=b;
b=temp;
}

int main(){
int a=10;
int b=20;
myswap(a,b); //引用传递,形参会修饰实参的
cout<<"a= "<<a<<endl;
cout<<"b= "<<b<<endl;
return 0;
}

总结:通过引用参数产生的效果同地址传递是一样的,引用的语法更清楚更简单

引用做函数返回值

作用:引用是可以作为函数的返回值存在的

注意不要返回局部变量引用

用法:函数调用作为左值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

//不要返回局部变量引用
int& test01(){
int a=10;//局部变量存放在栈区
return a;
}
//函数的调用可以作为左值
int& test02(){
static int a=10; //静态变量,存放在全局区,全局区上的数据在程序结束后系统释放
return a;
}

int main(){
int &ref=test01(); //非法操作
cout<<"ref= "<<ref<<endl;//第一次结果正确是编译器做了保留
cout<<"ref= "<<ref<<endl;//第二次结果错误是因为a的内存已经释放
int &ref2=test02();
cout<<"ref2= "<<ref2<<endl;//输出10
cout<<"ref2= "<<ref2<<endl;
test02()=1000; //如果函数的返回值是引用,这个函数的调用可以作为左值
cout<<"ref2= "<<ref2<<endl;//输出1000
cout<<"ref2= "<<ref2<<endl;
return 0;
}

引用的本质

本质引用的本质在C++内部实现是一个指针常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

//发现是引用,转换为int* const ref=&a;
void func(int& ref){
ref=100;//ref是引用,转换为*ref=100
}

int main(){
int a=10;
//自动转换为int* const ref=&a;指针常量是指针指向不可改,也说明为什么引用不可更改
int& ref=a;
ref=20; //内部发现ref是引用,自动帮我们转换为:*ref=20;
cout<<"a:"<<a<<endl;
cout<<"ref:"<<ref<<endl;
func(a);
return 0;
}

总结:C++推荐使用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了

常量引用

作用:常量引用主要用来修饰形参,防止误操作

在函数形参列表中,可以加const修饰形参,防止形参改变实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
//打印数据函数
void showValue(const int &val){
//val=1000; 如果没加上const,外面的a也会更改
//加上了const,上段代码就会报错
cout<<"val= "<<val<<endl;
}

int main(){
//常量引用
int &ref=10;//错误,引用必须引一块合法的内存空间
//加上const之后,编译器将代码修改为 int temp=10; const int &ref=temp;
const int &ref=10;//正确
//使用场景:用来修饰形参,防止误操作
int a=100;
showValue(a);
cout<<"a= "<<a<<endl;
return 0;
}

函数提高

函数默认参数

在C++中,函数的形参列表中的形参是可以有默认值的。

语法返回值类型 函数名 (参数 = 默认值){}

注意事项

  • 如果某个位置已经有了默认参数,那么从这个位置往后,从左向右的形参都必须要有默认参数
  • 如果函数声明有默认参数,函数实现就不能有默认参数
  • 声明和实现只能有一个有默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

//函数默认参数
int func(int a,int b=20,int c=30){
return a+b+c;
}

int main(){
cout<<func(10)<<endl;//有默认值可以只传没有默认值的形参
cout<<func(10,30)<<endl;//如果自己传入数据,就用自己的数据

return 0;
}

函数占位参数

C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

语法返回值类型 函数名(数据类型){}

在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

void func(int a,int){
cout<<"this is func"<<endl;
}

//占位参数还可以默认参数
void func1(int a,int =10){
cout<<"this is func1"<<endl;
}

int main(){
func(10,10);
func1(10);
return 0;
}

函数重载

概述

作用:函数名可以相同,提高复用性

函数重载满足条件

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同或者个数不同或者顺序不同

注意:函数的返回值类型不可以作为函数重载的条件

例如:函数func返回值类型为int,而还有一个func函数返回值为void,这个时候编译器会报错!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;
//函数重载需要在同一个作用域中
void func(){
cout<<"func的调用"<<endl;
}

//个数不同
void func(int a){
cout<<"func(int a)的调用"<<endl;
}

//参数类型不同
void func(double a){
cout<<"func(double a)的调用"<<endl;
}

//顺序不同
void func(int a,double b){
cout<<"func(int a,double b)的调用"<<endl;
}

void func(double a,int b){
cout<<"func(double a,int b)的调用"<<endl;
}

//注意事项
//函数的返回值类型不能作为函数重载的条件
int main(){
func();
func(10);
func(0.1);
func(10,0.1);
func(0.1,10);
return 0;
}

函数重载注意事项

  • 引用作为函数重载条件
  • 函数重载碰到函数默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;

//引用作为函数重载条件
void func(int &a){ //int &a=10;不合法
cout<<"func(int &a)的调用"<<endl;
}

void func(const int &a){ //const int &a=10;合法
cout<<"func(const int &a)的调用"<<endl;
}

//函数重载碰到默认参数,出现二义性,报错,尽量避免这种情况
void func2(int a,int b=10){
cout<<"func2(int a,int b)的调用"<<endl;
}

void func2(int a){
cout<<"func2(int a)的调用"<<endl;
}

int main(){
int a=10;
func(a);
func(10);
func2(10);
return 0;
}

内联函数

概念

  • 内联函数内联函数其实就是在程序编译时,会将该函数的代码副本放置在每个调用该函数的地方,省去了函数调用的开销。

  • 使用场景对于代码量少且函数执行的操作简单,以及调用函数开销要大于执行函数体所需要的开销,我们可以考虑将其变为内联函数,提高函数的执行效率

    注:

    • 内联是以代码膨胀(拷贝)为代价仅仅省去了函数调用的开销,并不包括执行函数体所需要的开销,而是仅指参数压栈、跳转、退栈和返回等操作。
    • 如果执行函数体内代码的时间比函数调用的开销大,则inline也就是内联的效率收益会很小
    • 每一处内联函数的调用都要拷贝代码,将使程序的总代码量增大,消耗更多的内存空间
  • 使用方法:**使用inline关键字放在函数定义(不是声明,而是函数实现)**,将函数变为内联函数来提高函数的执行效率

  • inline是一种”用于实现的关键字”,也就是应在函数定义使用,而不是声明

  • 类中的成员函数自动内联

  • 使用限制只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句,例如while、switch,并且内联函数本身不能是直接递归函数(即自己内部还调用自己)

缺点

  1. 消耗更多的内存空间
  2. 对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
//错误示例
inline int Foo(int a,int b);
int Foo(int a,int b){
return a+b;
}


//正确示例
int Foo(int a,int b);
inline int Foo(int a,int b){
return a+b;
}

类和对象

C++面向对象的三大特性为:封装继承多态

C++认为万事万物都皆为对象,对象上有其属性和行为

封装

封装的意义

封装是C++面向对象三大特性之一

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

封装意义一

​ 在设计类的时候,属性和行为写在一起,表现事物

语法class 类名{ 访问权限:属性 / 行为 };

类中的属性和行为,我们统一称为成员

属性:成员属性 成员变量

行为:成员函数 成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

//设计一个圆类,求圆的周长
class Circle{
//访问权限
public: //公共权限
//属性
int m_r;
//行为
double calculateZC(){
return 2*PI*m_r;
}
};

int main(){
//通过圆类,创建具体的圆(对象)
//实例化 (通过一个类,创建一个对象的过程)
Circle c1;
//给圆对象的属性进行赋值
c1.m_r=10;
cout<<"圆的周长为:"<<c1.calculateZC()<<endl;
return 0;
}

封装的意义二

类在设计时,可以把属性和行为放在不同的权限下,加以控制

访问权限有三种:

  1. public 公共权限,成员类内外可以访问
  2. protected 保护权限,成员类内可以访问,类外不可以访问,在继承中,父类的保护权限的内容,子类也可以访问
  3. private 私有权限,成员类内可以访问,类外不可以访问,父类的私有权限的内容,子类不可以访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

class Person{
//公共权限
public:
string m_Name;
//保护权限
protected:
string m_car;
//私有权限
private:
int m_password;
public:
void func(){
m_Name="张三";
m_car="拖拉机";
m_password=123456;
}
};

int main(){
Person p1;
p1.m_Name="李四";
p1.m_car="奔驰"; //报错,保护权限类外不可访问
p1.m_password=12; //报错,私有权限类外不可访问
p1.func(); //公共权限可以访问
return 0;
}

struct和class区别

在C++中struct和class唯一的区别就在于默认的访问权限不同

区别:

  • struct 默认权限为公共
  • class 默认权限为私有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

class C1{
int m_A; //默认权限 是私有
};

struct C2{
int m_A; //默认权限为公有
}

int main(){
C1 c1;
c1.m_A=100; //报错
C2 c2;
c2.m_A=100; //不会报错,在struct默认的权限为公共的,所以可以访问
return 0;
}

成员属性设置为私有

优点1:将所有成员属性设置为私有,可以自己控制读写权限

优点2:对于写权限,我们可以监测数据的有效性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <string>
using namespace std;

class Person{
public:
//设置姓名
void setName(string name){
m_Name=name;
}
//获取姓名
string getName(){
return m_Name;
}
private:
//姓名 可读可写
string m_Name;
//年龄 只读
int m_Age;
// 只写
string m_lover;
}

int main(){
Person p1;
p1.setName("张三");
cout<<"姓名:"<<p1.getName()<<endl;

return 0;
}

对象的初始化和清理

C++中的面向对象来源于生活,每个对象也会有初始化设置以及对象销毁前的清理数据的设置

构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题

  • 一个对象或者变量没有初始状态,对其使用后果是未知的
  • 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

C++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作。

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供

编译器提供的构造函数和析构函数是空实现

  • 构造函数:主要用于创建的对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数

语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数

语法:*~类名(){}*

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动化调用,无须手动调用,而且只会调用一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

class Person{
public:
//构造函数进行初始化操作
Person(){
cout<<"Person 构造函数的调用"<<endl;
}
//析构函数进行清理的操作
~Person(){
cout<<"Person 析构函数调用"<<endl;
}
};

void test01(){
Person p; //创建对象就会自动调用构造函数
//在栈上的数据,test01执行完毕后,释放这个对象,对象销毁前会执行析构函数的操作
}

int main(){
//test01();
Person p; //main函数内,析构执行代码的操作输出我们看不到
return 0;
}

析构函数的分类及调用

两种分类方式:

  • 按参数分为:有参构造和无参构造
  • 按类型分为:普通构造和拷贝构造

三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法

注意事项:

  1. 调用默认构造函数的时候不用加(),加入了括号,编译器会认为 是一个函数的声明,不会认为在创建对象
  2. 不要利用拷贝构造函数初始化匿名对象,编译器会认为Person(p3)==Person p3; 造成重定义报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
using namespace std;
//分类
class Person{
public:
//普通构造函数
//无参构造
Person(){
cout<<"Person的无参构造函数的调用"<<endl;
}
//有参构造
Person(int a){
age=a;
cout<<"Person的有参构造函数的调用"<<endl;
}
//拷贝构造函数
Person(const Person &p){ //将实例化对象传入,将这个对象的属性拷贝到这个函数的实例化对象中
age=p.age;
}
~Person(){
cout<<"Person 析构函数调用"<<endl;
}
int age;
};

//调用
void test01(){
//括号法
Person p; //默认构造函数调用
Person p2(10);//有参构造函数
Person p3(p2);//拷贝构造函数

//显示法
Person p1; //默认
Person p2=Person(10); //调用有参构造函数
Person p3=Person(p2);//调用拷贝构造函数
Person(10); //匿名对象,特点:当前行执行结束后,系统会立即回收掉匿名对象

//隐式转换法
Person p4=10; //相当于写了Person p4(10); 有参构造
Person p5=p4; //拷贝构造函数

}

int main(){
test01();
return 0;
}

拷贝构造函数调用时机

C++中拷贝构造函数调用时机通常有三种情况:

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
using namespace std;

class Person{
public:
Person(){
cout<<"Person默认构造函数调用"<<endl;
}
Person(int age){
cout<<"Person有参构造函数调用"<<endl;
m_Age=age;
}
Person(const Person &p){
cout<<"Person拷贝构造函数调用"<<endl;
m_Aoge=p.m_Age;
}
};
//使用一个已经创建完毕的对象来初始化一个新对象
void test01(){
Person p1(20);
Person p2(p1);
}

//值传递的方式给函数参数传值
void doWork(Person p){
}
void test02(){
Person p;
doWotk(p); //值传递时,会拷贝一个副本,所以会执行拷贝构造函数
}

//以值方式返回局部对象
Person doWork2(){
Person p1;
cout<<(int*)&p1<<endl;
return p1; //局部对象会拷贝出新对象返回
}

void test03(){
Person p=doWork2();
cout<<(int*)&p<<endl;
}
int main(){
test01();
test02();
test03();
return 0;
}

构造函数调用规则

默认情况下,C++编译器至少给一个类添加3个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

class Person{
public:
Person(){
cout<<"Person默认构造函数调用"<<endl;
}
Person(int age){
cout<<"Person有参构造函数调用"<<endl;
m_Age=age;
}
Person(const Person &p){
cout<<"Person拷贝构造函数调用"<<endl;
m_Age=p.m_Age;
}
~Person(){
cout<<"Person析构函数调用"<<endl;
}
int m_Age;
};

void test01(){
Person p;
p.m_Age=18;
Person p2(p);
cout<<"p2的年龄为:"<<p2.m_Age<<endl;
}

int main(){

return 0;
}

深拷贝与浅拷贝

深浅拷贝是面试的经典问题,也是一个常见的坑

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

浅拷贝带来的问题就是堆区的内存重复释放

浅拷贝的问题要利用深拷贝进行解决,自己写一个拷贝函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
using namespace std;

class Person{
public:
Person(){
cout<<"Person的默认构造函数调用"<<endl;
}
Person(int age,int height){
m_Age=age;
m_Height=new int(height); //需要程序员手动销毁
cout<<"Person有参构造函数调用"<<endl;
}
//自己实现拷贝函数来解决浅拷贝带来的问题
Person(const Person &p){
cout<<"拷贝构造函数的调用"<<endl;
m_Age=p.m_Age;
//m_Height=p.m_Height; 编译器默认实现
//深拷贝操作
m_Height=new int(*p.m_Height);
}
~Person(){
//析构代码,将堆区开辟数据做释放操作
if(m_Height!=NULL){
delete m_Height;
m_Height=NULL;
}
cout<<"Person析构函数调用"<<endl;
}
int m_Age;
int *m_Height
};

void test01(){
Person p1(18,160);
cout<<"p1的年龄为: "<<p1.m_Age<<"p1的身高为:"<<*p1.m_Height<<endl;
Person p2(p1); //编译器给的拷贝是浅拷贝操作
cout<<"p2的年龄为:"<<p2.m_Age<<"p2的身高为:"<<*p2.m_Height<<endl;
}

int main(){
test01();
return 0;
}

总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

初始化列表

作用

C++提供了初始化列表语法,用来初始化属性

语法构造函数():属性1(值1),属性2(值2) …{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

class Person{
public:
//传统初始化操作
//Person(int a,int b,int c){
//m_A=a;
//m_B=b;
//m_C=c;
//}
//初始化列表初始化属性
Person(int a,int b,int c):m_A(a),m_B(b),m_C(c){

}
int m_A;
int m_B;
int m_C;
}

void test01(){
Person p(10,20,30); //传统给属性赋初值方法
Person p1(30,20,10); //初始化列表赋初值方法
}

int main(){

return 0;
}

类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为对象成员

例如:

1
2
3
4
class A{};
class B{
A a;
};

B类中有对象作为一个成员,A为对象成员

那么创建B对象时,A与B的构造和析构的顺序是谁先谁后?

先构造A对象再构造B对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Phone{
public:

Phone(string PName){
m_PName=PName;
cout<<"Phone的构造函数调用"<<endl;
}
~Phone(){
cout<<"Phone的析构函数调用"<<endl;
}
//手机品牌名称
string m_PName;

};


class Person{
public:
Person(string name,string pName):m_Name(name),m_Phone(pName){
cout<<"Person构造函数调用"<<endl;
}
~Person(){
cout<<"Person析构函数调用"<<endl;
}
//姓名
string m_Name;
//手机
Phone m_Phone;
};

void test01(){
Person p("张三","苹果MAX");
}

总结:当其他类对象作为本类成员,我们称该成员为对象成员,构造顺序是:先调用对象成员的构造,再调用本类构造,析构顺序与构造相反。

静态成员

静态成员就是在成员变量和成员函数前加上关键词static,称为静态成员

静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化(必须的操作)
  • 静态成员函数
    • 所有成员共享同一个函数
    • 静态成员函数只能访问静态成员变量

静态成员变量:

注意事项:静态成员变量也是有访问权限的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//静态成员变量
class Person{
public:
//所有对象都共享同一份数据
static int m_A;
};

int Person::m_A=100; //类外初始化

void test01(){
Person p;
cout<<p.m_A<<endl;
Person p2;
p2.m_A=200;
cout<<p.m_A<<endl; //所有对象共享同一份数据,会输出200
}

void test02(){
//静态成员变量不属于某个对象上,所有对象都共享同一分数据
//1.通过对象进行访问
Person p;
cout<<p.m_A<<endl;
//2.通过类名进行访问
cout<<Person::m_A<<endl;
}

静态成员函数:

注意事项:静态成员函数也是有访问权限的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//静态成员函数
class Person{
public:
//静态成员函数
static void func(){
m_A=100;//静态成员函数可以访问静态成员变量
m_B=200;//静态成员函数不可以访问非静态成员变量,无法区分是哪个对象的属性
cout<<"static void func函数的调用"<<endl;
}

static int m_A;
int m_B;//非静态成员变量
};

int Person::m_A=0;

void test01(){
//通过对象访问
Person p;
p.func();
//通过类名访问
Person::func();
}

C++对象模型和this指针

成员变量和成员函数分开存储

在C++中,类内的成员变量和成员函数分开存储

只有非静态成员变量才属于类的对象上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person{
public:
int m_A; //非静态成员变量属于类的对象上的
static int m_B; //静态成员变量不属于类的对象上
void func(){}//非静态成员函数不属于类的对象上的
static void func2(){}//静态成员函数不属于类的对象上的
};

int Person::m_B=0;

void test01(){
Person p;
//空对象占用空间为:1
//C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
//每个空对象也应该有一个独一无二的内存地址
cout<<"size of p= "<<sizeof(p)<<endl;
}

void test02(){
Person p;
//非空对象占用空间为:根据类对象非静态成员变量的字节数计算
cout<<"size of p= "<<sizeof(p)<<endl;
}

this指针

通过上一个章节我们知道在C++中成员变量和成员函数是分开存储的

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

概念

C++通过提供特殊的对象指针—-this指针,来进行区分哪个对象调用哪一块代码,this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

this指针的用途

  • 解决名称冲突:当形参和成员变量同名时,可用this指针来区分
  • 返回对象本身:在类的非静态成员函数中返回对象本身,可使用*return this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person{
public:
Person(int age){
//this指针指向被调用的成员函数所属的对象
this->age=age;
}
Person& PersonAddAge(Person &p){ //如果返回是值的类型会发生拷贝函数,最后无法对对象属性进行修改,所以得用引用的方式返回
this->age+=p.age;
return *this; //返回执行完函数代码的对象本身
}
int age;
};

//解决名称冲突
void test01(){
Person p1(18);
cout<<"p1的年龄为:"<<p1.age<<endl;
}

//返回对象本身
void test02(){
Person p1(10);
Person p2(10);
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);//多次调用,可以函数返回对象本身然后就可以继续调用
cout<<"p2的年龄为:"<<p2.age<<endl;
}

空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针

如果用到this指针,需要加以判断保证代码的健壮性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
public:
void showClassName(){
cout<<"this is Person class"<<endl;
}
void showPersonAge(){
if(this==NULL){
return;
}
cout<<"age = "<<m_Age<<endl;
}
int m_Age;
};

void test01(){
Person *p=NULL;
p->showClassName();
p->showPersonAge();//报错,传入的指针是空指针,无法访问类的属性
}

const修饰成员函数

常函数

  • 成员函数后加入const后我们称这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键词mutable时,在常函数和常对象中依然可以修改

常对象

  • 声明对象前加const后称该对象为常对象
  • 常对象只能调用常函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person{
public:
//this指针的本质是指针常量,指针的指向是不可以修改的
void showPerson() const{ //常函数
this->m_B=100;
//this->m_A=100;
//this=NULL; this指针不可以修改指针的指向的
}
void func(){
m_A=100;
}
int m_A;
mutable int m_B;//特殊变量,即使在常函数中,也可以修改这个值
};

void test01(){
Person p;
p.showPerson();
}
void test02(){
//常对象
const Person p;//在对象前加const,变为常对象
p.m_A=100;//报错
p.m_B=100;//mutable的变量在常对象下也可以调用
//常对象只能调用常函数
p.showPerson();
p.func();//报错,常对象不可以调用普通成员函数
}

友元

在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术

友元的目的就是让一个函数或者类访问另一个类中私有成员

友元的关键词为 friend

友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

全局函数做友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Building{
//test01全局函数可以访问Building的私有成员
friend void test01(Building *building);
public:
Building(){
m_SittingRoom="客厅";
m_BedRoom="卧室";
}
string m_SittingRoom;
private:
string m_BedRoom;
};

//全局函数
void test01(Building *building){
cout<<"全局函数正在访问:"<<building->m_SittingRoom<<endl;
cout<<"全局函数正在访问:"<<building->m_BedRoom<<endl; //私有属性
}

void test02(){
Building building;
test01(&building);
}

类做友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Building;
class gy{
public:
gy();
void visit();//参观函数访问Building中的属性
Building *building;
};

class Building{
friend class gy; //可以访问Building的私有成员
public:
Building();
string m_SittingRoom;
private:
string m_BedRoom;
};

//类外写成员函数
Building::Building(){
m_SttingRoom="客厅";
m_BedRoom="卧室";
}

gy::gy(){
building=new Building;
}

void gy::visit(){
cout<<"gy类正在访问:"<<building->m_SttingRoom<<endl;
cout<<"gy类正在访问:"<<building->m_BedRoom<<endl;
}

void test01(){
gy gg;
gg.visit();
}

成员函数做友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Building;
class gy{
public:
gy();
void visit();//让visit函数可以访问building中私有成员
void visit1();//让其不可以访问Building中的私有成员
Building *building;
};

class Building{
//gy类下的visit函数可以访问building的私有属性
friend void gy::visit();
public:
Building();
string m_SttingRoom;
private:
string m_BedRoom;
};

Building::Building(){
m_SttingRoom="客厅";
m_BedRoom="卧室";
}

gy::gy(){
building=new Building;
}

void gy::visit(){
cout<<"visit函数正在访问:"<<building->m_SttingRoom<<endl;
cout<<"visit函数正在访问:"<<building->m_BedRoom<<endl;
}

void gy::visit1(){
cout<<"visit1函数正在访问:"<<building->m_SttingRoom<<endl;
cout<<"visit函数正在访问:"<<building->m_BedRoom<<endl;//不可以访问
}

void test01(){
gy gg;
gg.visit();
gg.visit1();
}

运算符重载

运算符重载概念对已有的运算符重新定义,赋予其另一种功能,以适应不同的数据类型

注意事项:运算符重载也可以发生函数重载

operator运算符作为运算符的重载的运算符标记

加号运算符重载

作用:实现两个自定义数据类型相加的运算

重载实现方法:

  • 通过成员函数重载+号
  • 通过全局函数重载+号

用**operator+**作为加号运算符的重载的运算符标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Person{
public:
//成员函数重载+号
Person operator+(Person &p){
Person temp;
temp.m_A=this->m_A+p.m_A;
temp.m_B=this->m_B+p.m_B;
return temp;
};
int m_A;
int m_B;
};
//全局函数重载+号
Person operator+(Person &p1,Person &p2){
Person temp;
temp.m_A=p1.m_A+p2.m_A;
temp.m_B=p1.m_B+p2.m_B;
return temp;
}

//函数重载版本
Person operator+(Person &p1,int num){
Person temp;
temp.m_A=p1.m_A+num;
temp.m_B=p1.m_B+num;
return temp;
}

void test01(){
Person p1;
p1.m_A=10;
p1.m_B=10;
Person p2;
p2.m_A=10;
p2.m_B=10;
//本质调用
Person p3=p1.operator+(p2);
//简化调用
Person p3=p1+p2;
//运算符重载也可以发生函数重载
Person p4=p1+10;
cout<<"p3.m_A= "<<p3.m_A<<endl;
cout<<"p3.m_B= "<<p3.m_B<<endl;
}

总结

  • 对于内置的数据类型的表达式的运算符是不可能改变的
  • 不要滥用运算符重载

左移运算符重载

作用:可以输出自定义数据类型

重载实现方法:

  • 只能通过全局函数重载左移运算符

注意事项:重载的成员函数要想访问类的私有成员,就得利用友元技术

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person{
friend ostream &operator<<(ostream &cout,Person &p);
public:
Person(int a,int b){
m_A=a;
m_B=b;
}
private:
int m_A:
int m_B;
};

//只能利用全局函数重载左移运算符
ostream &operator<<(ostream &cout,Person &p){
cout<<"m_A= "<<p.m_A<<" m_B= "<<p.m_B;
return cout;
}

void test01(){
Person p(10,10);
cout<<p<<endl;
}

总结:重载左移运算符配合友元可以实现输出自定义数据类型

递增运算符重载

作用:通过重载递增运算符,实现自己的整型数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class MyInteger{
friend ostream operator<<(ostream &cout,MyInteger myint)
public:
MyInteger(){
m_Num=0;
}
//重载前置++运算符,前置返回引用
MyInteger& operator++(){
m_Num++;
return *this
}
//重载后置++运算符,后置返回值
//int代表的占位参数,用于区分前置和后置递增
MyInteger operator++(int){
//先记录当时结果
MyInteger temp=*this;
//后递增
m_Num++;
//最后将记录结果做返回操作
return temp;
}
private:
int m_Num;
};

//重载左移运算符
ostream operator<<(ostream &cout,MyInteger myint){
cout<<myint.m_Num;
return cout;
}

void test01(){
MyInteger myint;
cout<<myint<<endl;
}

void test02(){
MyInteger myint;
cout<<myint++<<endl;
cout<<myint<<endl;
}

总结:前置递增返回引用,后置递增返回值

赋值运算符重载

C++编译器至少给一个类添加4个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符operator=,对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题(堆区内存重复释放,导致程序崩溃)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Person{
public:
Person(int age){
m_Age=new int(age);
}
~Person(){
if(m_Age!=NULL){
delete m_Age;
m_Age=NULL;
}
}
//重载赋值运算符
Person& operator=(Person &p){
//编译器提供浅拷贝
//m_Age=p.m_Age;
//应先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
if(m_Age!=NULL){
delete m_Age;
m_Age=NULL;
}
//深拷贝
m_Age=new int(*p.m_Age);
//返回对象本身
return *this;
}
int *m_Age;
};

void test01(){
Person p1(18);
Person p2(20);
Person p3(30);
p3=p2=p1; //赋值操作
cout<<"p1的年龄:"<<*p1.m_Age<<endl;
cout<<"p2的年龄:"<<*p2.m_Age<<endl;
cout<<"p3的年龄:"<<*p3.m_Age<<endl;
}

关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Person{
public:
Person(string name,int age){
m_Name=name;
m_Age=age;
}
bool operator==(Person &p){
if(this->m_Name==p.m_Name&&this->m_Age==p.m_Age){
return true;
}
return false;
}
bool operator!=(Person &p){
if(this->m_Name!=p.m_Name&&this->m_Age!=p.m_Age){
return false;
}
return true;
}
string m_Name;
int m_Age;
};

void test01(){
Person p1("Tom",18);
Person p2("Jerry",18);
if(p1==p2){
cout<<"p1和p2是相等的"<<endl;
}else{
cout<<"p1和p2不相等"<<endl;
}
if(p1!=p2){
cout<<"p1和p2不相等"<<endl;
}else{
cout<<"p1和p2是相等的"<<endl;
}
}

函数调用运算符重载(仿函数)

  • 函数调用运算符()也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MyPrint{
public:
//重载函数调用运算符
void operator()(string test){
cout<<test<<endl;
}
};

void test01(){
MyPrint myprint;
//由于使用非常像函数调用,因此称为仿函数
myprint("hello world");
}

//仿函数非常灵活,没有固定的写法
class MyAdd{
public:
int operator()(int num1,int num2){
return num1+num2;
}
};

void test02(){
MyAdd myadd;
int ret=myadd(100,100);
cout<<"ret= "<<ret<<endl;

//匿名函数对象
cout<<MyAdd()(100,100)<<endl;
}

继承

继承是面向对象的三大特性之一

我们发现,定义一些类时,下级别的成员除了拥有上一级的共性,还有自己的特性

这个时候我们就考虑利用继承的技术,减少重复代码

继承的基本语法

语法class 子类:继承方式 父类类名

子类 也成为派生类

父类 也称为基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//继承实现页面
//公共页面类
class BasePage{
public:
void header(){
cout<<"首页、公开课(公共头部)"<<endl;
}
void footer(){
cout<<"帮助中心...(公共脚部)"<<endl;
}
};

//继承语法
class Java:public BasePage{
public:
void content(){
cout<<"Java学科视频"<<endl;
}
};
//Python页面
class Python:public BasePage{
public:
void content(){
cout<<"python学科视频"<<endl;
}
};

//c++页面
class Cpp:public BasePage{
public:
void content(){
cout<<"C++学科视频"<<endl;
}
};

void test01(){
cout<<"Java下载视频页面如下:"<<endl;
Java ja;
ja.header();
ja.footer();
ja.content();
cout<<"python下载视频页面如下:"<<endl;
Python py;
py.header();
py.footer();
py.content();
cout<<"C++下载视频页面如下:"<<endl;
Cpp cpp;
cpp.header();
cpp.footer();
cpp.content();
}

总结

继承的好处:可以减少重复的代码

派生类中的成员,包含两大部分:

  • 一类是从基类继承过来的,从基类继承过来的表现其共性
  • 一类是自己增加的成员,新增的成员体现其个性

继承方式

继承方式一共有三种

  • 公共继承
    • 父类的公共权限到子类依然是公共权限
    • 父类的保护权限到子类中依然是保护权限
    • 父类中的私有权限成员子类访问不到
  • 保护继承
    • 父类中公共成员到子类中,变为保护权限
    • 父类中保护成员到子类中,依然为保护权限
    • 父类中的私有权限成员子类访问不到
  • 私有继承
    • 父类的公共成员到子类变为私有成员
    • 父类的保护成员到子类变为私有成员
    • 父类中的私有权限成员子类访问不到

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Base1{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};

//公共继承
class Son1:public Base1{
public:
void func(){
m_A=10; //父类的公共权限到子类依然是公共权限
m_B=10; //父类的保护权限到子类中依然是保护权限
//m_C=10; //父类中的私有权限成员子类访问不到
}
};

//保护继承
class Son2:protected Base1{
public:
void func(){
m_A=100; //父类中公共成员到子类中,变为保护权限
m_B=100; //父类中保护成员到子类中,变为保护权限
//m_C=10; //父类中的私有权限成员子类访问不到
}
};

//私有继承
class Son3:private Base1{
public:
m_A=100; //父类的公共成员到子类变为私有成员
m_B=100; //父类的保护成员到子类变为私有成员
//m_C=10; //父类中的私有权限成员子类访问不到
};

void test01(){
Son1 s1;
Son2 s2;
Son3 s3;
s1.m_A=100;
//s1.m_B=100; 保护权限类外不能访问
//s2.m_A=1000; 保护权限类外不能访问
//s3.m_A=1000; //私有权限类外不能访问
}

继承中的对象模型

父类的所有非静态成员属性,子类都继承下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};

class Son:public Base{
public:
int m_D;
};

void test01(){
//结果为16,父类的所有属性,子类都继承下来
cout<<"size of Son = "<<sizeof(Son)<<endl;
}

总结:父类的私有属性也被子类继承下来了,只是访问不到,但是依然存在于子类中

继承中的构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Base{
public:
Base(){
cout<<"Base构造函数"<<endl;
}
~Base(){
cout<<"Base析构函数"<<endl;
}
};

class Son:public Base{
public:
Son(){
cout<<"Base构造函数"<<endl;
}
~Son(){
cout<<"Base析构函数"<<endl;
}
};


void test01(){
//Base b;
Son s;
}

总结

先构造父类,再构造子类,析构顺序与构造顺序相反

继承同名成员处理方式

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域

访问父类同名成员语法:

子类实例对象.父类名称::要调用的父类同名成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Base{
public:
Base(){
m_A=100;
}
void func(){
cout<<"Base-func函数调用"<<endl;
}
void func(int a){
cout<<"Base-func(int)函数调用"<<endl;
}
int m_A;
};


class Son:public Base{
public:
Son(){
m_A=200;
}
void func(){
cout<<"Son-func函数调用"<<endl;
}
void func(int a){
cout<<"Son-func(int)函数调用"<<endl;
}
int m_A;
};
//同名成员属性处理
void test01(){
Son s;
cout<<"Son 下m_A"<<s.m_A<<endl;
cout<<"Son 下m_A"<<s.Base::m_A<<endl;
}

//同名成员函数处理
void test02(){
Son s;
s.func();
s.Base::func();
}

总结

  • 子类对象可以直接访问到子类中同名成员
  • 子类对象加作用域可以访问到父类同名成员
  • 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

继承同名静态成员处理方式

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Base{
public:
static int m_A;
static void func(){
cout<<"Base-static func的调用"<<endl;
}
};

int Base::m_A=100;

class Son:public Base{
public:
static int m_A;
static void func(){
cout<<"Son-static func的调用"<<endl;
}
};
int Son::m_A=200;

//同名静态成员属性
void test01(){
Son s;
//1.通过对象访问静态成员
cout<<"Son m_A="<<s.m_A<<endl;
cout<<"Base m_A="<<s.Base::m_A<<endl;
//2.通过类名访问静态成员
cout<<"通过类名访问静态成员"<<endl;
cout<<"Son m_A="<<Son::m_A<<endl;
cout<<"Base m_A="<<Son::Base::m_A<<endl;
}

//同名静态成员函数
void test02(){
Son s;
s.func();
s.Base::func();
Son::func();
Son::Base::func();
}

总结

同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和通过类名)

多继承语法

C++中允许一个类继承多个类

语法

class 子类:继承方式 父类1,继承方式 父类2…

注意事项

  • 多继承可能会引发父类中有同名成员出现,需要加作用域区分
  • C++实际开发中不建议用多继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Base1{
public:
Base(){
m_A=100;
}
int m_A;
};

class Base2{
public:
Base2(){
m_A=200;
}
int m_A;
};

class Son:public Base1,public Base2{
public:
Son(){
m_C=300;
m_D=400;
}
int m_C;
int m_D;
};

void test01(){
Son s;
cout<<"sizeof Son="<<sizeof(s)<<endl;
cout<<"m_A="<<s.Base1::m_A<<endl;
}

菱形继承

概念

两个派生类继承同一个基类,又有某个类同时继承着两个派生类,这种继承被称为菱形继承,或者钻石继承

问题

同名成员产生二义性,并且继承两个派生类的类会继承两份来自同一个基类的数据(资源浪费),但是实际只需要一份就行了

解决方法

利用虚继承解决菱形继承的问题,语法class 子类:virtual public 父类{};,则父类被称为虚基类,进行了虚继承(底层利用虚基类指针,让两个类都指向同一份数据),则继承的数据是共享的,更改两份继承下来的数据就是更改一份数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Animal{
public:
int m_Age;
};

class Sheep:virtual public Animal{

};

class Tuo:virtual public Animal{

};

class SheepTuo:public Sheep,public Tuo{

};

void test01(){
SheepTuo st;
st.Sheep::m_Age=18;
st.Tuo::m_Age=28;
//输出为28
cout<<"st.Sheep::m_Age="<<st.Sheep::m_Age<<endl;
//输出为28
cout<<"st.Tuo::m_Age= "<<st.Tuo::m_Age<<endl;
//数据只需要一份

}

多态

多态的基本概念

多态是C++面向对象的三大特性之一

多态分为两类

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名
  • 动态多态:派生类和虚函数(加virtual关键词)实现运行时多态

静态多态和动态多态的区别

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Animal{
public:
//虚函数,加入virtual关键词
virtual void speak(){
cout<<"动物在说话"<<endl;
}
};

class Cat:public Animal{
public:
void speak(){
cout<<"小猫在说话"<<endl;
}
};

//没加virtual关键词,地址早绑定,在编译阶段就确定函数地址
//如果想执行让猫说话,则函数地址不能提前绑定,需要晚绑定
void doSpeak(Animal &animal){
animal.speak();
}

void test01(){
Cat cat;
doSpeak(cat);//执行动物在说话
}

总结

动态多态满足条件:

  1. 有继承关系
  2. 子类有重写父类的虚函数

动态多态的使用:父类的指针或引用指向子类对象

原理剖析

image-20240620174711199

多态的优点

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期扩展以及维护

总结:C++开发提倡利用多态设计程序架构,因为多态的优点很多

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

语法virtual 返回值类型 函数名 (参数列表) = 0;

当类中有了纯虚函数,这个类也成为抽象类

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//抽象类
class Base{
public:
//纯虚函数
virtual void func()=0;
};

class Son:public Base{
public:
void func(){
cout<<"Son func函数调用"<<endl;
}
};

void test01(){
//Base b; 抽象类无法实例化对象
Son s;//如果没重写父类(抽象类)的纯虚函数,则该子类也为抽象类
}

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象的问题
  • 都需要有具体的函数实现

虚析构和纯虚析构区别

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法virtual ~类名(){}

纯虚析构语法

virtual ~类名()=0; 类外:类名::~类名(){}

注意事项:需要声明,也需要具体的实现代码(在类外实现),有了纯虚析构函数之后,这个类也属于抽象类,无法实例化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Animal{
public:
Animal(){
cout<<"Animal构造函数调用"<<endl;
}
//利用虚析构可以解决父类指针释放子类对象时不干净的问题
virtual ~Animal(){
cout<<"Animal析构函数调用"<<endl;
}
//纯虚析构函数
virtual ~Animal()=0;
virtual void speak()=0;
};
//纯虚析构也需要代码实现
Animal::~Animal(){
cout<<"Animal纯虚析构函数调用"<<endl;
}

class Cat:public Animal{
public:
virtual void speak(){
cout<<*m_Name<<"小猫在说话"<<endl;
}
Cat(string name){
cout<<"cat构造函数调用"<<endl;
m_Name=new string(name);
}
~Cat(){
if(m_Name!=NULL){
cout<<"cat析构函数调用"<<endl;
delete m_Name;
m_Name=NULL;
}
}
string *m_Name;
};

void test01(){
Animal *animal new Cat("Tom");
animal->speak();
//父类指针在析构时候不会调用子类中析构函数,导致子类如果有堆区属性,会出现内存泄漏
delete animal;
}

总结

  1. 虚析构或纯虚析构就是用来解决通过父类指针释放的子类对象
  2. 如果子类中没有堆区数据,可以不写虚析构或纯虚析构函数
  3. 拥有纯虚析构函数的类也属于抽象类

文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放

通过文件可以将数据持久化

C++中对文件操作需要包含头文件****

文件类型分为两种:

  • 文本文件 - 文件以文本的ASCII码形式存储在计算机中
  • 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

  • ofstream:写操作
  • ifstream:读操作
  • fstream:读写操作

文本文件

写文件

写文件步骤如下:

  1. 包含头文件

    1
    #include <fstream>
  2. 创建流对象

    1
    ofstream ofs;
  3. 打开文件

    1
    ofs.open("文件路径",打开方式);
  4. 写数据

    1
    ofs<<"写入的数据";
  5. 关闭文件

    1
    ofs.close();

文件打开方式

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

注意:文件打开方式可以配合使用,利用*|*操作符

例如:用二进制方式写文件ios::binary|ios::out

读文件

读文件与写文件步骤相似,但是读取方式相对于比较多

读文件步骤如下

  1. 包含头文件

    1
    #include <fstream>
  2. 创建流对象

    1
    ifstream ifs;
  3. 打开文件并判断文件是否打开成功

    1
    ifs.open("文件路径",打开方式);
  4. 读数据

    四种方式读取

  5. 关闭文件

    1
    ifs.close();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
ifstream ifs;
ifs.open("test.txt",ios::in);
if(!ifs.is_open()){
cout<<"文件打开失败"<<endl;
return 0;
}
//读数据
//第一种
// char buf[1024]={0};
// while(ifs>>buf){
// cout<<buf<<endl;
// }

//第二种
// char buf[1024]={0};
// while(ifs.getline(buf,sizeof(buf))){
// cout<<buf<<endl;
// }

//第三种
// string buf;
// while (getline(ifs,buf)){
// cout<<buf<<endl;
// }

//第四种,不推荐
char c;
while((c=ifs.get())!=EOF){ //EOF end of file
cout<<c;
}
ifs.close();
return 0;
}

总结

  • 读文件可以利用ifstream,或者fstream类
  • 利用is_open函数可以判断文件是否打开成功
  • close关闭文件

二进制文件

以二进制的方式对文件进行读写操作

打开方式要指定为iso::binary

写文件

二进制方式写文件主要利用流对象调用成员函数write

函数原型:ostream& write(const char * buffer,int len);

参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <fstream>
using namespace std;

class Person{
public:
char m_Name[64];
int m_Age;
};

void test01() {
ofstream ofs("test1.txt",ios::out|ios::binary);
// ofs.open("test1.txt",ios::out|ios::binary);
Person p={"张三",18};
//要把数据地址转化成const char*
ofs.write((const char*)&p,sizeof(Person));
ofs.close();
}

int main() {
test01();
return 0;
}

读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型:istream& read(char *buffer,int len);

参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <fstream>
using namespace std;

class Person{
public:
char m_Name[64];
int m_Age;
};

void test01(){
ifstream ifs("test1.txt",ios::in|ios::binary);
if(!ifs.is_open()){
cout<<"文件打开失败"<<endl;
return;
}
Person p;
ifs.read((char*)&p, sizeof(Person));
cout<<p.m_Name<<" "<<p.m_Age<<endl;
ifs.close();
}

int main() {
test01();
return 0;
}

模板

概念

模板就是建立通用的模具,大大提高复用性

特点

  • 模板不可以直接使用,它只是一个框架
  • 模板的通用并不是万能的

函数模板

  • C++另一种编程思想称为泛型编程,主要利用的技术就是模板
  • C++提供两种模板机制:函数模板类模板

函数模板语法

作用

建立一个通用函数,其返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表

语法

1
2
template<typename T>
函数声明或定义

解释

template — 声明创建模板

typename — 表现其后面的符号是一种数据类型,可以用class代替

T — 通用的数据类型,名称可以替换,通常为大写字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//函数模板
template<typename T> //声明一个函数模板
void myswap(T &a,T &b){
T temp=a;
a=b;
b=a;
}

void swapInt(int &a,int &b){
int temp=a;
a=b;
b=temp;
}

void swapDouble(double &a,double &b){
double temp=a;
a=b;
b=temp;
}

void test01(){
int a=10;
int b=20;
//swapInt(a,b);
//利用函数模板交换
//两种方式使用函数模板
//1. 自动类型推导
myswap(a,b);
//2. 显示指定类型
myswap<int>(a,b);
cout<<"a= "<<a<<endl;
cout<<"b= "<<b<<endl;
double c=1.1;
double d=2.2;
cout<<"c= "<<c<<endl;
cout<<"d= "<<d<<endl;
}

总结

  • 函数模板利用关键词template
  • 使用函数模板有两种方式:自动类型推导、显示指定类型
  • 模板的目的是为了提高复用性,将类型参数化

函数模板注意事项

注意事项

  • 自动类型推导,必须推到出一致的数据类型T,才可以使用
  • 模板必须要确定出T的数据类型,才可以使用

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<class T>
void myswap(T&a,T&b){
T temp=a;
a=b;
b=temp;
}

//模板必须要确定出T的数据类型,才可以使用
template<class T>
void func(){
cout<<"func调用"<<endl;
}


//自动类型推导,必须推到出一致的数据类型T,才可以使用
void test01(){
int a=10;
int b=20;
char c='c';
myswap(a,b);
myswap(a,c);//错误,数据类型不同
func<int>();//如果函数没有用到T,调用时,也必须需要给出T才能调用
}

普通函数与函数模板的区别

普通函数和函数模板的区别

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//普通函数
int myAdd01(int a,int b){
return a+b;
}
//函数模板
template<class T>
T myAdd02(T a,T b){
return a+b;
}

void test01(){
int a=10;
int b=20;
char c='c';//普通函数调用会把传入类型进行一个强制转换
//自动类型推导
myAdd02(a,c);//模板函数使用自动类型推导调用不会发生强制转换
//显示指定类型
myAdd02<int>(a,c);//模板函数使用显示指定类型调用,能够发生类型转换的
cout<<myAdd01(a,c)<<endl;
}

总结

建议使用指定类型的方,调用函数模板,因为可以自己确定通用类型T

普通函数和函数模板的调用规则

调用规则如下

  • 如果函数模板和普通函数都可以实现,优先调用普通函数
  • 可以通过空模板参数列表来强制调用函数模板
  • 函数模板也可以发生重载
  • 如果函数模板可以产生更好的匹配,优先调用函数模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void myprint(int a,int b)
{
cout<<"调用的普通函数"<<endl;
}

template<class T>
void myprint(T a,T b){
cout<<"调用的模板"<<endl;
}

template<class T>
void myprint(T a,T b,T c){
cout<<"调用的重载模板"<<endl;
}

void test01(){
int a=10;
b=20;
myprint(a,b); //优先调用普通函数
//通过空模板参数列表,强制调用函数调用
myprint<>(a,b);
myprint(a,b,100);
//如果函数模板可以产生更好的匹配,优先调用函数模板
char c1='a';
char c2='b';
myprint(c1,c2); //普通函数需要转类型,而模板不需要,所以优先调用模板函数
}

总结

既然有了函数模板,最好就不要提供同名普通函数,否则容易出现二义性

模板的局限性以及具体化

局限性

  • 模板的通用性并不是万能的
1
2
3
4
template<class T>
void f(T a,T b){
a=b;
}

在上述代码中提供的赋值操作,如果传入的a和b是一个数组,就无法实现了

再例如:

1
2
3
4
5
6
template<class T>
void f(T a,T b){
if(a>b){
....
}
}

在上述代码中,如果T的数据类型传入是像Person这样自定义的数据类型,也无法正常运行

因此C++为了解决这种问题,提供了模板的重载,可以为这些特定的类型提供具体化的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Person{
public:
Person(string name,int age){
this->m_name=name;
this->m_age=age;
}
string m_name;
int m_age;
};


template<class T>
bool mycompare(T &a,T &b){
if(a==b){
return true;
}else{
return false;
}
}
//利用具体化自定义类型的版本实现代码,具体化优先调用
template<>
bool mycompare(Person &p1,Person &p2){
if(p1.m_name==p2.m_name&&p1.m_age==p2.m_age){
return true;
}else{
return false;
}
}

void test01(){
int a=10;
int b=20;
bool ret=mycompare(a,b);
if(ret){
cout<<"a==b"<<endl;
}else{
cout<<"a!=b"<<endl;
}
}

void test02(){
Person p1("Tom",10);
Person p2("Tom",10);
bool ret=mycompare(p1,p2);
if(ret){
cout<<"p1==p2"<<endl;
}else{
cout<<"p1!=p2"<<endl;
}
}

总结

  • 利用具体化的模板,可以解决自定义类型的通用化
  • 学习模板并不是为了写模板,而是在STL能够运用系统提供的模板

类模板

类模板语法

作用

  • 建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟的类型来代表

语法:template<class T>

​ 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class nametype,class agetype>
class Person{
public:
Person(nametype name,agetype age){
this->m_name=name;
this->m_age=age;
}
void show(){
cout<<"name: "<<this->m_name<<"age= "<<this->m_age<<endl;
}
nametype m_name;
agetype m_age;
};


void test01(){
Person<string,int> p1("孙悟空",999);
}

类模板与函数模板区别

区别

  • 类模板没有自动类型推导的使用方式
  • 类模板在模板参数列表中可以有默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//agetype默认参数为int
template<class nametype,class agetype=int>
class Person{
public:
Person(nametype name,agetype age){
this->m_name=name;
this->m_age=age;
}
void show(){
cout<<"name: "<<this->m_name<<"age= "<<this->m_age<<endl;
}
nametype m_name;
agetype m_age;
};


void test01(){
Person p("孙悟空",999); //报错,类模板是没有自动类型推导
Person p1<string,int>p1("孙悟空",999);
Person p1<string>p1("孙悟空",999);//模板类型默认为int,所以不写就用int
p1.show();
}

总结

  • 类模板使用只能用显示指定类型方式
  • 类模板中的模板参数列表可以有默认参数

类模板中成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机是有区别的:

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Person1{
public:
void show1(){
cout<<"person1show"<<endl;
}
};

class Person2{
public:
void show2(){
cout<<"person2show"<<endl;
}
};

template<class T>
class myclass{
public:
T obj;
//类模板中成员函数
void func1(){
obj.show1();
}
void func2(){
obj.show2();
}
};

void test01(){
myclass<Person1>m;
m.func1();
//m.func2();
}

总结:类模板中的成员函数并不是一开始就创建的,在调用时才去创建

类模板对象做函数参数

类模板实例化出的对象,向函数传参的方式

三种传参方式:

  • 指定传入的类型 — 直接显示对象的数据类型
  • 参数模板化 — 将对象中的参数变为模板进行传递
  • 整个类模板化 — 将这个对象类型模板化进行传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
template<class T1,class T2=int>
class Person{
public:
Person(T1 name,T2 age){
this->m_name=name;
this->m_age=age;
}
void show(){
cout<<"name: "<<this->m_name<<"age= "<<this->m_age<<endl;
}
T1 m_name;
T2 m_age;
};

void test01(){
Person<string,int>p("孙悟空",100);
Person<string,int>p1("孙悟空",100);
Person<string,int>p2("孙悟空",100);
printp1(p);
printp2(p1);
printp3(p2);
}

//指定传入类型(最常用)
void printp1(Person<string,int>&p){
p.show();
}

//参数模板化
template<class T1,class T2>
void printp2(Person<T1,T2>&p){
p.show();
cout<<"T1的类型为:"<<typeid(T1).name()<<endl; //看推导出来的T1是什么类型
}

//整个类模板化
template<class T>
void printp3(T &p){
p.show();
}

总结

  • 通过类模板创建的对象,可以有三种方式向函数中进行传参
  • 使用比较广泛的是第一种:指定传入的类型

类模板与继承

当类模板碰到继承时,需要注意一下几点:

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定出父类中T的类型,子类也需要变为类模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template<class T>
class Base{
public:
T m;
};

//class Son:public Base //错误,c++编译器需要给子类分配内存,必须知道父类中T的类型才可以向下继承
class Son:public Base<int>{};

//如果想灵活指定出父类中T的类型,子类也需要变为类模板
template<class T1,class T2>
class Son2:public Base<T2>{
public:
Son2(){
cout<<"T1的类型为:"<<typeid(T1).name()<<endl;
cout<<"T2的类型为:"<<typeid(T2).name()<<endl;
}
T1 obj;
};

void test01(){
Son s1;
Son2<int,char>s2;

}

类模板成员函数类外实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template<class T1,class T2>
class Person{
public:
Person(T1 name,T2 age);
void show();
T1 m_name;
T2 m_age;
};

//构造函数的类外实现
template<class T1,class T2>
Person<T1,T2>::Person(T1 name,T2 age){
this->m_name=name;
this->m_age=age;
}

//成员函数的类外实现
template<class T1,class T2>
void Person<T1,T2>::show(){
cout<<"name: "<<this->m_name<<"age= "<<this->m_age<<endl;
}

void test01(){
Person<string,int>P("tom",20);
p.show();
}

总结:类模板中成员函数类外实现时,需要加入模板的参数列表

类模板分文件编写

类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

解决方法

  1. 直接包含.cpp源文件
  2. 将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制

分文件编写的.h写法

1
2
3
#pragma once  //防止重复
#include <iostream>
using namespace std;

示例:

//第一种解决方法

Person.h代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once 
#include <iostream>
using namespace std;
#include <string>

template<class T1,class T2>
class Person{
public:
Person(T1 name,T2 age);
void show();
T1 m_name;
T2 m_age;
};

Person.cpp代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include "Person.h"

template<class T1,class T2>
Person<T1,T2>::Person(T1 name,T2 age){
this->m_name=name;
this->m_age=age;
}

template<class T1,class T2>
void Person<T1,T2>::show(){
cout<<"name: "<<this->m_name<<"age= "<<this->m_age<<endl;
}

main.cpp代码:

1
2
3
4
5
6
7
8
9
10
11
#include "Person.cpp"

void test01(){
Person<string,int>P("tom",20);
P.show();
}

int main(){
test01();
return 0;
}

//第二种解决方法

将.h和.cpp中的内容写到一起,将后缀名改为.hpp文件

.hpp被称为类模板

Person.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once 
#include <iostream>
using namespace std;
#include <string>

template<class T1,class T2>
class Person{
public:
Person(T1 name,T2 age);
void show();
T1 m_name;
T2 m_age;
};

template<class T1,class T2>
Person<T1,T2>::Person(T1 name,T2 age){
this->m_name=name;
this->m_age=age;
}

template<class T1,class T2>
void Person<T1,T2>::show(){
cout<<"name: "<<this->m_name<<"age= "<<this->m_age<<endl;
}

类模板与友元

类模板模板配合友元函数的类内和类外实现

全局函数类外实现 - 直接在类内声明友元即可

全局函数类外实现 - 需要提前让编译器知道全局函数的存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//类外实现,需要让编译器提前知道Person类的存在
template<class T1,class T2>
class Person;


//通过全局函数打印Person信息
template<class T1,class T2>
class Person{
//全局函数类内实现
friend void print(Person<T1,T2>p){
cout<<"name: "<<p.m_name<<"age= "<<p.m_age<<endl;
}
//全局函数类外实现
//需要加空模板参数列表
//如果全局函数是类外实现的话,需要提前让编译器知道全局函数的存在
friend void print1<>(Person<T1,T2>p);
public:
Person(T1 name,T2 age){
this->m_name=name;
this->m_age=age;
}
private:
T1 m_name;
T2 m_age;
};

template<class T1,class T2>
void print(Person<T1,T2> p){
cout<<"类外实现name: "<<p.m_name<<"age= "<<p.m_age<<endl;
}

void test01(){
Person<string,int>p("tom",20);
print(p);
print1(p);
}

总结:建议全局函数做类内实现,用法简单,而且编译器可以直接识别

类模板案例

案例描述:实现一个通用的数组类,要求如下:

  • 可以对内置数据类型以及自定义数据类型的数据进行存储
  • 将数组中的数据存储在堆区
  • 构造函数中可以传入数组的容量
  • 提供对应的拷贝构造函数以及operator=防止浅拷贝问题
  • 提供尾插法和尾删法对数组中的数据进行增加和删除
  • 可以通过下标的方式访问数组中的元素
  • 可以获取数组中当前元素个数和数组的容量

myArray.hpp代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#pragma once
#include <iostream>
using namespace std;

template<class T>
class myArray{
public:
//有参构造
myArray(int capacity){
this->m_capacity=capacity;
this->m_size=0;
this->paddress=new T[this->m_capacity];
}
//拷贝构造,深拷贝
myArray(const myArray&arr){
this->m_capacity=arr.m_capacity;
this->m_size=arr.m_size;
this->paddress=new T[arr.m_capacity];
//将arr中的数组都拷贝过来
for(int i=0;i< this->m_size;i++){
this->paddress[i]=arr.paddress[i];
}
}

//operator=防止浅拷贝问题
myArray& operator=(const myArray& arr){
//先判断原来堆区是否有数据,如果有先释放
if(this->paddress!=NULL){
delete[] this->paddress;
this->paddress=NULL;
this->m_capacity=0;
this->m_size=0;
}
this->m_capacity=arr.m_capacity;
this->m_size=arr.m_size;
this->paddress=new T[arr.m_capacity];
for(int i=0;i< this->m_size;i++){
this->paddress[i]=arr.paddress[i];
}
return *this;
}

//尾插法
void push_back(const T& val){
//先判断容量是否等于大小
if(this->m_capacity== this->m_size){
return;
}
this->paddress[this->m_size]=val; //在数组末尾插入数据
this->m_size++; //更新数组大小
}

//尾删法,让用户访问不到最后一个元素,逻辑删除
void pop_back(){
if(this->m_size==0){
return;
}
this->m_size--;
}

//通过下标来访问数组中的元素,重载中括号,引用可做左值存在
T& operator[](int index){
return this->paddress[index];
}

//返回数组容量
int get_capacity(){
return this->m_capacity;
}

//返回数组大小
int get_size(){
return this->m_size;
}

//析构函数
~myArray(){
if(this->paddress!=NULL){
delete [] this->paddress;
this->paddress=NULL;
}
}

private:
T * paddress; //指针指向堆区开辟的真实的数组
int m_capacity; //数组容量
int m_size; //数组大小
};

myArray.cpp代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include "myArray.hpp"
using namespace std;


class Person {
public:
Person() {};
Person(string name, int age) {
this->m_name = name;
this->m_age = age;
}

string m_name;
int m_age;
};

void printarr(myArray<Person> &arr) {
for (int i = 0; i < arr.get_size(); i++) {
cout << "姓名:" << arr[i].m_name << "年龄:" << arr[i].m_age << endl;
}
}

void print(myArray<int> &arr) {
for (int i = 0; i < arr.get_size(); i++) {
cout << arr[i] << endl;
}
}

void test01() {
myArray<int>arr(5);
for (int i = 0; i < 5; i++) {
//利用尾插法,向数组插入数据
arr.push_back(i);
}
cout << "arr的打印输出:" << endl;
print(arr);
cout << "arr的容量:" << arr.get_capacity() << endl;
cout << "arr的大小:" << arr.get_size() << endl;
myArray<int>arr1(arr);
cout << "arr1的打印输出:" << endl;
print(arr1);
//尾删法
arr1.pop_back();
cout << "arr1的容量:" << arr1.get_capacity() << endl;
cout << "arr1的大小:" << arr1.get_size() << endl;
}

void test02() {
myArray<Person>arr(10);
Person p1("孙悟空", 999);
Person p2("韩信", 20);
Person p3("妲己", 10);
Person p4("安其拉", 27);
Person p5("赵云", 28);
arr.push_back(p1);
arr.push_back(p2);
arr.push_back(p3);
arr.push_back(p4);
arr.push_back(p5);
printarr(arr);
cout << "arr的容量:" << arr.get_capacity() << endl;
cout << "arr的大小:" << arr.get_size() << endl;
}


int main() {
// test01();
test02();
return 0;
}

STL初识

STL的诞生

  • 长久以来,软件界一直希望建立一种可重复利用的东西
  • C++的面向对象泛型编程思想,目的就是复用性的提升
  • 大多情况下,数据结构和算法都未能有一套标准,导致被迫从事大量重复工作
  • 为了建立数据结构和算法的一套标准,诞生了STL

STL基本概念

  • STL(Standard Template Library,标准模板库)
  • STL从广义上分为:容器(container) 算法(algorithm) 迭代器(iterator)
  • 容器算法之间通过迭代器进行无缝连接
  • STL几乎所有代码都采用了模板类或者模板函数

STL六大组件

STL大体分为六大组件,分别为:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器

  1. 容器:各种数据结构,如:vector、list、deque、set、map等,用来存放数据
  2. 算法:各种常用的算法,如:sort、find、copy、for_each等
  3. 迭代器:扮演了容器与算法之间的胶合剂
  4. 仿函数:行为类似函数,可作为算法的某种策略
  5. 适配器:一种用来修饰容器或者仿函数或者迭代器接口的东西
  6. 空间配置器:负责空间的配置与管理。

STL中容器、算法、迭代器

容器:置物之所也

STL容器就是将运用最广泛的一些数据结构闪现出来

常用的数据结构:数组,链表,树,栈,队列,集合,映射表等

这些容器分为序列式容器关联式容器两种:

  • 序列式容器:强调值的排序,序列式容器中的每个元素均有固定的位置
  • 关联式容器:二叉树结构,各元素之间没有严格的物理上的顺序关系

算法:问题之解法也

有限的步骤,解决逻辑或数学上的问题,这一门学科我们称为算法(Algorithms)

算法分为质变算法非质变算法

  • 质变算法:是指运算过程中会更改区间内的元素的内容,例如:拷贝,替换,删除等等
  • 非质变算法:是指运算过程中不会更改区间内的元素内容,例如:查找,计数,遍历,寻找极值等等

迭代器:容器和算法之间粘合剂

提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式

每个容器都有自己专属的迭代器

迭代器使用非常类似于指针,初学阶段我们可以先理解迭代器为指针

迭代器种类

种类 功能 支持运算
输入迭代器 对数据的只读访问 只读,支持++、==、!=
输出迭代器 对数据的只写访问 只写,支持++
前向迭代器 读写操作,并能向前推进迭代器 读写,支持++、==、!=
双向迭代器 读写操作,并能向前和向后操作 读写,支持++、–
随机访问迭代器 读写操作,可以以跳跃的方式访问任意数据,功能最强的迭代器 读写操作,支持++、–、[n]、-n、<、<=、>、>=

常用的容器中迭代器种类为双向迭代器,和随机访问迭代器

容器算法迭代器初识

STL中最常用的容器为Vector,可以理解为数组,下面我们将学习如何向这个容器插入数据,并遍历这个容器

vector存放内置数据类型

容器vector

算法for_each

迭代器:vector<int>::iterator

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <vector>
#include <alogrithm>
using namespace std;

void myprint(int val){
cout<<val<<endl;
}


void test01() {
//创建一个vector容器,数组
vector<int> v;
v.push_back(10); //尾插法,插入数据
v.push_back(20);
v.push_back(30);
//通过迭代器访问容器中的数据
//第一种遍历方式
vector<int>::iterator itBegin = v.begin(); //起始迭代器指向容器中第一个元素
vector<int>::iterator itEnd = v.end(); //结束迭代器,指向容器中最后一个元素的下一个位置
while (itBegin != itEnd) {
cout << *itBegin << endl;
itBegin++;
}
//第二种方法
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<endl;
}

//第三种方式,利用STL中的遍历算法
for_each(v.begin(),v.end(),myprint);
}


int main() {
test01();

return 0;
}

vector容器中存放自定义数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <vector>
using namespace std;


class Person{
public:
Person(string name,int age){
this->m_name=name;
this->m_age=age;
}
string m_name;
int m_age;
};

void test01(){
vector<Person> v;
Person p1("aaa",10);
Person p2("bbb",20);
Person p3("ccc",30);
Person p4("ddd",40);
Person p5("eee",50);
v.push_back(p1);
v.push_back(p2);
v.push_back(p3);
v.push_back(p4);
v.push_back(p5);
for(vector<Person>::iterator it=v.begin();it!=v.end();it++){
//cout<<"name:"<<(*it).m_name<<" age:"<<(*it).m_age<<endl;
cout<<"name:"<<it->m_name<<" age:"<<it->m_age<<endl;
}
}

void test02(){
vector<*Person> v;
Person p1("aaa",10);
Person p2("bbb",20);
Person p3("ccc",30);
Person p4("ddd",40);
Person p5("eee",50);
v.push_back(&p1);
v.push_back(&p2);
v.push_back(&p3);
v.push_back(&p4);
v.push_back(&p5);
for(vector<Person*>::iterator it=v.begin();it!=v.end();it++){
cout<<"name:"<<(*it)->m_name<<" age:"<<(*it)->m_age<<endl;
}
}

vector容器嵌套容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <vector>
using namespace std;

void test01(){
vector<vector<int>>v;
vector<int>v1;
vector<int>v2;
vector<int>v3;
vector<int>v4;
//向小容器中添加数据
for(int i=0;i<4;i++){
v1.push_back(i+1);
v2.push_back(i+2);
v3.push_back(i+3);
v4.push_back(i+4);
}
//将小容器插入到大容器中
v.push_back(v1);
v.push_back(v2);
v.push_back(v3);
v.push_back(v4);
//通过大容器,把所有数据遍历一遍
for(vector<vector<int>>::iterator it=v.begin();it!=v.end();it++){
//(*it) --- 容器vector<int>
for(vector<int>::iterator vit=(*it).begin();vit!=(*it).end();vit++){
cout<<*vit<<" ";
}
cout<<endl;
}
}

STL –常用容器

string容器

string基本概念

本质

  • string是C++风格的字符串,而string本质上是一个类

string和char*的区别

  • char*是一个指针
  • string是一个类,类内部封装了char*,管理这个字符串,是一个char指针型容器

特点

string类内部封装了很多成员的方法

例如:查找find,拷贝copy,删除delete,替换replace,插入insert

string管理char*所分配的内存,不用担心复制越界和取值越界等,由类内部进行负责

string构造函数

构造函数原型:

  • string(); //创建一个空的字符串,例如:string str;

    string(const char* s); //使用字符串s初始化

  • string(const string& str); //使用一个string对象初始化另一个string对象

  • string(int n,char c); //使用n个字符c初始化

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>
using namespace std;

//string的构造函数
void test01(){
string s1; //默认构造
const char *str="hello world";
string s2(str);
cout<<"s2= "<<s2<<endl;
string s3(s2);
cout<<"s3= "<<s3<<endl;
string s4(10,'a');
cout<<"s4= "<<s4<<endl;
}

string赋值操作

功能描述:

  • 给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); //把n个字符c赋给当前字符串

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <string>
using namespace std;

void test01(){
//第一种
string str1;
str1="hello world";
cout<<"str1= "<<str1<<endl;
//第二种
string str2;
str2=str1;
cout<<"str2= "<<str2<<endl;
//第三种
string str3;
str3='a';
cout<<"str3= "<<str3<<endl;
//第四种
string str4;
str4.assgin("hello c++");
cout<<"str4= "<<str4<<endl;
//第五种
string str5;
str5.assign("hello c++",5);
cout<<"str5= "<<str5<<endl;
//第六种
string str6;
str6.assign(str5);
cout<<"str6= "<<str6<<endl;
//第七种
string str7;
str7.assign(10,'w');
cout<<"str7= "<<str7<<endl;
}

总结:string的赋值方式狠多,operator=这种方式是比较实用的

string字符串拼接

功能描述:

  • 实现在字符串末尾拼接字符串

函数原型

  • string& operator+=(const char* str); //重载+=操作符
  • string& operator+=(const char c); //重载+=操作符
  • string& operator+=(const string& str); //重载+=操作符
  • string& append(const char *s); //把字符s连接到当前字符串末尾
  • string& append(const char *s,int n); //把字符串s的前n个字符连接到当前字符串结尾
  • string& append(const string &s); //同operator+=(const string& str);
  • string& append(const string &s,int pos,int n); //字符串s中从pos开始的n个字符连接到字符串末尾

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <string>
using namespace std;

void test01(){
//第一种
string str1="我";
str1+="爱玩游戏";
cout<<"str1= "<<str1<<endl;
//第二种
str1+=':';
cout<<"str1= "<<str1<<endl;
//第三种
string str2="LOL DNF";
str1+=str2;
cout<<"str1= "<<str1<<endl;
//第四种
string str3="I";
str3.append("love");
cout<<"str3= "<<str3<<endl;
//第五种
str3.append("game abcde",4);
cout<<"str3= "<<str3<<endl;
//第六种
str3.append(str2);
cout<<"str3= "<<str3<<endl;
//第七种
str3.append(str2,0,3);
cout<<"str3= "<<str3<<endl;
}

string查找和替换

功能描述

  • 查找:查找指定字符串是否存放
  • 替换:在指定的位置替换字符串

函数原型

  • 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,itn 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=npos) const; //查找s最后一次出现位置,从pos开始查找
  • int rfind(const char* s,int pos,int n) const; //从pos查找s的前n个字符最后一次位置
  • int rfind(const char c,int pos=0) const; //查找字符c最后一次出现位置
  • string& replace(int pos,int n,const string string& str); //替换从pos开始n个字符为字符串str
  • string& replace(int pos,int n,const char* s); //替换从pos开始的n个字符为字符串

find和rfind的区别

  • find是从左往右查找
  • rfind是从右往左查找,但下标返回值是从左往右数的

注意事项

  • 如果查找不到字符或者字符串,会返回值-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string>
using namespace std;

//查找
void test01(){
//find
string str1="abcdefgede";
int pos=str1.find("de"); //查找不到会返回-1
cout<<"pos= "<<pos<<endl; //3
//rfind
pos=str1.rfind("de");
cout<<"pos= "<<pos<<endl; //7
}

//替换
void test02(){
string str1="abcdefg";
str1.replace(1,3,"1111");
cout<<"str1= "<<str1<<endl; //a1111efg
}

string字符串比较

功能描述

  • 字符串之间的比较

比较方式

字符串比较是按字符的ASCII码进行对比

  • *=*返回 0
  • *>*返回 1
  • *<*返回 -1

函数原型

  • int compare(const string &s) const;//与字符串s比较
  • int compare(const char *s) const; //与字符串s比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>
using namespace std;

void test01(){
string str1="hello";
string str2="hello";
if(str1.compare(str2)==0){
cout<<"str1=str2"<<endl;
}else if(str1.compare(str2)>0){
cout<<"str1>str2"<<endl;
}else if(str1.compare(str2)<0){
cout<<"str1<str2"<<endl;
}
}

string字符存取

string中单个字符存取方式有两种:

  • char& operator[](int n); //通过[]方式取字符
  • char& at(int n); //通过at方法获取字符

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>
using namespace std;


void test01(){
string sr1="hello";
//通过[]访问单个字符
for(int i=0;i<str1.size();i++){
cout<<str1[i]<<" ";
}
cout<<endl;
//通过at方式访问单个字符
for(int i=0;i<str1.size();i++){
cout<<str1.at(i)<<" ";
}
cout<<endl;
//修改单个字符
str1[0]='x';
cout<<"str1= "<<str1<<endl;
str1.at(1)='x';
cout<<"str1= "<<str1<<endl;
}

string插入和删除

功能描述

  • 对string字符串进行插入和删除字符操作

函数原型

  • string& insert(int pos,const char* s); //插入字符串
  • string& insert(int pos,const string& str); //插入字符串
  • string& insert(int pos,int n,char c); //在指定位置插入n个字符c
  • string& erase(int pos,int n=npos); //删除从pos开始的n个字符

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
using namespace std;


void test01(){
string str="hello";
//插入
str.insert(1,"111");
cout<<"str= "<<str<<endl;
//删除
str.erase(1,3);
cout<<"str= "<<str<<endl;
}

string子串

功能描述

  • 从字符串中获取想要的子串

函数原型

  • string substr(int pos=0,int n=npos) const; //返回由pos开始的n个字符组成的字符串

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>
using namespace std;

void test01(){
string str="abcedf";
string subStr=str.substr(1,3);
cout<<"subStr= "<<subStr<<endl;
}

//实用操作
void test02(){
string email="zhangsan@sina.com";
//从邮件地址中获取用户名信息
int pos=email.find("@");
string username=email.substr(0,pos);
cout<<"username= "<<username<<endl;
}

vector容器

vector基本概念

功能

  • vector数据结构和数组非常相似,也成为单端数组

vector与普通数组区别

  • 不同之处在于数组是静态空间,而vector可以动态扩展

动态扩展

  • 并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间
  • vector容器的迭代器是支持随机访问的迭代器

vector构造函数

功能描述

  • 创建vector容器

函数原型

  • vector<T> v; //采用模块实现类实现,默认构造函数
  • vector(v.begin(),v.end()); //将v[begin(),end())区间中的元素拷贝给本身
  • vector(n,elem); //构造函数将n个elem拷贝给本身
  • vector(const vector &vec); //拷贝构造函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <vector>
using namespace std;

void print(vector<int>&v){
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
vector<int>v; //默认无参构造
for(int i=0;i<10;i++){
v.push_back(i);
}
print(v);
//通过区间方式进行构造
vector<int>v1(v.begin(),v.end());
print(v1);
//n个elem方式构造
vector<int>v2(10,100);
print(v2);
//拷贝构造
vector<int>v3(v2);
print(v3);
}

vector赋值操作

功能描述

  • 给vector容器进行赋值

函数原型

  • vector& operator=(const vector &vec); //重载等号操作符
  • assign(beg,end); //将[beg,end)区间中的数据拷贝赋值给本身
  • assign(n,elem); //将n个elem拷贝赋值给本身

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <vector>
using namespace std;

void print(vector<int>&v){
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}
print(v);

//赋值 operator=
vector<int>v1;
v1=v;
print(v1);

//assign
vector<int>v2;
v2.assign(v1.begin(),v1.end());
print(v2);

//n个elem方式赋值
vector<int>v3;
v3.assign(10,100);
print(v3);
}

vector容量和大小

功能描述

  • 对vector容器的容量和大小操作

函数原型

  • empty(); //判断容器是否为空
  • capacity(); //容器的容量
  • size(); //返回容器中元素的个数
  • resize(int num); //重新指定容器的长度为num,若容器变长,则以默认值(0)填充新位置,如果容器变短,则末尾超出容器长度的元素被删除
  • resize(int num,elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <vector>
using namespace std;

void print(vector<int>&v){
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}
print(v);
if(v.empty()){
cout<<"v为空"<<endl;
}else{
cout<<"v不为空"<<endl;
cout<<"v的容量为:"<<v.capacity()<<endl;
cout<<"v的大小为:"<<v.size()<<endl;
}
//重新指定大小
v.resize(15,100); //可以指定默认填充值
print(v); //补的默认值为0
v.resize(5);
print(v);
}

vector插入和删除

功能描述

  • 对vector容器进行插入、删除操作

函数原型

  • push_back(ele); //尾部插入元素ele
  • pop_back(); //删除最后一个元素
  • insert(const_iterator pos,ele); //迭代器指向位置pos插入元素ele
  • insert(const_iterator pos,int count,ele); //迭代器指向位置pos插入count个元素ele
  • erase(const_iterator pos); //删除迭代器指向的元素
  • erase(const_iterator start,const_iterator end);//删除迭代器从start到end之间的元素
  • clear(); //删除容器中所有元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <vector>
using namespace std;

void print(vector<int>&v){
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}


void test01(){
vector<int>v;
for(int i=1;i<=5;i++){
v.push_back(i*10);
}
print(v);
v.pop_back(); //尾删
print(v);
//插入
v.insert(v.begin(),100); //第一个参数为迭代器
print(v);
v.insert(v.begin(),2,1000);
print(v);
//删除
v.erase(v.begin()); //参数也为迭代器
print(v);
//清空
//v.erase(v.begin(),v.end()); //从头到尾删除
v.clear(); //也是清空操作
print(v);
}

vector数据存取

功能描述

  • 对vector中的数据的存取操作

函数原型

  • at(int idx); //返回索引idx所指的数据
  • operator[]; //返回索引idx所指的数据
  • front(); //返回容器中第一个数据元素
  • back(); //返回容器中最后一个数据元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
using namespace std;


void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}
//利用[]方式访问数组中元素
for(int i=0;i<v.size();i++){
cout<<v[i]<<" ";
}
cout<<endl;
//利用at方式访问元素
for(int i=0;i<v.size();i++){
cout<<v.at(i)<<" ";
}
cout<<endl;

cout<<"第一个元素为:"<<v.front()<<endl;
cout<<"最后一个元素为:"<<v.back()<<endl;
}

vector互换容器

功能描述

  • 实现两个容器内元素进行互换

函数原型

  • swap(vec); //将vec与本身大的元素互换

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <vector>
using namespace std;

void print(vector<int>&v){
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}
print(v);
vector<int>v1;
for(int i=10;i>0;i--){
v1.push_back(i);
}
print(v1);
cout<<"交换后:"<<endl;
v1.swap(v);
print(v);
print(v1);
}

//实际用途
//巧用swap可以收缩内存空间
void test02(){
vector<int>v;
for(int i=0;i<10000;i++){
v.push_back(i);

}
cout<<"v的容量:"<<v.capacity()<<endl;
cout<<"v的大小为:"<<v.size()<<endl;
v.resize(3);
cout<<"v的容量:"<<v.capacity()<<endl; //容量不变
cout<<"v的大小为:"<<v.size()<<endl;
//巧用swap收缩内存
vector<int>(v).swap(v);
//vector<int>(v) 匿名对象,当前行执行完,就将匿名对象回收
//.swap(v); 容量交换
//用swap会根据交换的容器大小来初始化容器的容量
cout<<"v的容量:"<<v.capacity()<<endl; //容量收缩了
cout<<"v的大小为:"<<v.size()<<endl;
}

vector预留空间

功能描述

  • 减少vector在动态扩展容量时的扩展次数

函数原型

  • reserve(int len); //容器预留len个元素长度,预留位置不初始化,元素不可访问

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <vector>
using namespace std;

void test01(){
vector<int>v;
//利用reserve预留空间,减少开辟次数
v.reserve(100000);
int num=0;//统计容器开辟次数
int *p=NULL;
for(int i=0;i<100000;i++){
v.push_back(i);
if(p!=&v[0]){
p=&v[0];
num++;
}
}
cout<<"num= "<<num<<endl;
}

deque容器

基本概念

功能

  • 双端数组,可以对头端进行插入删除操作

deque与vector区别

  • vector对于头部的插入删除效率低,数据量越大,效率越低
  • deque相对而言,对头部的插入删除速度会比vector快
  • vector访问元素时速度会比deque快,这和两者内部实现有关

deque内部工作原理

deque内部有一个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据

中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间

image

  • deque容器的迭代器也是支持随机访问的

deque构造函数

功能描述

  • deque容器构造

函数原型

  • deque<T> deqT; //默认构造形式
  • deque(beg,end); //构造函数将[beg,end)区间中的元素拷贝给本身
  • deque(n,elem); //构造函数将n个elem拷贝给本身
  • deque(const deque &deq); //拷贝构造函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <deque>
using namespace std;

void print(const deque<int> &d){ //内部的*it不能更改了
for(deque<int>:: const_iterator it=d.begin();it!=d.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
deque<int>d1;
for(int i=0;i<10;i++){
d1.push_back(i);
}
print(d1);
deque<int>d2(d1.begin(),d1.end());
print(d2);
deque<int>d3(10,100);
print(d3);
deque<int>d4(d3);
print(d4);
}

deque赋值操作

功能描述

  • 给deque容器进行赋值

函数原型

  • deque& operator=(const deque &deq); //重载等号操作符
  • assign(beg,end); //将[beg,end)区间中的数据拷贝赋值给本身
  • assign(n,elem); //将n个elem赋值给本身

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <deque>
using namespace std;

void print(const deque<int> &d){ //内部的*it不能更改了
for(deque<int>:: const_iterator it=d.begin();it!=d.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
deque<int>d1;
for(int i=0;i<10;i++){
d1.push_back(i);
}
print(d1);
//第一种赋值
deque<int>d2;
d2=d1;
print(d2);
//第二种
deque<int>d3;
d3.assign(d1.begin(),d1.end());
print(d3);
//第三种
deque<int>d4;
d4.assign(10,100);
print(d4);
}

deque大小操作

功能描述

  • 对deque容器的大小进行操作

函数原型

  • deque.empty(); //判断容器是否为空
  • deque.size(); //返回容器中的元素个数
  • deque.resize(num); //重新指定容器的长度num,若容器变长,则以默认值填充新位置,如果容器变短,则末尾超出容器长度的元素被删除
  • deque.resize(num,elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <deque>
using namespace std;


void print(const deque<int>&d){
for(deque<int>::const_iterator it=d.begin();it!=d.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
deque<int>d1;
for(int i=0;i<10;i++){
d1.push_back(i);
}
print(d1);
if(d1.empty()){
cout<<"d1为空"<<endl;
}else{
cout<<"d1不为空"<<endl;
cout<<"d1的大小为:"<<d1.size()<<endl;
//deque容器没有容量概念
}
//重新指定大小
d1.resize(15);
print(d1);
d1.resize(15,1);
print(d1);
d1.resize(5);
print(d1);
}

总结:deque容器是没有容量的概念的。

deque插入和删除

功能描述

  • 向deque容器中插入和删除数据

函数原型

两端插入删除操作:

  • push_back(elem); //在容器尾部添加一个数据
  • push_front(elem); //在容器的头部插入一个数据
  • pop_back(); //删除容器最后一个数据
  • 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位置的数据,返回下一个数据的位置

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <deque>
using namespace std;

void print(const deque<int>&d){
for(deque<int>::const_iterator it=d.begin();it!=d.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
deque<int>d1;
//尾插法
d1.push_back(10);
d1.push_back(20);
//头插
d1.push_front(100);
d1.push_front(200);
print(d1);
//尾删
d1.pop_back();
print(d1);
//头删
d1.pop_front();
print(d1);
}

void test02(){
deque<int>d1;
d1.push_back(10);
d1.push_back(20);
d1.push_front(100);
d1.push_front(200);
print(d1);
//insert插入
d1.insert(d1.begin(),1000);
print(d1);
d1.insert(d1.begin(),2,10000);
print(d1);
//按照区间进行插入
deque<int>d2;
d2.push_back(1);
d2.push_back(2);
d2.push_back(3);
d1.insert(d1.begin(),d2.begin(),d2.end());
print(d1);
}


void test03(){
deque<int>d1;
d1.push_back(10);
d1.push_back(20);
d1.push_front(100);
d1.push_front(200);
//删除
deque<int>::iterator it=d1.begin();
it++;
d1.erase(it);
print(d1);
//按照区间方式删除
d1.erase(d1.begin(),d1.end());
//清空
d1.clear();
print(d1);
}

总结

  • 插入和删除提供的位置是迭代器!

deque数据存取

功能描述

  • 对deque中的数据的存取操作

函数原型

  • at(int idx); //返回索引idx所指数据
  • operator[]; //返回索引idx所指数据
  • front(); //返回容器中第一个数据元素
  • back(); //返回容器中最后一个数据元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <deque>
using namespace std;

void test01(){
deque<int>d;
d.push_back(10);
d.push_back(20);
d.push_back(30);
d.push_front(100);
d.push_front(200);
d.push_front(300);
//通过[]方式访问元素
for(int i=0;i<d.size();i++){
cout<<d[i]<<" ";
}
cout<<endl;

//通过at方式访问元素
for(int i=0;i<d.size();i++){
cout<<d.at(i)<<" ";
}
cout<<endl;
cout<<"第一个元素为:"<<d.front()<<endl;
cout<<"最后一个元素为:"<<d.back()<<endl;
}

deque排序

功能描述

  • 利用算法实现deque容器进行排序

算法

  • sort(iterator beg,iterator end) //对beg和end区间内元素进行排序

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <deque>
#include <algorithm>
using namespace std;

void print(const deque<int>&d){
for(deque<int>::const_iterator it=d.begin();it!=d.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
deque<int>d;
d.push_back(10);
d.push_back(20);
d.push_back(30);
d.push_front(100);
d.push_front(200);
d.push_front(300);
//排序
//对于支持随机访问的迭代器的容器,都可以利用sort算法直接进行排序
sort(d.begin(),a.end());
cout<<"排序后结果:"<<endl;
print(d); //默认排序规则从小到大升序
}

stack(栈)容器

基本概念

概念:stack是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口

image

栈中只有顶端的元素才可以被外界使用,因此栈不允许有遍历行为

栈中进入数据称为 — 入栈push

栈中弹出数据称为 — 出栈pop

stack常用接口

功能描述

  • 栈容器常用的对外接口

构造函数

  • stack<T> stk; //stack采用模板类实现,stack对象的默认构造形式
  • stack(const stack &stk); //拷贝构造函数

赋值操作

  • stack& operator=(const stack &stk); //重载等号操作符

数据存取

  • push(elem); //向栈顶添加元素
  • pop(); //从栈顶移除第一个元素
  • top(); //返回栈顶元素

大小操作

  • empty(); //判断堆栈是否为空
  • size(); //返回栈的大小

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <stack>
using namespace std;

void test01(){
stack<int>s;
//入栈
s.push(10);
s.push(20);
s.push(30);
s.push(40);
cout<<"栈的大小:"<<s.size()<<endl;
//只要栈不为空,查看栈顶,并且执行出栈操作
while(!s.empty()){
cout<<"栈顶元素为:"<<s.top()<<endl;
//出栈
s.pop();
}
cout<<"栈的大小为:"<<s.size()<<endl;
}

queue(队列)容器

基本概念

概念:Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口

image

队列容器允许从一端新增元素,从另一端移除元素

队列中只有队头和队尾才可以被外界使用,因此队列不允许有遍历行为

队列中进数据称为 — 入队push

队列中出数据称为 — 出队pop

queue常用接口

功能描述

  • 队列容器常用的对外接口

构造函数

  • queue<T> que; //queue采用模板类实现,queue对象的默认构造形式
  • queue(const queue &que); //拷贝构造函数

赋值操作

  • queue& operator=(const queue &que); //重载等号操作符

数据存取

  • push(elem); //往队尾添加元素
  • pop(); //从队头移除第一个元素
  • back(); //返回最后一个元素
  • front(); //返回第一个元素

大小操作

  • empty(); //判断堆栈是否为空
  • size(); //返回栈的大小

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <queue>
#include <string>
using namespace std;

class Person{
public:
Person(string name,int age){
this->m_name=name;
this->m_age=age;
}
string m_name;
int m_age;
};

void test01(){
queue<Person>q;
Person p1("p1",20);
Person p2("p2",30);
Person p3("p3",40);
Person p4("p4",50);
//入队
q.push(p1);
q.push(p2);
q.push(p3);
q.push(p4);
cout<<"队列大小:"<<q.size()<<endl;
//判断只要队列不为空,查看队头,查看队尾,出队
while(!q.empty()){
//查看队头
cout<<"队头元素:"<<q.front().m_name<<" "<<q.front().m_age<<endl;
//查看队尾
cout<<"队尾元素:"<<q.back().m_name<<" "<<q.back().m_age<<endl;
//出队
q.pop();
}
cout<<"队列大小:"<<q.size()<<endl;
}

priority_queue(优先队列)容器

优先队列定义

  • 使用优先队列,**定义参数<数据类型,容器类型,比较方法>**,默认是大根堆
    例子: priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;

数据存取操作

  • push(elem); //添加元素
  • pop(); //从队列中删除最高优先级的元素
  • top(); //获取队列中最高优先级的元素

大小操作

  • empty(); //判断是否为空
  • size(); //获取队列大小

list(链表)容器

功能:将数据进行链式存储

链表(list):是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的

链表的组成:链表由一系列结点组成

结点的组成:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域

STL的链表是一个双向循环链表

image

由于链表的存储方式不是连续的内存内存空间,因此链表list中的迭代器只支持前移和后移属于双向迭代器

优点

  • 采用动态存储分配,不会造成内存浪费和溢出
  • 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素

缺点

  • 链表灵活,但是空间(指针域)和时间(遍历)额外耗费较大

List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效,这在vector是不成立的

总结:STL中的List和vector是两个最常被使用的容器,各有优缺点

list构造函数

功能描述

  • 创建list容器

函数原型

  • list<T> lst; //list采用模板类实现,对象的默认构造形式
  • list(beg,end); //构造函数将[beg,end)区间中的元素拷贝给本身
  • list(n,elem); //构造函数将n个elem拷贝给本身
  • list(const list &lst); //拷贝构造函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <list>
using namespace std;

void print(const list<int>&l){
for(list<int>::const_iterator it=l.begin();it!=l.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
list<int>l;
//添加数据
l.push_back(10);
l.push_back(20);
l.push_back(30);
l.push_back(40);
//遍历容器
print(l);
//区间方式构造
list<int>l1(l.begin(),l.end());
print(l1);
//拷贝构造
list<int>l2(l1);
print(l2);
//n个elem
list<int>l3(10,1000);
print(l3);
}

list赋值和交换

功能描述

  • 给list容器进行赋值,以及交换list容器

函数原型

  • assign(beg,end); //将[beg,end)区间中的数据拷贝赋值给本身
  • assign(n,elem); //将n个elem拷贝赋值给本身
  • list& operator=(const list &lst); //重载等号操作符
  • swap(lst); //将lst与本身的元素互换

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <list>
using namespace std;

void print(const list<int>&l){
for(list<int>::const_iterator it=l.begin();it!=l.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
list<int>l;
l.push_back(10);
l.push_back(20);
l.push_back(30);
l.push_back(40);
print(l);
list<int>l1;
l1=l; //operator= 赋值
print(l1);
list<int>l2;
l2.assign(l1.begin(),l1.end());
print(l2);
list<int>l3;
l3.assign(10,100);
print(l3);
}

//交换
void test02(){
list<int>l;
l.push_back(10);
l.push_back(20);
l.push_back(30);
l.push_back(40);
list<int>l1;
l1.assign(10,100);
cout<<"交换前:"<<endl;
print(l);
print(l1);
cout<<"交换后:"<<endl;
l.swap(l1);
print(l);
print(l1);
}

list大小操作

功能描述

  • 对list容器的大小进行操作

函数原型

  • size(); //返回容器中元素的个数
  • empty(); //判断容器是否为空
  • resize(num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置,如果容器变短,则末尾超出容器长度的元素被删除
  • resize(num,elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <list>
using namespace std;

void print(const list<int>&l){
for(list<int>::const_iterator it=l.begin();it!=l.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
list<int>l;
l.push_back(10);
l.push_back(20);
l.push_back(30);
l.push_back(40);
print(l);

//判断容器是否为空
if(l.empty()){
cout<<"l为空"<<endl;
}else{
cout<<"l不为空"<<endl;
cout<<"l的大小为:"<<l.size()<<endl;
}

//重新指定大小
l.resize(10);
print(l);
l.resize(11,10000);
print(l);
l.resize(2);
print(l);
}

list插入和删除

功能描述

  • 对list容器进行数据的插入和删除

函数原型

  • push_back(elem); //在容器尾部加入一个元素
  • pop_back(); //删除容器中最后一个元素
  • push_front(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值匹配的元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <list>
using namespace std;

void print(const list<int>l){
for(list<int>::const_iterator it=l.begin();it!=l.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
list<int>l;
//尾插
l.push_back(10);
l.push_back(20);
l.push_back(30);

//头插
l.push_front(100);
l.push_front(200);
l.push_front(300);
print(l);
//尾删
l.pop_back();
print(l);
//头删
l.pop_front();
print(l);

//insert插入
list<int>::iterator it=l.begin();
l.insert(++it,1000);
print(l);

//删除
it=l.begin();
l.erase(++it);
print(l);

//移除
l.push_back(10000);
l.push_back(10000);
l.push_back(10000);
print(l);
l.remove(10000);
print(l);

//清空
l.clear();
print(l);

}

list数据存取

功能·描述

  • 对list容器中数据进行存取

函数原型

  • front(); //返回第一个元素
  • back(); //返回最后一个元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <list>
using namespace std;

void print(const list<int>l){
for(list<int>::const_iterator it=l.begin();it!=l.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
list<int>l;
l.push_back(10);
l.push_back(20);
l.push_back(30);
l.push_back(40);

//l[0] 不可以用[]访问list容器中数据
//list本质是个链表,每个数据不是用连续的线性空间存储数据,迭代器也是不支持随机访问
cout<<"第一个元素为:"<<l.front()<<endl;
cout<<"最后一个元素为:"<<l.back()<<endl;

//验证迭代器是不支持随机访问的
list<int>::iterator it=l.begin();
//it=it+2;//不允许跳跃式访问(随机访问)
it++;
it--; //支持双向
}

总结:list容器中不可以通过[]或者at方式访问数据

list反转和排序

功能描述

  • 将容器中的元素反转,以及将容器中的数据进行排序

函数原型

  • reverse(); //反转链表
  • sort(); //链表排序

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <list>
using namespace std;

void print(const list<int>l){
for(list<int>::const_iterator it=l.begin();it!=l.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}


void test01(){
//反转
list<int>l;
l.push_back(10);
l.push_back(20);
l.push_back(30);
l.push_back(40);
l.push_back(50);
print(l);
cout<<"反转后:"<<endl;
l.reverse();
print(l);
}

bool mycompare(int v1,int v2){
//降序,就让第一个数大于第二个数
return v1>v2;
}

void test02(){
list<int>l;
l.push_back(60);
l.push_back(10);
l.push_back(5);
l.push_back(40);
l.push_back(50);
cout<<"排序前:"<<endl;
print(l);
cout<<"排序后:"<<endl;
//所有不支持随机访问迭代器的容器,不可以用标准算法
//不支持随机访问迭代器的容器,内部会提供对应一些算法
l.sort();//默认为升序
print(l);
//降序
l.sort(mycompare);
print(l);
}

set/multiset(集合)容器

基本概念

简介

  • 所有元素都会插入时自动被排序

本质

  • set/multiset属于关联式容器,底层结构是用二叉树实现

set和multiset区别

  • set不允许容器中有重复的元素
  • multiset允许容器中有重复的元素

set构造和赋值

功能描述

  • 创建set容器以及赋值

构造

  • set<T>st; //默认构造函数
  • set(const set &st); //拷贝构造函数

赋值

  • set& operator=(const set &st); //重载等号操作符

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <set>
using namespace std;

void print(const set<int>s){
for(set<int>::const_iterator it=s.begin();it!=s.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
set<int>s1;
//插入数据只有insert方式
s1.insert(10);
s1.insert(30);
s1.insert(40);
s1.insert(20);
s1.insert(30);
//set容器特点:所有元素插入时候自动排序
//set容器不允许插入重复值
print(s1);
//拷贝构造
set<int>s2(s1);
print(s2);
//赋值
set<int>s3;
s3=s2;
print(s3);
}

set大小和交换

功能描述

  • 统计set容器大小以及交换set容器

函数原型

  • size(); //返回容器中元素的数目
  • empty(); //判断容器是否为空
  • swap(st); //交换两个集合容器

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <set>
using namespace std;

void print(const set<int>s){
for(set<int>::const_iterator it=s.begin();it!=s.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

//大小
void test01(){
set<int>s1;
s1.insert(10);
s1.insert(20);
s1.insert(30);
s1.insert(40);
print(s1);

if(s1.empty()){
cout<<"s1为空"<<endl;
}else{
cout<<"s1不为空"<<endl;
cout<<"s1的大小为:"<<s1.size()<<endl;
}
}

void test02(){
set<int>s1;
s1.insert(10);
s1.insert(20);
s1.insert(30);
s1.insert(40);
set<int>s2;
s2.insert(100);
s2.insert(200);
s2.insert(300);
s2.insert(400);
cout<<"交换前:"<<endl;
print(s1);
print(s2);
cout<<"交换后:"<<endl;
s1.swap(s2);
print(s1);
print(s2);
}

set插入和删除

功能描述

  • set容器进行插入数据和删除数据

函数原型

  • insert(elem); //在容器中插入元素
  • clear(); //清除所有元素
  • erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器
  • erase(beg,end); //删除区间[beg,end)的所有元素,返回下一个元素的迭代器
  • erase(elem); //删除容器中值为elem的元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <set>
using namespace std;

void print(const set<int>s){
for(set<int>::const_iterator it=s.begin();it!=s.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

void test01(){
set<int>s1;
s1.insert(10);
s1.insert(30);
s1.insert(20);
s1.insert(40);
print(s1);

//删除
s1.erase(s1.begin());
print(s1);
//删除重载版本
s1.erase(30);
print(s1);

//清空
//s1.erase(s1.begin(),s1.end());
s1.clear();
print(s1);
}

set查找和统计

功能描述

  • 对set容器进行查找数据以及统计数据

函数原型

  • find(key); //查找key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end();
  • count(key); //统计key的元素个数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <set>
using namespace std;

void test01(){
set<int>s1;
s1.insert(10);
s1.insert(20);
s1.insert(30);
s1.insert(40);
//查找
set<int>::iterator pos=s1.find(30);
if(pos!=s1.end()){
cout<<"找到元素:"<<*pos<<endl;
}else{
cout<<"未找到元素"<<endl;
}
}

void test02(){
set<int>s1;
s1.insert(10);
s1.insert(20);
s1.insert(30);
s1.insert(40);
s1.insert(30);
s1.insert(30);
//统计,对于set而言,统计结果要么是0,要么是1
int num=s1.count(30);
cout<<"num= "<<num<<endl;
}

set和multiset区别

区别

  • set不可以插入重复数据,而multiset可以
  • set插入数据的同时会返回插入结果,表示插入是否成功
  • multiset不会检测数据,因此可以插入重复数据

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <set>
using namespace std;


void test01(){
set<int>s;
pair<set<int>::iterator,bool> ret=s.insert(10);
if(ret.second){
cout<<"第一次插入成功"<<endl;
}else{
cout<<"第一次插入失败"<<endl;
}
ret=s.insert(10);
if(ret.second){
cout<<"第二次插入成功"<<endl;
}else{
cout<<"第二次插入失败"<<endl;
}

multiset<int>m;
//允许插入重复的值
m.insert(10);
m.insert(10);
for(multiset<int>::iterator it=m.begin();it!=m.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

总结

  • 如果不允许插入重复数据可以利用set
  • 如果需要插入重复数据利用multiset

pair队组创建

功能描述

  • 成对出现的数据,利用队组可以返回两个数据

两种创建方式

  • pair<type,type> p(value1,value2);
  • pair<type,type> p=make_pair(value1,value2);

注意事项

  • 使用时不需要包含头文件

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
using namespace std;


void test01(){
//第一种方式
pair<string,int>p("Tom",20);
cout<<"姓名:"<<p.first<<"年龄:"<<p.second<<endl;

//第二种方式
pair<string,int>p2=make_pair("jerry",30);
cout<<"姓名:"<<p2.first<<"年龄:"<<p2.second<<endl;
}

set容器排序

学习目标:

  • set容器默认排序规则是从小到大,掌握如何改变排序规则

主要技术点:

  • 利用仿函数,可以改变排序规则

示例一 set存放内置数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <set>
using namespace std;

class mycompare{
public:
bool operator()(int v1,int v2){
return v1>v2;
}
};


void test01(){
set<int>s1;
s1.insert(10);
s1.insert(20);
s1.insert(30);
s1.insert(40);
s1.insert(50);
for(set<int>::iterator it=s1.begin();it!=s1.end();it++){
cout<<*it<<" ";
}
cout<<endl;
//指定排序规则为从小到大
set<int,mycompare>s2;

s2.insert(10);
s2.insert(20);
s2.insert(30);
s2.insert(40);
s2.insert(50);
for(set<int,mycompare>::iterator it=s2.begin();it!=s2.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

示例2 set存放自定义数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <set>
using namespace std;

class Person{
public:
Person(string name,int age){
this->m_name=name;
this->m_age=age;
}
string m_name;
int m_age;
};

class mycompare{
public:
bool operator()(const Person&p1,const Person&p2){
return p1.m_age>p2.m_age;
}
};

void test01(){
//自定义数据类型都会指定排序规则
set<Person,mycompare>s;
Person p1("张1",24);
Person p1("张2",28);
Person p1("张3",25);
Person p1("张4",21);
s.insert(p1);
s.insert(p2);
s.insert(p3);
s.insert(p4);
for(set<Person>::iterator it=s.begin();it!=s.end();it++){
cout<<"姓名:"<<it->m_name<<"年龄:"<<it->m_age<<endl;
}
}

map/multimap容器

map基本概念

简介

  • map中所有元素都是pair(队组)
  • pair中第一个元素为key(键值),起到索引作用,第二个元素为value(实值)
  • 所有元素都会根据元素的键值自动排序

本质

  • map/multimap属于关联式容器,底层使用二叉树实现

优点

  • 可以根据key值快速找到value值

map和multimap区别

  • map不允许容器中有重复key值元素
  • multimap允许容器中有重复key值元素

map构造和赋值

功能描述

  • 对map容器进行构造和赋值操作

函数原型

构造

  • map<T1,T2>mp; //map默认构造函数
  • map(const map &mp); //拷贝构造函数

赋值

  • map& operator=(const map &mp); //重载等号操作符

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <map>
using namespace std;

void print(const map<int,int>&m){
for(map<int,int>::const_iterator it=m.begin();it!=m.end();it++){
cout<<"key= "<<(*it).first<<"value= "<<(*it).second<<endl;
}
cout<<endl;
}


void test01(){
map<int,int>m;
m.insert(pair<int,int>(1,10));
m.insert(pair<int,int>(3,30));
m.insert(pair<int,int>(2,20));
m.insert(pair<int,int>(4,40));

print(m);

//拷贝构造
map<int,int>m2(m);
print(m2);

//赋值
map<int,int>m3;
m3=m2;
print(m3);
}

map大小和交换

功能描述

  • 统计map容器大小以及交换map容器

函数原型

  • size(); //返回容器中元素的数目
  • empty(); //判断容器是否为空
  • swap(st); //交换两个集合容器

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <map>
using namespace std;

void print(const map<int,int>&m){
for(map<int,int>::const_iterator it=m.begin();it!=m.end();it++){
cout<<"key= "<<(*it).first<<"value= "<<(*it).second<<endl;
}
cout<<endl;
}

//大小
void test01(){
map<int,int>m;
m.insert(pair<int,int>(1,10));
m.insert(pair<int,int>(2,20));
m.insert(pair<int,int>(3,30));
if(m.empty()){
cout<<"m为空"<<endl;
}else{
cout<<"m不为空"<<endl;
cout<<"m的大小为:"<<m.size()<<endl;
}
}
//交换
void test02(){
map<int,int>m;
m.insert(pair<int,int>(1,10));
m.insert(pair<int,int>(2,20));
m.insert(pair<int,int>(3,30));
map<int,int>m2;
m2.insert(pair<int,int>(4,100));
m2.insert(pair<int,int>(5,200));
m2.insert(pair<int,int>(6,300));
cout<<"交换前:"<<endl;
print(m);
print(m2);
cout<<"交换后:"<<endl;
m.swap(m2);
print(m);
print(m2);
}

map插入和删除

功能描述

  • map容器进行插入数据和删除数据

函数原型

  • insert(elem); //在容器中插入元素
  • clear(); //清除所有元素
  • erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器
  • erase(beg,end); //删除区间[beg,end)的所有有元素,返回下一个元素的迭代器
  • erase(key); //删除容器中值为key的元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <map>
using namespace std;

void print(const map<int,int>&m){
for(map<int,int>::const_iterator it=m.begin();it!=m.end();it++){
cout<<"key= "<<it->first<<" value= "<<it->second<<endl;
}
cout<<endl;
}

void test01(){
map<int,int>m;
//第一种
m.insert(pair<int,int>(1,10));
//第二种
m.insert(make_pair(2,20));
//第三种
m.insert(map<int,int>::value_type(3,30));
//第四种,不建议使用
m[4]=40;
//[]不建议插入,用途可以利用key访问到value
cout<<m[5]<<endl; //如果容器没有这个key值会直接插入该键值实值为0的元素
print(m);

//删除
m.erase(m.begin());
print(m);
m.erase(3); //按照key删除
print(m);
//清空
//m.erase(m.begin(),m.end());
m.clear();
print(m);
}

map查找和统计

功能描述

  • 对map容器进行查找数据以及统计数据

函数原型

  • find(key); //查找key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end();
  • count(key); //统计key的元素个数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <map>
using namespace std;

void test01(){
map<int,int>m;
m.insert(make_pair(1,10));
m.insert(make_pair(2,20));
m.insert(make_pair(3,30));
m.insert(make_pair(4,40));
map<int,int>::iterator pos=m.find(3);
if(pos!=m.end()){
cout<<"查到了元素key= "<<pos->first<<" value= "<<pos->second<<endl;
}else{
cout<<"未找到元素"<<endl;
}
//统计
//map不允许插入重复key元素,count统计而言,结果要么0,要么1
int num=m.count(3);
cout<<"num= "<<num<<endl;
}

map容器排序

学习目标

  • map容器默认排序规则为按照key值进行从小到大排序,掌握如何改变排序规则

主要技术点

  • 利用仿函数,可以改变排序规则

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <map>
using namespace std;

class mycompare{
public:
bool operator()(int v1,int v2){
//降序
return v1>v2;
}
};

void test01(){
map<int,int,mycompare>m;
m.insert(make_pair(1,10));
m.insert(make_pair(2,30));
m.insert(make_pair(3,30));
m.insert(make_pair(4,40));
m.insert(make_pair(5,50));
for(map<int,int>::iterator it=m.begin();it!=m.end();it++){
cout<<"key= "<<it->first<<" value= "<<it->second<<endl;
}
}

unordered_map(哈希)容器

功能描述

  • 是一个底层由哈希表实现的无序容器,可存储若干个键值对,键有唯一性。

常用接口

构造函数

  • unordered_map<key_type,val_type> um //key_type为键的类型一般为int型,val_type为存储数据类型

赋值操作

  • unordered_map& operator=(const unordered_map &um); //重载等号操作符

数据存取

  • insert(make_pair(key,val)) //向哈希表中插入键值对

数据查找/修改

  • count(const key_type& key); //返回键为key的元素个数
  • find(const key_type& key); //返回键为key的第一个元素,返回迭代器,没找到返回end()
  • emplace(const key_type& key,const val_type& val); //如果容器中没有对应的键值,则插入一个新元素,如果有,就修改该键值对应的value值
  • clear(); //清除容器中所有元素
  • iterator erase ( const_iterator position ); //删除指定迭代器的元素
  • size_type erase ( const key_type& k ); //删除指定键值的元素
  • iterator erase ( const_iterator first, const_iterator last ); //删除指定迭代器范围内的元素
  • swap(unordered_map& ump); //互相交换两个容器的内容,容器类型必须一致,但大小可以不同

大小操作

  • empty(); //判断哈希表是否为空
  • size(); //返回哈希表的大小
  • max_size(); //返回哈希容器最大容纳数量

STL –函数对象

函数对象

函数对象概念

概念

  • 重载函数调用操作符的类,其对象常称为函数对象
  • 函数对象使用重载的()时,行为类似函数调用,也叫仿函数

本质

函数对象(仿函数)是一个,不是一个函数

函数对象使用

特点

  • 函数对象在使用时,可以像普通函数那样调用,可以有参数,可以有返回值
  • 函数对象超出普通函数的概念,函数对象可以有自己的状态
  • 函数对象可以作为参数传递

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <string>
using namespace std;

class myadd{
public:
int operator()(int v1,int v2){
return v1+v2;
}
};

void test01(){
myadd a;
cout<<a(10,10)<<endl;
}

class myprint{
public:
myprint(){
this->cnt=0;
}
void operator()(string test){
cout<<test<<endl;
cnt++;
}
int cnt;
};

void test02(){
myprint p;
p("hello world");
cout<<"myprint调用次数为:"<<p.cnt<<endl;
}

void doprint(myprint &mp,string test){
mp(test);
}

void test03(){
myprint p;
doprint(p,"hello C++");
}

总结

  • 仿函数写法非常灵活,可以作为参数进行传递

谓词

谓词概念

概念

  • 返回bool类型的仿函数称为谓词
  • 如果operator()接收一个参数,那么叫做一元谓词
  • 如果operator()接收两个参数,那么叫做二元谓词

一元谓词

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class greaterfive{
public:
bool operator()(int val){
return val>5;
}
};

void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}
//查找容器中有没有大于5的数字
//greatefive()匿名函数对象
vector<int>::iterator it=find_if(v.begin(),v.end(),greaterfive());
if(it==v.end()){
cout<<"未找到"<<endl;
}else{
cout<<"找到了大于5的数字为:"<<*it<<endl;
}
}

二元谓词

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class mycompare{
public:
bool operator()(int val1,int val2){
return val1>val2;
}
};

void test01(){
vector<int>v;
v.push_back(10);
v.push_back(40);
v.push_back(20);
v.push_back(30);
v.push_back(50);
sort(v.begin(),v.end());
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
//使用函数对象,改变算法策略,变为降序排序规则
sort(v.begin(),v.end(),mycompare());
cout<<"---------------------"<<endl;
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

内建函数对象

内建函数对象意义

概念

  • STL内建了一些函数对象

分类

  • 算术仿函数
  • 关系仿函数
  • 逻辑仿函数

用法

  • 这些仿函数所产生的对象,用法和一般函数完全相同
  • 使用内建函数对象,需要引入头文件#include <functional>

算术仿函数

功能描述

  • 实现四则运算
  • 其中negate是一元运算,其他都是二元运算

仿函数原型

  • template<class T> T plus<T> //加法仿函数
  • template<class T> T minus<T> //减法仿函数
  • template<class T> T multiplies<T> //乘法仿函数
  • template<class T> T divides<T> //除法仿函数
  • template<class T> T modulus<T> //取模仿函数
  • template<class T> T negate<T> //取反仿函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <functional>
using namespace std;

void test01(){
negate<int>n;
cout<<n(50)<<endl;
}

void test02(){
plus<int>p;
cout<<p(10,10)<<endl;
}

关系仿函数

功能描述

  • 实现关系对比

仿函数原型

  • template<class T> bool equal_to<T> //等于
  • template<class T> bool not_equal_to<T> //不等于
  • template<class T> bool greater<T> //大于
  • template<class T> bool greater_equal<T> //大于等于
  • template<class T> bool less<T> //小于
  • template<class T> bool less_equal<T> //小于等于

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
using namespace std;

class mycompare{
public:
bool operator()(int v1,int v2){
return v1>v2;
}
};

void test01(){
vector<int>v;
v.push_back(10);
v.push_back(30);
v.push_back(40);
v.push_back(20);
v.push_back(50);
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
//降序
//sort(v.begin(),v.end(),mycompare());
//使用内建仿函数
sort(v.begin(),v.end(),greater<int>());
for(vector<int>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

逻辑仿函数

功能描述

  • 实现逻辑运算

函数原型

  • template<class T> bool logical_and<T> //逻辑与
  • template<class T> bool logical_or<T> //逻辑或
  • template<class T> bool logical_not<T> //逻辑非

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;

void test01(){
vector<bool>v;
v.push_back(true);
v.push_back(false);
v.push_back(true);
v.push_back(false);

for(vector<bool>::iterator it=v.begin();it!=v.end();it++){
cout<<*it<<" ";
}
cout<<endl;
//利用逻辑非将容器v搬运到容器v2中,并执行取反的操作
vector<bool>v2;
v2.resize(v.size());
transform(v.begin(),v.end(),v2.begin(),logical_not<bool>());
cout<<"--------------------"<<endl;
for(vector<bool>::iterator it=v2.begin();it!=v2.end();it++){
cout<<*it<<" ";
}
cout<<endl;
}

总结:逻辑仿函数实际应用较少,了解即可

STL –常用算法

概述

  • 算法主要是由头文件<algorithm> <functional> <numeric>组成
  • <algorithm>是所有STL头文件中最大的一个,范围涉及到比较、交换、查找、遍历操作、复制、修改等等
  • <numeric>体积很小,只包括几个在序列上面进行简单数学运算的模板函数
  • <functional>定义了一些模板类,用以声明函数对象

常用遍历算法

学习目标

  • 掌握常用的遍历算法

算法简介

  • for_each //遍历容器
  • transform //搬运容器到另一个容器中

for_each算法

功能描述

  • 实现遍历容器

函数原型

  • for_each(iterator beg,iterator end,_func);

    //遍历算法 遍历容器元素

    //beg开始迭代器

    //end结束迭代器

    //_func函数或者函数对象

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <algorithm>
#incude <vector>
using namespace std;

//普通函数
void print01(int val){
cout<<val<<" ";
}

//仿函数
class print02{
public:
void operator()(int val){
cout<<val<<" ";
}
};

void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}
for_each(v.begin(),v.end(),print01);
cout<<endl;
for_each(v.begin(),v.end(),print02());
cout<<endl;
}

总结:for_each在实际开发中是最常用遍历算法,需要熟练掌握

transform算法

功能描述

  • 搬运容器到另一个容器中

函数原型

  • transform(iterator beg1,iterator end1,iterator beg2,_func);

    //beg1 源容器开始迭代器

    //end1 源容器结束迭代器

    //beg2 目标容器开始迭代器

    //_func 函数或者函数对象

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class Transform{
public:
int operator()(int v){
return v+1000;
}
};

class myprint{
public:
void operator()(int val){
cout<<val<<" ";
}
};

void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}

vector<int>vt; //目标容器
vt.resize(v.size()); //目标容器需要提前开辟空间
transform(v.begin(),v.end(),vt.begin(),Transform());
for_each(vt.begin(),vt.end(),myprint());
cout<<endl;
}

总结:搬运的目标容器必须要提前开辟空间,否则无法正常搬运

常用查找算法

学习目标

  • 掌握常用的查找算法

算法简介

  • find //查找元素
  • find_if //按条件查找元素
  • abjacent_find //查找相邻重复元素
  • binary_search //二分查找法
  • count //统计元素个数
  • count_if //按条件统计元素个数

find 算法

功能描述

  • 查找指定元素,找到返回指定元素的迭代器,找不到返回结束迭代器end()

函数原型

  • find(iterator beg,iterator end,value);

    //按值查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置

    //beg 开始迭代器

    //end 结束迭代器

    //value 查找的元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;

//查找内置数据类型
void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}
vector<int>::iterator it=find(v.begin(),v.end(),5);
if(it==v.end()){
cout<<"未找到"<<endl;
}else{
cout<<"找到:"<<*it<<endl;
}
}

class Person{
public:
Person(string name,int age){
this->m_name=name;
this->m_age=age;
}
//重载==号 让底层find知道如何对比person数据类型
bool operator==(const Person &p){
if(this->m_name==p.m_name&&this->m_age==p.m_age){
return true;
}else{
return false;
}
}
string m_name;
int m_age;
};

//查找自定义数据类型
void test02(){
vector<Person>v;
Person p1("aaa",10);
Person p2("bbb",20);
Person p3("ccc",30);
Person p4("ddd",40);
v.push_back(p1);
v.push_back(p2);
v.push_back(p3);
v.push_back(p4);
vector<Person>::iterator it=find(v.begin(),v.end(),p2);
if(it==v.end()){
cout<<"未找到"<<endl;
}else{
cout<<"找到元素 姓名:"<<it->m_name<<" 年龄:"<<it->m_age<<endl;
}
}

find_if 算法

功能描述

  • 按条件查找元素

函数原型

  • find_if(iterator beg,iterator end,_Pred);

    //按值查找元素,找到返回指定位置的迭代器,找不到返回结束迭代器位置

    //beg 开始迭代器

    //end 结束迭代器

    //_Pred 函数或者谓词(返回bool类型的仿函数)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

class greaterfive{
public:
bool operator()(int val){
return val>5;
}
};

//查找内置数据类型
void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}
vector<int>::iterator it=find_if(v.begin(),v.end(),greaterfive());
if(it==v.end()){
cout<<"没有找到"<<endl;
}else{
cout<<"找到大于5的数字为:"<<*it<<endl;
}
}

class Person{
public:
Person(string name,int age){
this->m_name=name;
this->m_age=age;
}
string m_name;
int m_age;
};

class greater20{
public:
bool operator()(Person &p){
return p.m_age>20;
}
};

//查找自定义数据类型
void test02(){
vector<Person>v;
Person p1("a",10);
Person p2("b",20);
Person p3("c",30);
Person p4("d",40);
v.push_back(p1);
v.push_back(p2);
v.push_back(p3);
v.push_back(p4);
vector<Person>::iterator it=find_if(v.begin(),v.end(),greater20());
if(it==v.end()){
cout<<"未找到"<<endl;
}else{
cout<<"找到姓名:"<<it->m_name<<" 年龄:"<<it->m_age<<endl;
}
}

adjacent_find 算法

功能描述

  • 查找相邻重复元素

函数原型

  • adjacent_find(iterator beg,iterator end);

    //查找相邻重复元素,返回相邻元素的第一个位置的迭代器

    //beg开始迭代器

    //end 结束迭代器

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;


void test01(){
vector<int>v;
v.push_back(0);
v.push_back(2);
v.push_back(0);
v.push_back(3);
v.push_back(1);
v.push_back(4);
v.push_back(3);
v.push_back(3);
vector<int>::iterator pos=adjacent_find(v.begin(),v.end());
if(pos==v.end()){
cout<<"未找到相邻重复元素"<<endl;
}else{
cout<<"找到相邻重复元素:"<<*pos<<endl;
}
}

总结:面试题中如果出现查找相邻重复元素,记得用STL中的adjacemt_find算法

binary_search 算法

功能描述

  • 查找指定元素是否存在

函数原型

  • bool binary_search(iterator beg,iterator end,value);

    //查找指定的元素,查到返回true,否则返回false

    //注意:在无序序列中不可用

    //beg 开始迭代器

    //end 结束迭代器

    //value 查找的元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;


void test01(){
vector<int>v;
for(int i=0;i<10;i++){
v.push_back(i);
}
//注意:容器必须是有序的序列,如果是无序序列结果未知
bool ret=binary_search(v.begin(),v.end(),9);
if(ret){
cout<<"找到了元素"<<endl;
}else{
cout<<"未找到"<<endl;
}
}

binary_search 算法

功能描述

  • 查找指定元素是否存在

函数原型

  • bool binary_search(iterator beg,iterator end,value);

    //查找指定的元素,查到返回true,否则false

    //注意:在无序序列中不可用

    //beg 开始迭代器

    //end 结束迭代器

    //value 查找的元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;


void test01() {
vector<int>v;
for (int i = 0; i < 10; i++) {
v.push_back(i);
}
//注意:容器必须是有序的序列,如果是无序序列结果未知
bool ret = binary_search(v.begin(), v.end(), 9);
if (ret) {
cout << "找到了元素" << endl;
} else {
cout << "未找到" << endl;
}
}

总结:二分查找法查找效率很高,值得注意的是查找的容器中必须是有序序列

count 算法

功能描述

  • 统计元素个数

函数原型

  • count(iterator beg,iterator end,value);

    //统计元素出现次数

    //beg 开始迭代器

    //end 结束迭代器

    //value 统计的元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

//统计内置数据类型
void test01() {
vector<int>v;
v.push_back(10);
v.push_back(40);
v.push_back(30);
v.push_back(40);
v.push_back(40);
int num = count(v.begin(), v.end(), 40);
cout << "40的元素个数为:" << num << endl;
}

//统计自定义数据类型
class Person {
public:
Person(string name, int age) {
this->m_name = name;
this->m_age = age;
}
bool operator==(const Person &p) {
if (this->m_age == p.m_age) {
return true;
} else {
return false;
}
}
string m_name;
int m_age;
};

void test02() {
vector<Person>v;
Person p1("a", 35);
Person p2("b", 35);
Person p3("c", 35);
Person p4("d", 40);
Person p5("e", 40);
Person p6("f", 35);
v.push_back(p1);
v.push_back(p2);
v.push_back(p3);
v.push_back(p4);
v.push_back(p5);
int num = count(v.begin(), v.end(), p6);
cout << "与f同岁数的人员个数为:" << num << endl;
}

总结:统计自定义数据类型时候,需要配合重载operator==

count_if 算法

功能描述

  • 按条件统计元素个数

函数原型

  • count_if(iterator beg,iterator end,_Pred);

    //按条件统计元素出现次数

    //beg 开始迭代器

    //end 结束迭代器

    //_Pred 谓词

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class greater20 {
public:
bool operator()(int val) {
return val > 20;
}
};

//统计内置数据类型
void test01() {
vector<int>v;
v.push_back(10);
v.push_back(40);
v.push_back(30);
v.push_back(20);
v.push_back(40);
int num = count_if(v.begin(), v.end(), greater20());
cout << "大于20的元素个数为:" << num << endl;
}

class Person {
public:
Person(string name, int age) {
this->m_name = name;
this->m_age = age;
}
string m_name;
int m_age;
};

class agegreater {
public:
bool operator()(const Person &p) {
return p.m_age > 20;
}
};

//统计自定义数据类型
void test02() {
vector<Person>v;
Person p1("a", 35);
Person p2("b", 35);
Person p3("c", 35);
Person p4("d", 40);
Person p5("e", 20);
v.push_back(p1);
v.push_back(p2);
v.push_back(p3);
v.push_back(p4);
v.push_back(p5);
int num = count_if(v.begin(), v.end(), agegreater());
cout << "大于20岁的人员个数:" << num << endl;
}

常用排序算法

算法简介

  • sort //对容器内元素进行排序
  • random_shuffle //洗牌 指定范围内的元素随机调整次序
  • merge //容器元素合并,并存储到另一个容器
  • reverse //反转指定范围的元素

sort 算法

功能描述

  • 对容器内元素进行排序

函数原型

  • sort(iterator beg,iterator end,_Pred);

    //按值查找元素,找到返回指定值位置迭代器,找不到返回结束迭代器位置

    //beg 开始迭代器

    //end 结束迭代器

    //_Pred 谓词

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
using namespace std;

void myprint(int val) {
cout << val << " ";
}

void test01() {
vector<int>v;
v.push_back(10);
v.push_back(30);
v.push_back(50);
v.push_back(20);
v.push_back(40);
sort(v.begin(), v.end());
for_each(v.begin(), v.end(), myprint);
cout << endl;
sort(v.begin(), v.end(), greater<int>());
for_each(v.begin(), v.end(), myprint);
}

random_shuffle 算法

功能描述

  • 洗牌 指定范围内的元素随机调整次序

函数原型

  • random_shuffle(iterator beg,iterator end);

    //指定范围内的元素随机调整次序

    //beg 开始迭代器

    //end 结束迭代器

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>
using namespace std;

class print {
public:
void operator()(int val) {
cout << val << " ";
}
};

void test01() {
srand((unsigned int)time(NULL));
vector<int>v;
for (int i = 0; i < 10; i++) {
v.push_back(i);
}
random_shuffle(v.begin(), v.end());
for_each(v.begin(), v.end(), print());
cout << endl;
}

总结:random_shuffle算法使用时记得加随机数种子

merge 算法

功能描述

  • 两个容器元素合并,并存储到另一个容器中

函数原型

  • merge(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator dest);

    //容器元素合并,并存储到另一容器中

    //注意:两个容器必须是有序的

    //beg1 容器1开始迭代器

    //end1 容器1结束迭代器

    //beg2 容器2开始迭代器

    //end2 容器2结束迭代器

    //dest 目标容器开始迭代器

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(int val){
cout<<val<<" ";
}


void test01(){
vector<int>v1;
vector<int>v2;
for(int i=0;i<10;i++){
v1.push_back(i);
v2.push_back(i+1);
}
//目标容器
vector<int>v;
//提前给目标容器分配空间
v.resize(v1.size()+v2.size());
merge(v1.begin(),v1.end(),v2.begin(),v2.end(),v.begin());
for_each(v.begin(),v.end(),print);
cout<<endl;
}

总结:merge合并的两个容器必须的有序序列

reverse 算法

功能描述

  • 将容器内元素进行反转

函数原型

  • reverse(iterator beg,iterator end);

    //反转指定范围的元素

    //beg 开始迭代器

    //end 结束迭代器

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(int val){
cout<<val<<" ";
}

void test01(){
vector<int>v;
v.push_back(10);
v.push_back(30);
v.push_back(50);
v.push_back(20);
v.push_back(40);
cout<<"反转前:"<<endl;
for_each(v.begin(),v.end(),print);
cout<<endl;
cout<<"反转后:"<<endl;
reverse(v.begin(),v.end());
for_each(v.begin(),v.end(),print);
}

常用拷贝和替换算法

算法简介

  • copy //容器内指定范围的元素拷贝到另一容器中
  • replace //将容器内指定范围的旧元素修改为新元素
  • replace_if //容器内指定范围满足条件的元素替换为新元素
  • swap //互换两个容器的元素

copy 算法

功能描述

  • 容器内指定范围的元素拷贝到另一个容器中

函数原型

  • copy(iterator beg,iterator end,iterator dest);

    //按值查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置

    //beg 开始迭代器

    //end 结束迭代器

    //dest 目标容器开始迭代器

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(int val){
cout<<val<<" ";
}

void test01(){
vector<int>v1;
for(int i=0;i<10;i++){
v1.push_back(i);
}
vector<int>v2;
v2.resize(v1.size());
copy(v1.begin(),v1.end(),v2.begin());
for_each(v2.begin(),v2.end(),print);
cout<<endl;
}

replace 算法

功能描述

  • 将容器内指定范围的旧元素修改为新元素

函数原型

  • replace(iterator beg,iterator end,oldvalue,newvalue);

    //将区间内旧元素替换成新元素

    //beg 开始迭代器

    //end 结束迭代器

    //oldvalue 旧元素

    //newvalue 新元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class print{
public:
void operator()(int val){
cout<<val<<" ";
}
};

void test01(){
vector<int>v;
v.push_back(20);
v.push_back(30);
v.push_back(50);
v.push_back(20);
v.push_back(10);
v.push_back(20);
cout<<"替换前:"<<endl;
for_each(v.begin(),v.end(),print());
cout<<endl;
replace(v.begin(),v.end(),20,2000);
cout<<"替换后:"<<endl;
for_each(v.begin(),v.end(),print());
cout<<endl;
}

replace_if 算法

功能描述

  • 将区间内满足条件的元素,替换成指定元素

函数原型

  • replace_if(iterator beg,iterator end,_pred,newvalue);

    //按条件替换元素,满足条件的替换成指定元素

    //beg 开始迭代器

    //end 结束迭代器

    //_pred 谓词

    //newvalue 替换的新元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(int val){
cout<<val<<" ";
}

class greater30{
public:
bool operator()(int val){
return val>=30;
}
};

void test01(){
vector<int>v;
v.push_back(10);
v.push_back(40);
v.push_back(20);
v.push_back(30);
v.push_back(40);
v.push_back(50);
cout<<"替换前:"<<endl;
for_each(v.begin(),v.end(),print);
cout<<endl;
replace_if(v.begin(),v.end(),greater30(),3000);
cout<<"替换后:"<<endl;
for_each(v.begin(),v.end(),print);
cout<<endl;
}

swap 算法

功能描述

  • 互换两个容器的元素

函数原型

  • swap(container c1,container c2);

    //互换两个容器的元素

    //c1容器1

    //c2容器2

注意

  • 必须是同种容器才能交换

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(int val){
cout<<val<<" ";
}

void test01(){
vector<int>v1;
vector<int>v2;
for(int i=0;i<10;i++){
v1.push_back(i);
v2.push_back(i+100);
}
cout<<"交换前:"<<endl;
for_each(v1.begin(),v1.end(),print);
cout<<endl;
for_each(v2.begin(),v2.end(),print);
cout<<endl;
cout<<endl;
cout<<"交换后:"<<endl;
swap(v1,v2);
for_each(v1.begin(),v1.end(),print);
cout<<endl;
for_each(v2.begin(),v2.end(),print);
cout<<endl;
}

常用算术生成算法

注意

  • 算术生成算法属于小型算法,使用时包含头文件为#include <numeric>

算法简介

  • accumulate //计算容器元素累计总和
  • fill //向容器种添加元素

accumulate 算法

功能描述

  • 计算区间内容器元素累计总和

函数原型

  • accumulate(iterator beg,iterator end,value);

    //计算容器元素累计总和

    //beg 开始迭代器

    //end 结束迭代器

    //value 起始值

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;


void test01(){
vector<int>v;
for(int i=0;i<100;i++){
v.push_back(i);
}
//value 是起始的累加值
int total=accumulate(v.begin(),v.end(),0);
cout<<"total = "<<total<<endl;
}

fill 算法

功能描述

  • 向容器中填充指定的元素

函数原型

  • fill(iterator beg,iterator end,value);

    //向容器中填充元素

    //beg 开始迭代器

    //end 结束迭代器

    //value 填充的值

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;

void print(int val){
cout<<val<<" ";
}

void test01(){
vector<int>v;
v.resize(10);
//后期重新填充
fill(v.begin(),v.end(),100);
for_each(v.begin(),v.end(),print);
cout<<endl;
}

常用集合算法

算法简介

  • set_intersection //求两个容器的并集
  • set_union //求两个容器的并集
  • set_difference //求两个容器的差集

set_intersection 算法

功能描述

  • 求两个容器的交集

函数原型

  • set_intersection(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator dest);

    //求两个集合的交集

    //beg1 容器1开始迭代器

    //end1 容器1结束迭代器

    //beg2 容器2开始迭代器

    //end2 容器2结束迭代器

    //dest 目标容器开始迭代器

注意事项

  • 两个集合必须是有序序列

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

void print(int val){
cout<<val<<" ";
}

void test01(){
vector<int>v1;
vector<int>v2;
for(int i=0;i<10;i++){
v1.push_back(i);
v2.push_back(i+5);
}
vector<int>v;
v.resize(min(v1.size(),v2.size()));
vector<int>::iterator it=set_intersection(v1.begin(),v1.end(),v2.begin(),v2.end(),v.begin());
for_each(v.begin(),it,print);
cout<<endl;
}

总结

  • 求交集的两个集合必须是有序序列
  • 目标容器开辟空间需要从两个容器中取小值
  • set_intersection返回值即是交集中最后一个元素的位置

set_union 算法

功能描述

  • 求两个集合的并集

函数原型

  • set_union(iterator beg1,itertaor end1,iterator beg2,iterator end2,iterator dest);

    //求两个集合的并集

    //beg1 容器1开始迭代器

    //end1 容器1结束迭代器

    //beg2 容器2开始迭代器

    //end2 容器2结束迭代器

    //dest 目标容器开始迭代

注意事项

  • 两个集合必须是有序序列

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(int val){
cout<<val<<" ";
}

void test01(){
vector<int>v1;
vector<int>v2;
vector<int>v;
for(int i=0;i<10;i++){
v1.push_back(i);
v2.push_back(i+5);
}
v.resize(v1.size()+v2.size());
vector<int>::iterator it=set_union(v1.begin(),v1.end(),v2.begin(),v2.end(),v.begin());
for_each(v.begin(),it,print);
cout<<endl;
}

总结

  • 求并集的两个集合必须是有序序列
  • 目标容器开辟空间需要从两个容器大小相加
  • set_union返回值即是并集中最后一个元素的位置

set_difference 算法

功能描述

  • 求两个集合的差集

函数原型

  • set_difference(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator dest);

    //求两个集合的差集

    //beg1 容器1开始迭代器

    //end1 容器1结束迭代器

    //beg2 容器2开始迭代器

    //end2 容器2结束迭代器

    //dest 目标容器开始迭代器

注意事项

  • 两个集合必须是有序序列

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(int val){
cout<<val<<" ";
}

void test01(){
vector<int>v1;
vector<int>v2;
vector<int>v;
for(int i=0;i<10;i++){
v1.push_back(i);
v2.push_back(i+5);
}
v.resize(max(v1.size(),v2.size()));
vector<int>::iterator it=set_difference(v1.begin(),v1.end(),v2.begin(),v2.end(),v.begin());
for_each(v.begin(),it,print);
cout<<endl;
}

总结

  • 求差集的两个集合必须的有序序列
  • 目标容器开辟空间需要从两个容器大小取大值
  • set_difference返回值即是差集中最后一个元素的位置

异常处理

概念

  • 异常就是错误,而在程序中难免会有异常出现,但为了让程序正常运行而不是崩溃,则需要使用到异常处理,来解决
  • 传统的异常处理是通过判断返回值来判断程序是否异常。
  • c++中提供了try、catch、throw的异常处理表达式。

函数

  • throw 表达式 throw语句用来抛出异常,当有异常发生时,可以通过throw创建一个异常对象并抛掷

  • try{复合语句} catch(异常声明){异常处理程序} try内部的异常语句是保护段语句,当try内部的程序出现异常,则catch子句就会根据异常不同进行捕获异常,然后执行相对应的异常处理程序。如果出现异常但是catch子句未匹配到异常,则库函数terminate将被自动调用,默认是调用abort终止程序。当捕获异常并执行catch子句的时候则会自动帮你将try语块中的执行了尚未析构的语句析构了

异常声明:

  • 可以在函数的声明中列出这个函数可能抛掷的所有异常类型

    1
    void func() throw(A,B,C,D);
  • 若无异常接口声明,则此函数可以抛掷任何类型的异常

  • 不抛掷任何类型异常的函数声明如下:

    1
    2
    3
    void func() throw();
    或者:
    void func() noexcept; //这个c++推荐使用

异常类

image

智能指针

概念

  • 智能指针是原始指针的封装,其优点是会自动分配内存,不用担心潜在的内存泄露

普通指针的不足

  • new和new[]的内存需要用delete和delete[]释放
  • 程序员的主观失误,忘了或漏了释放
  • 程序员也不确定何时释放

智能指针的设计思路

  • 智能指针是类模板,在栈上创建智能指针对象
  • 普通指针交给智能指针对象
  • 智能指针对象过期时,调用析构函数释放普通指针的内存

独占指针 unique_ptr

概念

  • unique_ptr**独享一个原始指针(也就是unique_ptr指向的对象)**,也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁

源码简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T,typename D =default_delete<T> >
class unique_ptr{
public:
explicit unique_ptr(pointer p) noexcept; //不可用于转换函数
~unique_ptr() noexcept;
T& operator*() const; //重载*操作符
T* operator->() const noexcept; //重载->运算符
unique_ptr(const unique_ptr&)=delete; //禁用拷贝构造函数
unique_ptr& operator=(const unique_ptr &)=delete; //禁用赋值函数
unique_ptr(unique_ptr &&) noexcept; //右值引用
unique_ptr& operator=(unique_ptr&&) noexcept; //右值引用
//...
private:
pointer ptr; //内置的指针
}

功能描述

  • 在任何给定时候,只能有一个指针管理内存(也就是独占指针对象只管理一个指针资源)
  • 当指针超出作用域时,内存将自动释放
  • unique_ptr指针不可Copy,也不可以进行赋值,只可以Move
  • 可以通过get()获取裸指针地址(&unique_ptr对象得到的是对象指针地址不是裸指针)
  • 可以通过->调用成员函数
  • 可以通过*调用dereferencing

作为函数的参数

  • 传引用(不能传值,因为unique_ptr没有拷贝构造函数)
  • 裸指针

注意

  • 不要用同一个裸指针初始化多个unique_ptr对象
  • 不要用unique_ptr管理不是new分配的内存
  • 不支持指针的运算(+、-、++、–)

**创建方式(初始化)**:

  • 通过已有裸指针创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class AA{
    ....
    };

    int main(){
    AA* p=new AA("小明");
    unique_ptr<AA> pu1=p;
    return 0;
    }
  • 通过new来创建

    1
    unique_ptr<AA> pu1(new AA("小明"));
  • 通过std::make_unique创建(推荐)

    1
    unique_ptr<AA> pu1=make_unique<AA>("小明");   //C++14标准

内置函数和使用方法

  1. get():获取裸指针的地址

    1
    2
    unique_ptr<int> p1=make_unique<int>(10);
    cout<<p1.get()<<endl; //0x6498f94942b0
  2. release()释放对原始指针的控制权,将unique_ptr对象置为空,返回裸指针地址(之前管理的指针地址)。可用于把unique_ptr传递给子函数,子函数将负责释放对象

    1
    2
    3
    unique_ptr<int> p1=make_unique<int>(10);
    cout<<p1.release()<<endl; //0x5fb45e2472b0
    if(p1==nullptr) cout<<"p1为空"<<endl; //p1为空
  3. std::move()转移对原始指针的控制权。(可用于把unique_ptr传递给子函数,子函数形参也是unique_ptr,传值的方法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //函数func4()需要一个unique_ptr对象,并且会对这个对象负责
    void func4(unique_ptr<AA> a){
    cout<<a->m_name<<endl;
    }


    int main(int argc, char *argv[])
    {
    unique_ptr<AA> p(new AA("小明"));
    cout<<"开始调用函数.\n";
    func4(std::move(p)); //将原始指针的控制权交给func4函数的形参
    cout<<"调用函数完成.\n";
    return 0;
    }
  4. reset()释放unique_ptr指向资源,以及更改指向

    1
    2
    3
    4
    5
    6
    //函数原型
    void reset(T* _ptr=(T*)nullptr);
    unique_ptr<AA>p(new AA("小明"));
    p.reset(); //释放p对象所指向的资源对象
    p.reset(nullptr); //释放p对象所指向的资源对象
    p.reset(new AA("小蓝")); //释放p所指向对象,并指向新的对象
  5. swap()交换两个unique_ptr所指向的对象的控制权

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //函数原型
    void swap(unique_ptr<T>&_Right);

    unique_ptr<AA>p1(new AA("小明"));
    unique_ptr<AA>p2(new AA("小红"));
    p1.swap(p2);
    cout<<p1->m_name<<endl;
    cout<<p2->m_name<<endl;
    //输出:
    //小红
    //小明
  6. []:unique_ptr重载了操作符[],提供了支持数组的具体化版本,操作符[]返回的是引用,可以作为左值

    使用

    1
    2
    3
    4
    5
    //unique_ptr<int[]> parr1(new int[3]);  //不指定初始值
    unique_ptr<int[]> parr1(new int[3](33,22,11)); //指定初始值
    cout<<"parr1[0]="<<parr1[0]<<endl;
    cout<<"parr1[1]="<<parr1[1]<<endl;
    cout<<"parr1[2]="<<parr1[2]<<endl;

使用技巧

  1. 如果源unique_ptr对象是一个临时右值(一般是函数的返回值),编译器允许将这个unique_ptr赋给另一个unique_ptr对象。如果源unique_ptr将存在一段时间,则编译器不允许赋值!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <iostream>
    #include <memory>
    using namespace std;


    unique_ptr<int>func(){

    unique_ptr<int> pp=make_unique<int>(100);
    return pp;
    }


    int main(int argc, char *argv[])
    {
    unique_ptr<int> p1=make_unique<int>(10);
    unique_ptr<int> p2;
    p2=p1; //错误,不可以

    p2=unique_ptr<int>(new int(1)); //使用匿名对象赋值,可以
    p1=func(); //利用函数返回值赋值,可以
    std::cout<<*p1<<std::endl;
    return 0;
    }
  2. 用nullptr给unique_ptr赋值将会释放对象,空的unique_ptr==nullptr

    image-20240417141128949

  3. 一些情况使用技巧

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    #include <iostream>
    #include <memory>
    using namespace std;

    class AA{
    public:
    string m_name;
    AA(){
    cout<<m_name<<"调用构造函数AA().\n";
    }
    AA(const string &name):m_name(name){
    cout<<"调用构造函数AA("<<m_name<<").\n";
    }
    ~AA(){
    cout<<"调用了析构函数~AA("<<m_name<<").\n";
    }
    };


    //函数func1()需要一个原始指针,但不对指针负责
    void func1(const AA* a){
    cout<<a->m_name<<endl;
    }

    //函数func2()需要一个裸指针,并且对这个指针负责
    void func2(AA* a){
    cout<<a->m_name<<endl;
    delete a;
    }

    //函数func3()需要一个unique_ptr对象,不会对这个对象负责
    void func3(const unique_ptr<AA> &a){
    cout<<a->m_name<<endl;
    }

    //函数func4()需要一个unique_ptr对象,并且会对这个对象负责
    void func4(unique_ptr<AA> a){
    cout<<a->m_name<<endl;
    }


    int main(int argc, char *argv[])
    {
    unique_ptr<AA> p(new AA("小明"));
    cout<<"开始调用函数.\n";
    func1(p.get());
    func2(p.release());
    func3(p);
    func4(std::move(p));
    cout<<"调用函数完成.\n";
    return 0;
    }
  4. unique_ptr也可以像普通指针一样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样

  5. unique_ptr不是绝对安全,如果程序中调用exit()退出,全局的unique_ptr可以自动释放,但局部的unique_ptr无法释放

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #include <iostream>
    #include <memory>
    using namespace std;

    class AA{
    public:
    string m_name;
    AA(){
    cout<<m_name<<"调用构造函数AA().\n";
    }
    AA(const string &name):m_name(name){
    cout<<"调用构造函数AA("<<m_name<<").\n";
    }
    ~AA(){
    cout<<"调用了析构函数~AA("<<m_name<<").\n";
    }
    };

    unique_ptr<AA> p1(new AA("小明全局"));


    int main(int argc, char *argv[])
    {
    unique_ptr<AA> p(new AA("小明局部"));
    //return 0; 使用return全部自动释放
    exit(0); //局部无法进行释放
    }
    //打印日志
    || 调用构造函数AA(小明全局).
    || 调用构造函数AA(小明局部).
    || 调用了析构函数~AA(小明全局).
    || Exited with code 0

共享指针 shared_ptr

概念

  • shared_ptr共享它指向的对象,多个shared_ptr可以指向(关联)相同的对象,在内部采用计数机制来实现,共享不是复制,资源只有一个
  • 新的shared_ptr与对象关联时,引用计数增加1
  • shared_ptr超出作用域时,引用计数减1
  • 引用计数为0时,则表示没有任何shared_ptr与对象相联,则释放该对象

基本用法:

注:shared_ptr的构造函数也是explicit,但是,没有删除拷贝构造函数和赋值函数

  1. 初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //方法1:
    shared_ptr p(new AA("小明")); //分配内存并初始化

    //方法2[推荐]:
    shared_ptr p=make_shared<AA>("小明"); //C++11标准,效率更高

    //方法3:
    AA* p=new AA("小明");
    shared_ptr<AA> p0(p); //用已存在的裸指针初始化

    //方法4:
    shared_ptr<AA> p0(new AA("小明"));
    shared_ptr<AA> p1(p0); //用已存在的shared_ptr初始化,计数加1
    shared_ptr<AA> p2=p0; //用已存在的shared_ptr初始化,计数加1
  2. use_count()显示shared_ptr所指向的资源引用次数

    1
    2
    3
    4
    shared_ptr<AA> p0=make_shared<AA>("小明");
    shared_ptr<AA> p1(p0);
    shared_ptr<AA> p2=p0;
    cout<<p0.use_count()<<"\n"; //3
  3. get()显示shared_ptr所指向的资源地址

  4. unique()判断shared_ptr所指向的资源是否被其他shared_ptr对象所共享,如果use.count()为1,返回true,否则返回false

    1
    2
    3
    4
    5
    6
    shared_ptr<AA> p0=make_shared<AA>("小明");
    shared_ptr<AA> p1(p0);
    shared_ptr<AA> p2=p0;
    cout<<p0.unique()<<"\n"; //0
    shared_ptr<AA> p3=make_shared<AA>("111");
    cout<<p3.unique()<<"\n"; //1
  5. =:shared_ptr重载了=操作符,支持赋值,因为将右值shared_ptr赋值给了左值shared_ptr,因此右值shared_ptr所指向资源计数+1,则左值shared_ptr所指向资源计数-1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int main(int argc, char *argv[])
    {
    shared_ptr<AA> p0=make_shared<AA>("小明");
    shared_ptr<AA> p1(p0);
    shared_ptr<AA> p2=p0;
    shared_ptr<AA> p3=make_shared<AA>("111");
    shared_ptr<AA> p4=p3;
    shared_ptr<AA> p5=p3;
    p2=p5; //p3+1,p0-1
    cout<<p0.use_count()<<"\n"; //2
    cout<<p3.use_count()<<"\n"; //4
    return 0;
    }
  6. []:shared_ptr重载了操作符[],提供了支持数组的具体化版本,操作符[]返回的是引用,可以作为左值

    使用

  7. std::move():可以将转移对原始指针的控制权,还可以将unique_ptr转移成shared_ptr(反过来不行)

  8. reset()改变与资源的关联关系,以及指向

    1
    2
    3
    shared_ptr<AA> p=make_shared<AA>("小明");
    p.reset(); //解除与资源的关系,资源的引用计数减1
    p.reset(new AA("111")); //解除与资源的关系,原资源的引用计数-1,关联新的资源,新资源引用计数+1
  9. swap()交换两个shared_ptr的控制权

    1
    2
    //函数原型
    void swap(shared_ptr<T> &_Right);

作为函数的参数

  • 传引用(不能传值,因为unique_ptr没有拷贝构造函数)
  • 裸指针

注意

  • 不要用同一个裸指针初始化多个shared_ptr对象
  • 不要用shared_ptr管理不是new分配的内存
  • 不支持指针的运算(+、-、++、–)

使用技巧

  1. 用nullptr给shared_ptr赋值时将计数减1,如果计数为0,将释放对象,空的shared_ptr==nullptr
  2. shared_ptr也可以像普通指针一样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样
  3. shared_ptr不是绝对安全,如果程序中调用exit()退出,全局的shared_ptr可以自动释放,但局部的shared_ptr无法释放
  4. 如果unique_ptr能解决问题,就不要在用shared_ptr,unique_ptr的效率更高,占用的资源更少

shared_ptr的线程安全性

  • shared_ptr的引用计数本身是线程安全(引用计数是原子操作)
  • 多个线程同时读同一个shared_ptr对象是线程安全的
  • 如果多个线程对同一个shared_ptr对象进行读和写,则需要加锁
  • 多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护

shared_ptr存在问题

  • shared_ptr内部维护一个共享的引用计数器,多个shared_ptr可以指向同一个资源
  • 如果出现了循环引用的情况,引用计数永远无法归0,资源不会被释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <memory>
using namespace std;

class BB;

class AA{
public:
string m_name;
AA(){
cout<<m_name<<"调用构造函数AA().\n";
}
AA(const string &name):m_name(name){
cout<<"调用构造函数AA("<<m_name<<").\n";
}
~AA(){
cout<<"调用了析构函数~AA("<<m_name<<").\n";
}
//引用BB类资源
shared_ptr<BB> m_p;
};


class BB{
public:
string m_name;
BB(){
cout<<m_name<<"调用构造函数BB().\n";
}
BB(const string &name):m_name(name){
cout<<"调用构造函数BB("<<m_name<<").\n";
}
~BB(){
cout<<"调用了析构函数~BB("<<m_name<<").\n";
}
//引用AA资源
shared_ptr<AA> m_p;
};


int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
shared_ptr<BB> p1=make_shared<BB>("小明2");
//循环引用
p0->m_p=p1;
p1->m_p=p0;

return 0;
}
//输出结果
|| 调用构造函数AA(小明1).
|| 调用构造函数BB(小明2).
|| Exited with code 0
//没有调用析构函数

智能指针删除器

概念

  • 在默认情况下,智能指针过期时,它会自动调用缺省的删除器来删除资源
  • 程序员可以自定义删除器,改变智能指针释放资源的行为
  • 删除器可以是全局函数、仿函数和Lambda表达式,形参为原始指针

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <memory>
using namespace std;

class AA{
public:
string m_name;
AA(){
cout<<m_name<<"调用构造函数AA().\n";
}
AA(const string &name):m_name(name){
cout<<"调用构造函数AA("<<m_name<<").\n";
}
~AA(){
cout<<"调用了析构函数~AA("<<m_name<<").\n";
}
};

//删除器,普通函数
void deletefunc(AA* a){
cout<<"自定义删除器(全局函数)。\n";
delete a;
}

//删除器,仿函数
struct deleteclass{
void operator()(AA* a){
cout<<"自定义删除器(仿函数).\n";
delete a;
}
};

//删除器,Lambda表达式
auto deletelamb=[](AA* a){
cout<<"自定义删除器(Lambda).\n";
delete a;
};

int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明"); //使用缺省的删除器

//shared_ptr自定义删除器
shared_ptr<AA> p1(new AA("111"),deletefunc);
shared_ptr<AA> p2(new AA("111"),deleteclass());
shared_ptr<AA> p3(new AA("111"),deletelamb);

//unique_ptr自定义删除器
unique_ptr<AA,decltype(deletefunc)*> pu1(new AA("111"),deletefunc); //模板参数使用了decltype推导,也可以直接写
//unique_ptr<AA,void(*)(AA*)> pu1(new AA("111"),deletefunc);
unique_ptr<AA,deleteclass> pu2(new AA("111"),deleteclass());
unique_ptr<AA,decltype(deletelamb)> pu3(new AA("111"),deletelamb);
return 0;
}

弱智能指针 weak_ptr

引言:为了解决shared_ptr循环引用带来的未自动清理的情况,C++还引入了weak_ptr指针来帮助shared_ptr解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//将循环引用的shared_ptr改成使用weak_ptr可以解决问题
#include <iostream>
#include <memory>
using namespace std;

class BB;

class AA{
public:
string m_name;
AA(){
cout<<m_name<<"调用构造函数AA().\n";
}
AA(const string &name):m_name(name){
cout<<"调用构造函数AA("<<m_name<<").\n";
}
~AA(){
cout<<"调用了析构函数~AA("<<m_name<<").\n";
}
//引用BB类资源
weak_ptr<BB> m_p;
};


class BB{
public:
string m_name;
BB(){
cout<<m_name<<"调用构造函数BB().\n";
}
BB(const string &name):m_name(name){
cout<<"调用构造函数BB("<<m_name<<").\n";
}
~BB(){
cout<<"调用了析构函数~BB("<<m_name<<").\n";
}
//引用AA资源
weak_ptr<AA> m_p;
};


int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
shared_ptr<BB> p1=make_shared<BB>("小明2");
//循环引用
p0->m_p=p1;
p1->m_p=p0;

return 0;
}
//输出结果
|| 调用构造函数AA(小明1).
|| 调用构造函数BB(小明2).
|| 调用了析构函数~BB(小明2).
|| 调用了析构函数~AA(小明1).
|| Exited with code 0

概念

  • weak_ptr是为了配合shared_ptr而引入的,它指向一个由shared_ptr管理的资源但不影响资源的生命周期,也就是说,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
  • 不论是否有weak_ptr指向,如果最后一个指向资源的shared_ptr被销毁,资源就会被释放
  • weak_ptr更像是shared_ptr的助手而不是智能指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <memory>
using namespace std;

class BB;

class AA{
public:
string m_name;
AA(){
cout<<m_name<<"调用构造函数AA().\n";
}
AA(const string &name):m_name(name){
cout<<"调用构造函数AA("<<m_name<<").\n";
}
~AA(){
cout<<"调用了析构函数~AA("<<m_name<<").\n";
}
//引用BB类资源
weak_ptr<BB> m_p;
};


class BB{
public:
string m_name;
BB(){
cout<<m_name<<"调用构造函数BB().\n";
}
BB(const string &name):m_name(name){
cout<<"调用构造函数BB("<<m_name<<").\n";
}
~BB(){
cout<<"调用了析构函数~BB("<<m_name<<").\n";
}
//引用AA资源
weak_ptr<AA> m_p;
};


int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
shared_ptr<BB> p1=make_shared<BB>("小明2");

cout<<p0.use_count()<<endl;
cout<<p1.use_count()<<endl;
//循环引用
p0->m_p=p1;
p1->m_p=p0;

cout<<p0.use_count()<<endl;
cout<<p1.use_count()<<endl;
return 0;
}
//输出结果
|| 调用构造函数AA(小明1).
|| 调用构造函数BB(小明2).
|| 1
|| 1
|| 1
|| 1
|| 调用了析构函数~BB(小明2).
|| 调用了析构函数~AA(小明1).
|| Exited with code 0

基本用法

  • operator=();:重载了=操作符,可以将shared_ptr或weak_ptr赋值给weak_ptr
  • expired()判断weak_ptr所指资源是否过期(是否被销毁)
  • lock()将weak_ptr升级成shared_ptr,如果指向资源存在,将weak_ptr升级成shared_ptr,返回shared_ptr;如果资源已过期返回空的shared_ptr
  • reset():将当前weak_ptr指针置为空
  • swap():交换两个weak_ptr控制权

注意

  • weak_ptr不控制对象的生命周期,但是它可以知道对象是否还活着
  • lock()函数是线程安全的,因为它是一个原子操作的(对于多线程最好使用lock而不是expired)

(多线程)错误做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
{

shared_ptr<BB> p1=make_shared<BB>("小明2");

p0->m_p=p1;
p1->m_p=p0;

if(p0->m_p.expired()) cout<<"语句块内部:p0->m_p已过期.\n";
else cout<<"语句块内部:p0->m_p.lock()->m_name"<<p0->m_p.lock()->m_name<<endl;
}

if(p0->m_p.expired()) cout<<"语句块外部:p0->m_p已过期.\n";
else cout<<"语句块外部:p0->m_p.lock()->m_name"<<p0->m_p.lock()->m_name<<endl;
return 0;
}

(多线程)正确做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(int argc, char *argv[])
{
shared_ptr<AA> p0=make_shared<AA>("小明1");
{

shared_ptr<BB> p1=make_shared<BB>("小明2");

p0->m_p=p1;
p1->m_p=p0;

shared_ptr<BB> pp=p0->m_p.lock(); //把weak_ptr提升为shared_ptr
if(pp==nullptr) cout<<"语句块内部:p0->m_p已过期.\n";
else cout<<"语句块内部:pp->m_name="<<pp->m_name<<endl;
}

shared_ptr<BB> pp=p0->m_p.lock();
if(pp==nullptr) cout<<"语句块外部:p0->m_p已过期.\n";
else cout<<"语句块外部:pp->m_name="<<pp->m_name<<endl;
return 0;
}

Lambda函数

概念

  • Lambda表达式用于创建和定义匿名函数
  • 匿名函数:顾名思义也就是可以没有名字的函数可以看成是一个内联函数,在python、C#等语言都有这种函数表达式的实现。
  • 通常适用于创建一些代码量少的函数以及在当前所处函数中调用次数少的函数

声明Lambda表达式

1
2
3
4
//Lambda表达式的完整声明
[capture list](param list) mutable exception->return_type{
funtion body
};
  • capture list捕获外部变量列表

    Lambda捕获列表:

    • []空捕获列表,lambda不能使用所在函数中的变量
    • [=]函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this指针),并且使用值传递的方式传递(编译器自动做的)
    • [&]函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this指针),并且使用引用传递方式传递(编译器自动做的)
    • [this]函数体内可以使用Lambda所在类中的所有成员变量
    • [x]将外部变量x按值进行传递,不能修改x值,可以添加mutable修饰符允许修改
    • [&x]将外部变量x按引用方式进行传递
    • [=,&x,&y]除外部变量x和y按引用方式传递,其他参数都按值传递方式传递
    • [&,x,y]除外部变量x和y按值进行传递外,其他参数都按引用方式进行传递
  • param list匿名函数形参列表

  • mutable(可选):是一种修饰符,用来声明Lambda函数体能否修改以值传递的捕获变量的值(值传递是会在Lambda体中复制形参值形成新对象,所以在Lambda的操作并不会修改Lambda外部变量的值,以引用形式传递的可以),当需要Lambda函数体能够修改以值传递的捕获变量值,需要在声明时加上该规范

  • exception(可选):**异常规范(设定)**,可以使用noexcept或者throw()设定函数不抛出异常

  • return_type返回类型

  • function body函数体

一些格式

Lambda表达式是可以省略某些成分来进行声明,常用的有如下几种:

  1. **声明了const类型的表达式(没有加mutable修饰符,默认都是const类型)**,这种类型的表达式不能修改捕获列表中的值

    1
    [capture list](param list)->return_type {function body};
  2. 省略了返回值类型,编译器会根据Lambda体自动推导返回值类型,如果有返回值则编译器会自动推导返回值类型,如果没有返回值则默认为void类型

    1
    [capture list](param list){function body};
  3. 省略了参数列表,则这个函数就是无参函数

    1
    [capture list]{function body};

悬垂引用:

  • 若以引用隐式或显式捕获非引用实体,而在该实体的生存期结束之后调用lambda对象的函数调用运算符,则发生未定义行为。C++ 的闭包并不延长被捕获的引用的生存期。这同样适用于被捕获的this指针所指向的对象的生存期
  • 简单来说就是在Lambda表达式捕获的实体生命周期结束后再调用Lambda函数则会发生找不到对应实体,导致发生未定义错误

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <functional>


int main (int argc, char *argv[]) {
int c=2;
auto foo=[&](int a,int b)->void{
std::cout<<a+b+c<<std::endl;
};
//结合function的使用案例
std::function<void(int,int)>f=[=](int a,int b){
std::cout<<a+b+c<<std::endl;
};
f(1,2);
foo(1,2);
return 0;
}

完美转发

左值引用和右值引用

左值非临时,具有持续性,可取地址的对象(常量、变量或表达式),例如int a=1,a就为左值

1
2
int a=1;  //a就为左值
++a; //前缀自增/自减表达式的返回值为左值

右值临时的对象,不能取地址,例如int a=1,a+1,则a+1其实就是一个临时生成的对象和值或者1就是右值

1
2
3
int a=1;	//1就为一个右值
a+1; //临时生成,这里的a+1为一个右值
a++; //后缀自增/自减表达式的返回值为右值

左值引用:**对左值的引用就是左值引用(由单个&组成)**,例如int& a,就是一个左值引用

1
2
int a=1;
int &b=a; //int& b就是一个左值引用

右值引用:**对右值的引用就是右值引用(由一对&组成)**,例如int a=1,int&& x=a+1,int&& x就是一个右值引用

1
2
int a=1;
int &&x=a+1; //int &&x就是一个右值引用

注意:

  • 前缀自增/自减表达式的返回值为左值,后缀自增/自减表达式的返回值为右值
  • 常量左值引用是一个可以绑定到任何类型的引用,左值,右值都可以,例如int& a=1
  • 非常量左值引用不能绑定到右值上

万能引用和引用折叠

万能引用:不是一种引用类型,实现了传入左值或左值引用推导出左值引用,传入右值或右值引用推导出右值引用,形式与右值引用一样(一对&)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <type_traits>



template<typename T>
void foo(T&& x){
if(std::is_same_v<T,int&>)
printf("x=%d,是左值引用\n",x);
else if(std::is_same_v<T,int>)
printf("x=%d,是右值引用\n",x);
}

int main (int argc, char *argv[]) {
int a=1;
foo(a);
foo(2);
return 0;
}

//输出结果
x=1,是左值引用
x=2,是右值引用

注意

  1. 只出现在模板类型推导或者auto中
  2. const关键字会剥夺万能引用资格,例如void fun(const T&&)

引用折叠:是C++模板推导的一种规则,当类型出现左值引用折叠到另一个任意类型引用时则折叠成左值引用,当出现右值引用折叠到右值引用时则折叠成右值引用

引用1 引用2 引用拆分 引用折叠
左值引用 左值引用 T& & 左值引用 &
左值引用 右值引用 T& && 左值引用 &
右值引用 左值引用 T&& & 左值引用 &
右值引用 右值引用 T&& && 右值引用 &&

注意:我们并不能在代码中显示写出”引用的引用”这样的代码来表示引用折叠,引用折叠只出现在模板推导中

forward

概念

  • 完美转发是用于泛型编程中,利用引用折叠,将参数类型以及属性性质原封不懂的转发传入到函数中,例如左值,右值,const或者volatile参数是为了解决传递参数时的临时对象(右值)被强制转换为左值的问题
  • 在C++中可以使用std::forward来实现完美转发

函数原型

  • _Tp&& forward<_Tp>(__t)C++实现的完美转发函数,内部实现也是利用了引用折叠的原理实现的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <utility>

    //函数原型

    //针对模板参数除了右值的情况
    template<typename _Tp>
    _GLIBCXX_NODISCARD
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
    static_assert(!std::is_lvalue_reference<_Tp>::value,
    "std::forward must not be used to convert an rvalue to an lvalue");
    return static_cast<_Tp&&>(__t);
    }


    //针对传入右值,万能引用推导成右值引用,但是进入函数内部变为左值的情况,因为这个时候是例如_Tp=int&&,针对int&& &&折叠后还是int&&,但是右值引用又与左值不匹配导致无法转发
    template<typename _Tp>
    _GLIBCXX_NODISCARD
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

**自定义实现(引用折叠)**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

template<typename T>
T&& forward(T&& x){
return static_cast<T&&>(x);
}

//如果没有以下这个方法实现,会发现调用forward报错
//是因为在foo中,当传入x为右值,会被推导成右值引用,但是进入函数内部被强转成左值
//而T=int&&,是右值引用,明显跟x左值类型int不对应,所以需要实现下面这部分才行
template<typename T>
T&& forward(T& x){
return static_cast<T&&>(x);
}


void foo1(int& x){
printf("左值引用:x=%d\n",x);
}

void foo1(int&& x){
printf("右值引用:x=%d\n",x);
}
template<typename T>
void foo(T&& x){
foo1(forward<T>(x));
}

int main (int argc, char *argv[]) {
int a=1;
foo(a);
foo(2);
return 0;
}

//输出结果
左值引用:x=1
右值引用:x=2

问题抛出

问题:我有以下代码,当我利用万能引用时正确的推导出了x的类型以及属性,但是x的类型以及属性却不能正确的传入到foo1函数,传入右值后,尽管在模板推导正确成为右值引用,但是进入函数内部时,右值引用变为了左值(因为x作为持续性变量),导致输出结果并不是正确的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

void foo1(int& x){
printf("左值引用:x=%d\n",x);
}

void foo1(int&& x){
printf("右值引用:x=%d\n",x);
}
template<typename T>
void foo(T&& x){
foo1(x);
}

int main (int argc, char *argv[]) {
int a=1;
foo(a);
foo(2);
return 0;
}

//输出结果
左值引用:x=1
左值引用:x=2

利用引用折叠解决

因为模板推导的万能引用的作用导致最终结果只能为左值引用和进入函数内部转为左值的右值引用,因此我们可以利用引用折叠特性,将x强转成T&&类型,当x为左值则会直接转成右值引用,另一情况就是x为左值引用,根据引用折叠依然为左值引用,来实现完美转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

void foo1(int& x){
printf("左值引用:x=%d\n",x);
}

void foo1(int&& x){
printf("右值引用:x=%d\n",x);
}
template<typename T>
void foo(T&& x){
//强转成T&&
foo1(static_cast<T&&>(x));
}

int main (int argc, char *argv[]) {
int a=1;
foo(a);
foo(2);
return 0;
}

//输出结果
左值引用:x=1
右值引用:x=2

使用std::forward解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <utility>

void foo1(int& x){
printf("左值引用:x=%d\n",x);
}

void foo1(int&& x){
printf("右值引用:x=%d\n",x);
}
template<typename T>
void foo(T&& x){
foo1(std::forward<T>(x));
}

int main (int argc, char *argv[]) {
int a=1;
foo(a);
foo(2);
return 0;
}

//输出结果
左值引用:x=1
右值引用:x=2

C++学习
https://moonfordream.github.io/posts/C-学习/
作者
Moon
发布于
2024年5月19日
许可协议